AleksanderObuchowski's picture
Initial commit for Hugging Face Spaces
e4f1db2
raw
history blame
15 kB
import React, { useState, useEffect } from 'react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
import { Loader2, Calendar, TrendingUp, Database, Clock } from 'lucide-react';
import api from '../services/api';
interface TimelineData {
year: number;
[key: string]: number;
}
interface AlgorithmTimeline {
algorithm: string;
name: string;
category: string;
data: { year: number; count: number }[];
}
interface CacheStats {
cached: number;
fetched: number;
}
interface ProgressState {
current: number;
total: number;
currentAlgorithm: string;
}
const Timeline: React.FC = () => {
const [timelineData, setTimelineData] = useState<TimelineData[]>([]);
const [algorithms, setAlgorithms] = useState<AlgorithmTimeline[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [selectedAlgorithms, setSelectedAlgorithms] = useState<string[]>([]);
const [yearRange, setYearRange] = useState({ start: 2015, end: 2024 });
const [cacheStats, setCacheStats] = useState<CacheStats | null>(null);
const [progress, setProgress] = useState<ProgressState | null>(null);
useEffect(() => {
fetchTimelineData();
}, [yearRange]);
const fetchTimelineData = async () => {
try {
setLoading(true);
setError(null);
setProgress(null);
setCacheStats(null);
// Use Server-Sent Events for real-time progress
const eventSource = new EventSource(`/api/search/timeline-stream?startYear=${yearRange.start}&endYear=${yearRange.end}`);
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
switch (data.type) {
case 'init':
setProgress({
current: 0,
total: data.totalOperations,
currentAlgorithm: data.message
});
setCacheStats({
cached: data.cachedResults,
fetched: data.fetchedResults
});
break;
case 'algorithm_start':
setProgress({
current: data.completed,
total: data.total,
currentAlgorithm: `Processing ${data.algorithm}...`
});
break;
case 'year_complete':
setProgress({
current: data.completed,
total: data.total,
currentAlgorithm: `${data.algorithm} (${data.year}): ${data.count} papers`
});
break;
case 'algorithm_complete':
setProgress({
current: data.completed,
total: data.total,
currentAlgorithm: `Completed ${data.algorithm}`
});
break;
case 'cache_saved':
setProgress(prev => prev ? {
...prev,
currentAlgorithm: data.message
} : null);
break;
case 'complete':
setTimelineData(data.timelineData);
setAlgorithms(data.algorithms);
setCacheStats(data.cacheStats);
// Auto-select top 5 algorithms if none selected
if (selectedAlgorithms.length === 0) {
const topAlgorithms = data.algorithms
.sort((a: AlgorithmTimeline, b: AlgorithmTimeline) => {
const aTotal = a.data.reduce((sum, item) => sum + item.count, 0);
const bTotal = b.data.reduce((sum, item) => sum + item.count, 0);
return bTotal - aTotal;
})
.slice(0, 5)
.map((algo: AlgorithmTimeline) => algo.algorithm);
setSelectedAlgorithms(topAlgorithms);
}
setLoading(false);
setProgress(null);
eventSource.close();
break;
case 'error':
setError(`Failed to load timeline data: ${data.error}`);
setLoading(false);
setProgress(null);
eventSource.close();
break;
}
};
eventSource.onerror = () => {
setError('Connection lost while loading timeline data');
setLoading(false);
setProgress(null);
eventSource.close();
};
} catch (err) {
setError('Failed to start timeline data loading');
setLoading(false);
setProgress(null);
}
};
const handleAlgorithmToggle = (algorithmKey: string) => {
setSelectedAlgorithms(prev =>
prev.includes(algorithmKey)
? prev.filter(key => key !== algorithmKey)
: [...prev, algorithmKey]
);
};
const getAlgorithmColor = (algorithmKey: string, index: number): string => {
// Define a palette of distinct colors for better visualization
const colors = [
'#3B82F6', // Blue
'#EF4444', // Red
'#10B981', // Green
'#F59E0B', // Yellow
'#8B5CF6', // Purple
'#EC4899', // Pink
'#06B6D4', // Cyan
'#84CC16', // Lime
'#F97316', // Orange
'#6366F1', // Indigo
'#14B8A6', // Teal
'#F43F5E', // Rose
'#8B5A3C', // Brown
'#6B7280', // Gray
'#DC2626', // Dark Red
'#059669', // Dark Green
'#7C3AED', // Dark Purple
'#DB2777', // Dark Pink
'#0891B2', // Dark Cyan
'#65A30D' // Dark Lime
];
// Use algorithm key to get consistent color assignment
const colorIndex = selectedAlgorithms.indexOf(algorithmKey) % colors.length;
return colors[colorIndex];
};
if (loading) {
return (
<div className="space-y-8">
<div className="text-center">
<h1 className="text-4xl font-bold text-gray-900 mb-2">
Algorithm Usage Timeline
</h1>
<p className="text-xl text-gray-600">
Track how the usage of AI algorithms in medical research has evolved over time
</p>
</div>
<div className="bg-white p-8 rounded-lg shadow-lg max-w-2xl mx-auto">
<div className="flex items-center justify-center mb-6">
<Loader2 className="h-8 w-8 animate-spin text-blue-600" />
<span className="ml-3 text-lg text-gray-700">Loading timeline data...</span>
</div>
{progress && (
<div className="space-y-4">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600">
{progress.currentAlgorithm}
</span>
<span className="text-sm font-medium text-gray-900">
{Math.round((progress.current / progress.total) * 100)}%
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all duration-300 ease-out"
style={{ width: `${Math.min((progress.current / progress.total) * 100, 100)}%` }}
></div>
</div>
<div className="flex justify-between text-xs text-gray-500">
<span>{progress.current} / {progress.total} requests</span>
<span>
{progress.total > 0 ? Math.round((progress.current / progress.total) * 100) : 0}% complete
</span>
</div>
</div>
)}
<div className="mt-6 p-4 bg-blue-50 rounded-lg">
<div className="flex items-center mb-2">
<Database className="h-4 w-4 text-blue-600 mr-2" />
<span className="text-sm font-medium text-blue-900">Cache Information</span>
</div>
<p className="text-xs text-blue-700">
Historical data is cached on disk to speed up future requests.
Only the current year data needs to be fetched from PubMed each time.
</p>
</div>
</div>
</div>
);
}
if (error) {
return (
<div className="text-center text-red-600 p-8">
<p>{error}</p>
<button
onClick={fetchTimelineData}
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Retry
</button>
</div>
);
}
const filteredTimelineData = timelineData.map(yearData => {
const filtered: TimelineData = { year: yearData.year };
selectedAlgorithms.forEach(algoKey => {
if (yearData[algoKey] !== undefined) {
filtered[algoKey] = yearData[algoKey];
}
});
return filtered;
});
return (
<div className="space-y-8">
<div className="text-center">
<h1 className="text-4xl font-bold text-gray-900 mb-2">
Algorithm Usage Timeline
</h1>
<p className="text-xl text-gray-600">
Track how the usage of AI algorithms in medical research has evolved over time
</p>
</div>
<div className="bg-white p-6 rounded-lg shadow-lg">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between mb-6">
<div className="flex items-center mb-4 lg:mb-0">
<Calendar className="h-5 w-5 text-gray-500 mr-2" />
<span className="text-sm font-medium text-gray-700">Year Range:</span>
<input
type="number"
min="2010"
max="2024"
value={yearRange.start}
onChange={(e) => setYearRange(prev => ({ ...prev, start: parseInt(e.target.value) }))}
className="ml-2 px-2 py-1 border border-gray-300 rounded text-sm w-20"
/>
<span className="mx-2 text-gray-500">to</span>
<input
type="number"
min="2010"
max="2024"
value={yearRange.end}
onChange={(e) => setYearRange(prev => ({ ...prev, end: parseInt(e.target.value) }))}
className="px-2 py-1 border border-gray-300 rounded text-sm w-20"
/>
</div>
<div className="flex items-center space-x-6">
<div className="flex items-center">
<TrendingUp className="h-5 w-5 text-green-500 mr-2" />
<span className="text-sm text-gray-600">
{selectedAlgorithms.length} algorithm{selectedAlgorithms.length !== 1 ? 's' : ''} selected
</span>
</div>
{cacheStats && (
<div className="flex items-center space-x-4">
<div className="flex items-center">
<Database className="h-4 w-4 text-blue-500 mr-1" />
<span className="text-xs text-gray-600">
{cacheStats.cached} cached
</span>
</div>
<div className="flex items-center">
<Clock className="h-4 w-4 text-orange-500 mr-1" />
<span className="text-xs text-gray-600">
{cacheStats.fetched} fetched
</span>
</div>
</div>
)}
</div>
</div>
<ResponsiveContainer width="100%" height={500}>
<LineChart data={filteredTimelineData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis
dataKey="year"
type="number"
scale="linear"
domain={[yearRange.start, yearRange.end]}
/>
<YAxis />
<Tooltip />
<Legend />
{selectedAlgorithms.map((algoKey, index) => {
const algo = algorithms.find(a => a.algorithm === algoKey);
if (!algo) return null;
return (
<Line
key={algoKey}
type="monotone"
dataKey={algoKey}
stroke={getAlgorithmColor(algoKey, index)}
strokeWidth={2}
name={algo.name}
connectNulls={false}
/>
);
})}
</LineChart>
</ResponsiveContainer>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="bg-white p-6 rounded-lg shadow-lg">
<h3 className="text-lg font-bold text-gray-900 mb-4">Classical ML</h3>
<div className="space-y-2">
{algorithms
.filter(algo => algo.category === 'classical_ml')
.slice(0, 8)
.map(algo => (
<label key={algo.algorithm} className="flex items-center cursor-pointer">
<input
type="checkbox"
checked={selectedAlgorithms.includes(algo.algorithm)}
onChange={() => handleAlgorithmToggle(algo.algorithm)}
className="mr-2"
/>
<span className="text-sm text-gray-700">{algo.name}</span>
</label>
))}
</div>
</div>
<div className="bg-white p-6 rounded-lg shadow-lg">
<h3 className="text-lg font-bold text-gray-900 mb-4">Deep Learning</h3>
<div className="space-y-2">
{algorithms
.filter(algo => algo.category === 'deep_learning')
.slice(0, 8)
.map(algo => (
<label key={algo.algorithm} className="flex items-center cursor-pointer">
<input
type="checkbox"
checked={selectedAlgorithms.includes(algo.algorithm)}
onChange={() => handleAlgorithmToggle(algo.algorithm)}
className="mr-2"
/>
<span className="text-sm text-gray-700">{algo.name}</span>
</label>
))}
</div>
</div>
<div className="bg-white p-6 rounded-lg shadow-lg">
<h3 className="text-lg font-bold text-gray-900 mb-4">Large Language Models</h3>
<div className="space-y-2">
{algorithms
.filter(algo => algo.category === 'llms')
.slice(0, 8)
.map(algo => (
<label key={algo.algorithm} className="flex items-center cursor-pointer">
<input
type="checkbox"
checked={selectedAlgorithms.includes(algo.algorithm)}
onChange={() => handleAlgorithmToggle(algo.algorithm)}
className="mr-2"
/>
<span className="text-sm text-gray-700">{algo.name}</span>
</label>
))}
</div>
</div>
</div>
</div>
);
};
export default Timeline;