Skip to content

Adding Timer bar#15

Open
matteomekhail wants to merge 4 commits intoT3-Content:mainfrom
matteomekhail:feature/phase-timer
Open

Adding Timer bar#15
matteomekhail wants to merge 4 commits intoT3-Content:mainfrom
matteomekhail:feature/phase-timer

Conversation

@matteomekhail
Copy link
Contributor

@matteomekhail matteomekhail commented Feb 23, 2026

Timer at the top explaining what is happening
image

Summary by CodeRabbit

  • New Features
    • Phase Timer UI added: progress bar, elapsed time display, countdown and indeterminate animation states for phase visualization.
    • Phase tracking enhanced: rounds now record when a phase starts to improve timing accuracy and previous-round preview timing.

@coderabbitai
Copy link

coderabbitai bot commented Feb 23, 2026

Warning

Rate limit exceeded

@matteomekhail has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 19 minutes and 45 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

Adds a phaseChangedAt timestamp to RoundState and updates game logic to set it at phase transitions. Introduces a frontend PhaseTimer UI (component, hook, and CSS) and wires it into Arena/App to display phase progress and indeterminate states.

Changes

Cohort / File(s) Summary
Type definitions & game state
broadcast.ts, game.ts, frontend.tsx
Added phaseChangedAt?: number to RoundState; initialize and update phaseChangedAt at phase transitions in game logic.
Frontend component & wiring
frontend.tsx
Added PhaseTimer component, useElapsed utility, DONE_PHASE_DURATION_MS, and isShowingPrevious prop on Arena; App now passes isShowingPrevious to Arena.
Styling
frontend.css
Added Phase Timer styles: container, bar, fill variants (countdown / indeterminate), label, and @keyframes timer-indeterminate animation.

Sequence Diagram(s)

sequenceDiagram
  participant GameServer
  participant Broadcast
  participant Frontend
  Note over GameServer,Broadcast: Phase transition occurs
  GameServer->>GameServer: set round.phase = "answering"/"voting"/"done"
  GameServer->>GameServer: set round.phaseChangedAt = now
  GameServer->>Broadcast: broadcast updated RoundState
  Broadcast->>Frontend: push RoundState (includes phaseChangedAt)
  Frontend->>Frontend: PhaseTimer.useElapsed(phaseChangedAt) -> render progress/label
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

A rabbit taps the clock with care,
Phase ticks and fills the glowing bar,
Timestamps whisper when each phase began,
Progress hops forward as only rabbits can,
Hooray for timers near and far! 🐰⏱️

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Adding Timer bar' directly describes the main change: introducing a phase timer UI component with visual feedback for phase transitions.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@macroscopeapp
Copy link

macroscopeapp bot commented Feb 23, 2026

Add a phase timer bar across arena views to show a 5,000 ms done-phase countdown and indeterminate progress for prompting, answering, voting, and next-prompt loading

Introduce phaseChangedAt timestamps in game state and render a new timer bar with countdown or indeterminate animation in both canvas and React arena views, including a special label while showing the previous round.

📍Where to Start

Start with runGame phase transitions that set phaseChangedAt in game.ts, then review timer rendering in broadcast.ts drawPhaseTimer and the React PhaseTimer component in frontend.tsx.


Macroscope summarized fd53118.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
game.ts (1)

338-349: ⚠️ Potential issue | 🟡 Minor

Missing phaseChangedAt update on the error path to "done".

When prompt generation fails, round.phase is set to "done" on Line 344 without updating round.phaseChangedAt. This leaves the timestamp pointing at the start of the "prompting" phase. If the frontend briefly renders this round in the "done" state, the PhaseTimer countdown would use a stale phaseChangedAt, producing an incorrect (likely already-expired) timer display.

In practice, the round is immediately archived and state.active nulled, so visibility is brief—but for consistency:

Proposed fix
       round.promptTask.error = "Failed after 3 attempts";
+      round.phaseChangedAt = Date.now();
       round.phase = "done";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@game.ts` around lines 338 - 349, When handling the failed prompt generation
in the catch block you set round.phase = "done" but forget to update
round.phaseChangedAt, causing the UI PhaseTimer to read a stale timestamp; fix
this by assigning round.phaseChangedAt = Date.now() immediately when you set
round.phase = "done" (before adding the round to state.completed and nulling
state.active), keeping the existing assignments to round.promptTask.finishedAt,
round.promptTask.error, state.completed, state.active, and the rerender() call.
🧹 Nitpick comments (2)
frontend.tsx (2)

113-188: PhaseTimer — two minor observations.

  1. remaining on Line 146 is computed from Date.now() during render rather than derived from the progress state. This works because setProgress triggers frequent re-renders, but it means the label text and bar width can disagree by up to 50 ms. Consider deriving both from progress:
-    const remaining = Math.max(0, Math.ceil((DONE_PHASE_DURATION_MS - (Date.now() - round.phaseChangedAt)) / 1000));
+    const remaining = Math.max(0, Math.ceil(((1 - progress) * DONE_PHASE_DURATION_MS) / 1000));
  1. The 50 ms interval keeps ticking after progress reaches 1. This is harmless since the round transitions away shortly, but you could short-circuit:
if (pct >= 1) { clearInterval(id); }

Neither is urgent—more of a polish item.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend.tsx` around lines 113 - 188, PhaseTimer: derive the remaining
seconds from the progress state instead of recomputing Date.now() in render and
stop the interval once progress reaches 1; specifically, in the useEffect that
defines tick (used to setProgress) compute pct, call setProgress(pct), and if
pct >= 1 setProgress(1) and clearInterval(id) so the interval stops, then
replace the remaining calculation (the remaining constant used in the "done"
branch) to compute remaining from progress and DONE_PHASE_DURATION_MS (e.g.
remaining = Math.max(0, Math.ceil((1 - progress) * DONE_PHASE_DURATION_MS /
1000))) so the label and bar width always agree.

145-146: Clock-skew consideration with server-generated timestamps.

round.phaseChangedAt is set via Date.now() on the server, but compared against Date.now() on the client. If the server and client clocks differ by more than a second or so, the countdown will appear skewed (e.g., starting at 6 s or 4 s instead of 5 s). This is inherent to the current approach and probably fine for a game UI, but worth noting.

If precision becomes important later, you could send a relative offset or do a lightweight clock-sync handshake over the WebSocket.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend.tsx` around lines 145 - 146, The client is comparing its Date.now()
to the server-set round.phaseChangedAt, causing visible skew when clocks differ;
update the calculation in the place that computes remaining (the expression
using round.phaseChangedAt and DONE_PHASE_DURATION_MS) to apply a clock offset
derived at connection time (or use a server-provided remaining duration).
Concretely, compute and store clientServerOffset = Date.now() -
serverNowAtHandshake (or accept serverRemainingSeconds on the round object),
then change the remaining calculation to use Date.now() - clientServerOffset (or
use serverRemainingSeconds) so countdowns align with the server timestamp while
keeping DONE_PHASE_DURATION_MS and round.phaseChangedAt as the authoritative
values.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@game.ts`:
- Around line 338-349: When handling the failed prompt generation in the catch
block you set round.phase = "done" but forget to update round.phaseChangedAt,
causing the UI PhaseTimer to read a stale timestamp; fix this by assigning
round.phaseChangedAt = Date.now() immediately when you set round.phase = "done"
(before adding the round to state.completed and nulling state.active), keeping
the existing assignments to round.promptTask.finishedAt, round.promptTask.error,
state.completed, state.active, and the rerender() call.

---

Nitpick comments:
In `@frontend.tsx`:
- Around line 113-188: PhaseTimer: derive the remaining seconds from the
progress state instead of recomputing Date.now() in render and stop the interval
once progress reaches 1; specifically, in the useEffect that defines tick (used
to setProgress) compute pct, call setProgress(pct), and if pct >= 1
setProgress(1) and clearInterval(id) so the interval stops, then replace the
remaining calculation (the remaining constant used in the "done" branch) to
compute remaining from progress and DONE_PHASE_DURATION_MS (e.g. remaining =
Math.max(0, Math.ceil((1 - progress) * DONE_PHASE_DURATION_MS / 1000))) so the
label and bar width always agree.
- Around line 145-146: The client is comparing its Date.now() to the server-set
round.phaseChangedAt, causing visible skew when clocks differ; update the
calculation in the place that computes remaining (the expression using
round.phaseChangedAt and DONE_PHASE_DURATION_MS) to apply a clock offset derived
at connection time (or use a server-provided remaining duration). Concretely,
compute and store clientServerOffset = Date.now() - serverNowAtHandshake (or
accept serverRemainingSeconds on the round object), then change the remaining
calculation to use Date.now() - clientServerOffset (or use
serverRemainingSeconds) so countdowns align with the server timestamp while
keeping DONE_PHASE_DURATION_MS and round.phaseChangedAt as the authoritative
values.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
game.ts (1)

342-353: ⚠️ Potential issue | 🟡 Minor

phaseChangedAt not updated when prompt fails and transitions directly to "done".

The early error exit sets round.phase = "done" (line 348) but leaves phaseChangedAt at the round-creation timestamp. While the frontend's isShowingPrevious guard prevents any visible countdown mis-rendering in the current flow, the field is semantically incorrect for that lastCompleted snapshot.

🛡️ Proposed fix
      round.promptTask.finishedAt = Date.now();
      round.promptTask.error = "Failed after 3 attempts";
      round.phase = "done";
+     round.phaseChangedAt = Date.now();
      state.completed = [...state.completed, round];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@game.ts` around lines 342 - 353, When the prompt failure branch sets
round.phase = "done" it doesn't update the timestamp field that records when the
phase changed; update round.phaseChangedAt = Date.now() in the same catch block
(alongside setting round.promptTask.finishedAt and round.promptTask.error) so
the lastCompleted snapshot has the correct phase change time before pushing the
round into state.completed and nulling state.active (affecting the same catch
handling that calls rerender()).
🧹 Nitpick comments (1)
frontend.tsx (1)

102-102: DONE_PHASE_DURATION_MS is duplicated with the server-side 5000 ms delay in game.ts.

game.ts line 481 hardcodes the same 5000 ms pause:

await new Promise((r) => setTimeout(r, 5000));

If either value is changed independently, the countdown bar will run under or over the actual transition. Consider extracting a shared constant (e.g. in a constants.ts module) imported by both game.ts and frontend.tsx.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend.tsx` at line 102, The DONE_PHASE_DURATION_MS constant is duplicated;
extract a single shared constant by creating a constants module (e.g. export
const DONE_PHASE_DURATION_MS = 5000 in constants.ts) and update both
frontend.tsx (replace its local DONE_PHASE_DURATION_MS) and game.ts (replace the
hardcoded setTimeout delay 5000) to import and use that exported symbol; ensure
both files import { DONE_PHASE_DURATION_MS } from the new module and remove the
duplicate local values.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend.tsx`:
- Around line 104-114: The useElapsed hook can compute a negative value when
Date.now() is less than startTime; update the tick calculation in useElapsed to
clamp the computed difference to zero before converting to seconds (e.g.,
compute diff = Math.max(0, Date.now() - startTime) and then
setElapsed(Math.floor(diff / 1000))). Ensure the initial setElapsed and the tick
both apply the clamp so the hook never returns negative seconds.

---

Outside diff comments:
In `@game.ts`:
- Around line 342-353: When the prompt failure branch sets round.phase = "done"
it doesn't update the timestamp field that records when the phase changed;
update round.phaseChangedAt = Date.now() in the same catch block (alongside
setting round.promptTask.finishedAt and round.promptTask.error) so the
lastCompleted snapshot has the correct phase change time before pushing the
round into state.completed and nulling state.active (affecting the same catch
handling that calls rerender()).

---

Nitpick comments:
In `@frontend.tsx`:
- Line 102: The DONE_PHASE_DURATION_MS constant is duplicated; extract a single
shared constant by creating a constants module (e.g. export const
DONE_PHASE_DURATION_MS = 5000 in constants.ts) and update both frontend.tsx
(replace its local DONE_PHASE_DURATION_MS) and game.ts (replace the hardcoded
setTimeout delay 5000) to import and use that exported symbol; ensure both files
import { DONE_PHASE_DURATION_MS } from the new module and remove the duplicate
local values.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant