sql-agent-openenv / frontend /src /components /PerformanceGraph.tsx
ar9avg's picture
fix: align rl-state API shape with frontend, add error boundary
ce1c471
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>
)
}