import React, { useState, useEffect } from "react"; import { createRoot } from "react-dom/client"; import "./frontend.css"; // ── Types ──────────────────────────────────────────────────────────────────── 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; viewerVotesA?: number; viewerVotesB?: number; viewerVotingEndsAt?: number; }; type GameState = { lastCompleted: RoundState | null; active: RoundState | null; scores: Record; done: boolean; isPaused: boolean; generation: number; }; type StateMessage = { type: "state"; data: GameState; totalRounds: number; viewerCount: number; version?: string; }; type ViewerCountMessage = { type: "viewerCount"; viewerCount: number; }; type VotedAckMessage = { type: "votedAck" }; type ServerMessage = StateMessage | ViewerCountMessage | VotedAckMessage; // ── Model colors & logos ───────────────────────────────────────────────────── 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; } // ── Helpers ────────────────────────────────────────────────────────────────── function Dots() { return ( . . . ); } function ModelTag({ model, small }: { model: Model; small?: boolean }) { const logo = getLogo(model.name); const color = getColor(model.name); return ( {logo && } {model.name} ); } // ── Prompt ─────────────────────────────────────────────────────────────────── function PromptCard({ round }: { round: RoundState }) { if (round.phase === "prompting" && !round.prompt) { return (
is writing a prompt
); } if (round.promptTask.error) { return (
Prompt generation failed
); } return (
Prompted by
{round.prompt}
); } // ── Contestant ─────────────────────────────────────────────────────────────── function ContestantCard({ task, voteCount, totalVotes, isWinner, showVotes, voters, viewerVotes, totalViewerVotes, votable, onVote, }: { task: TaskInfo; voteCount: number; totalVotes: number; isWinner: boolean; showVotes: boolean; voters: VoteInfo[]; viewerVotes?: number; totalViewerVotes?: number; votable?: boolean; onVote?: () => void; }) { const color = getColor(task.model.name); const pct = totalVotes > 0 ? Math.round((voteCount / totalVotes) * 100) : 0; const showViewerVotes = showVotes && totalViewerVotes !== undefined && totalViewerVotes > 0; const viewerPct = showViewerVotes && totalViewerVotes > 0 ? Math.round(((viewerVotes ?? 0) / totalViewerVotes) * 100) : 0; return (
{ if (e.key === "Enter" || e.key === " ") onVote?.(); } : undefined} >
{isWinner && WIN}
{!task.finishedAt ? (

) : task.error ? (

{task.error}

) : (

“{task.result}”

)}
{showVotes && (
{voteCount} vote{voteCount !== 1 ? "s" : ""} {voters.map((v, i) => { const logo = getLogo(v.voter.name); return logo ? ( {v.voter.name} ) : ( {v.voter.name[0]} ); })}
{showViewerVotes && ( <>
{viewerVotes ?? 0} viewer vote{(viewerVotes ?? 0) !== 1 ? "s" : ""} 👥
)}
)}
); } // ── Arena ───────────────────────────────────────────────────────────────────── function Arena({ round, total, hasVoted, onVote, viewerVotingSecondsLeft, }: { round: RoundState; total: number | null; hasVoted: boolean; onVote: (side: "A" | "B") => void; viewerVotingSecondsLeft: 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 totalViewerVotes = (round.viewerVotesA ?? 0) + (round.viewerVotesB ?? 0); const canVote = round.phase === "voting" && !hasVoted && viewerVotingSecondsLeft > 0 && round.answerTasks[0].finishedAt && round.answerTasks[1].finishedAt; const showCountdown = round.phase === "voting" && viewerVotingSecondsLeft > 0; const phaseText = round.phase === "prompting" ? "Writing prompt" : round.phase === "answering" ? "Answering" : round.phase === "voting" ? "Judges voting" : "Complete"; return (
Round {round.num} {total ? /{total} : null} {phaseText} {showCountdown && ( {viewerVotingSecondsLeft}s )}
{canVote && (
Pick the funnier answer!
)} {round.phase !== "prompting" && (
votesB} showVotes={showVotes} voters={votersA} viewerVotes={round.viewerVotesA} totalViewerVotes={totalViewerVotes} votable={!!canVote} onVote={() => onVote("A")} /> votesA} showVotes={showVotes} voters={votersB} viewerVotes={round.viewerVotesB} totalViewerVotes={totalViewerVotes} votable={!!canVote} onVote={() => onVote("B")} />
)} {isDone && votesA === votesB && totalVotes > 0 && (
Tie
)}
); } // ── Game Over ──────────────────────────────────────────────────────────────── 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
)}
); } // ── Standings ──────────────────────────────────────────────────────────────── function Standings({ scores, activeRound, }: { scores: Record; activeRound: RoundState | null; }) { 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(); return ( ); } // ── Connecting ─────────────────────────────────────────────────────────────── function ConnectingScreen() { return (
quipslop
Connecting
); } // ── App ────────────────────────────────────────────────────────────────────── function App() { const [state, setState] = useState(null); const [totalRounds, setTotalRounds] = useState(null); const [viewerCount, setViewerCount] = useState(0); const [connected, setConnected] = useState(false); const [hasVoted, setHasVoted] = useState(false); const [votedRound, setVotedRound] = useState(null); const [viewerVotingSecondsLeft, setViewerVotingSecondsLeft] = useState(0); const wsRef = React.useRef(null); // Reset hasVoted when round changes useEffect(() => { const currentRound = state?.active?.num ?? null; if (currentRound !== null && currentRound !== votedRound) { setHasVoted(false); setVotedRound(null); } }, [state?.active?.num, votedRound]); // Countdown timer for viewer voting useEffect(() => { const endsAt = state?.active?.viewerVotingEndsAt; if (!endsAt || state?.active?.phase !== "voting") { setViewerVotingSecondsLeft(0); return; } function tick() { const remaining = Math.max(0, Math.ceil((endsAt! - Date.now()) / 1000)); setViewerVotingSecondsLeft(remaining); } tick(); const interval = setInterval(tick, 1000); return () => clearInterval(interval); }, [state?.active?.viewerVotingEndsAt, state?.active?.phase]); useEffect(() => { const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:"; const wsUrl = `${wsProtocol}//${window.location.host}/ws`; let ws: WebSocket; let reconnectTimer: ReturnType; let knownVersion: string | null = null; function connect() { ws = new WebSocket(wsUrl); wsRef.current = ws; ws.onopen = () => setConnected(true); ws.onclose = () => { setConnected(false); wsRef.current = null; reconnectTimer = setTimeout(connect, 2000); }; ws.onmessage = (e) => { const msg: ServerMessage = JSON.parse(e.data); if (msg.type === "state") { if (msg.version) { if (!knownVersion) knownVersion = msg.version; else if (knownVersion !== msg.version) return location.reload(); } setState(msg.data); setTotalRounds(msg.totalRounds); setViewerCount(msg.viewerCount); } else if (msg.type === "viewerCount") { setViewerCount(msg.viewerCount); } else if (msg.type === "votedAck") { setHasVoted(true); } }; } connect(); return () => { clearTimeout(reconnectTimer); ws?.close(); }; }, []); const handleVote = (side: "A" | "B") => { if (hasVoted || !wsRef.current) return; wsRef.current.send(JSON.stringify({ type: "vote", votedFor: side })); setVotedRound(state?.active?.num ?? null); }; if (!connected || !state) return ; const isNextPrompting = state.active?.phase === "prompting" && !state.active.prompt; const displayRound = isNextPrompting && state.lastCompleted ? state.lastCompleted : state.active; return (
quipslop
{state.isPaused && (
Paused
)}
{viewerCount} viewer{viewerCount === 1 ? "" : "s"} watching
{state.done ? ( ) : displayRound ? ( ) : (
Starting
)} {isNextPrompting && state.lastCompleted && (
is writing the next prompt
)}
); } // ── Mount ──────────────────────────────────────────────────────────────────── const root = createRoot(document.getElementById("root")!); root.render();