From 0afb2aaf5a218f8af31f4d9be917519e0333d04b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 1 Mar 2026 17:51:37 +0000
Subject: [PATCH 1/2] Initial plan
From b3c450e0b33d56b55be790b6d5777257012d56c1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 1 Mar 2026 18:24:39 +0000
Subject: [PATCH 2/2] Add word-by-word reading option for bonuses
Add readBonuses setting to BonusRoom that reads bonus leadin and
parts word by word (like tossup reading). After each part is fully
read, a dead time timer starts followed by the answer period.
Includes:
- Reading speed slider and toggle UI in bonus settings
- Pause button for bonus reading
- Client-side word-by-word display with proper targeting
- Settings persistence via localStorage
Co-authored-by: geoffrey-wu <42471355+geoffrey-wu@users.noreply.github.com>
---
client/play/BonusClient.js | 63 +++++++++++--
client/play/bonuses/SoloBonusClient.js | 10 +++
client/play/bonuses/index.html | 10 +++
client/play/bonuses/index.jsx | 25 ++++++
quizbowl/BonusRoom.js | 120 ++++++++++++++++++++++++-
5 files changed, 219 insertions(+), 9 deletions(-)
diff --git a/client/play/BonusClient.js b/client/play/BonusClient.js
index 2d0c8db1d..9439022a6 100644
--- a/client/play/BonusClient.js
+++ b/client/play/BonusClient.js
@@ -8,13 +8,19 @@ export const BonusClientMixin = (ClientClass) => class extends ClientClass {
switch (data.type) {
case 'end-current-bonus': return this.endCurrentBonus(data);
case 'give-bonus-answer': return this.giveBonusAnswer(data);
+ case 'pause': return this.pause(data);
case 'reveal-leadin': return this.revealLeadin(data);
case 'reveal-next-answer': return this.revealNextAnswer(data);
case 'reveal-next-part': return this.revealNextPart(data);
+ case 'set-reading-speed': return this.setReadingSpeed(data);
case 'start-bonus-answer': return this.startBonusAnswer(data);
case 'start-next-bonus': return this.startNextBonus(data);
case 'toggle-bonus-part': return this.toggleBonusPart(data);
+ case 'toggle-read-bonuses': return this.toggleReadBonuses(data);
case 'toggle-three-part-bonuses': return this.toggleThreePartBonuses(data);
+ case 'update-question':
+ if (data.target) { return this.updateQuestion(data); }
+ return super.onmessage(message);
default: return super.onmessage(message);
}
}
@@ -31,7 +37,7 @@ export const BonusClientMixin = (ClientClass) => class extends ClientClass {
}
if (directive !== 'prompt') {
- document.getElementById('reveal').disabled = false;
+ document.getElementById('reveal').disabled = !this.room.settings.readBonuses;
}
}
@@ -54,10 +60,12 @@ export const BonusClientMixin = (ClientClass) => class extends ClientClass {
}
revealNextPart ({ bonusEligibleTeamId, currentPartNumber, part, value }) {
- document.getElementById('reveal').disabled = !(
- bonusEligibleTeamId === undefined ||
- bonusEligibleTeamId === this.room.players[this.USER_ID]?.teamId
- );
+ if (!this.room.settings.readBonuses) {
+ document.getElementById('reveal').disabled = !(
+ bonusEligibleTeamId === undefined ||
+ bonusEligibleTeamId === this.room.players[this.USER_ID]?.teamId
+ );
+ }
const input = document.createElement('input');
input.id = `checkbox-${currentPartNumber + 1}`;
@@ -93,6 +101,11 @@ export const BonusClientMixin = (ClientClass) => class extends ClientClass {
startNextBonus ({ bonus, packetLength }) {
this.startNextQuestion({ packetLength, question: bonus });
document.getElementById('next').textContent = 'Skip';
+ const pauseButton = document.getElementById('pause');
+ if (pauseButton) {
+ pauseButton.textContent = 'Pause';
+ pauseButton.disabled = !this.room.settings.readBonuses;
+ }
}
setMode ({ mode }) {
@@ -116,6 +129,46 @@ export const BonusClientMixin = (ClientClass) => class extends ClientClass {
toggleThreePartBonuses ({ threePartBonuses }) {
document.getElementById('toggle-three-part-bonuses').checked = threePartBonuses;
}
+
+ pause ({ paused }) {
+ const pauseButton = document.getElementById('pause');
+ if (pauseButton) {
+ pauseButton.textContent = paused ? 'Resume' : 'Pause';
+ }
+ }
+
+ setReadingSpeed ({ readingSpeed }) {
+ const el = document.getElementById('reading-speed');
+ if (el) {
+ el.value = readingSpeed;
+ }
+ const display = document.getElementById('reading-speed-display');
+ if (display) {
+ display.textContent = readingSpeed;
+ }
+ }
+
+ toggleReadBonuses ({ readBonuses }) {
+ const el = document.getElementById('toggle-read-bonuses');
+ if (el) {
+ el.checked = readBonuses;
+ }
+ const readingSpeedSettings = document.getElementById('reading-speed-settings');
+ if (readingSpeedSettings) {
+ readingSpeedSettings.classList.toggle('d-none', !readBonuses);
+ }
+ }
+
+ updateQuestion ({ word, target, currentPartNumber }) {
+ if (target === 'leadin') {
+ document.getElementById('leadin').innerHTML += word + ' ';
+ } else {
+ const partEl = document.getElementById(`bonus-part-${currentPartNumber + 1}`);
+ if (partEl) {
+ partEl.querySelector('p').innerHTML += word + ' ';
+ }
+ }
+ }
};
const BonusClient = BonusClientMixin(QuestionClient);
diff --git a/client/play/bonuses/SoloBonusClient.js b/client/play/bonuses/SoloBonusClient.js
index b7e458644..626054ccc 100644
--- a/client/play/bonuses/SoloBonusClient.js
+++ b/client/play/bonuses/SoloBonusClient.js
@@ -124,6 +124,16 @@ export default class SoloBonusClient extends BonusClient {
window.localStorage.setItem('singleplayer-bonus-settings', JSON.stringify({ ...this.room.settings, version: settingsVersion }));
}
+ setReadingSpeed ({ readingSpeed }) {
+ super.setReadingSpeed({ readingSpeed });
+ window.localStorage.setItem('singleplayer-bonus-settings', JSON.stringify({ ...this.room.settings, version: settingsVersion }));
+ }
+
+ toggleReadBonuses ({ readBonuses }) {
+ super.toggleReadBonuses({ readBonuses });
+ window.localStorage.setItem('singleplayer-bonus-settings', JSON.stringify({ ...this.room.settings, version: settingsVersion }));
+ }
+
/**
* Calculates the points per bonus and updates the display.
*/
diff --git a/client/play/bonuses/index.html b/client/play/bonuses/index.html
index 2780751fb..1cb2628fc 100644
--- a/client/play/bonuses/index.html
+++ b/client/play/bonuses/index.html
@@ -169,6 +169,14 @@
+
+
+
+
+
+
+
+
@@ -220,6 +228,8 @@
+