Branch: feat/frontend
Make this demo-able. Judges won't read code — they'll watch the dashboard. Make the agentic loop visible: questions stream in, builders complete, games play out on real boards, the Elo curve climbs.
Files owned:
frontend/
├── package.json # done
├── vite.config.ts # done — proxies /api and /ws to backend
├── tsconfig.json # done
├── tailwind.config.js # done
├── postcss.config.js # done
├── index.html # done
└── src/
├── main.tsx # done — imports index.css
├── index.css # done — tailwind base + dark body
├── App.tsx # IMPLEMENT — top-level layout
├── api/
│ ├── events.ts # FROZEN — do not edit
│ └── client.ts # IMPLEMENT — extend the stub
├── components/
│ ├── LiveBoard.tsx # CREATE
│ ├── EloChart.tsx # CREATE
│ ├── Bracket.tsx # CREATE
│ ├── StrategistFeed.tsx # CREATE
│ └── GenerationTimeline.tsx # CREATE
└── hooks/
├── useEventStream.ts # CREATE
└── mockEvents.ts # CREATE — for offline dev
Read first:
docs/proposal.pdf§11 — the demo plan, your North Star.plans/README.md— merge order.frontend/src/api/events.ts— the frozen event types you consume. Do not edit this file without team sync.backend/darwin/api/websocket.py— the Python source of truth that mirrorsevents.ts.
Already done for you:
- Vite + React + TypeScript skeleton with HMR working.
- Tailwind preconfigured with dark mode and a dark body background.
vite.config.tsproxies/api(REST) and/ws(WebSocket) to127.0.0.1:8000.events.tshas all WS event types — import from here, don't redefine.client.tshasconnectEvents(onEvent)to start with.
- WebSocket events (frontend mirror) —
frontend/src/api/events.ts. Consumed; do not modify without team sync.
git checkout -b feat/frontend
cd frontend && npm install
npm run dev # opens http://localhost:5173You should see the placeholder <h1>Darwin</h1> page on a dark background.
Create src/hooks/mockEvents.ts. This is what unblocks you from the backend entirely:
import type { DarwinEvent } from "../api/events";
export function startMockStream(onEvent: (e: DarwinEvent) => void): () => void {
const seq: { delay: number; event: DarwinEvent }[] = [
{ delay: 0, event: { type: "generation.started", number: 1, champion: "baseline-v0" } },
{ delay: 800, event: { type: "strategist.question", index: 0, category: "book", text: "Would adding a 6-move opening book reduce early blunders?" } },
{ delay: 800, event: { type: "strategist.question", index: 1, category: "prompt", text: "Would prompting for threat detection first improve tactics?" } },
{ delay: 800, event: { type: "strategist.question", index: 2, category: "search", text: "Would 1-ply lookahead with LLM eval beat raw selection?" } },
{ delay: 800, event: { type: "strategist.question", index: 3, category: "evaluation", text: "Would scoring positions before moving improve endgames?" } },
{ delay: 800, event: { type: "strategist.question", index: 4, category: "sampling", text: "Would majority vote across 3 samples reduce variance?" } },
{ delay: 2000, event: { type: "builder.completed", question_index: 0, engine_name: "gen1-book-a3f", ok: true, error: null } },
{ delay: 500, event: { type: "builder.completed", question_index: 1, engine_name: "gen1-prompt-b1d", ok: true, error: null } },
{ delay: 500, event: { type: "builder.completed", question_index: 2, engine_name: "gen1-search-c2e", ok: false, error: "smoke game timeout" } },
// ... add a few game.move and game.finished events
{ delay: 8000, event: { type: "generation.finished", number: 1, new_champion: "gen1-book-a3f", elo_delta: 28.5, promoted: true } },
];
const timers: number[] = [];
let t = 0;
for (const { delay, event } of seq) {
t += delay;
timers.push(window.setTimeout(() => onEvent(event), t));
}
return () => timers.forEach(clearTimeout);
}src/hooks/useEventStream.ts:
import { useEffect, useState } from "react";
import { connectEvents } from "../api/client";
import { startMockStream } from "./mockEvents";
import type { DarwinEvent } from "../api/events";
export function useEventStream(): DarwinEvent[] {
const [events, setEvents] = useState<DarwinEvent[]>([]);
useEffect(() => {
const useMock = new URLSearchParams(location.search).has("mock");
const push = (e: DarwinEvent) => setEvents((prev) => [...prev, e]);
if (useMock) return startMockStream(push);
const ws = connectEvents((e) => push(e));
return () => ws.close();
}, []);
return events;
}Use ?mock=1 in the URL to develop offline.
Build these one at a time and drop them into App.tsx as you go. Use useEventStream() and filter the array client-side — don't add complex state management.
4a. StrategistFeed.tsx — highest demo value. Renders one card per strategist.question event with a category-color badge and a status indicator that flips on the matching builder.completed event.
4b. EloChart.tsx — Recharts LineChart. Build an Elo history from generation.finished events: [{gen: 0, elo: 1500}, {gen: 1, elo: 1528}, ...]. Animated entry on each new point.
4c. LiveBoard.tsx — wraps react-chessboard. Find the most recent game.move, render its FEN. Show White/Black names and a "thinking..." indicator after 2s of no new move event for that game.
4d. Bracket.tsx — for the active generation, a 6×6 grid of pairings. Cells fill in W/L/D as game.finished events arrive. Champion column highlighted. Final column = total points.
4e. GenerationTimeline.tsx — full history. Each row: gen number, 5 questions, winner, Elo delta, promoted/persisted badge.
Tailwind grid:
import { useEventStream } from "./hooks/useEventStream";
import LiveBoard from "./components/LiveBoard";
import EloChart from "./components/EloChart";
import StrategistFeed from "./components/StrategistFeed";
import Bracket from "./components/Bracket";
import GenerationTimeline from "./components/GenerationTimeline";
export default function App() {
const events = useEventStream();
return (
<div className="min-h-screen p-6">
<header className="flex items-center justify-between mb-6">
<h1 className="text-2xl font-bold">DARWIN</h1>
<button
onClick={() => fetch("/api/generations/run", { method: "POST" })}
className="px-4 py-2 bg-blue-600 rounded hover:bg-blue-500"
>
Run Generation
</button>
</header>
<div className="grid grid-cols-3 gap-6 mb-6">
<LiveBoard events={events} />
<StrategistFeed events={events} />
<EloChart events={events} />
</div>
<div className="grid grid-cols-2 gap-6 mb-6">
<Bracket events={events} />
<GenerationTimeline events={events} />
</div>
</div>
);
}When Person E pings you that the WS is up: drop the ?mock=1 from the URL. If anything breaks, the gap is in events.ts vs websocket.py — page E.
git add -A && git commit -m "feat: dashboard with live board, elo chart, strategist feed, bracket"
git push -u origin feat/frontend
gh pr create --title "Frontend dashboard" --body "Closes plan D."- Person E runs the backend you connect to. Coordinate only on the WS URL — event shapes are frozen.
- Person C's questions appear verbatim in your strategist feed. If anything is illegible, ping them.
- Person B's
play_gameevents driveLiveBoard— thefenfield ingame.moveis your source of truth.
- Step 5 layout renders end-to-end with
?mock=1. - Real WS events drive the dashboard once E ships.
- All 5 components render meaningfully during a real generation.
- One-click "Run Generation" button hitting
POST /api/generations/run. - PR opened, then merged after review.
- Don't edit
events.tswithout paging Person E — it must stay aligned with the backend. - Volume. A real generation may emit ~2400
game.moveevents. Don't render every single one.LiveBoardshould track only the most recently active game; drop the rest into a counter. - Animations matter. The Elo line climbing is the demo's hero shot — make sure it animates on each new point, doesn't snap.
- Pre-record a backup video before demo day. The Procfile + Vite + WS pipeline can flake live. A 30-second screen recording is your insurance policy.
Merged. Note: post-hackathon the single LiveBoard was generalized to a LiveBoards grid with per-game termination badges (see followup-4-frontend-polish.md).