From c9d68d58ebded0b01b4cf25dc079977b7b4593c7 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Thu, 19 Feb 2026 23:40:03 -0800 Subject: [PATCH] more complete --- frontend.css | 333 +++++++++++++++++++++++---------------------------- frontend.tsx | 210 ++++++++++++++------------------ 2 files changed, 239 insertions(+), 304 deletions(-) diff --git a/frontend.css b/frontend.css index 04f21e5..c4f0d59 100644 --- a/frontend.css +++ b/frontend.css @@ -19,7 +19,7 @@ body { font-family: 'Inter', -apple-system, sans-serif; font-size: 15px; line-height: 1.5; - min-height: 100vh; + height: 100vh; overflow: hidden; -webkit-font-smoothing: antialiased; } @@ -70,12 +70,12 @@ body { .main { flex: 1; - overflow-y: auto; - padding: 48px 64px; + overflow: hidden; + padding: 32px 64px; display: flex; flex-direction: column; + justify-content: center; align-items: center; - scroll-behavior: smooth; } /* ── Sidebar ──────────────────────────────────────────────────── */ @@ -86,33 +86,53 @@ body { background: var(--surface); display: flex; flex-direction: column; - overflow-y: auto; + overflow: hidden; flex-shrink: 0; } +.sidebar__section { + display: flex; + flex-direction: column; + flex-shrink: 0; +} + +.sidebar__section--history { + flex: 1; + border-top: 1px solid var(--border); + min-height: 0; +} + .sidebar__header { font-family: 'JetBrains Mono', monospace; font-size: 14px; font-weight: 700; letter-spacing: 1px; text-transform: uppercase; - padding: 32px 32px 16px; + padding: 24px 32px 16px; color: var(--text-dim); } .sidebar__list { - flex: 1; - padding: 0 16px 32px; + padding: 0 16px 16px; display: flex; flex-direction: column; gap: 4px; } +.sidebar__history-list { + flex: 1; + overflow-y: auto; + padding: 0 16px 24px; + display: flex; + flex-direction: column; + gap: 8px; +} + .standing { display: flex; align-items: center; gap: 16px; - padding: 16px; + padding: 12px 16px; border-radius: 8px; transition: background 0.2s; } @@ -162,7 +182,7 @@ body { display: flex; align-items: center; gap: 12px; - margin-top: 8px; + margin-top: 6px; } .standing__bar { @@ -189,7 +209,7 @@ body { } .sidebar__legend { - padding: 24px 32px; + padding: 16px 32px; border-top: 1px solid var(--border); display: flex; flex-direction: column; @@ -199,23 +219,85 @@ body { color: var(--text-muted); } +/* ── Past Rounds Mini ─────────────────────────────────────────── */ + +.past-round-mini { + background: var(--surface-2); + border: 1px solid var(--border); + padding: 12px; + border-radius: 8px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.past-round-mini__top { + display: flex; + gap: 12px; + align-items: flex-start; +} + +.past-round-mini__num { + font-family: 'JetBrains Mono', monospace; + font-size: 12px; + font-weight: 700; + color: var(--text-muted); + flex-shrink: 0; +} + +.past-round-mini__prompt { + font-family: 'Inter', sans-serif; + font-size: 13px; + color: var(--text); + line-height: 1.4; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.past-round-mini__winner { + font-size: 12px; + color: var(--text-muted); + display: flex; + align-items: center; + gap: 6px; + padding-left: 28px; +} + +.small-model-name { + font-size: 12px; +} + +.small-model-name .model-logo { + width: 14px; + height: 14px; +} + +.past-round-mini__tie { + font-family: 'JetBrains Mono', monospace; + font-weight: 700; + color: var(--text-muted); +} + /* ── Arena ─────────────────────────────────────────────────────── */ .arena { width: 100%; max-width: 1100px; - margin: 0 auto; display: flex; flex-direction: column; + max-height: 100%; } .arena__header-row { display: flex; justify-content: space-between; align-items: baseline; - margin-bottom: 48px; - padding-bottom: 24px; + margin-bottom: 32px; + padding-bottom: 16px; border-bottom: 1px solid var(--border); + flex-shrink: 0; } .arena__round-badge { @@ -241,14 +323,15 @@ body { /* ── Prompt Card ──────────────────────────────────────────────── */ .prompt-card { - margin-bottom: 64px; + margin-bottom: 48px; + flex-shrink: 0; } .prompt-card__by { font-family: 'JetBrains Mono', monospace; font-size: 12px; color: var(--text-muted); - margin-bottom: 24px; + margin-bottom: 16px; text-transform: uppercase; letter-spacing: 1px; display: flex; @@ -258,7 +341,7 @@ body { .prompt-card__text { font-family: 'DM Serif Display', serif; - font-size: 56px; + font-size: 48px; line-height: 1.1; color: var(--text); max-width: 95%; @@ -275,7 +358,9 @@ body { display: grid; grid-template-columns: 1fr 1fr; gap: 48px; - margin-bottom: 64px; + margin-bottom: 32px; + flex: 1; + min-height: 0; } .contestant { @@ -285,15 +370,17 @@ body { position: relative; border-top: 4px solid var(--border); padding-top: 24px; + overflow: hidden; } .contestant__header { display: flex; align-items: center; justify-content: space-between; - margin-bottom: 32px; - padding-bottom: 24px; + margin-bottom: 24px; + padding-bottom: 16px; border-bottom: 1px solid var(--border); + flex-shrink: 0; } .contestant__name { @@ -318,12 +405,13 @@ body { .contestant__answer { flex: 1; - min-height: 180px; + overflow-y: auto; + min-height: 120px; } .contestant__text { font-family: 'DM Serif Display', serif; - font-size: 36px; + font-size: 28px; line-height: 1.3; color: var(--text-dim); letter-spacing: -0.5px; @@ -336,7 +424,7 @@ body { .contestant__thinking { color: var(--text-muted); font-family: 'DM Serif Display', serif; - font-size: 36px; + font-size: 28px; } .contestant__error { @@ -345,11 +433,15 @@ body { font-size: 14px; } -.contestant__votes { - margin-top: 48px; -} +/* ── Votes within Contestant ──────────────────────────────────── */ -/* ── Vote Bar ─────────────────────────────────────────────────── */ +.contestant__votes-container { + margin-top: 24px; + display: flex; + flex-direction: column; + gap: 16px; + flex-shrink: 0; +} .vote-bar { height: 2px; @@ -365,7 +457,7 @@ body { .vote-bar__label { display: flex; justify-content: space-between; - margin-top: 16px; + margin-top: 12px; font-family: 'JetBrains Mono', monospace; font-size: 13px; color: var(--text-dim); @@ -377,73 +469,44 @@ body { font-size: 18px; } -/* ── Vote Ticker ──────────────────────────────────────────────── */ - -.vote-ticker { - border-top: 1px solid var(--border); - padding-top: 48px; - margin-bottom: 64px; -} - -.vote-ticker__header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 32px; -} - -.vote-ticker__title { - font-family: 'JetBrains Mono', monospace; - font-size: 13px; - font-weight: 700; - letter-spacing: 1px; - color: var(--text-dim); -} - -.vote-ticker__status { - font-family: 'JetBrains Mono', monospace; - font-size: 13px; - color: var(--text-muted); -} - -.vote-ticker__list { +.contestant__voters { display: flex; flex-wrap: wrap; - gap: 16px; + gap: 8px; } -.vote-entry { - display: flex; +.voter-badge { + display: inline-flex; align-items: center; - gap: 12px; - font-family: 'Inter', sans-serif; - font-size: 14px; - font-weight: 500; - padding: 12px 16px; - border-radius: 8px; + gap: 6px; + background: var(--surface-2); border: 1px solid var(--border); - background: var(--surface); + padding: 6px 10px; + border-radius: 6px; + font-size: 12px; + font-family: 'Inter', sans-serif; + font-weight: 500; } -.vote-entry--pending { +.voter-badge--pending { opacity: 0.5; border-style: dashed; } -.vote-entry__arrow { +.voter-badge--error { + opacity: 0.5; color: var(--text-muted); } -.vote-entry__pending { - color: var(--text-muted); - font-style: italic; - font-size: 13px; -} - -.vote-entry__error { - color: var(--text-muted); - font-style: italic; - font-size: 13px; +.pending-votes { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 8px; + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid var(--border); + flex-shrink: 0; } /* ── Round Result (Tie) ───────────────────────────────────────── */ @@ -454,10 +517,12 @@ body { font-size: 16px; font-weight: 700; letter-spacing: 2px; - padding: 32px; + padding: 24px; color: var(--text-dim); border: 1px solid var(--border); border-radius: 8px; + margin-top: 24px; + flex-shrink: 0; } /* ── Waiting State ────────────────────────────────────────────── */ @@ -473,102 +538,6 @@ body { opacity: 0.5; } -/* ── History ──────────────────────────────────────────────────── */ - -.history { - width: 100%; - max-width: 1100px; - margin: 80px auto 0; -} - -.history__title { - font-family: 'JetBrains Mono', monospace; - font-size: 14px; - font-weight: 700; - letter-spacing: 1px; - color: var(--text-dim); - margin-bottom: 32px; - padding-bottom: 24px; - border-bottom: 1px solid var(--border); -} - -.past-round { - padding: 32px 0; - border-bottom: 1px solid var(--border); - display: flex; - flex-direction: column; - gap: 24px; -} - -.past-round__header { - display: flex; - gap: 24px; - align-items: baseline; -} - -.past-round__num { - font-family: 'JetBrains Mono', monospace; - font-size: 14px; - color: var(--text-muted); - width: 32px; -} - -.past-round__prompt { - font-family: 'Inter', sans-serif; - font-size: 18px; - font-weight: 600; - color: var(--text); - flex: 1; -} - -.past-round__detail { - padding-left: 56px; - display: grid; - grid-template-columns: 1fr 1fr; - gap: 48px; -} - -.past-round__competitor { - display: flex; - flex-direction: column; - gap: 12px; -} - -.past-round__competitor-header { - display: flex; - justify-content: space-between; - align-items: center; - font-size: 14px; -} - -.past-round__answer { - font-family: 'DM Serif Display', serif; - font-size: 20px; - color: var(--text-dim); - line-height: 1.4; -} - -.past-round__competitor--winner .past-round__answer { - color: var(--text); -} - -.past-round__score { - font-family: 'JetBrains Mono', monospace; - font-weight: 700; - color: var(--text-muted); -} - -.past-round__winner-tag { - font-family: 'JetBrains Mono', monospace; - font-weight: 700; - font-size: 10px; - padding: 2px 6px; - background: var(--text); - color: var(--bg); - border-radius: 4px; - text-transform: uppercase; -} - /* ── Game Over ────────────────────────────────────────────────── */ .game-over { @@ -663,10 +632,6 @@ body { object-fit: contain; } -.model-logo.invert-dark { - /* Some logos might need this if they are dark naturally, but SVG styling handles it or we'll assume they're visible. */ -} - .timer { color: var(--text-muted); font-family: 'JetBrains Mono', monospace; @@ -700,7 +665,7 @@ body { border-left: none; border-top: 1px solid var(--border); max-height: 300px; + overflow-y: auto; } - .showdown { grid-template-columns: 1fr; gap: 48px; } - .past-round__detail { grid-template-columns: 1fr; } -} \ No newline at end of file + .showdown { grid-template-columns: 1fr; gap: 32px; } +} diff --git a/frontend.tsx b/frontend.tsx index 7d7a31b..9dd4f7f 100644 --- a/frontend.tsx +++ b/frontend.tsx @@ -112,12 +112,14 @@ function ContestantPanel({ 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; @@ -144,13 +146,22 @@ function ContestantPanel({ {showVotes && ( -
-
-
+
+
+
+
+
+
+ {voteCount} + {pct}% +
-
- {voteCount} - {pct}% +
+ {voters.map((v, i) => ( +
+ +
+ ))}
)} @@ -158,37 +169,16 @@ function ContestantPanel({ ); } -function VoteTicker({ votes }: { votes: VoteInfo[] }) { - const finishedVotes = votes.filter((v) => v.finishedAt); - const pendingVotes = votes.filter((v) => !v.finishedAt); - +function PendingVotes({ votes }: { votes: VoteInfo[] }) { + if (votes.length === 0) return null; return ( -
-
- JUDGES - - {finishedVotes.length} / {votes.length} - -
-
- {finishedVotes.map((vote, i) => ( -
- - - {vote.error || !vote.votedFor ? ( - abstained - ) : ( - - )} -
- ))} - {pendingVotes.map((vote, i) => ( -
- - deliberating… -
- ))} -
+
+ {votes.map((v, i) => ( +
+ + {!v.finishedAt ? " deliberating…" : " abstained"} +
+ ))}
); } @@ -205,6 +195,10 @@ function Arena({ round, total }: { round: RoundState; total: number }) { 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" @@ -235,6 +229,7 @@ function Arena({ round, total }: { round: RoundState; total: number }) { totalVotes={totalVotes} isWinner={isDone && votesA > votesB} showVotes={showVotes} + voters={votersA} /> votesA} showVotes={showVotes} + voters={votersB} />
- {showVotes && } + {showVotes && } {isDone && votesA === votesB && (
IT’S A TIE!
@@ -256,45 +252,23 @@ function Arena({ round, total }: { round: RoundState; total: number }) { ); } -function PastRoundEntry({ round }: { round: RoundState }) { +function PastRoundMini({ round }: { round: RoundState }) { const [contA, contB] = round.contestants; - let votesA = 0, - votesB = 0; + 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 isAWinner = votesA > votesB; - const isBWinner = votesB > votesA; + const winner = votesA > votesB ? contA : votesB > votesA ? contB : null; return ( -
-
- R{round.num} - {round.prompt} +
+
+ R{round.num} + "{round.prompt}"
-
-
-
- -
- {votesA} - {isAWinner && WINNER} -
-
- “{round.answerTasks[0].result}” -
-
-
- -
- {votesB} - {isBWinner && WINNER} -
-
- “{round.answerTasks[1].result}” -
+
+ {winner ? <> won : Tie}
); @@ -321,7 +295,7 @@ function GameOver({ scores }: { scores: Record }) { ); } -function Sidebar({ scores, activeRound }: { scores: Record; activeRound: RoundState | null }) { +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; @@ -333,44 +307,57 @@ function Sidebar({ scores, activeRound }: { scores: Record; acti return ( @@ -422,14 +409,6 @@ function App() { }; }, []); - useEffect(() => { - if (mainRef.current) { - // Don't auto-scroll aggressively if they are just reading past rounds - // but maybe scroll to top of arena when round changes? - // Leaving this simple for now. - } - }, [state?.active?.num]); - if (!connected || !state) { return ; } @@ -458,18 +437,9 @@ function App() { )} {state.done && } - - {state.completed.length > 0 && ( -
-
PAST ROUNDS
- {[...state.completed].reverse().map((round) => ( - - ))} -
- )} - +
); @@ -478,4 +448,4 @@ function App() { // ── Mount ─────────────────────────────────────────────────────────────────── const root = createRoot(document.getElementById("root")!); -root.render(); +root.render(); \ No newline at end of file