File size: 5,467 Bytes
cd201c7
7a85021
85181c4
7a85021
22932a5
cd201c7
7a85021
cd201c7
6c9b36e
7a85021
 
 
7fc47b1
 
7a85021
fdb7c58
 
7a85021
7fc47b1
cd201c7
cca9c34
c608763
 
7fc47b1
 
 
 
c608763
cd201c7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ab1abdd
c608763
22932a5
 
 
 
 
 
 
198dbcf
22932a5
 
 
 
929d75e
22932a5
 
 
cd201c7
22932a5
 
 
cd201c7
 
 
 
 
 
 
 
 
cca9c34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22932a5
cca9c34
 
 
 
929d75e
cca9c34
 
 
 
 
 
 
 
 
 
 
 
ab1abdd
22932a5
 
cca9c34
22932a5
 
 
ab1abdd
03b5e31
c07926b
7a85021
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7fc47b1
7a85021
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
import React, { useState, useEffect, useMemo } from "react";
import { ProviderInfo, ModelData, CalendarData } from "../types/heatmap";
import OrganizationButton from "../components/OrganizationButton";
import HeatmapGrid from "../components/HeatmapGrid";
import Navbar from "../components/Navbar";
import TagSelector from "../components/TagSelector";
import { getProviders } from "../utils/ranking";
import { ORGANIZATIONS, SCIENTIFIC_TAGS } from "../constants/organizations";

interface PageProps {
  calendarData: CalendarData;
  providers: ProviderInfo[];
}

function Page({
  calendarData,
  providers,
}: PageProps) {
  const [isLoading, setIsLoading] = useState(true);
  const [selectedTags, setSelectedTags] = useState<string[]>([]);
  const [showAllOrgs, setShowAllOrgs] = useState(false);

  useEffect(() => {
    if (calendarData && Object.keys(calendarData).length > 0) {
      setIsLoading(false);
    }
  }, [calendarData]);

  // Filter providers based on selected tags
  const filteredProviders = useMemo(() => {
    if (selectedTags.length === 0) {
      return providers;
    }
    
    return providers.filter(provider => {
      if (!provider.tags) return false;
      return selectedTags.some(tag => provider.tags!.includes(tag));
    });
  }, [providers, selectedTags]);

  const handleTagToggle = (tagId: string) => {
    setSelectedTags(prev => {
      if (prev.includes(tagId)) {
        return prev.filter(t => t !== tagId);
      } else {
        return [...prev, tagId];
      }
    });
  };

  return (
    <div className="w-full">
      <Navbar />
      
      <div className="w-full p-4 py-16">
        <div className="text-center mb-16 max-w-4xl mx-auto">
          <h1 className="text-2xl sm:text-3xl md:text-4xl lg:text-6xl font-bold text-foreground mb-6 bg-gradient-to-r from-foreground to-foreground/80 bg-clip-text">
            <span className="inline-flex items-center gap-1 sm:gap-2">
              Hugging Science Heatmap
              <img
                src="/hf-icon.svg"
                alt="Hugging Face icon"
                className="size-6 sm:size-8 md:size-10"
              />
            </span>
          </h1>
          <p className="text-base sm:text-lg lg:text-xl text-muted-foreground max-w-2xl mx-auto leading-relaxed px-4">
            Open models, datasets, and apps from orgs contributing to AI4Science in the last year.
          </p>
        </div>

        {/* Tag Selector */}
        <div className="mb-16">
          <TagSelector
            tags={SCIENTIFIC_TAGS}
            selectedTags={selectedTags}
            onTagToggle={handleTagToggle}
          />
        </div>

        <div className="mb-16 mx-auto max-w-full">
          {/* Organization Buttons with Horizontal Scroll */}
          <div className="relative px-4">
            {/* Scrollable container with visible scrollbar */}
            <div 
              className="overflow-x-auto overflow-y-hidden pb-4 scrollbar-thin scrollbar-thumb-muted-foreground/30 scrollbar-track-transparent hover:scrollbar-thumb-muted-foreground/50"
              style={{ 
                scrollbarWidth: 'thin',
                scrollbarColor: 'rgba(115, 115, 115, 0.3) transparent'
              }}
            >
              <div className="flex gap-6 py-2 w-max mx-auto">
                {(showAllOrgs ? filteredProviders : filteredProviders.slice(0, 10)).map((provider, index) => (
                  <OrganizationButton
                    key={provider.fullName || provider.authors[0]}
                    provider={provider}
                    calendarData={calendarData}
                    rank={index + 1}
                  />
                ))}
              </div>
            </div>
            
            {/* Visual scroll indicators */}
            <div className="absolute left-0 top-0 bottom-4 w-8 bg-gradient-to-r from-background via-background/80 to-transparent pointer-events-none" />
            <div className="absolute right-0 top-0 bottom-4 w-8 bg-gradient-to-l from-background via-background/80 to-transparent pointer-events-none" />
          </div>
          
          {/* Show More/Less Button */}
          {filteredProviders.length > 10 && (
            <div className="flex justify-center mt-4">
              <button
                onClick={() => setShowAllOrgs(!showAllOrgs)}
                className="px-6 py-2 text-sm font-medium text-foreground bg-muted hover:bg-muted/80 rounded-full transition-colors duration-200 border border-border"
              >
                {showAllOrgs ? `Show Less (Top 10)` : `Show All ${filteredProviders.length} Organizations`}
              </button>
            </div>
          )}
        </div>
             
        <HeatmapGrid 
          sortedProviders={showAllOrgs ? filteredProviders : filteredProviders.slice(0, 10)}
          calendarData={calendarData}
          isLoading={isLoading}
        />
      </div>
    </div>
  );
}


export async function getStaticProps() {
  try {
    const { calendarData, providers } = await getProviders(ORGANIZATIONS);

    return {
      props: {
        calendarData,
        providers,
      },
      revalidate: 3600, // regenerate every hour
    };
  } catch (error) {
    console.error("Error fetching data:", error);
    return {
      props: {
        calendarData: {},
        providers: ORGANIZATIONS,
      },
      revalidate: 60, // retry after 1 minute if there was an error
    };
  }
}

export default Page;