chessecon / frontend /src /pages /Home.tsx
suvasis's picture
code add
e4d7d50
/**
* ChessEcon Live Dashboard — Main Page
* Design: Quantitative Finance Dark (Bloomberg-inspired trading terminal)
*
* Data source: toggles between browser simulation and real Python backend WebSocket.
* When backend is available, all events are real agent moves, Claude coaching calls,
* and GRPO training steps from the Python process.
*/
import { useEffect, useState, useRef, useCallback } from "react";
import { sim, type GameState, type GameEvent, type TrainingMetrics, type EconomicDataPoint, type EventType } from "@/lib/simulation";
import { useBackendWS, type WSMessage } from "@/lib/useBackendWS";
import ChessBoard from "@/components/ChessBoard";
import EventFeed from "@/components/EventFeed";
import TrainingCharts from "@/components/TrainingCharts";
import WalletChart from "@/components/WalletChart";
import EconomicPerformance from "@/components/EconomicPerformance";
import { Panel, PanelHeader, PanelDot } from "@/components/Panel";
import ArchitectureDiagram from "@/components/ArchitectureDiagram";
const HERO_BG = "https://d2xsxph8kpxj0f.cloudfront.net/92838043/eBShsyyL7vn2AEWucukL8e/chessecon-hero-bg-oC98shsc44Ruy4yPBMWoLr.webp";
// ── KPI Card ──────────────────────────────────────────────────────────────
interface KpiCardProps {
label: string;
value: string;
sub?: string;
variant: "white" | "black" | "claude" | "green";
}
const KPI_COLORS = {
white: { accent: "#2D9CDB", bg: "rgba(45,156,219,0.08)", border: "rgba(45,156,219,0.25)" },
black: { accent: "#E05C5C", bg: "rgba(224,92,92,0.08)", border: "rgba(224,92,92,0.25)" },
claude: { accent: "#F5A623", bg: "rgba(245,166,35,0.08)", border: "rgba(245,166,35,0.25)" },
green: { accent: "#27AE60", bg: "rgba(39,174,96,0.08)", border: "rgba(39,174,96,0.25)" },
};
function KpiCard({ label, value, sub, variant }: KpiCardProps) {
const c = KPI_COLORS[variant];
return (
<div style={{ background: c.bg, border: `1px solid ${c.border}`, borderRadius: "0.25rem", padding: "0.5rem 0.75rem", display: "flex", flexDirection: "column", gap: "0.125rem", position: "relative", overflow: "hidden" }}>
<div style={{ position: "absolute", top: 0, left: 0, right: 0, height: "2px", background: c.accent }} />
<span style={{ fontFamily: "IBM Plex Mono, monospace", fontSize: "0.5625rem", textTransform: "uppercase" as const, letterSpacing: "0.1em", color: "rgba(255,255,255,0.35)" }}>
{label}
</span>
<span style={{ fontFamily: "IBM Plex Mono, monospace", fontSize: "1.125rem", fontWeight: 600, lineHeight: 1.2, color: c.accent, fontVariantNumeric: "tabular-nums" }}>
{value}
</span>
{sub && <span style={{ fontFamily: "IBM Plex Mono, monospace", fontSize: "0.5625rem", color: "rgba(255,255,255,0.3)" }}>{sub}</span>}
</div>
);
}
// ── Move History ──────────────────────────────────────────────────────────
function MoveHistory({ moves }: { moves: string[] }) {
const pairs: [string, string | undefined][] = [];
for (let i = 0; i < moves.length; i += 2) {
pairs.push([moves[i], moves[i + 1]]);
}
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (ref.current) ref.current.scrollTop = ref.current.scrollHeight;
}, [moves.length]);
return (
<div ref={ref} style={{ overflowY: "auto", height: "100%" }}>
<table style={{ width: "100%", fontSize: "0.625rem", fontFamily: "IBM Plex Mono, monospace", borderCollapse: "collapse" }}>
<tbody>
{pairs.map(([w, b], i) => (
<tr key={i} style={{ transition: "background 0.15s" }}>
<td style={{ padding: "0.125rem 0.25rem", color: "rgba(255,255,255,0.3)", width: "1.5rem", textAlign: "right" }}>{i + 1}.</td>
<td style={{ padding: "0.125rem 0.5rem", color: "#2D9CDB" }}>{w}</td>
<td style={{ padding: "0.125rem 0.5rem", color: "#E05C5C" }}>{b ?? ""}</td>
</tr>
))}
</tbody>
</table>
{moves.length === 0 && (
<div style={{ textAlign: "center", color: "rgba(255,255,255,0.3)", fontSize: "0.625rem", padding: "1rem", fontFamily: "IBM Plex Mono, monospace" }}>
No moves yet
</div>
)}
</div>
);
}
// ── Agent Status Card ─────────────────────────────────────────────────────
interface AgentStatusProps {
color: "white" | "black";
wallet: number;
coachingCalls: number;
isActive: boolean;
}
function AgentStatus({ color, wallet, coachingCalls, isActive }: AgentStatusProps) {
const isWhite = color === "white";
const accent = isWhite ? "#2D9CDB" : "#E05C5C";
const label = isWhite ? "White Agent" : "Black Agent";
const model = isWhite ? "Qwen2.5-7B" : "Llama-3.2-3B";
const piece = isWhite ? "♔" : "♚";
return (
<div style={{
borderRadius: "0.25rem",
padding: "0.625rem",
display: "flex",
flexDirection: "column",
gap: "0.5rem",
background: `${accent}0d`,
border: `1px solid ${accent}40`,
boxShadow: isActive ? `0 0 12px ${accent}30` : "none",
transition: "box-shadow 0.3s",
}}>
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
<span style={{ fontSize: "1.25rem", lineHeight: 1 }}>{piece}</span>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: "flex", alignItems: "center", gap: "0.375rem" }}>
<span style={{ fontSize: "0.75rem", fontWeight: 600, color: accent }}>{label}</span>
{isActive && (
<span style={{ fontSize: "0.5rem", fontFamily: "IBM Plex Mono, monospace", padding: "0.125rem 0.25rem", borderRadius: "0.125rem", background: `${accent}20`, color: accent, animation: "pulse-live 1.5s ease-in-out infinite" }}>
THINKING
</span>
)}
</div>
<div style={{ fontSize: "0.5625rem", fontFamily: "IBM Plex Mono, monospace", color: "rgba(255,255,255,0.35)" }}>{model}</div>
</div>
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.375rem" }}>
<div style={{ borderRadius: "0.125rem", padding: "0.375rem", background: "rgba(255,255,255,0.04)" }}>
<div style={{ fontSize: "0.5rem", fontFamily: "IBM Plex Mono, monospace", color: "rgba(255,255,255,0.3)", textTransform: "uppercase" as const, letterSpacing: "0.08em" }}>Wallet</div>
<div style={{ fontSize: "0.875rem", fontFamily: "IBM Plex Mono, monospace", fontWeight: 600, color: accent, fontVariantNumeric: "tabular-nums" }}>{wallet.toFixed(1)}</div>
</div>
<div style={{ borderRadius: "0.125rem", padding: "0.375rem", background: "rgba(255,255,255,0.04)" }}>
<div style={{ fontSize: "0.5rem", fontFamily: "IBM Plex Mono, monospace", color: "rgba(255,255,255,0.3)", textTransform: "uppercase" as const, letterSpacing: "0.08em" }}>Claude calls</div>
<div style={{ fontSize: "0.875rem", fontFamily: "IBM Plex Mono, monospace", fontWeight: 600, color: "#F5A623", fontVariantNumeric: "tabular-nums" }}>{coachingCalls}</div>
</div>
</div>
</div>
);
}
// ── Data Source Badge ─────────────────────────────────────────────────────
function DataSourceBadge({ isBackend, isConnected }: { isBackend: boolean; isConnected: boolean }) {
const color = isBackend ? (isConnected ? "#27AE60" : "#F5A623") : "#2D9CDB";
const label = isBackend
? (isConnected ? "LIVE BACKEND" : "CONNECTING...")
: "SIMULATION";
return (
<div style={{
display: "flex", alignItems: "center", gap: "0.375rem",
padding: "0.25rem 0.5rem", borderRadius: "0.125rem",
background: `${color}15`, border: `1px solid ${color}40`,
}}>
<div style={{ width: "0.375rem", height: "0.375rem", borderRadius: "50%", background: color,
animation: isConnected ? "pulse-live 1.5s ease-in-out infinite" : "none" }} />
<span style={{ fontSize: "0.5625rem", fontFamily: "IBM Plex Mono, monospace", color, letterSpacing: "0.08em" }}>
{label}
</span>
</div>
);
}
// ── Main Dashboard ────────────────────────────────────────────────────────
export default function Home() {
const [gameState, setGameState] = useState<GameState>(sim.state);
const [events, setEvents] = useState<GameEvent[]>([]);
const [metrics, setMetrics] = useState<TrainingMetrics>(sim.metrics);
const [walletHistory, setWalletHistory] = useState<{ game: number; white: number; black: number }[]>([]);
const [economicData, setEconomicData] = useState<EconomicDataPoint[]>([]);
const cumulativePnlRef = useRef(0);
const [isRunning, setIsRunning] = useState(false);
const [gameCount, setGameCount] = useState(0);
const [trainingStep, setTrainingStep] = useState(0);
// Backend WebSocket mode
const [useBackend, setUseBackend] = useState(false);
const [backendConnected, setBackendConnected] = useState(false);
const [showArch, setShowArch] = useState(false);
// Determine WebSocket URL: same host as the page, /ws endpoint
const wsUrl = typeof window !== "undefined"
? `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}/ws`
: "ws://localhost:8000/ws";
// ── Backend WebSocket handler ─────────────────────────────────────────
const handleWsMessage = useCallback((msg: WSMessage) => {
const d = msg.data;
if (msg.type === "move") {
setGameState(prev => ({
...prev,
fen: (d.fen as string) ?? prev.fen,
moves: [...prev.moves, (d.move as string) ?? ""],
turn: (d.turn as "white" | "black") ?? prev.turn,
moveNumber: (d.move_number as number) ?? prev.moveNumber,
walletWhite: (d.wallet_white as number) ?? prev.walletWhite,
walletBlack: (d.wallet_black as number) ?? prev.walletBlack,
}));
const ev: GameEvent = {
id: String(Date.now()),
type: "move",
agent: (d.player as "white" | "black") ?? "white",
move: (d.move as string) ?? "",
timestamp: Date.now(),
};
setEvents(prev => [ev, ...prev].slice(0, 200));
}
if (msg.type === "game_start") {
setGameState(prev => ({
...prev,
fen: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
moves: [],
moveNumber: 0,
isOver: false,
result: undefined,
gameId: (d.game_id as number) ?? prev.gameId,
walletWhite: (d.wallet_white as number) ?? prev.walletWhite,
walletBlack: (d.wallet_black as number) ?? prev.walletBlack,
coachingCallsWhite: 0,
coachingCallsBlack: 0,
}));
const ev: GameEvent = {
id: String(Date.now()),
type: "game_start",
agent: "white",
timestamp: Date.now(),
};
setEvents(prev => [ev, ...prev].slice(0, 200));
setGameCount(c => c + 1);
}
if (msg.type === "game_end") {
const result = (d.result as "1-0" | "0-1" | "1/2-1/2") ?? "1/2-1/2";
setGameState(prev => ({
...prev,
isOver: true,
result: result as "1-0" | "0-1" | "1/2-1/2",
walletWhite: (d.wallet_white as number) ?? prev.walletWhite,
walletBlack: (d.wallet_black as number) ?? prev.walletBlack,
}));
// Economic data point
const prizeIncome = (d.prize_income as number) ?? 0;
const coachingSpend = (d.coaching_cost as number) ?? 0;
const entryFee = (d.entry_fee as number) ?? 10;
const netPnl = prizeIncome - entryFee - coachingSpend;
cumulativePnlRef.current += netPnl;
const point: EconomicDataPoint = {
game: gameCount,
prizeIncome,
coachingSpend: -coachingSpend,
entryFee: -entryFee,
netPnl,
cumulativePnl: cumulativePnlRef.current,
whiteWallet: (d.wallet_white as number) ?? 0,
blackWallet: (d.wallet_black as number) ?? 0,
};
setEconomicData(prev => [...prev, point].slice(-80));
setWalletHistory(prev => [
...prev,
{ game: gameCount, white: (d.wallet_white as number) ?? 0, black: (d.wallet_black as number) ?? 0 },
].slice(-60));
const ev: GameEvent = {
id: String(Date.now()),
type: "game_end",
agent: "white",
result,
reward: (d.reward as number) ?? 0,
walletWhite: (d.wallet_white as number) ?? 0,
walletBlack: (d.wallet_black as number) ?? 0,
timestamp: Date.now(),
};
setEvents(prev => [ev, ...prev].slice(0, 200));
}
if (msg.type === "coaching_request" || msg.type === "coaching_result") {
const ev: GameEvent = {
id: String(Date.now()),
type: msg.type as EventType,
agent: (d.player as "white" | "black") ?? "white",
coachingFee: (d.fee as number) ?? 5,
timestamp: Date.now(),
};
setEvents(prev => [ev, ...prev].slice(0, 200));
if (msg.type === "coaching_request") {
const isWhite = d.player === "white";
setGameState(prev => ({
...prev,
coachingCallsWhite: isWhite ? prev.coachingCallsWhite + 1 : prev.coachingCallsWhite,
coachingCallsBlack: !isWhite ? prev.coachingCallsBlack + 1 : prev.coachingCallsBlack,
}));
}
}
if (msg.type === "training_step") {
const step = (d.step as number) ?? 0;
setTrainingStep(step);
setMetrics(prev => ({
step,
loss: [...prev.loss, (d.loss as number) ?? 0].slice(-100),
reward: [...prev.reward, (d.reward as number) ?? 0].slice(-100),
kl: [...prev.kl, (d.kl_div as number) ?? 0].slice(-100),
winRate: [...prev.winRate, (d.win_rate as number) ?? 0].slice(-100),
avgProfit: [...(prev.avgProfit ?? []), (d.avg_profit as number) ?? 0].slice(-100),
coachingRate: [...prev.coachingRate, (d.coaching_rate as number) ?? 0].slice(-100),
steps: [...(prev.steps ?? []), step].slice(-100),
}));
const ev: GameEvent = {
id: String(Date.now()),
type: "training_step",
agent: "white",
trainingStep: step,
timestamp: Date.now(),
};
setEvents(prev => [ev, ...prev].slice(0, 200));
}
}, [gameCount]);
// ── Backend WS connection ─────────────────────────────────────────────
const { send: sendWs } = useBackendWS({
url: wsUrl,
onMessage: handleWsMessage,
onOpen: () => setBackendConnected(true),
onClose: () => setBackendConnected(false),
enabled: useBackend,
});
// ── Simulation mode ───────────────────────────────────────────────────
useEffect(() => {
if (useBackend) return; // Skip simulation when using real backend
const unsubState = sim.onState((s) => {
setGameState({ ...s });
setGameCount(sim.gameCount);
if (s.isOver) {
setWalletHistory((prev) => [
...prev,
{ game: sim.gameCount, white: s.walletWhite, black: s.walletBlack },
].slice(-60));
}
});
const unsubEvents = sim.on((e) => {
setEvents([...sim.events]);
if (e.type === "training_step") setTrainingStep(e.trainingStep ?? 0);
if (e.type === "game_end" && e.walletWhite !== undefined && e.walletBlack !== undefined) {
const prizeIncome = e.reward === 1 ? 18 : e.reward === 0 ? 5 : 0;
const coachingSpend = -((e.walletWhite ?? 0) + (e.walletBlack ?? 0) > 0 ? Math.random() * 8 : 0);
const entryFee = -10;
const netPnl = prizeIncome + coachingSpend + entryFee;
cumulativePnlRef.current += netPnl;
const point: EconomicDataPoint = {
game: sim.gameCount,
prizeIncome,
coachingSpend: parseFloat(coachingSpend.toFixed(2)),
entryFee,
netPnl: parseFloat(netPnl.toFixed(2)),
cumulativePnl: parseFloat(cumulativePnlRef.current.toFixed(2)),
whiteWallet: e.walletWhite ?? 0,
blackWallet: e.walletBlack ?? 0,
};
setEconomicData((prev) => [...prev, point].slice(-80));
}
});
const unsubMetrics = sim.onMetrics((m) => setMetrics({ ...m }));
return () => { unsubState(); unsubEvents(); unsubMetrics(); };
}, [useBackend]);
const handleToggle = useCallback(() => {
if (useBackend) {
// In backend mode, send start/stop command to backend
sendWs(isRunning ? "stop_game" : "start_game");
setIsRunning(r => !r);
} else {
if (isRunning) { sim.stop(); setIsRunning(false); }
else { sim.start(); setIsRunning(true); }
}
}, [isRunning, useBackend, sendWs]);
const handleReset = useCallback(() => {
sim.stop();
setIsRunning(false);
setEvents([]);
setWalletHistory([]);
setTrainingStep(0);
setGameCount(0);
cumulativePnlRef.current = 0;
setEconomicData([]);
}, []);
const handleToggleSource = useCallback(() => {
if (isRunning) {
sim.stop();
setIsRunning(false);
}
setUseBackend(b => !b);
setEvents([]);
setWalletHistory([]);
setEconomicData([]);
setTrainingStep(0);
setGameCount(0);
cumulativePnlRef.current = 0;
}, [isRunning]);
const lastEvent = events[0];
const isCoachingActive = lastEvent?.type === "coaching_request" || lastEvent?.type === "coaching_response";
return (
<div style={{ minHeight: "100vh", display: "flex", flexDirection: "column", background: "oklch(0.09 0.018 240)", fontFamily: "'Space Grotesk', sans-serif" }}>
{/* ── Header ── */}
<header style={{ position: "relative", borderBottom: "1px solid rgba(255,255,255,0.08)", overflow: "hidden", background: "oklch(0.10 0.018 240)" }}>
<div style={{ position: "absolute", inset: 0, opacity: 0.2, backgroundImage: `url(${HERO_BG})`, backgroundSize: "cover", backgroundPosition: "center" }} />
<div style={{ position: "relative", zIndex: 10, display: "flex", alignItems: "center", justifyContent: "space-between", padding: "0.625rem 1rem" }}>
{/* Logo */}
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem" }}>
<span style={{ fontSize: "1.5rem" }}></span>
<div>
<div style={{ fontSize: "1rem", fontWeight: 700, letterSpacing: "-0.02em" }}>
Chess<span style={{ color: "#F5A623" }}>Econ</span>
</div>
<div style={{ fontSize: "0.5625rem", fontFamily: "IBM Plex Mono, monospace", color: "rgba(255,255,255,0.35)", letterSpacing: "0.12em", textTransform: "uppercase" as const }}>
Multi-Agent RL Training Dashboard
</div>
</div>
</div>
{/* Center KPIs */}
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem" }}>
{[
{ label: "Games", value: String(gameCount), color: "rgba(255,255,255,0.85)" },
{ label: "GRPO Step", value: String(trainingStep), color: "#a78bfa" },
{ label: "Move", value: String(gameState.moveNumber), color: "rgba(255,255,255,0.85)" },
].map((item, i) => (
<div key={i} style={{ display: "flex", alignItems: "center", gap: "0.75rem" }}>
{i > 0 && <div style={{ width: "1px", height: "1.5rem", background: "rgba(255,255,255,0.1)" }} />}
<div style={{ textAlign: "center" }}>
<div style={{ fontSize: "0.5rem", fontFamily: "IBM Plex Mono, monospace", color: "rgba(255,255,255,0.3)", textTransform: "uppercase" as const, letterSpacing: "0.1em" }}>{item.label}</div>
<div style={{ fontSize: "0.875rem", fontFamily: "IBM Plex Mono, monospace", fontWeight: 700, color: item.color, fontVariantNumeric: "tabular-nums" }}>{item.value}</div>
</div>
</div>
))}
{isCoachingActive && (
<div style={{ display: "flex", alignItems: "center", gap: "0.375rem", padding: "0.25rem 0.5rem", borderRadius: "0.125rem", background: "rgba(245,166,35,0.15)", border: "1px solid rgba(245,166,35,0.4)", animation: "pulse-live 1.5s ease-in-out infinite" }}>
<span style={{ fontSize: "0.625rem", color: "#F5A623" }}></span>
<span style={{ fontSize: "0.5625rem", fontFamily: "IBM Plex Mono, monospace", color: "#F5A623" }}>Claude Active</span>
</div>
)}
</div>
{/* Controls */}
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
{/* Architecture view toggle */}
<button
onClick={() => setShowArch(a => !a)}
style={{
padding: "0.25rem 0.5rem",
borderRadius: "0.125rem",
fontSize: "0.5625rem",
fontFamily: "IBM Plex Mono, monospace",
cursor: "pointer",
border: `1px solid ${showArch ? "rgba(167,139,250,0.5)" : "rgba(255,255,255,0.15)"}`,
background: showArch ? "rgba(167,139,250,0.15)" : "rgba(255,255,255,0.06)",
color: showArch ? "#a78bfa" : "rgba(255,255,255,0.5)",
letterSpacing: "0.08em",
}}
>
⬡ ARCH
</button>
{/* Data source toggle */}
<DataSourceBadge isBackend={useBackend} isConnected={backendConnected} />
<button
onClick={handleToggleSource}
title={useBackend ? "Switch to simulation mode" : "Switch to live backend mode"}
style={{
padding: "0.25rem 0.5rem",
borderRadius: "0.125rem",
fontSize: "0.5625rem",
fontFamily: "IBM Plex Mono, monospace",
cursor: "pointer",
border: "1px solid rgba(255,255,255,0.15)",
background: "rgba(255,255,255,0.06)",
color: "rgba(255,255,255,0.5)",
}}
>
{useBackend ? "⇄ SIM" : "⇄ LIVE"}
</button>
<div style={{ width: "1px", height: "1.5rem", background: "rgba(255,255,255,0.1)" }} />
<div style={{ display: "flex", alignItems: "center", gap: "0.375rem" }}>
<div className="live-dot" style={{ opacity: isRunning ? 1 : 0.3 }} />
<span style={{ fontSize: "0.5625rem", fontFamily: "IBM Plex Mono, monospace", color: "rgba(255,255,255,0.35)" }}>{isRunning ? "LIVE" : "PAUSED"}</span>
</div>
<button
onClick={handleToggle}
style={{
padding: "0.375rem 0.75rem",
borderRadius: "0.125rem",
fontSize: "0.6875rem",
fontFamily: "IBM Plex Mono, monospace",
fontWeight: 500,
cursor: "pointer",
border: `1px solid ${isRunning ? "rgba(224,92,92,0.4)" : "rgba(45,156,219,0.4)"}`,
background: isRunning ? "rgba(224,92,92,0.15)" : "rgba(45,156,219,0.15)",
color: isRunning ? "#E05C5C" : "#2D9CDB",
transition: "all 0.2s",
}}
>
{isRunning ? "⏸ Pause" : "▶ Start"}
</button>
<button
onClick={handleReset}
style={{
padding: "0.375rem 0.75rem",
borderRadius: "0.125rem",
fontSize: "0.6875rem",
fontFamily: "IBM Plex Mono, monospace",
cursor: "pointer",
border: "1px solid rgba(255,255,255,0.12)",
background: "rgba(255,255,255,0.05)",
color: "rgba(255,255,255,0.4)",
}}
>
↺ Reset
</button>
</div>
</div>
</header>
{/* ── Main Content ── */}
<main style={{ flex: 1, display: "flex", flexDirection: "column", gap: "0.5rem", padding: "0.5rem", overflow: "hidden", minHeight: 0 }}>
{showArch ? (
<div style={{ flex: 1, overflow: "auto" }}>
<ArchitectureDiagram />
</div>
) : (<>
{/* KPI strip */}
<div style={{ display: "grid", gridTemplateColumns: "repeat(8, 1fr)", gap: "0.5rem", flexShrink: 0 }}>
<KpiCard label="White Wallet" value={gameState.walletWhite.toFixed(1)} sub="units" variant="white" />
<KpiCard label="Black Wallet" value={gameState.walletBlack.toFixed(1)} sub="units" variant="black" />
<KpiCard label="W Coaching" value={String(gameState.coachingCallsWhite)} sub="calls" variant="claude" />
<KpiCard label="B Coaching" value={String(gameState.coachingCallsBlack)} sub="calls" variant="claude" />
<KpiCard
label="Last Reward"
value={metrics.reward.at(-1) !== undefined ? `${metrics.reward.at(-1)! >= 0 ? "+" : ""}${metrics.reward.at(-1)!.toFixed(3)}` : "—"}
variant={metrics.reward.at(-1) !== undefined && metrics.reward.at(-1)! >= 0 ? "green" : "black"}
/>
<KpiCard label="Win Rate" value={metrics.winRate.at(-1) !== undefined ? `${(metrics.winRate.at(-1)! * 100).toFixed(1)}%` : "—"} variant="white" />
<KpiCard label="GRPO Loss" value={metrics.loss.at(-1) !== undefined ? metrics.loss.at(-1)!.toFixed(4) : "—"} variant="black" />
<KpiCard label="KL Div" value={metrics.kl.at(-1) !== undefined ? metrics.kl.at(-1)!.toFixed(4) : "—"} variant="claude" />
</div>
{/* Main 3-column layout */}
<div style={{ flex: 1, display: "flex", flexDirection: "column", gap: "0.5rem", overflow: "hidden", minHeight: 0 }}>
<div style={{ flex: 1, display: "grid", gridTemplateColumns: "minmax(0,2fr) minmax(0,1.5fr) minmax(0,2.5fr)", gap: "0.5rem", overflow: "hidden", minHeight: 0 }}>
{/* LEFT: Agent status + chess board
FIX: overflow:hidden on column prevents growth; agent cards have fixed height;
board panel uses position:relative + absolute fill so it never shifts */}
<div style={{ display: "flex", flexDirection: "column", gap: "0.5rem", minHeight: 0, overflow: "hidden" }}>
{/* Fixed height agent cards — never push board down */}
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.5rem", flexShrink: 0, height: "88px", overflow: "hidden" }}>
<AgentStatus color="white" wallet={gameState.walletWhite} coachingCalls={gameState.coachingCallsWhite} isActive={gameState.turn === "white" && !gameState.isOver} />
<AgentStatus color="black" wallet={gameState.walletBlack} coachingCalls={gameState.coachingCallsBlack} isActive={gameState.turn === "black" && !gameState.isOver} />
</div>
{/* Board panel: position:relative so absolute-fill child works correctly */}
<Panel style={{ flex: 1, display: "flex", flexDirection: "column", minHeight: 0, overflow: "hidden", position: "relative" }}>
<PanelHeader>
<PanelDot color="#2D9CDB" />
<span>LIVE BOARD</span>
<span style={{ marginLeft: "auto", fontSize: "0.625rem", color: "rgba(255,255,255,0.5)" }}>
Game #{gameState.gameId} · Move {gameState.moveNumber}
</span>
{gameState.isOver && gameState.result && (
<span style={{
fontFamily: "IBM Plex Mono, monospace",
fontSize: "0.5625rem",
padding: "0.125rem 0.375rem",
borderRadius: "0.125rem",
marginLeft: "0.25rem",
background: gameState.result === "1-0" ? "rgba(45,156,219,0.2)" : gameState.result === "0-1" ? "rgba(224,92,92,0.2)" : "rgba(255,255,255,0.1)",
color: gameState.result === "1-0" ? "#2D9CDB" : gameState.result === "0-1" ? "#E05C5C" : "rgba(255,255,255,0.5)",
}}>
{gameState.result}
</span>
)}
</PanelHeader>
{/* Absolute fill: board always occupies the full panel interior regardless of surrounding content */}
<div style={{ position: "absolute", inset: "1.75rem 0 0 0", padding: "0.5rem", display: "flex", alignItems: "center", justifyContent: "center", overflow: "hidden" }}>
<div style={{ width: "100%", maxWidth: "280px", aspectRatio: "1" }}>
<ChessBoard state={gameState} />
</div>
</div>
</Panel>
</div>
{/* CENTER: Move history + wallet chart */}
<div style={{ display: "flex", flexDirection: "column", gap: "0.5rem", minHeight: 0 }}>
<Panel style={{ flex: "0 0 45%", display: "flex", flexDirection: "column" }}>
<PanelHeader>
<PanelDot color="#F5A623" />
<span>MOVE HISTORY</span>
<span style={{ marginLeft: "auto", fontSize: "0.625rem", color: "rgba(255,255,255,0.5)" }}>{gameState.moves.length} moves</span>
</PanelHeader>
<div style={{ flex: 1, overflow: "hidden", padding: "0.25rem" }}>
<MoveHistory moves={gameState.moves} />
</div>
</Panel>
<Panel style={{ flex: 1, display: "flex", flexDirection: "column", minHeight: 0 }}>
<PanelHeader>
<PanelDot color="#27AE60" />
<span>WALLET HISTORY</span>
<span style={{ marginLeft: "auto", fontSize: "0.625rem", color: "rgba(255,255,255,0.5)" }}>{walletHistory.length} games</span>
</PanelHeader>
<div style={{ flex: 1, padding: "0.5rem", minHeight: 0 }}>
<WalletChart history={walletHistory} />
</div>
</Panel>
</div>
{/* RIGHT: Training charts + event feed */}
<div style={{ display: "flex", flexDirection: "column", gap: "0.5rem", minHeight: 0 }}>
<Panel style={{ flex: "0 0 55%", display: "flex", flexDirection: "column" }}>
<PanelHeader>
<PanelDot color="#a78bfa" />
<span>GRPO TRAINING METRICS</span>
<span style={{ marginLeft: "auto", fontSize: "0.625rem", color: "rgba(255,255,255,0.5)" }}>step {trainingStep}</span>
</PanelHeader>
<div style={{ flex: 1, padding: "0.5rem", minHeight: 0 }}>
<TrainingCharts metrics={metrics} />
</div>
</Panel>
<Panel style={{ flex: 1, display: "flex", flexDirection: "column", minHeight: 0 }}>
<PanelHeader>
<span style={{ width: "0.375rem", height: "0.375rem", borderRadius: "50%", background: "#27AE60", flexShrink: 0, display: "inline-block" }} />
<span>LIVE EVENT FEED</span>
<span style={{ marginLeft: "auto", fontSize: "0.625rem", color: "rgba(255,255,255,0.5)" }}>{events.length} events</span>
{isRunning && <div className="live-dot" style={{ marginLeft: "0.25rem" }} />}
</PanelHeader>
<div style={{ flex: 1, overflow: "hidden" }}>
<EventFeed events={events} />
</div>
</Panel>
</div>
</div>
{/* BOTTOM ROW: Economic Performance Over Time */}
<Panel style={{ flexShrink: 0, height: "180px", display: "flex", flexDirection: "column" }}>
<PanelHeader>
<PanelDot color="#27AE60" />
<span>ECONOMIC PERFORMANCE OVER TIME</span>
<span style={{ marginLeft: "0.5rem", fontSize: "0.5625rem", color: "rgba(255,255,255,0.3)" }}>
prize income · coaching cost · entry fee · net P&L · cumulative P&L
</span>
<span style={{ marginLeft: "auto", fontSize: "0.625rem", color: "rgba(255,255,255,0.5)" }}>
{economicData.length} games
</span>
{economicData.length > 0 && (
<span style={{
marginLeft: "0.5rem",
fontFamily: "IBM Plex Mono, monospace",
fontSize: "0.6875rem",
fontWeight: 600,
color: (economicData.at(-1)?.cumulativePnl ?? 0) >= 0 ? "#27AE60" : "#E05C5C",
}}>
Cumulative: {(economicData.at(-1)?.cumulativePnl ?? 0) >= 0 ? "+" : ""}{economicData.at(-1)?.cumulativePnl.toFixed(2) ?? "0.00"}
</span>
)}
</PanelHeader>
<div style={{ flex: 1, padding: "0.375rem 0.5rem 0.25rem", minHeight: 0 }}>
<EconomicPerformance data={economicData} />
</div>
</Panel>
</div>
)}
</main>
{/* ── Footer ── */}
<footer style={{ borderTop: "1px solid rgba(255,255,255,0.06)", padding: "0.375rem 1rem", display: "flex", alignItems: "center", justifyContent: "space-between", flexShrink: 0 }}>
<span style={{ fontFamily: "IBM Plex Mono, monospace", fontSize: "0.5625rem", color: "rgba(255,255,255,0.25)" }}>
ChessEcon · TextArena + Meta OpenEnv + GRPO · Hackathon 2026
</span>
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem" }}>
<span style={{ fontFamily: "IBM Plex Mono, monospace", fontSize: "0.5625rem", color: "rgba(255,255,255,0.25)" }}>
Trainable: <span style={{ color: "#2D9CDB" }}>Qwen/Llama</span>
</span>
<span style={{ fontFamily: "IBM Plex Mono, monospace", fontSize: "0.5625rem", color: "rgba(255,255,255,0.25)" }}>
Coach: <span style={{ color: "#F5A623" }}>Claude claude-opus-4-5</span>
</span>
<span style={{ fontFamily: "IBM Plex Mono, monospace", fontSize: "0.5625rem", color: "rgba(255,255,255,0.25)" }}>
Reward: <span style={{ color: "#27AE60" }}>0.4×game + 0.6×profit</span>
</span>
<span style={{ fontFamily: "IBM Plex Mono, monospace", fontSize: "0.5625rem", color: "rgba(255,255,255,0.25)" }}>
Source: <span style={{ color: useBackend ? "#27AE60" : "#2D9CDB" }}>{useBackend ? "LIVE BACKEND" : "SIMULATION"}</span>
</span>
</div>
</footer>
</div>
);
}