Xianbao QIAN
show navigation bar
1f1c117
import React, { useState, useEffect } from 'react';
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer
} from 'recharts';
import { PROVIDERS_MAP } from '../../utils/providers';
import {
fetchAllModelData,
generateMonthlyData,
getTotalMonthlyData,
processDetailedModelData,
MonthlyActivity,
DetailedModelData,
convertToCSV
} from '../../utils/modelData';
import Link from 'next/link';
interface TrendProps {
monthlyData: MonthlyActivity[];
totalData: MonthlyActivity[];
detailedData: DetailedModelData[];
error?: string;
}
const COLORS = Object.fromEntries(
Object.entries(PROVIDERS_MAP).map(([provider, { color }]) => [provider, color])
);
COLORS['Total'] = '#4CAF50';
const CustomTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="bg-white p-4 border border-gray-200 rounded shadow dark:bg-gray-800 dark:border-gray-700">
<p className="text-sm font-bold mb-2 dark:text-white">{label}</p>
{payload.map((entry: any) => (
<p
key={entry.name}
className="text-sm"
style={{ color: entry.color }}
>
{entry.name}: {entry.value} models
</p>
))}
</div>
);
}
return null;
};
const formatDate = (dateStr: string) => {
const date = new Date(dateStr);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
};
const TrendPage: React.FC<TrendProps> = ({ monthlyData = [], totalData = [], detailedData = [], error }) => {
const [showTotal, setShowTotal] = useState(true);
const [selectedProviders, setSelectedProviders] = useState<string[]>([]);
const [minLikes, setMinLikes] = useState(100);
const [isLoading, setIsLoading] = useState(false);
const [contentType, setContentType] = useState<'all' | 'models' | 'datasets'>('all');
const toggleProvider = (provider: string) => {
setSelectedProviders(prev =>
prev.includes(provider)
? prev.filter(p => p !== provider)
: [...prev, provider]
);
};
// Filter total data based on content type
const filteredTotalData = totalData.filter(d => {
if (contentType === 'all') {
return d.isDataset === null; // Show only combined counts for 'all'
}
return (contentType === 'datasets' && d.isDataset === true) ||
(contentType === 'models' && d.isDataset === false);
});
// Group data by provider
const providerData = Object.fromEntries(
Object.keys(PROVIDERS_MAP).map(provider => {
const providerMonthlyData = monthlyData.filter(d => {
if (contentType === 'all') {
return d.provider === provider; // Show all items for the provider
}
const matchesContentType = (contentType === 'datasets' && d.isDataset === true) ||
(contentType === 'models' && d.isDataset === false);
return d.provider === provider && matchesContentType;
});
// If showing all, aggregate the counts for each month
if (contentType === 'all') {
const aggregatedData: Record<string, MonthlyActivity> = {};
providerMonthlyData.forEach(d => {
if (!aggregatedData[d.date]) {
aggregatedData[d.date] = {
date: d.date,
count: 0,
provider: d.provider,
isDataset: null
};
}
aggregatedData[d.date].count += d.count;
});
return [provider, Object.values(aggregatedData).sort((a, b) => a.date.localeCompare(b.date))];
}
return [provider, providerMonthlyData || []];
})
);
// Filter and group detailed model data
const filteredModels = (detailedData || [])
.filter(model => {
const matchesContentType = contentType === 'all' ||
(contentType === 'datasets' && model.isDataset) ||
(contentType === 'models' && !model.isDataset);
return model.likes >= minLikes &&
(selectedProviders.length === 0 || selectedProviders.includes(model.provider)) &&
matchesContentType;
})
.reduce<Record<string, DetailedModelData[]>>((acc, model) => {
if (!acc[model.monthKey]) {
acc[model.monthKey] = [];
}
acc[model.monthKey].push(model);
return acc;
}, {});
useEffect(() => {
console.log('Content type changed:', contentType);
console.log('Selected providers:', selectedProviders);
console.log('Min likes:', minLikes);
}, [contentType, selectedProviders, minLikes]);
if (error) {
return (
<div className="w-full max-w-screen-xl mx-auto p-4">
<h1 className="text-3xl lg:text-5xl mt-16 font-bold text-center mb-2 dark:text-white">Chinese AI Model Release Trends 📈</h1>
<div className="text-center text-red-600 dark:text-red-400 mt-8">
<p>Error loading data: {error}</p>
<p className="mt-2">Please try again later.</p>
</div>
</div>
);
}
return (
<div className="w-full max-w-screen-lg mx-auto p-4">
<div className="flex justify-between items-center mb-8">
<div>
<h1 className="text-3xl lg:text-4xl font-bold mb-2">Chinese AI Open Source Trends 🤗</h1>
<p className="text-gray-600 dark:text-gray-400">
Track the growth of Chinese AI models and datasets over time
</p>
</div>
<Link
href="/heatmap"
className="px-4 py-2 rounded-lg bg-blue-500 text-white hover:bg-blue-600 transition-colors"
>
View Heatmap
</Link>
</div>
<p className="text-center text-sm mb-8 dark:text-gray-300">
Track the growth of Chinese AI models and datasets over time
</p>
<div className="flex flex-col gap-4 p-4">
<div className="flex justify-center mb-6 space-x-4">
<button
onClick={() => setContentType('all')}
className={`px-4 py-2 rounded-lg ${
contentType === 'all'
? 'bg-blue-500 text-white'
: 'bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-300'
}`}
>
All
</button>
<button
onClick={() => setContentType('models')}
className={`px-4 py-2 rounded-lg ${
contentType === 'models'
? 'bg-blue-500 text-white'
: 'bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-300'
}`}
>
Models
</button>
<button
onClick={() => setContentType('datasets')}
className={`px-4 py-2 rounded-lg ${
contentType === 'datasets'
? 'bg-blue-500 text-white'
: 'bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-300'
}`}
>
Datasets
</button>
</div>
<div className="flex flex-wrap gap-4 mb-6 justify-center items-center">
<div className="flex gap-2">
<button
onClick={() => setShowTotal(!showTotal)}
className={`px-4 py-2 rounded-lg ${
showTotal
? 'bg-blue-500 text-white'
: 'bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-300'
}`}
>
{showTotal ? 'Hide Total' : 'Show Total'}
</button>
{Object.entries(PROVIDERS_MAP).map(([provider, { color }]) => (
<button
key={provider}
onClick={() => toggleProvider(provider)}
className={`px-4 py-2 rounded-lg transition-colors ${
selectedProviders.includes(provider)
? 'text-white'
: 'text-gray-600 dark:text-gray-400'
}`}
style={{
backgroundColor: selectedProviders.includes(provider)
? color
: 'transparent'
}}
>
{provider}
</button>
))}
</div>
</div>
{/* Chart */}
<div className="w-full h-[600px] dark:bg-gray-900 p-4 rounded-lg">
<ResponsiveContainer width="100%" height="100%">
<LineChart
margin={{ top: 20, right: 30, left: 20, bottom: 20 }}
>
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
<XAxis
dataKey="date"
type="category"
allowDuplicatedCategory={false}
tick={{ fontSize: 12 }}
interval="preserveStartEnd"
stroke="#9CA3AF"
/>
<YAxis stroke="#9CA3AF" />
<Tooltip content={<CustomTooltip />} />
<Legend />
{/* Total Line */}
{showTotal && (
<Line
data={filteredTotalData}
type="monotone"
dataKey="count"
stroke={COLORS['Total']}
name="Total"
strokeWidth={2}
dot={false}
/>
)}
{/* Provider Lines */}
{selectedProviders.map(provider => (
<Line
key={provider}
data={providerData[provider]}
type="monotone"
dataKey="count"
stroke={COLORS[provider]}
name={provider}
strokeWidth={1.5}
dot={false}
/>
))}
</LineChart>
</ResponsiveContainer>
</div>
{/* Download Button */}
<div className="flex justify-center mt-4 mb-8">
<button
className="px-4 py-2 rounded-lg bg-green-500 text-white hover:bg-green-600"
onClick={() => {
// Get the currently visible data based on filters
const visibleData = [];
// Add provider data if showing
selectedProviders.forEach(provider => {
visibleData.push(...providerData[provider].map(d => ({ ...d, provider })));
});
// Add total data if showing
if (showTotal) {
visibleData.push(...filteredTotalData);
}
const csvContent = convertToCSV(visibleData);
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `${contentType}_counts.csv`;
link.click();
URL.revokeObjectURL(link.href);
}}
>
Download CSV
</button>
</div>
{/* Major Releases Section */}
<div className="mt-12">
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold dark:text-white">
Major Releases ({Object.values(filteredModels).flat().length})
</h2>
<div className="flex items-center gap-3 bg-white dark:bg-gray-800 px-4 py-2 rounded-lg shadow-sm">
<label className="text-sm font-medium dark:text-gray-300">Min Likes:</label>
<input
type="number"
value={minLikes}
onChange={(e) => setMinLikes(Math.max(0, parseInt(e.target.value) || 0))}
className="w-20 px-2 py-1 border rounded dark:bg-gray-700 dark:border-gray-600 dark:text-white focus:outline-none focus:ring-2 focus:ring-green-500"
/>
</div>
</div>
<div className="space-y-8">
{Object.entries(filteredModels)
.map(([monthKey, models]) => ({
monthKey,
models,
sortKey: models[0]?.sortKey || '' // Use the first model's sortKey
}))
.sort((a, b) => b.sortKey.localeCompare(a.sortKey)) // Sort by sortKey in descending order
.map(({ monthKey, models }) => (
<div key={monthKey} className="border-b border-gray-200 dark:border-gray-700 pb-4">
<h3 className="text-xl font-semibold mb-3 dark:text-white">{monthKey}</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{models
.sort((a, b) => b.likes - a.likes) // Sort models within each month by likes
.map(model => (
<div
key={model.id}
className="p-4 rounded-lg border dark:border-gray-700 hover:shadow-md transition-shadow bg-white dark:bg-gray-800"
style={{ borderColor: COLORS[model.provider] }}
>
<div className="flex justify-between items-start">
<div>
<a
href={`https://huggingface.co/${model.id}`}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline font-medium dark:text-blue-400"
>
{model.name}
</a>
<p className="text-sm text-gray-600 dark:text-gray-400">{model.provider}</p>
</div>
<div className="text-right">
<p className="text-sm font-medium dark:text-white">❤️ {model.likes}</p>
<p className="text-xs text-gray-500 dark:text-gray-400">{formatDate(model.createdAt)}</p>
</div>
</div>
</div>
))}
</div>
</div>
))}
</div>
</div>
</div>
</div>
);
};
export const getStaticProps = async () => {
try {
const modelData = await fetchAllModelData();
const monthlyData = generateMonthlyData(modelData);
const totalData = getTotalMonthlyData(monthlyData);
const filteredModels = processDetailedModelData(modelData);
return {
props: {
monthlyData,
totalData,
filteredModels,
},
// Revalidate every 12 hours
revalidate: 43200,
};
} catch (error) {
console.error('Error in getStaticProps:', error);
return {
props: {
monthlyData: [],
totalData: [],
filteredModels: {},
},
// Retry more frequently if there was an error
revalidate: 3600,
};
}
};
export default TrendPage;