Spaces:
Running
Running
import React, { useState, useEffect, useMemo } from "react"; | |
import { generateCalendarData } from "../utils/calendar"; | |
import { | |
OpenSourceHeatmapProps, | |
ProviderInfo, | |
ModelData, | |
CalendarData, | |
} from "../types/heatmap"; | |
import Heatmap from "../components/Heatmap"; | |
import { fetchAllProvidersData, fetchAllAuthorsData } from "../utils/authors"; | |
import UserSearchDialog from "../components/UserSearchDialog"; | |
const PROVIDERS: ProviderInfo[] = [ | |
{ color: "#ff7000", authors: ["mistralai"] }, | |
{ color: "#1877F2", authors: ["meta-llama", "facebook"] }, | |
{ color: "#10A37F", authors: ["openai"] }, | |
{ color: "#cc785c", authors: ["Anthropic"] }, | |
{ color: "#DB4437", authors: ["google"] }, | |
{ color: "#5E35B1", authors: ["allenai"] }, | |
{ color: "#0066CC", authors: ["apple"] }, | |
{ color: "#FEB800", authors: ["microsoft"] }, | |
{ color: "#76B900", authors: ["nvidia"] }, | |
{ color: "#00A8E0", authors: ["deepseek-ai"] }, | |
{ color: "#6366F1", authors: ["Qwen"] }, | |
{ color: "#FF6B6B", authors: ["CohereLabs"] }, | |
{ color: "#4ECDC4", authors: ["ibm-granite"] }, | |
{ color: "#A855F7", authors: ["stabilityai"] }, | |
]; | |
export async function getStaticProps() { | |
try { | |
const allAuthors = PROVIDERS.flatMap(({ authors }) => authors); | |
const uniqueAuthors = Array.from(new Set(allAuthors)); | |
const flatData: ModelData[] = await fetchAllAuthorsData(uniqueAuthors); | |
const updatedProviders = await fetchAllProvidersData(PROVIDERS); | |
const calendarData = generateCalendarData(flatData, updatedProviders); | |
return { | |
props: { | |
calendarData, | |
providers: updatedProviders, | |
}, | |
revalidate: 3600, | |
}; | |
} catch (error) { | |
console.error("Error fetching data:", error); | |
return { | |
props: { | |
calendarData: {}, | |
providers: PROVIDERS, | |
}, | |
revalidate: 60, | |
}; | |
} | |
} | |
const ProviderCard = React.memo(({ | |
provider, | |
calendarData, | |
isExpanded, | |
onToggle | |
}: { | |
provider: ProviderInfo, | |
calendarData: CalendarData, | |
isExpanded: boolean, | |
onToggle: () => void | |
}) => { | |
const providerName = provider.fullName || provider.authors[0]; | |
const totalActivity = calendarData[providerName]?.reduce((sum, day) => sum + day.count, 0) || 0; | |
const recentActivity = calendarData[providerName]?.slice(-30).reduce((sum, day) => sum + day.count, 0) || 0; | |
return ( | |
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden transition-all duration-200 hover:shadow-md"> | |
<button | |
onClick={onToggle} | |
className="w-full px-6 py-4 flex items-center justify-between hover:bg-gray-50 transition-colors" | |
> | |
<div className="flex items-center gap-4"> | |
{provider.avatarUrl && ( | |
<img | |
src={provider.avatarUrl} | |
alt={providerName} | |
className="w-12 h-12 rounded-lg object-cover" | |
/> | |
)} | |
<div className="text-left"> | |
<h3 className="font-semibold text-lg text-gray-900">{providerName}</h3> | |
<div className="flex gap-4 text-sm text-gray-600"> | |
<span>Total: <span className="font-medium text-gray-900">{totalActivity.toLocaleString()}</span></span> | |
<span>Last 30 days: <span className="font-medium text-gray-900">{recentActivity.toLocaleString()}</span></span> | |
</div> | |
</div> | |
</div> | |
<div className="flex items-center gap-3"> | |
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: provider.color }}></div> | |
<svg | |
className={`w-5 h-5 text-gray-400 transition-transform ${isExpanded ? 'rotate-180' : ''}`} | |
fill="none" | |
stroke="currentColor" | |
viewBox="0 0 24 24" | |
> | |
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /> | |
</svg> | |
</div> | |
</button> | |
{isExpanded && ( | |
<div className="px-6 pb-6 border-t border-gray-100"> | |
<div className="pt-4"> | |
<Heatmap | |
data={calendarData[providerName]} | |
color={provider.color} | |
providerName={providerName} | |
fullName={provider.fullName ?? providerName} | |
avatarUrl={provider.avatarUrl ?? ''} | |
/> | |
</div> | |
</div> | |
)} | |
</div> | |
); | |
}); | |
const OpenSourceHeatmap: React.FC<OpenSourceHeatmapProps> = ({ | |
calendarData, | |
providers, | |
}) => { | |
const [isLoading, setIsLoading] = useState(true); | |
const [expandedProviders, setExpandedProviders] = useState<Set<string>>(new Set()); | |
const [filterQuery, setFilterQuery] = useState(""); | |
const [sortBy, setSortBy] = useState<"total" | "recent">("total"); | |
useEffect(() => { | |
if (calendarData && Object.keys(calendarData).length > 0) { | |
setIsLoading(false); | |
} | |
}, [calendarData]); | |
const sortedAndFilteredProviders = useMemo(() => { | |
let filtered = providers; | |
if (filterQuery) { | |
filtered = providers.filter(p => | |
(p.fullName || p.authors[0]).toLowerCase().includes(filterQuery.toLowerCase()) | |
); | |
} | |
return filtered.sort((a, b) => { | |
const aName = a.fullName || a.authors[0]; | |
const bName = b.fullName || b.authors[0]; | |
if (sortBy === "total") { | |
return calendarData[bName]?.reduce((sum, day) => sum + day.count, 0) || 0 - | |
calendarData[aName]?.reduce((sum, day) => sum + day.count, 0) || 0; | |
} else { | |
return calendarData[bName]?.slice(-30).reduce((sum, day) => sum + day.count, 0) || 0 - | |
calendarData[aName]?.slice(-30).reduce((sum, day) => sum + day.count, 0) || 0; | |
} | |
}); | |
}, [providers, calendarData, filterQuery, sortBy]); | |
const toggleProvider = (providerName: string) => { | |
setExpandedProviders(prev => { | |
const newSet = new Set(prev); | |
if (newSet.has(providerName)) { | |
newSet.delete(providerName); | |
} else { | |
newSet.add(providerName); | |
} | |
return newSet; | |
}); | |
}; | |
const toggleAll = () => { | |
if (expandedProviders.size === sortedAndFilteredProviders.length) { | |
setExpandedProviders(new Set()); | |
} else { | |
setExpandedProviders(new Set(sortedAndFilteredProviders.map(p => p.fullName || p.authors[0]))); | |
} | |
}; | |
return ( | |
<div className="min-h-screen bg-gray-50"> | |
<div className="w-full max-w-7xl mx-auto px-4 py-12"> | |
{/* Header */} | |
<div className="text-center mb-12"> | |
<h1 className="text-5xl font-bold text-gray-900 mb-4"> | |
AI Labs Activity Dashboard | |
</h1> | |
<p className="text-lg text-gray-600 mb-8"> | |
Track models, datasets, and spaces from leading AI organizations on Hugging Face | |
</p> | |
{/* Controls */} | |
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center max-w-2xl mx-auto"> | |
<div className="relative flex-1 w-full"> | |
<input | |
type="text" | |
placeholder="Search organizations..." | |
value={filterQuery} | |
onChange={(e) => setFilterQuery(e.target.value)} | |
className="w-full px-4 py-2 pl-10 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" | |
/> | |
<svg className="absolute left-3 top-2.5 w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> | |
</svg> | |
</div> | |
<div className="flex gap-2"> | |
<select | |
value={sortBy} | |
onChange={(e) => setSortBy(e.target.value as "total" | "recent")} | |
className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" | |
> | |
<option value="total">Sort by Total</option> | |
<option value="recent">Sort by Recent</option> | |
</select> | |
<button | |
onClick={toggleAll} | |
className="px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors" | |
> | |
{expandedProviders.size === sortedAndFilteredProviders.length ? 'Collapse All' : 'Expand All'} | |
</button> | |
<UserSearchDialog /> | |
</div> | |
</div> | |
</div> | |
{/* Stats Summary */} | |
{!isLoading && ( | |
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-8"> | |
<div className="bg-white rounded-lg p-6 text-center shadow-sm border border-gray-200"> | |
<div className="text-3xl font-bold text-gray-900"> | |
{providers.length} | |
</div> | |
<div className="text-sm text-gray-600">Organizations</div> | |
</div> | |
<div className="bg-white rounded-lg p-6 text-center shadow-sm border border-gray-200"> | |
<div className="text-3xl font-bold text-gray-900"> | |
{Object.values(calendarData).reduce((total, days) => | |
total + days.reduce((sum, day) => sum + day.count, 0), 0 | |
).toLocaleString()} | |
</div> | |
<div className="text-sm text-gray-600">Total Activities</div> | |
</div> | |
<div className="bg-white rounded-lg p-6 text-center shadow-sm border border-gray-200"> | |
<div className="text-3xl font-bold text-gray-900"> | |
{Object.values(calendarData).reduce((total, days) => | |
total + days.slice(-30).reduce((sum, day) => sum + day.count, 0), 0 | |
).toLocaleString()} | |
</div> | |
<div className="text-sm text-gray-600">Last 30 Days</div> | |
</div> | |
</div> | |
)} | |
{/* Provider List */} | |
{isLoading ? ( | |
<div className="flex justify-center items-center h-64"> | |
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-900"></div> | |
</div> | |
) : ( | |
<div className="space-y-4"> | |
{sortedAndFilteredProviders.length === 0 ? ( | |
<div className="text-center py-12 text-gray-500"> | |
No organizations found matching "{filterQuery}" | |
</div> | |
) : ( | |
sortedAndFilteredProviders.map((provider) => { | |
const providerName = provider.fullName || provider.authors[0]; | |
return ( | |
<ProviderCard | |
key={providerName} | |
provider={provider} | |
calendarData={calendarData} | |
isExpanded={expandedProviders.has(providerName)} | |
onToggle={() => toggleProvider(providerName)} | |
/> | |
); | |
}) | |
)} | |
</div> | |
)} | |
</div> | |
</div> | |
); | |
}; | |
export default React.memo(OpenSourceHeatmap); |