Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions broadcast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type VoteInfo = {
type RoundState = {
num: number;
phase: "prompting" | "answering" | "voting" | "done";
phaseChangedAt?: number;
prompter: Model;
promptTask: TaskInfo;
prompt?: string;
Expand Down
50 changes: 50 additions & 0 deletions frontend.css
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,56 @@ body {
color: var(--text-dim);
}

/* ── Phase Timer ─────────────────────────────────────────────── */

.phase-timer {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
flex-shrink: 0;
}

.phase-timer__bar {
flex: 1;
height: 3px;
background: var(--border);
border-radius: 2px;
overflow: hidden;
}

.phase-timer__fill {
height: 100%;
border-radius: 2px;
background: var(--text-muted);
}

.phase-timer__fill--countdown {
background: var(--accent);
transition: width 0.1s linear;
}

.phase-timer__fill--indeterminate {
width: 30%;
background: var(--text-muted);
animation: timer-indeterminate 1.5s ease-in-out infinite;
}

@keyframes timer-indeterminate {
0% { transform: translateX(-100%); }
100% { transform: translateX(433%); }
}

.phase-timer__label {
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.5px;
text-transform: uppercase;
color: var(--text-muted);
white-space: nowrap;
flex-shrink: 0;
}

/* ── Prompt ───────────────────────────────────────────────────── */

.prompt {
Expand Down
98 changes: 96 additions & 2 deletions frontend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type VoteInfo = {
type RoundState = {
num: number;
phase: "prompting" | "answering" | "voting" | "done";
phaseChangedAt?: number;
prompter: Model;
promptTask: TaskInfo;
prompt?: string;
Expand Down Expand Up @@ -95,6 +96,97 @@ function Dots() {
);
}

const DONE_PHASE_DURATION_MS = 5000;

function useElapsed(startTime: number | undefined) {
const [elapsed, setElapsed] = useState(0);
useEffect(() => {
if (!startTime) { setElapsed(0); return; }
const tick = () => setElapsed(Math.floor((Date.now() - startTime) / 1000));
tick();
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, [startTime]);
return elapsed;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

function PhaseTimer({ round, isShowingPrevious }: { round: RoundState; isShowingPrevious: boolean }) {
const [progress, setProgress] = useState(0);
const elapsed = useElapsed(round.phaseChangedAt);

useEffect(() => {
if (round.phase !== "done" || !round.phaseChangedAt) {
setProgress(0);
return;
}
const startTime = round.phaseChangedAt;
function tick() {
const pct = Math.min((Date.now() - startTime) / DONE_PHASE_DURATION_MS, 1);
setProgress(pct);
}
tick();
const id = setInterval(tick, 50);
return () => clearInterval(id);
}, [round.phase, round.phaseChangedAt]);

// Showing previous round's results while next prompt loads — no rush
if (isShowingPrevious) {
return (
<div className="phase-timer">
<div className="phase-timer__bar">
<div className="phase-timer__fill phase-timer__fill--indeterminate" />
</div>
<span className="phase-timer__label">Next prompt loading — no rush</span>
</div>
);
}

// Done phase: 5-second countdown before next round
if (round.phase === "done" && round.phaseChangedAt) {
const remaining = Math.max(0, Math.ceil((DONE_PHASE_DURATION_MS - (Date.now() - round.phaseChangedAt)) / 1000));
return (
<div className="phase-timer">
<div className="phase-timer__bar">
<div
className="phase-timer__fill phase-timer__fill--countdown"
style={{ width: `${(1 - progress) * 100}%` }}
/>
</div>
<span className="phase-timer__label">Next round in {remaining}s</span>
</div>
);
}

// Answering / voting: open-ended, show elapsed time
if (round.phase === "answering" || round.phase === "voting") {
const label = round.phase === "answering"
? `Waiting for answers — ${elapsed}s`
: `Judges voting — ${elapsed}s`;
return (
<div className="phase-timer">
<div className="phase-timer__bar">
<div className="phase-timer__fill phase-timer__fill--indeterminate" />
</div>
<span className="phase-timer__label">{label}</span>
</div>
);
}

// Prompting (showing current round's loading state)
if (round.phase === "prompting") {
return (
<div className="phase-timer">
<div className="phase-timer__bar">
<div className="phase-timer__fill phase-timer__fill--indeterminate" />
</div>
<span className="phase-timer__label">Writing prompt — {elapsed}s</span>
</div>
);
}

return null;
}

function ModelTag({ model, small }: { model: Model; small?: boolean }) {
const logo = getLogo(model.name);
const color = getColor(model.name);
Expand Down Expand Up @@ -235,7 +327,7 @@ function ContestantCard({

// ── Arena ─────────────────────────────────────────────────────────────────────

function Arena({ round, total }: { round: RoundState; total: number | null }) {
function Arena({ round, total, isShowingPrevious }: { round: RoundState; total: number | null; isShowingPrevious?: boolean }) {
const [contA, contB] = round.contestants;
const showVotes = round.phase === "voting" || round.phase === "done";
const isDone = round.phase === "done";
Expand Down Expand Up @@ -269,6 +361,8 @@ function Arena({ round, total }: { round: RoundState; total: number | null }) {
<span className="arena__phase">{phaseText}</span>
</div>

<PhaseTimer round={round} isShowingPrevious={!!isShowingPrevious} />

<PromptCard round={round} />

{round.phase !== "prompting" && (
Expand Down Expand Up @@ -484,7 +578,7 @@ function App() {
{state.done ? (
<GameOver scores={state.scores} />
) : displayRound ? (
<Arena round={displayRound} total={totalRounds} />
<Arena round={displayRound} total={totalRounds} isShowingPrevious={isNextPrompting && !!state.lastCompleted} />
) : (
<div className="waiting">
Starting
Expand Down
5 changes: 5 additions & 0 deletions game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export type VoteInfo = {
export type RoundState = {
num: number;
phase: "prompting" | "answering" | "voting" | "done";
phaseChangedAt: number;
prompter: Model;
promptTask: TaskInfo;
prompt?: string;
Expand Down Expand Up @@ -301,6 +302,7 @@ export async function runGame(
const round: RoundState = {
num: r,
phase: "prompting",
phaseChangedAt: now,
prompter,
promptTask: { model: prompter, startedAt: now },
contestants: [contA, contB],
Expand Down Expand Up @@ -348,6 +350,7 @@ export async function runGame(

// ── Answer phase ──
round.phase = "answering";
round.phaseChangedAt = Date.now();
const answerStart = Date.now();
round.answerTasks[0].startedAt = answerStart;
round.answerTasks[1].startedAt = answerStart;
Expand Down Expand Up @@ -389,6 +392,7 @@ export async function runGame(

// ── Vote phase ──
round.phase = "voting";
round.phaseChangedAt = Date.now();
const answerA = round.answerTasks[0].result!;
const answerB = round.answerTasks[1].result!;
const voteStart = Date.now();
Expand Down Expand Up @@ -451,6 +455,7 @@ export async function runGame(
round.scoreA = votesA * 100;
round.scoreB = votesB * 100;
round.phase = "done";
round.phaseChangedAt = Date.now();
if (votesA > votesB) {
state.scores[contA.name] = (state.scores[contA.name] || 0) + 1;
} else if (votesB > votesA) {
Expand Down