| |
| |
| |
| |
| import { |
| XAxis, YAxis, Tooltip, ResponsiveContainer, |
| ReferenceLine, Area, AreaChart, |
| } from "recharts"; |
| import type { TrainingMetrics } from "@/lib/simulation"; |
| import { Panel, PanelHeader, PanelDot, TooltipPanel } from "./Panel"; |
|
|
| interface TrainingChartsProps { |
| metrics: TrainingMetrics; |
| } |
|
|
| const HEX = { |
| white: "#2D9CDB", |
| black: "#E05C5C", |
| claude: "#F5A623", |
| profit: "#27AE60", |
| loss: "#a78bfa", |
| kl: "#f97316", |
| }; |
|
|
| const CustomTooltip = ({ active, payload, label }: any) => { |
| if (!active || !payload?.length) return null; |
| return ( |
| <TooltipPanel> |
| <div style={{ color: "rgba(255,255,255,0.4)", marginBottom: "2px" }}>step {label}</div> |
| {payload.map((p: any) => ( |
| <div key={p.dataKey} style={{ color: p.color }}> |
| {p.name}: {typeof p.value === "number" ? p.value.toFixed(4) : p.value} |
| </div> |
| ))} |
| </TooltipPanel> |
| ); |
| }; |
|
|
| interface MiniChartProps { |
| data: { step: number; value: number }[]; |
| color: string; |
| label: string; |
| refLine?: number; |
| domain?: [number | "auto", number | "auto"]; |
| } |
|
|
| function MiniChart({ data, color, label, refLine, domain }: MiniChartProps) { |
| if (data.length < 2) { |
| return ( |
| <div style={{ display: "flex", alignItems: "center", justifyContent: "center", height: "100%", color: "rgba(255,255,255,0.3)", fontSize: "0.625rem", fontFamily: "IBM Plex Mono, monospace" }}> |
| Collecting data... |
| </div> |
| ); |
| } |
| return ( |
| <ResponsiveContainer width="100%" height="100%"> |
| <AreaChart data={data} margin={{ top: 4, right: 4, bottom: 0, left: 0 }}> |
| <defs> |
| <linearGradient id={`grad-${label}`} x1="0" y1="0" x2="0" y2="1"> |
| <stop offset="5%" stopColor={color} stopOpacity={0.3} /> |
| <stop offset="95%" stopColor={color} stopOpacity={0.02} /> |
| </linearGradient> |
| </defs> |
| <XAxis dataKey="step" hide /> |
| <YAxis domain={domain ?? ["auto", "auto"]} hide /> |
| <Tooltip content={<CustomTooltip />} /> |
| {refLine !== undefined && ( |
| <ReferenceLine y={refLine} stroke="rgba(255,255,255,0.15)" strokeDasharray="3 3" /> |
| )} |
| <Area |
| type="monotone" |
| dataKey="value" |
| stroke={color} |
| strokeWidth={1.5} |
| fill={`url(#grad-${label})`} |
| dot={false} |
| name={label} |
| isAnimationActive={false} |
| /> |
| </AreaChart> |
| </ResponsiveContainer> |
| ); |
| } |
|
|
| interface ChartCardProps { |
| title: string; |
| value: string; |
| subValue?: string; |
| color: string; |
| children: React.ReactNode; |
| } |
|
|
| function ChartCard({ title, value, subValue, color, children }: ChartCardProps) { |
| return ( |
| <Panel style={{ display: "flex", flexDirection: "column", minHeight: 0 }}> |
| <PanelHeader> |
| <PanelDot color={color} /> |
| <span>{title}</span> |
| <span style={{ marginLeft: "auto", fontFamily: "IBM Plex Mono, monospace", fontSize: "0.6875rem", color }}> |
| {value} |
| </span> |
| {subValue && ( |
| <span style={{ fontSize: "0.5625rem", color: "rgba(255,255,255,0.3)" }}>{subValue}</span> |
| )} |
| </PanelHeader> |
| <div style={{ flex: 1, padding: "0.5rem", minHeight: 0 }}> |
| {children} |
| </div> |
| </Panel> |
| ); |
| } |
|
|
| export default function TrainingCharts({ metrics }: TrainingChartsProps) { |
| const toSeries = (arr: number[]) => |
| arr.map((v, i) => ({ step: metrics.steps[i] ?? i + 1, value: v })); |
|
|
| const lastLoss = metrics.loss.at(-1); |
| const lastReward = metrics.reward.at(-1); |
| const lastWinRate = metrics.winRate.at(-1); |
| const lastProfit = metrics.avgProfit.at(-1); |
| const lastCoaching = metrics.coachingRate.at(-1); |
| const lastKl = metrics.kl.at(-1); |
|
|
| return ( |
| <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gridTemplateRows: "repeat(3, 1fr)", gap: "0.5rem", height: "100%" }}> |
| <ChartCard title="GRPO LOSS" value={lastLoss !== undefined ? lastLoss.toFixed(4) : "—"} color={HEX.loss}> |
| <MiniChart data={toSeries(metrics.loss)} color={HEX.loss} label="loss" domain={[0, 3]} /> |
| </ChartCard> |
| |
| <ChartCard |
| title="POLICY REWARD" |
| value={lastReward !== undefined ? lastReward.toFixed(4) : "—"} |
| color={lastReward !== undefined && lastReward >= 0 ? HEX.profit : HEX.black} |
| > |
| <MiniChart |
| data={toSeries(metrics.reward)} |
| color={lastReward !== undefined && lastReward >= 0 ? HEX.profit : HEX.black} |
| label="reward" refLine={0} domain={[-0.6, 0.6]} |
| /> |
| </ChartCard> |
| |
| <ChartCard title="WIN RATE" value={lastWinRate !== undefined ? `${(lastWinRate * 100).toFixed(1)}%` : "—"} color={HEX.white}> |
| <MiniChart data={toSeries(metrics.winRate)} color={HEX.white} label="win_rate" refLine={0.5} domain={[0.2, 0.8]} /> |
| </ChartCard> |
| |
| <ChartCard |
| title="AVG PROFIT" |
| value={lastProfit !== undefined ? `${lastProfit >= 0 ? "+" : ""}${lastProfit.toFixed(2)}` : "—"} |
| subValue="units/game" |
| color={lastProfit !== undefined && lastProfit >= 0 ? HEX.profit : HEX.black} |
| > |
| <MiniChart |
| data={toSeries(metrics.avgProfit)} |
| color={lastProfit !== undefined && lastProfit >= 0 ? HEX.profit : HEX.black} |
| label="profit" refLine={0} |
| /> |
| </ChartCard> |
| |
| <ChartCard title="COACHING RATE" value={lastCoaching !== undefined ? `${(lastCoaching * 100).toFixed(1)}%` : "—"} color={HEX.claude}> |
| <MiniChart data={toSeries(metrics.coachingRate)} color={HEX.claude} label="coaching" domain={[0, 0.5]} /> |
| </ChartCard> |
| |
| <ChartCard title="KL DIVERGENCE" value={lastKl !== undefined ? lastKl.toFixed(4) : "—"} color={HEX.kl}> |
| <MiniChart data={toSeries(metrics.kl)} color={HEX.kl} label="kl" refLine={0.1} domain={[0, 0.2]} /> |
| </ChartCard> |
| </div> |
| ); |
| } |
|
|