From 1955e7f29b95b8b8274f012584cb1a44867cccba Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 25 Apr 2026 16:44:47 +0000
Subject: [PATCH 1/5] Initial plan
From c0d25ea963d9495bc8373d5401d06e55d419c592 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 25 Apr 2026 16:58:21 +0000
Subject: [PATCH 2/5] Add word-by-word bonus reading option (read bonus like a
tossup)
Agent-Logs-Url: https://github.com/qbreader/website/sessions/320ca3d4-93ef-4e83-8a8f-513ebde03fb6
Co-authored-by: geoffrey-wu <42471355+geoffrey-wu@users.noreply.github.com>
---
client/play/BonusClient.js | 27 +-
client/play/bonuses/SoloBonusClient.js | 10 +
client/play/bonuses/index.html | 8 +
client/play/bonuses/index.jsx | 19 +
package-lock.json | 1031 ++++++++++--------------
package.json | 6 +-
quizbowl/BonusRoom.js | 132 ++-
7 files changed, 622 insertions(+), 611 deletions(-)
diff --git a/client/play/BonusClient.js b/client/play/BonusClient.js
index c1cc4edb5..937fc735f 100644
--- a/client/play/BonusClient.js
+++ b/client/play/BonusClient.js
@@ -16,10 +16,13 @@ export const BonusClientMixin = (ClientClass) => class extends ClientClass {
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-bonus-like-a-tossup': return this.toggleReadBonusLikeATossup(data);
case 'toggle-three-part-bonuses': return this.toggleThreePartBonuses(data);
+ case 'update-bonus-question': return this.updateBonusQuestion(data);
default: return super.onmessage(message);
}
}
@@ -60,8 +63,10 @@ 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
+ !this.room.settings.readBonusLikeATossup && (
+ bonusEligibleTeamId === undefined ||
+ bonusEligibleTeamId === this.room.players[this.USER_ID]?.teamId
+ )
);
const input = document.createElement('input');
@@ -121,6 +126,24 @@ export const BonusClientMixin = (ClientClass) => class extends ClientClass {
toggleThreePartBonuses ({ threePartBonuses }) {
document.getElementById('toggle-three-part-bonuses').checked = threePartBonuses;
}
+
+ toggleReadBonusLikeATossup ({ readBonusLikeATossup }) {
+ document.getElementById('toggle-read-bonus-like-a-tossup').checked = readBonusLikeATossup;
+ document.getElementById('reading-speed-container').classList.toggle('d-none', !readBonusLikeATossup);
+ }
+
+ setReadingSpeed ({ readingSpeed }) {
+ document.getElementById('reading-speed').value = readingSpeed;
+ document.getElementById('reading-speed-display').textContent = readingSpeed;
+ }
+
+ updateBonusQuestion ({ word, currentPartNumber }) {
+ if (currentPartNumber === -1) {
+ document.getElementById('leadin').innerHTML += word + ' ';
+ } else {
+ document.getElementById(`bonus-part-${currentPartNumber + 1}`).querySelector('p').innerHTML += word + ' ';
+ }
+ }
};
function attachEventListeners (room, socket) {
diff --git a/client/play/bonuses/SoloBonusClient.js b/client/play/bonuses/SoloBonusClient.js
index 7a4fffd38..9bddc49bb 100644
--- a/client/play/bonuses/SoloBonusClient.js
+++ b/client/play/bonuses/SoloBonusClient.js
@@ -119,6 +119,16 @@ export default class SoloBonusClient extends BonusClient {
window.localStorage.setItem('singleplayer-bonus-settings', JSON.stringify({ ...this.room.settings, version: settingsVersion }));
}
+ toggleReadBonusLikeATossup ({ readBonusLikeATossup }) {
+ super.toggleReadBonusLikeATossup({ readBonusLikeATossup });
+ 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 }));
+ }
+
toggleTypeToAnswer ({ typeToAnswer }) {
document.getElementById('type-to-answer').checked = typeToAnswer;
window.localStorage.setItem('singleplayer-bonus-settings', JSON.stringify({ ...this.room.settings, version: settingsVersion }));
diff --git a/client/play/bonuses/index.html b/client/play/bonuses/index.html
index 34c27be24..cc2806481 100644
--- a/client/play/bonuses/index.html
+++ b/client/play/bonuses/index.html
@@ -92,6 +92,14 @@
Date: Sun, 10 May 2026 18:33:09 -0400
Subject: [PATCH 4/5] clean up naming and reveal behavior
---
client/play/BonusClient.js | 12 +++++-------
client/play/bonuses/SoloBonusClient.js | 4 ++--
client/play/bonuses/index.html | 4 ++--
client/play/bonuses/index.jsx | 16 +++++++---------
quizbowl/BonusRoom.js | 16 +++++++++-------
5 files changed, 25 insertions(+), 27 deletions(-)
diff --git a/client/play/BonusClient.js b/client/play/BonusClient.js
index 937fc735f..7dc399c78 100644
--- a/client/play/BonusClient.js
+++ b/client/play/BonusClient.js
@@ -20,7 +20,7 @@ export const BonusClientMixin = (ClientClass) => class extends ClientClass {
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-bonus-like-a-tossup': return this.toggleReadBonusLikeATossup(data);
+ case 'toggle-read-bonuses-like-tossups': return this.toggleReadBonusesLikeTossups(data);
case 'toggle-three-part-bonuses': return this.toggleThreePartBonuses(data);
case 'update-bonus-question': return this.updateBonusQuestion(data);
default: return super.onmessage(message);
@@ -63,10 +63,8 @@ export const BonusClientMixin = (ClientClass) => class extends ClientClass {
revealNextPart ({ bonusEligibleTeamId, currentPartNumber, part, value }) {
document.getElementById('reveal').disabled = !(
- !this.room.settings.readBonusLikeATossup && (
- bonusEligibleTeamId === undefined ||
- bonusEligibleTeamId === this.room.players[this.USER_ID]?.teamId
- )
+ bonusEligibleTeamId === undefined ||
+ bonusEligibleTeamId === this.room.players[this.USER_ID]?.teamId
);
const input = document.createElement('input');
@@ -127,8 +125,8 @@ export const BonusClientMixin = (ClientClass) => class extends ClientClass {
document.getElementById('toggle-three-part-bonuses').checked = threePartBonuses;
}
- toggleReadBonusLikeATossup ({ readBonusLikeATossup }) {
- document.getElementById('toggle-read-bonus-like-a-tossup').checked = readBonusLikeATossup;
+ toggleReadBonusesLikeTossups ({ readBonusLikeATossup }) {
+ document.getElementById('toggle-read-bonuses-like-tossups').checked = readBonusLikeATossup;
document.getElementById('reading-speed-container').classList.toggle('d-none', !readBonusLikeATossup);
}
diff --git a/client/play/bonuses/SoloBonusClient.js b/client/play/bonuses/SoloBonusClient.js
index 9bddc49bb..1413639b2 100644
--- a/client/play/bonuses/SoloBonusClient.js
+++ b/client/play/bonuses/SoloBonusClient.js
@@ -119,8 +119,8 @@ export default class SoloBonusClient extends BonusClient {
window.localStorage.setItem('singleplayer-bonus-settings', JSON.stringify({ ...this.room.settings, version: settingsVersion }));
}
- toggleReadBonusLikeATossup ({ readBonusLikeATossup }) {
- super.toggleReadBonusLikeATossup({ readBonusLikeATossup });
+ toggleReadBonusesLikeTossups ({ readBonusLikeATossup }) {
+ super.toggleReadBonusesLikeTossups({ readBonusLikeATossup });
window.localStorage.setItem('singleplayer-bonus-settings', JSON.stringify({ ...this.room.settings, version: settingsVersion }));
}
diff --git a/client/play/bonuses/index.html b/client/play/bonuses/index.html
index cc2806481..7f17be993 100644
--- a/client/play/bonuses/index.html
+++ b/client/play/bonuses/index.html
@@ -93,8 +93,8 @@
-
-
+
+
diff --git a/client/play/bonuses/index.jsx b/client/play/bonuses/index.jsx
index 958283a57..76e217bdb 100644
--- a/client/play/bonuses/index.jsx
+++ b/client/play/bonuses/index.jsx
@@ -9,7 +9,7 @@ import SoloBonusClient from './SoloBonusClient.js';
const modeVersion = '2025-01-14';
const queryVersion = '2025-05-07';
-const settingsVersion = '2024-11-02';
+const settingsVersion = '2026-05-10';
const USER_ID = 'user';
const TEAM_ID = 'team';
@@ -38,11 +38,6 @@ document.getElementById('local-packet-input').addEventListener('change', functio
reader.readAsText(file);
});
-document.getElementById('toggle-read-bonus-like-a-tossup').addEventListener('click', function () {
- this.blur();
- socket.sendToServer({ type: 'toggle-read-bonus-like-a-tossup', readBonusLikeATossup: this.checked });
-});
-
document.getElementById('reading-speed').addEventListener('change', function () {
socket.sendToServer({ type: 'set-reading-speed', readingSpeed: this.value });
});
@@ -56,6 +51,11 @@ document.getElementById('toggle-randomize-order').addEventListener('click', func
socket.sendToServer({ type: 'toggle-randomize-order', randomizeOrder: this.checked });
});
+document.getElementById('toggle-read-bonuses-like-tossups').addEventListener('click', function () {
+ this.blur();
+ socket.sendToServer({ type: 'toggle-read-bonuses-like-tossups', readBonusLikeATossup: this.checked });
+});
+
document.getElementById('toggle-three-part-bonuses').addEventListener('click', function () {
this.blur();
socket.sendToServer({ type: 'toggle-three-part-bonuses', threePartBonuses: this.checked });
@@ -127,9 +127,7 @@ if (window.localStorage.getItem('singleplayer-bonus-settings')) {
socket.sendToServer({ type: 'set-strictness', ...savedSettings });
socket.sendToServer({ type: 'toggle-timer', ...savedSettings });
socket.sendToServer({ type: 'toggle-type-to-answer', ...savedSettings });
- if (savedSettings.readBonusLikeATossup !== undefined) {
- socket.sendToServer({ type: 'toggle-read-bonus-like-a-tossup', readBonusLikeATossup: savedSettings.readBonusLikeATossup });
- }
+ socket.sendToServer({ type: 'toggle-read-bonuses-like-tossups', readBonusLikeATossup: savedSettings.readBonusLikeATossup });
if (savedSettings.readingSpeed !== undefined) {
socket.sendToServer({ type: 'set-reading-speed', readingSpeed: savedSettings.readingSpeed });
}
diff --git a/quizbowl/BonusRoom.js b/quizbowl/BonusRoom.js
index 1fb30b16b..a339e5e7b 100644
--- a/quizbowl/BonusRoom.js
+++ b/quizbowl/BonusRoom.js
@@ -5,6 +5,8 @@ export const BonusRoomMixin = (QuestionRoomClass) => class extends QuestionRoomC
constructor (name, categoryManager, supportedQuestionTypes = ['bonuses']) {
super(name, categoryManager, supportedQuestionTypes);
+ this.timeoutId = null;
+
this.bonus = {};
this.bonusProgress = BONUS_PROGRESS_ENUM.NOT_STARTED;
/**
@@ -17,7 +19,6 @@ export const BonusRoomMixin = (QuestionRoomClass) => class extends QuestionRoomC
*/
this.pointsPerPart = [];
- this.readingTimeoutID = null;
this.bonusQuestionSplit = [];
this.bonusWordIndex = 0;
@@ -40,7 +41,7 @@ export const BonusRoomMixin = (QuestionRoomClass) => class extends QuestionRoomC
case 'set-reading-speed': return this.setReadingSpeed({ userId, username }, message);
case 'start-bonus-answer': return this.startBonusAnswer({ userId, username }, message);
case 'toggle-bonus-part': return this.toggleBonusPart({ userId, username }, message);
- case 'toggle-read-bonus-like-a-tossup': return this.toggleReadBonusLikeATossup({ userId, username }, message);
+ case 'toggle-read-bonuses-like-tossups': return this.toggleReadBonusesLikeTossups({ userId, username }, message);
case 'toggle-three-part-bonuses': return this.toggleThreePartBonuses({ userId, username }, message);
default: return super.message({ userId, username }, message);
}
@@ -56,8 +57,8 @@ export const BonusRoomMixin = (QuestionRoomClass) => class extends QuestionRoomC
if (this.queryingQuestion) { return false; }
if (this.bonusProgress === BONUS_PROGRESS_ENUM.READING && !this.settings.skip) { return false; }
- clearTimeout(this.readingTimeoutID);
clearInterval(this.timer.interval);
+ clearTimeout(this.timeoutId);
this.emitMessage({ type: 'timer-update', timeRemaining: 0 });
const lastPartRevealed = this.bonusProgress === BONUS_PROGRESS_ENUM.LAST_PART_REVEALED;
@@ -82,6 +83,7 @@ export const BonusRoomMixin = (QuestionRoomClass) => class extends QuestionRoomC
this.liveAnswer = '';
clearInterval(this.timer.interval);
+ clearTimeout(this.timeoutId);
this.emitMessage({ type: 'timer-update', timeRemaining: ANSWER_TIME_LIMIT * 10 });
const { directive, directedPrompt } = this.checkAnswer(this.bonus.answers[this.currentPartNumber], givenAnswer);
@@ -174,7 +176,7 @@ export const BonusRoomMixin = (QuestionRoomClass) => class extends QuestionRoomC
this.bonus = await this.getNextQuestion('bonuses');
this.queryingQuestion = false;
if (!this.bonus) { return; }
- clearTimeout(this.readingTimeoutID);
+ clearTimeout(this.timeoutId);
this.emitMessage({ type: 'start-next-bonus', packetLength: this.packet.bonuses.length, bonus: this.bonus, userId, username });
this.currentPartNumber = -1;
this.pointsPerPart = [];
@@ -254,7 +256,7 @@ export const BonusRoomMixin = (QuestionRoomClass) => class extends QuestionRoomC
time = time * 0.9 * (140 - this.settings.readingSpeed);
const delay = time - Date.now() + expectedReadTime;
- this.readingTimeoutID = setTimeout(() => {
+ this.timeoutId = setTimeout(() => {
this.readBonusWord(time + expectedReadTime, onComplete);
}, delay);
}
@@ -267,9 +269,9 @@ export const BonusRoomMixin = (QuestionRoomClass) => class extends QuestionRoomC
this.emitMessage({ type: 'set-reading-speed', username, readingSpeed });
}
- toggleReadBonusLikeATossup ({ username }, { readBonusLikeATossup }) {
+ toggleReadBonusesLikeTossups ({ username }, { readBonusLikeATossup }) {
this.settings.readBonusLikeATossup = !!readBonusLikeATossup;
- this.emitMessage({ type: 'toggle-read-bonus-like-a-tossup', readBonusLikeATossup: this.settings.readBonusLikeATossup, username });
+ this.emitMessage({ type: 'toggle-read-bonuses-like-tossups', readBonusLikeATossup: this.settings.readBonusLikeATossup, username });
}
};
From 835b45aaa82dbb0dce7b9cce652b7afa3682d8d9 Mon Sep 17 00:00:00 2001
From: Geoffrey Wu
Date: Sun, 10 May 2026 18:35:08 -0400
Subject: [PATCH 5/5] put functions in alphabetical order
---
client/play/BonusClient.js | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/client/play/BonusClient.js b/client/play/BonusClient.js
index 7dc399c78..f4c950f62 100644
--- a/client/play/BonusClient.js
+++ b/client/play/BonusClient.js
@@ -117,6 +117,11 @@ export const BonusClientMixin = (ClientClass) => class extends ClientClass {
}
}
+ setReadingSpeed ({ readingSpeed }) {
+ document.getElementById('reading-speed').value = readingSpeed;
+ document.getElementById('reading-speed-display').textContent = readingSpeed;
+ }
+
toggleBonusPart ({ partNumber, correct }) {
document.getElementById(`checkbox-${partNumber + 1}`).checked = correct;
}
@@ -130,11 +135,6 @@ export const BonusClientMixin = (ClientClass) => class extends ClientClass {
document.getElementById('reading-speed-container').classList.toggle('d-none', !readBonusLikeATossup);
}
- setReadingSpeed ({ readingSpeed }) {
- document.getElementById('reading-speed').value = readingSpeed;
- document.getElementById('reading-speed-display').textContent = readingSpeed;
- }
-
updateBonusQuestion ({ word, currentPartNumber }) {
if (currentPartNumber === -1) {
document.getElementById('leadin').innerHTML += word + ' ';