Spaces:
Sleeping
Sleeping
| import { useEffect } from 'react' | |
| import { | |
| LineChart, Line, BarChart, Bar, | |
| XAxis, YAxis, CartesianGrid, Tooltip, | |
| ResponsiveContainer, ReferenceLine, | |
| } from 'recharts' | |
| import { TrendingUp, Loader2, RefreshCw } from 'lucide-react' | |
| import { useStore } from '../store/useStore' | |
| import { fetchRLState } from '../lib/api' | |
| const CustomTooltip = ({ | |
| active, | |
| payload, | |
| label, | |
| }: { | |
| active?: boolean | |
| payload?: { value: number; name: string; color: string }[] | |
| label?: string | number | |
| }) => { | |
| if (active && payload?.length) { | |
| return ( | |
| <div | |
| className="border border-white/10 rounded-lg px-3 py-2 text-xs" | |
| style={{ background: '#1a1a2e' }} | |
| > | |
| <p className="text-gray-400 mb-1">#{label}</p> | |
| {payload.map((p) => ( | |
| <p key={p.name} style={{ color: p.color }}> | |
| {p.name}: <span className="font-semibold">{p.value}</span> | |
| </p> | |
| ))} | |
| </div> | |
| ) | |
| } | |
| return null | |
| } | |
| export function PerformanceGraph() { | |
| const { rlState, setRlState } = useStore() | |
| const load = async () => { | |
| try { | |
| const data = await fetchRLState() | |
| setRlState(data) | |
| } catch { | |
| // noop — backend might not be up | |
| } | |
| } | |
| useEffect(() => { | |
| void load() | |
| const interval = setInterval(() => void load(), 10_000) | |
| return () => clearInterval(interval) | |
| // eslint-disable-next-line react-hooks/exhaustive-deps | |
| }, []) | |
| if (!rlState) { | |
| return ( | |
| <div className="flex flex-col items-center justify-center h-40 text-gray-600 gap-2"> | |
| <TrendingUp size={24} className="text-gray-700" /> | |
| <p className="text-[11px] text-center"> | |
| RL metrics appear after agent episodes | |
| </p> | |
| <Loader2 size={14} className="animate-spin text-gray-700" /> | |
| </div> | |
| ) | |
| } | |
| const totalEpisodes: number = rlState.totalEpisodes ?? 0 | |
| const successRate: number = rlState.successRate ?? 0 | |
| const currentAlpha: number = rlState.currentAlpha ?? 0 | |
| const episodes: { episode: number; totalReward: number; successRate: number }[] = Array.isArray(rlState.episodes) ? rlState.episodes : [] | |
| const actionDistribution: { action: string; count: number }[] = Array.isArray(rlState.actionDistribution) ? rlState.actionDistribution : [] | |
| return ( | |
| <div className="flex flex-col gap-3"> | |
| {/* Stats row */} | |
| <div className="grid grid-cols-3 gap-1.5"> | |
| {[ | |
| { label: 'Episodes', value: totalEpisodes, color: 'text-blue-400' }, | |
| { label: 'Success', value: `${(successRate * 100).toFixed(0)}%`, color: 'text-green-400' }, | |
| { label: 'Alpha', value: currentAlpha.toFixed(3), color: 'text-orange-400' }, | |
| ].map((s) => ( | |
| <div | |
| key={s.label} | |
| className="bg-white/5 rounded-xl p-2 text-center" | |
| > | |
| <div className={`text-sm font-bold font-mono ${s.color}`}>{s.value}</div> | |
| <div className="text-[9px] text-gray-500 mt-0.5">{s.label}</div> | |
| </div> | |
| ))} | |
| </div> | |
| {/* Reward per episode */} | |
| {episodes.length > 0 && ( | |
| <div> | |
| <div className="flex items-center justify-between mb-1.5"> | |
| <div className="text-[10px] text-gray-500 font-medium">Reward per Episode</div> | |
| <button | |
| onClick={() => void load()} | |
| className="p-1 rounded hover:bg-white/5 text-gray-600 hover:text-gray-400 transition-colors" | |
| title="Refresh" | |
| > | |
| <RefreshCw size={10} /> | |
| </button> | |
| </div> | |
| <ResponsiveContainer width="100%" height={110}> | |
| <LineChart data={episodes} margin={{ top: 4, right: 4, bottom: 0, left: -20 }}> | |
| <CartesianGrid strokeDasharray="3 3" stroke="#ffffff08" /> | |
| <XAxis dataKey="episode" tick={{ fontSize: 9, fill: '#6b7280' }} /> | |
| <YAxis domain={[-1, 1]} tick={{ fontSize: 9, fill: '#6b7280' }} /> | |
| <Tooltip content={<CustomTooltip />} /> | |
| <ReferenceLine y={0} stroke="#ffffff20" strokeDasharray="3 3" /> | |
| <Line | |
| type="monotone" | |
| dataKey="totalReward" | |
| name="Reward" | |
| stroke="#f97316" | |
| strokeWidth={2} | |
| dot={episodes.length < 30 ? { fill: '#f97316', r: 2 } : false} | |
| activeDot={{ r: 4 }} | |
| /> | |
| </LineChart> | |
| </ResponsiveContainer> | |
| </div> | |
| )} | |
| {/* Action distribution */} | |
| {actionDistribution.length > 0 && ( | |
| <div> | |
| <div className="text-[10px] text-gray-500 mb-1.5 font-medium"> | |
| LinUCB Action Distribution | |
| </div> | |
| <ResponsiveContainer width="100%" height={90}> | |
| <BarChart | |
| data={actionDistribution} | |
| margin={{ top: 4, right: 4, bottom: 0, left: -20 }} | |
| > | |
| <CartesianGrid strokeDasharray="3 3" stroke="#ffffff08" /> | |
| <XAxis | |
| dataKey="action" | |
| tick={{ fontSize: 8, fill: '#6b7280' }} | |
| tickFormatter={(v: string) => v.replace('FIX_', '').slice(0, 6)} | |
| /> | |
| <YAxis tick={{ fontSize: 9, fill: '#6b7280' }} /> | |
| <Tooltip content={<CustomTooltip />} /> | |
| <Bar dataKey="count" name="Uses" fill="#8b5cf6" radius={[3, 3, 0, 0]} /> | |
| </BarChart> | |
| </ResponsiveContainer> | |
| </div> | |
| )} | |
| {/* Success rate line */} | |
| {episodes.length >= 3 && ( | |
| <div> | |
| <div className="text-[10px] text-gray-500 mb-1.5 font-medium"> | |
| Rolling Success Rate | |
| </div> | |
| <ResponsiveContainer width="100%" height={80}> | |
| <LineChart data={episodes} margin={{ top: 4, right: 4, bottom: 0, left: -20 }}> | |
| <CartesianGrid strokeDasharray="3 3" stroke="#ffffff08" /> | |
| <XAxis dataKey="episode" tick={{ fontSize: 9, fill: '#6b7280' }} /> | |
| <YAxis domain={[0, 1]} tick={{ fontSize: 9, fill: '#6b7280' }} /> | |
| <Tooltip content={<CustomTooltip />} /> | |
| <Line | |
| type="monotone" | |
| dataKey="successRate" | |
| name="Success" | |
| stroke="#22c55e" | |
| strokeWidth={2} | |
| dot={false} | |
| /> | |
| </LineChart> | |
| </ResponsiveContainer> | |
| </div> | |
| )} | |
| </div> | |
| ) | |
| } | |