Multi-paragraph text + gist (meeting-notes) mode [Phase 1] - #8
Conversation
Phase 1 of the next simulation iteration. - Multi-paragraph: loadText preserves newlines (blank lines = paragraph breaks) for every mode except racing, which stays single-line. Rendering, the input reconcile, completion, and cursor auto-scroll already handle newlines. Verified long/multi-screen texts scroll to keep the cursor in view. - Gist / meeting-notes mode (audio + gradeMode:"gist"): notes are graded on meaning, not verbatim match. Accuracy/error stat cards are hidden (only Speed/Time show), and includeTranscript emits a "Key Points:" block (from config.keyPoints) before the verbatim transcript so the grader can score coverage and verify captured facts against ground truth. - audio.maxPlays: optional limit on how many times the clip can be played (a fresh play from the start consumes one; resuming a paused clip does not). For the meeting-notes task the clip plays only twice. Note: the base task's extract_solution.py must be updated to print the new "Key Points:" block (deployment step; snippet in the PR). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughThe README documents gist grading, key points, audio play limits, and paragraph handling. Gist transcript saves now prepend configured key points, and gist dashboards hide accuracy and error metrics. Audio games enforce optional play limits, update status messages, reset counters, and clean up listeners. Text loading now preserves paragraphs outside racing mode and normalizes whitespace. Suggested reviewers: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@client/completion.js`:
- Around line 33-38: The gist-specific logic in the completion flow must apply
only to audio tasks. Define and reuse an isGistAudio condition combining
gameType === 'audio' with gradeMode === 'gist' for both the Key Points block and
the related accuracy/error-card branches, preserving existing behavior for valid
audio gist tasks.
- Around line 29-41: Update extract_solution.py to parse the saved Key Points,
Expected Transcription, and Submitted Transcription blocks from stats.txt and
print them before or alongside the existing Generated output. Preserve the
existing generated-stats parsing while ensuring all three fields are emitted for
the grader in the same labels and content written by completion.js.
In `@client/games/audio-game.js`:
- Around line 135-145: Update the playback-start logic around audioEl, atStart,
and listenInProgress so a new play attempt consumes a play whenever
listenInProgress is false, regardless of currentTime. Also detect backward seeks
during an active listen or prevent replay seeking, ensuring restarting from the
beginning cannot bypass the max limit and that exhausted attempts still pause,
reset, and return.
- Around line 124-126: Update _maxPlays() to accept audio.maxPlays only when it
is a positive integer, rejecting fractional and other invalid values. Return 0
for invalid configuration so playback remains unlimited, while preserving the
existing valid-limit behavior.
In `@client/text.js`:
- Around line 18-31: Fix the client build failure caused by the missing modal
module imported by client/help.js: either add the required
client/design-system/components/modal/modal.js implementation or update
client/help.js to import the correct existing modal module. Verify that npm run
build succeeds after resolving the import.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: b87828be-f7a1-448f-ba57-43f7fdd444d2
📒 Files selected for processing (4)
README.mdclient/completion.jsclient/games/audio-game.jsclient/text.js
| // Gist / meeting-notes mode grades on meaning coverage, so prepend the | ||
| // authored key points. The verbatim transcript is still included so the | ||
| // grader can verify captured facts (dates, numbers) against ground truth. | ||
| let keyPointsBlock = ''; | ||
| if (state.config.gradeMode === 'gist') { | ||
| const kp = state.config.keyPoints; | ||
| const kpText = Array.isArray(kp) | ||
| ? kp.map(p => `- ${p}`).join('\n') | ||
| : (typeof kp === 'string' ? kp : ''); | ||
| if (kpText) keyPointsBlock = `Key Points:\n${kpText}\n\n`; | ||
| } | ||
| const transcripts = | ||
| keyPointsBlock + |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
test -f .codesignal/extract_solution.py
rg -n -C 6 \
'Key Points|Expected Transcription|Submitted Transcription|parseStatsText' \
.codesignal/extract_solution.py clientRepository: CodeSignal/learn_typing-simulator-game
Length of output: 176
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "candidate files:"
git ls-files | rg '(^|/)(completion\.js|extract_solution\.py|stats|README\.md|AGENTS\.md|config\.json|server\.js)$|server\.js$' || true
echo
echo "completion.js outline/sections:"
if test -f client/completion.js; then
ast-grep outline client/completion.js --view expanded || true
wc -l client/completion.js
sed -n '1,160p' client/completion.js
fi
echo
echo "python/codesignal files:"
git ls-files | rg '(^|/)\.codesignal/|extract_solution\.py|.*stats.*|solution' || true
if test -d .codesignal; then
find .codesignal -maxdepth 3 -type f | sort
if test -f .codesignal/extract_solution.py; then
wc -l .codesignal/extract_solution.py
sed -n '1,220p' .codesignal/extract_solution.py
fi
fi
echo
echo "stats-related occurrences:"
rg -n -C 4 'stats|recorded|save|statistics|parseStatsText|Expected Transcription|Submitted Transcription|Key Points' . || trueRepository: CodeSignal/learn_typing-simulator-game
Length of output: 50394
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "extract_solution.py excerpt:"
wc -l extract_solution.py
sed -n '85,180p' extract_solution.py
echo
echo "stats.js parser section:"
sed -n '375,409p' client/stats.js
echo
echo "all occurrences with exact key names:"
rg -n -C 3 '(^Key Points:|Expected Transcription:|Submitted Transcription:|Key Points:|Expected Transcription:|Submitted Transcription:)' . || true
echo
echo "read-only parser probes from source files:"
python3 - <<'PY'
import re
parser = re.compile(
r'Status:\s*(win|lose)|Score:\s*(\d+)|Total Errors Made:\s*(\d+)|Errors Left \(Unfixed\):\s*(\d+)|Total Time:\s*([\d.]+)\s*seconds|Accuracy:\s*([\d.]+)%|Speed:\s*([\d.]+)\s*words per minute|Generated:\s*(.+)',
re.I | re.M
)
samples = [
'Key Points:\n- first\n\nExpected Transcription:\nabc\n\nSubmitted Transcription:\nabz\n\nGenerated: now\n',
'Expected Transcription:\nabc\n\nKey Points:\n- first\n\nGenerated: now\n',
]
for i, text in enumerate(samples, 1):
stats = {}
for line in text.splitlines():
m = parser.search(line)
if m and m.lastindex:
if m.lastindex == 1:
stats['status'] = m.group(1).lower()
elif m.lastindex == 2:
stats['score'] = int(m.group(2))
elif m.lastindex == 3:
stats['totalErrors'] = int(m.group(3))
elif m.lastindex == 4:
stats['errorsLeft'
]
PYRepository: CodeSignal/learn_typing-simulator-game
Length of output: 4825
Emit the saved transcript block from extract_solution.py.
completion.js appends Key Points:, Expected Transcription:, and Submitted Transcription: after Generated: when saving, but extract_solution.py reads and prints the same stats.txt. Add parsing and printing for these fields, ordered before/with the existing generated stats output, so the grader receives the reference key points and transcript comparison data.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@client/completion.js` around lines 29 - 41, Update extract_solution.py to
parse the saved Key Points, Expected Transcription, and Submitted Transcription
blocks from stats.txt and print them before or alongside the existing Generated
output. Preserve the existing generated-stats parsing while ensuring all three
fields are emitted for the grader in the same labels and content written by
completion.js.
| if (state.config.gradeMode === 'gist') { | ||
| const kp = state.config.keyPoints; | ||
| const kpText = Array.isArray(kp) | ||
| ? kp.map(p => `- ${p}`).join('\n') | ||
| : (typeof kp === 'string' ? kp : ''); | ||
| if (kpText) keyPointsBlock = `Key Points:\n${kpText}\n\n`; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Restrict gist behavior to audio tasks.
Both branches check only state.config.gradeMode. A non-audio game with gradeMode: "gist" will receive a Key Points: transcript block and hide its accuracy and error cards.
Define and reuse isGistAudio as state.config.gameType === 'audio' && state.config.gradeMode === 'gist', or reject the invalid configuration during config loading.
Per README.md Line [42] and the PR objectives, gradeMode: "gist" is an audio/meeting-notes mode.
Also applies to: 137-147
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@client/completion.js` around lines 33 - 38, The gist-specific logic in the
completion flow must apply only to audio tasks. Define and reuse an isGistAudio
condition combining gameType === 'audio' with gradeMode === 'gist' for both the
Key Points block and the related accuracy/error-card branches, preserving
existing behavior for valid audio gist tasks.
| _maxPlays() { | ||
| const n = state.config.audio && state.config.audio.maxPlays; | ||
| return (typeof n === 'number' && n > 0) ? n : 0; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate and inspect the relevant audio-game implementation and related call sites.
if [ -f client/games/audio-game.js ]; then
echo "== lines 100-160 client/games/audio-game.js =="
sed -n '100,160p' client/games/audio-game.js | nl -ba -v100
echo
echo "== occurrences of _maxPlays / playsUsed / maxPlays =="
rg -n "_maxPlays|playsUsed|maxPlays|audio\\.maxPlays" client/games/audio-game.js client -g '*.js' || true
else
echo "client/games/audio-game.js not found"
git ls-files | rg '(^|[/])audio-game\.js$|audio|game' || true
fi
echo
echo "== behavioral probe for current _maxPlays and integer increment behavior =="
node - <<'JS'
function maxPlaysConfig(config) {
const n = config.audio && config.config.audio && config.config.audio.maxPlays;
// This mimics the exact shown expression.
return (typeof n === 'number' && n > 0) ? n : 0;
}
const cases = [
{},
{config:{audio:{}}},
{config:{audio:{maxPlays:2}}},
{config:{audio:{maxPlays:2.5}}},
{config:{audio:{maxPlays:2.9}}},
{config:{audio:{maxPlays:0}}},
{config:{audio:{maxPlays:-1}}},
];
for (const config of cases) {
const maxPlays = maxPlaysConfig(config);
let playsUsed = 0;
let started = 0;
if (maxPlays <= 0) {
started = 3; // no configured integer limit
} else {
while (playsUsed < maxPlays) {
playsUsed++;
started++;
}
}
console.log(JSON.stringify(config), "maxPlays=", maxPlays, "started=", started);
}
JSRepository: CodeSignal/learn_typing-simulator-game
Length of output: 263
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== lines 100-160 client/games/audio-game.js =="
sed -n '100,160p' client/games/audio-game.js
echo
echo "== occurrences of _maxPlays / playsUsed / maxPlays / audio.maxPlays =="
rg -n "_maxPlays|playsUsed|maxPlays|audio\.maxPlays" client/games/audio-game.js client -g '*.js' || true
echo
echo "== behavioral probe for current _maxPlays and integer increment behavior =="
node - <<'JS'
function maxPlays(config) {
const n = config.audio && config.audio.maxPlays;
return (typeof n === 'number' && n > 0) ? n : 0;
}
function startsUntilReached(maxPlays, maxLoops) {
let playsUsed = 0;
let started = 0;
while (playsUsed < maxPlays && started < maxLoops) {
playsUsed++;
started++;
}
return started;
}
const cases = [
{},
{audio:{}},
{audio:{maxPlays:2}},
{audio:{maxPlays:2.5}},
{audio:{maxPlays:2.9}},
{audio:{maxPlays:0}},
{audio:{maxPlays:-1}},
];
for (const audio of cases) {
const max = maxPlays(audio);
const started = startsUntilReached(max, 100);
console.log(JSON.stringify({audio}), "computed=", max, "integer_starts=", started);
}
JSRepository: CodeSignal/learn_typing-simulator-game
Length of output: 4470
Require a positive integer for audio.maxPlays.
_maxPlays() accepts fractional values while playsUsed increments as an integer, so maxPlays: 2.5 allows three fresh playback starts. Validate audio.maxPlays as a positive integer and treat invalid configured limits as unlimited playback.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@client/games/audio-game.js` around lines 124 - 126, Update _maxPlays() to
accept audio.maxPlays only when it is a positive integer, rejecting fractional
and other invalid values. Return 0 for invalid configuration so playback remains
unlimited, while preserving the existing valid-limit behavior.
| const atStart = this.audioEl.currentTime < 0.3; | ||
| if (atStart && !this.listenInProgress) { | ||
| if (this.playsUsed >= max) { | ||
| this.audioEl.pause(); | ||
| try { this.audioEl.currentTime = 0; } catch (e) { /* ignore */ } | ||
| this._resetStatus(); | ||
| return; | ||
| } | ||
| this.playsUsed++; | ||
| this.listenInProgress = true; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Prevent seeking from bypassing the playback limit.
A user can seek to 0.3 seconds or later before pressing play. Line 136 then does not consume a play. A user can also seek back to the beginning during an active listen and replay without a new play event.
Count a start when listenInProgress is false regardless of currentTime. Track backward seeks, or disable replay seeking, so a restart also consumes a play or is blocked.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@client/games/audio-game.js` around lines 135 - 145, Update the playback-start
logic around audioEl, atStart, and listenInProgress so a new play attempt
consumes a play whenever listenInProgress is false, regardless of currentTime.
Also detect backward seeks during an active listen or prevent replay seeking,
ensuring restarting from the beginning cannot bypass the max limit and that
exhausted attempts still pause, reset, and return.
| let raw = await response.text(); | ||
| // Normalize line endings so \r\n / \r behave like \n. | ||
| raw = raw.replace(/\r\n?/g, '\n'); | ||
| if (state.config.gameType === 'racing') { | ||
| // Racing is a single-line track — flatten newlines to spaces. | ||
| raw = raw.replace(/\n/g, ' '); | ||
| } else { | ||
| // Preserve paragraph breaks for multi-paragraph texts. Trim trailing spaces | ||
| // on each line and collapse 3+ blank lines to a single blank line so the | ||
| // reference stays clean and typeable. | ||
| raw = raw.replace(/[ \t]+$/gm, '').replace(/\n{3,}/g, '\n\n'); | ||
| } | ||
| // Trim trailing whitespace/newlines from the whole passage. | ||
| state.originalText = raw.trimEnd(); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
npm run buildRepository: CodeSignal/learn_typing-simulator-game
Length of output: 9108
🏁 Script executed:
#!/bin/bash
set -u
echo "Tracked files matching modal/help:"
git ls-files | rg '(^|/)(modal\.js$|help\.js$|index\.html$|text\.js$|completion\.js$|config\.json$)$|design-system$' || true
echo
echo "File sizes/existence:"
for f in client/help.js client/text.js client/index.html client/completion.js client/config.json client/design-system/components/modal/modal.js; do
if [ -e "$f" ]; then
w=$(wc -l < "$f")
echo "exists $f ($w lines)"
else
echo "missing $f"
fi
done
echo
echo "Relevant client/help.js:"
if [ -f client/help.js ]; then sed -n '1,80p' client/help.js | cat -n; fi
echo
echo "Tracked files under client/design-system/components/modal:"
git ls-files client/design-system/components/modal || true
echo
echo "Diff stat/name-status:"
git diff --stat || true
git diff --name-status || trueRepository: CodeSignal/learn_typing-simulator-game
Length of output: 2237
Do not merge until the client build succeeds.
npm run build fails because client/help.js imports ./design-system/components/modal/modal.js, but client/design-system/components/modal/modal.js is not present in the repository. This blocks the required JS build before merge.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@client/text.js` around lines 18 - 31, Fix the client build failure caused by
the missing modal module imported by client/help.js: either add the required
client/design-system/components/modal/modal.js implementation or update
client/help.js to import the correct existing modal module. Verify that npm run
build succeeds after resolving the import.
Source: Coding guidelines
Changes
① Multi-paragraph text
loadText()now preserves newlines (blank line = paragraph break) for every mode exceptracing(single-line track → flattened). It normalizes line endings, trims trailing spaces per line, and collapses 3+ blank lines to one. Rendering (\n → <br>), the input reconcile, completion counting, and cursor auto-scroll already handle newlines.② Long / multi-screen
③ Complex punctuation
Typeable punctuation —
()[];:/quotes — types/renders/compares exactly. (En/em dashes intentionally out of scope: too hard to type/distinguish in a simulator.)④ Gist / meeting-notes mode (
gradeMode: "gist")An
audiotask graded on meaning coverage, not verbatim match:includeTranscriptemits aKey Points:block (fromconfig.keyPoints) before the verbatim transcript, so the grader scores coverage and can verify captured facts (dates/numbers) against ground truth.audio.maxPlays: limits playback (a fresh play from the start consumes one; resuming a paused clip does not). For the meeting-notes task the clip plays only twice.stats.txtcontainsKey Points:+Expected Transcription:+Submitted Transcription:.Follow-up (deployment, not in this repo)
The base task's
.codesignal/extract_solution.pymust print the newKey Points:block. Snippet to add after the existing transcript block:Files
client/text.js— preserve newlines (multi-paragraph)client/games/audio-game.js—maxPlayslisten limit + gist-aware statusclient/completion.js— gistKey Points:output + hide accuracy/error cardsREADME.md— documentgradeMode,keyPoints,audio.maxPlays, multi-paragraph🤖 Generated with Claude Code