| |
| |
| |
| |
| import { useEffect, useRef } from "react"; |
| import type { GameEvent } from "@/lib/simulation"; |
|
|
| interface EventFeedProps { |
| events: GameEvent[]; |
| } |
|
|
| const EVENT_ICONS: Record<string, string> = { |
| game_start: "▶", |
| move: "→", |
| coaching_request: "◈", |
| coaching_response: "◉", |
| game_end: "■", |
| training_step: "⚡", |
| wallet_update: "$", |
| }; |
|
|
| function formatTime(ts: number): string { |
| const d = new Date(ts); |
| return `${d.getHours().toString().padStart(2, "0")}:${d.getMinutes().toString().padStart(2, "0")}:${d.getSeconds().toString().padStart(2, "0")}.${d.getMilliseconds().toString().padStart(3, "0").slice(0, 2)}`; |
| } |
|
|
| function getEventClass(event: GameEvent): string { |
| if (event.type === "game_start") return "event-item event-game-start"; |
| if (event.type === "game_end") return "event-item event-game-end"; |
| if (event.type === "training_step") return "event-item event-training"; |
| if (event.type === "coaching_request" || event.type === "coaching_response") return "event-item event-coaching"; |
| if (event.type === "move") { |
| return event.agent === "white" ? "event-item event-move-white" : "event-item event-move-black"; |
| } |
| return "event-item"; |
| } |
|
|
| function getAgentBadge(event: GameEvent) { |
| if (event.type === "coaching_request" || event.type === "coaching_response") { |
| return <span className="text-[9px] font-mono px-1 py-0.5 rounded-sm bg-agent-claude text-agent-claude border border-agent-claude">CLAUDE</span>; |
| } |
| if (event.agent === "white") { |
| return <span className="text-[9px] font-mono px-1 py-0.5 rounded-sm bg-agent-white text-agent-white border border-agent-white">WHITE</span>; |
| } |
| if (event.agent === "black") { |
| return <span className="text-[9px] font-mono px-1 py-0.5 rounded-sm bg-agent-black text-agent-black border border-agent-black">BLACK</span>; |
| } |
| if (event.type === "training_step") { |
| return <span className="text-[9px] font-mono px-1 py-0.5 rounded-sm" style={{background: "oklch(0.60 0.15 290 / 0.15)", color: "oklch(0.60 0.15 290)", border: "1px solid oklch(0.60 0.15 290 / 0.4)"}}>GRPO</span>; |
| } |
| if (event.type === "game_start" || event.type === "game_end") { |
| return <span className="text-[9px] font-mono px-1 py-0.5 rounded-sm" style={{background: "oklch(0.65 0.18 145 / 0.15)", color: "oklch(0.65 0.18 145)", border: "1px solid oklch(0.65 0.18 145 / 0.4)"}}>SYS</span>; |
| } |
| return null; |
| } |
|
|
| export default function EventFeed({ events }: EventFeedProps) { |
| const containerRef = useRef<HTMLDivElement>(null); |
|
|
| useEffect(() => { |
| if (containerRef.current) { |
| containerRef.current.scrollTop = 0; |
| } |
| }, [events.length]); |
|
|
| return ( |
| <div ref={containerRef} className="overflow-y-auto h-full" style={{ scrollBehavior: "smooth" }}> |
| {events.map((event) => ( |
| <div key={event.id} className={getEventClass(event)}> |
| <span className="font-mono text-[9px] text-muted-foreground shrink-0 mt-0.5 w-16"> |
| {formatTime(event.timestamp)} |
| </span> |
| <span className="text-muted-foreground shrink-0 w-3 text-center text-[10px]"> |
| {EVENT_ICONS[event.type] ?? "·"} |
| </span> |
| <div className="flex-1 min-w-0"> |
| <div className="flex items-center gap-1.5 flex-wrap"> |
| {getAgentBadge(event)} |
| <span className="text-[11px] text-foreground/80 truncate"> |
| {event.message} |
| </span> |
| </div> |
| {event.type === "training_step" && event.trainingLoss !== undefined && ( |
| <div className="flex gap-3 mt-0.5"> |
| <span className="text-[9px] font-mono text-muted-foreground"> |
| loss: <span className="text-destructive">{event.trainingLoss.toFixed(4)}</span> |
| </span> |
| <span className="text-[9px] font-mono text-muted-foreground"> |
| reward: <span style={{color: (event.combinedReward ?? 0) >= 0 ? "oklch(0.65 0.18 145)" : "oklch(0.60 0.20 25)"}}> |
| {(event.combinedReward ?? 0).toFixed(4)} |
| </span> |
| </span> |
| </div> |
| )} |
| {event.type === "coaching_request" && ( |
| <div className="flex gap-2 mt-0.5"> |
| <span className="text-[9px] font-mono text-muted-foreground"> |
| complexity: <span className="text-agent-claude">{event.complexityLabel}</span> |
| <span className="ml-1 opacity-60">({event.complexity?.toFixed(2)})</span> |
| </span> |
| </div> |
| )} |
| {event.type === "game_end" && event.result && ( |
| <div className="flex gap-2 mt-0.5"> |
| <span className="text-[9px] font-mono"> |
| result: <span className={event.result === "1-0" ? "text-agent-white" : event.result === "0-1" ? "text-agent-black" : "text-muted-foreground"}>{event.result}</span> |
| </span> |
| <span className="text-[9px] font-mono text-muted-foreground"> |
| R: <span style={{color: (event.combinedReward ?? 0) >= 0 ? "oklch(0.65 0.18 145)" : "oklch(0.60 0.20 25)"}}> |
| {(event.combinedReward ?? 0).toFixed(3)} |
| </span> |
| </span> |
| </div> |
| )} |
| </div> |
| </div> |
| ))} |
| {events.length === 0 && ( |
| <div className="flex items-center justify-center h-full text-muted-foreground text-xs font-mono"> |
| Waiting for events... |
| </div> |
| )} |
| </div> |
| ); |
| } |
|
|