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 }; // ── Model Assets & Colors ─────────────────────────────────────────────────── const MODEL_COLORS: Record = { "Gemini 3.1 Pro": "#4285F4", "Kimi K2": "#00E599", "DeepSeek 3.2": "#4D6BFE", "GLM-5": "#1F63EC", "GPT-5.2": "#10A37F", "Opus 4.6": "#D97757", "Sonnet 4.6": "#D97757", "Grok 4.1": "#FFFFFF", "MiniMax 2.5": "#FF3B30", }; function getColor(name: string): string { return MODEL_COLORS[name] ?? "#A1A1A1"; } function getLogo(name: string): string | null { if (name.includes("Gemini")) return "/assets/logos/gemini.svg"; if (name.includes("Kimi")) return "/assets/logos/kimi.svg"; if (name.includes("DeepSeek")) return "/assets/logos/deepseek.svg"; if (name.includes("GLM")) return "/assets/logos/glm.svg"; if (name.includes("GPT")) return "/assets/logos/openai.svg"; if (name.includes("Opus") || name.includes("Sonnet")) return "/assets/logos/claude.svg"; if (name.includes("Grok")) return "/assets/logos/grok.svg"; if (name.includes("MiniMax")) return "/assets/logos/minimax.svg"; return null; } // ── 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 ModelName({ model, className = "", showLogo = true }: { model: Model; className?: string, showLogo?: boolean }) { const logo = getLogo(model.name); const color = getColor(model.name); return ( {showLogo && logo && } {model.name} ); } function PromptCard({ round }: { round: RoundState }) { if (round.phase === "prompting" && !round.prompt) { return (
is cooking up a prompt…
...
); } if (round.promptTask.error) { return (
Prompt generation failed
); } return (
Prompted by
{round.prompt}
); } function ContestantPanel({ task, voteCount, totalVotes, isWinner, showVotes, voters, }: { task: TaskInfo; voteCount: number; totalVotes: number; isWinner: boolean; showVotes: boolean; voters: VoteInfo[]; }) { const color = getColor(task.model.name); const pct = totalVotes > 0 ? Math.round((voteCount / totalVotes) * 100) : 0; return (
{isWinner &&
WINNER
}
{!task.finishedAt ? ( ... ) : task.error ? ( ✗ {task.error} ) : ( “{task.result}” )}
{showVotes && (
{voteCount} {pct}%
{voters.map((v, i) => (
))}
)}
); } function PendingVotes({ votes }: { votes: VoteInfo[] }) { if (votes.length === 0) return null; return (
{votes.map((v, i) => (
{!v.finishedAt ? " deliberating…" : " abstained"}
))}
); } function Arena({ round, total }: { round: RoundState; total: number }) { const [contA, contB] = round.contestants; const showVotes = round.phase === "voting" || round.phase === "done"; const isDone = round.phase === "done"; let votesA = 0, votesB = 0; for (const v of round.votes) { if (v.votedFor?.name === contA.name) votesA++; else if (v.votedFor?.name === contB.name) votesB++; } const totalVotes = votesA + votesB; const votersA = round.votes.filter(v => v.votedFor?.name === contA.name); const votersB = round.votes.filter(v => v.votedFor?.name === contB.name); const pendingOrAbstained = round.votes.filter(v => !v.finishedAt || v.error || !v.votedFor); const phaseLabel = round.phase === "prompting" ? "✍️ WRITING PROMPT" : round.phase === "answering" ? "💭 ANSWERING" : round.phase === "voting" ? "🗳️ JUDGES VOTING" : "✅ ROUND COMPLETE"; return (
ROUND {round.num} {total !== null && / {total}}
{phaseLabel}
{round.phase !== "prompting" && ( <>
votesB} showVotes={showVotes} voters={votersA} /> votesA} showVotes={showVotes} voters={votersB} />
{showVotes && } {isDone && votesA === votesB && (
IT’S A TIE!
)} )}
); } function GameOver({ scores }: { scores: Record }) { const sorted = Object.entries(scores).sort((a, b) => b[1] - a[1]); const champion = sorted[0]; return (
GAME OVER
{champion && champion[1] > 0 && (
👑
{getLogo(champion[0]) && } {champion[0]}
is the funniest AI!
)}
); } function Sidebar({ scores, activeRound, completed }: { scores: Record; activeRound: RoundState | null; completed: RoundState[] }) { const sorted = Object.entries(scores).sort((a, b) => b[1] - a[1]); const maxScore = sorted[0]?.[1] || 1; const competing = activeRound ? new Set([activeRound.contestants[0].name, activeRound.contestants[1].name]) : new Set(); const judging = activeRound ? new Set(activeRound.votes.map((v) => v.voter.name)) : new Set(); const prompting = activeRound?.prompter.name ?? null; return ( ); } function ConnectingScreen() { return (
QUIPSLOP
Connecting...
); } // ── App ───────────────────────────────────────────────────────────────────── function App() { const [state, setState] = useState(null); const [totalRounds, setTotalRounds] = useState(5); const [connected, setConnected] = useState(false); const mainRef = 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(); }; }, []); if (!connected || !state) { return ; } return (
QUIPSLOP {(() => { const isGeneratingNextPrompt = state.active && state.active.phase === "prompting" && !state.active.prompt; const lastCompleted = state.completed[state.completed.length - 1]; if (isGeneratingNextPrompt && lastCompleted) { return ( <>
is cooking up the next prompt...
); } if (state.active) { return ; } if (!state.active && !state.done && state.completed.length > 0) { return ; } if (!state.active && !state.done && state.completed.length === 0) { return (
Game starting...
); } return null; })()} {state.done && }
); } // ── Mount ─────────────────────────────────────────────────────────────────── const root = createRoot(document.getElementById("root")!); root.render();