import React, { useState, useEffect, useRef } from "react"; import { createRoot } from "react-dom/client"; import "./frontend.css"; // ── Types (mirrors game.ts) ───────────────────────────────────────────────── type Model = { id: string; name: string }; type TaskInfo = { model: Model; startedAt: number; finishedAt?: number; result?: string; error?: string; }; type VoteInfo = { voter: Model; startedAt: number; finishedAt?: number; votedFor?: Model; error?: boolean; }; type RoundState = { num: number; phase: "prompting" | "answering" | "voting" | "done"; prompter: Model; promptTask: TaskInfo; prompt?: string; contestants: [Model, Model]; answerTasks: [TaskInfo, TaskInfo]; votes: VoteInfo[]; scoreA?: number; scoreB?: number; }; type GameState = { completed: RoundState[]; active: RoundState | null; scores: Record; done: boolean; }; type ServerMessage = { type: "state"; data: GameState; totalRounds: number; }; // ── Helpers ────────────────────────────────────────────────────────────────── function modelClass(name: string): string { return "model-" + name.toLowerCase().replace(/[\s.]+/g, "-"); } function barClass(name: string): string { return "bar-" + name.toLowerCase().replace(/[\s.]+/g, "-"); } // ── Components ────────────────────────────────────────────────────────────── function Timer({ startedAt, finishedAt }: { startedAt: number; finishedAt?: number }) { const [now, setNow] = useState(Date.now()); useEffect(() => { if (finishedAt) return; const id = setInterval(() => setNow(Date.now()), 100); return () => clearInterval(id); }, [finishedAt]); const elapsed = ((finishedAt ?? now) - startedAt) / 1000; return ({elapsed.toFixed(1)}s); } function MName({ model }: { model: Model }) { return {model.name}; } function RoundView({ round, total }: { round: RoundState; total: number }) { const [contA, contB] = round.contestants; return (
ROUND {round.num}/{total}
{"─".repeat(50)}
{/* Prompt */}
PROMPT {!round.prompt && !round.promptTask.error && ( writing a prompt... )}
{round.promptTask.error && (
✗ {round.promptTask.error}
)} {round.prompt && (
"{round.prompt}"
)}
{/* Answers */} {round.phase !== "prompting" && (
ANSWERS {round.answerTasks.map((task, i) => (
{!task.finishedAt ? ( thinking... ) : task.error ? ( ✗ {task.error} ) : ( "{task.result}" )} {task.startedAt > 0 && ( )}
))}
)} {/* Votes */} {(round.phase === "voting" || round.phase === "done") && (
VOTES {round.votes.map((vote, i) => (
{!vote.finishedAt ? ( voting... ) : vote.error || !vote.votedFor ? ( ✗ failed ) : ( )}
))}
)} {/* Round result */} {round.phase === "done" && round.scoreA !== undefined && round.scoreB !== undefined && (
{round.scoreA > round.scoreB ? ( wins! ({round.scoreA / 100} vs {round.scoreB / 100} votes) ) : round.scoreB > round.scoreA ? ( wins! ({round.scoreB / 100} vs {round.scoreA / 100} votes) ) : ( TIE! ({round.scoreA / 100} - {round.scoreB / 100}) )}
+{round.scoreA} {" | "} +{round.scoreB}
)}
); } function Scoreboard({ scores }: { scores: Record }) { const sorted = Object.entries(scores).sort((a, b) => b[1] - a[1]); const maxScore = sorted[0]?.[1] || 1; const medals = ["👑", "🥈", "🥉"]; return (
FINAL SCORES {sorted.map(([name, score], i) => { const pct = Math.round((score / maxScore) * 100); return (
{i + 1}. {name}
{score} {i < 3 && {medals[i]}}
); })} {sorted[0] && sorted[0][1] > 0 && (
🏆 {sorted[0][0]} is the funniest AI!
)}
); } function App() { const [state, setState] = useState(null); const [totalRounds, setTotalRounds] = useState(5); const [connected, setConnected] = useState(false); const bottomRef = useRef(null); useEffect(() => { const wsUrl = `ws://${window.location.host}/ws`; let ws: WebSocket; let reconnectTimer: ReturnType; function connect() { ws = new WebSocket(wsUrl); ws.onopen = () => setConnected(true); ws.onclose = () => { setConnected(false); reconnectTimer = setTimeout(connect, 2000); }; ws.onmessage = (e) => { const msg: ServerMessage = JSON.parse(e.data); if (msg.type === "state") { setState(msg.data); setTotalRounds(msg.totalRounds); } }; } connect(); return () => { clearTimeout(reconnectTimer); ws?.close(); }; }, []); useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: "smooth" }); }, [state]); if (!connected || !state) { return (
Connecting to Quipslop...
); } return (
QUIPSLOP
AI vs AI comedy showdown — {totalRounds} rounds
{state.completed.map((round) => ( ))} {state.active && } {state.done && }
); } // ── Mount ─────────────────────────────────────────────────────────────────── const root = createRoot(document.getElementById("root")!); root.render();