Spaces:
Running
Running
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; |