chessecon / frontend /src /components /EventFeed.tsx
suvasis's picture
code add
e4d7d50
/**
* EventFeed — live scrolling event log
* Design: Bloomberg terminal event stream with color-coded agent events
*/
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>
);
}