Skip to content

Multi-paragraph text + gist (meeting-notes) mode [Phase 1] - #8

Open
skuzi wants to merge 1 commit into
mainfrom
feat/multiparagraph-and-gist-mode
Open

Multi-paragraph text + gist (meeting-notes) mode [Phase 1]#8
skuzi wants to merge 1 commit into
mainfrom
feat/multiparagraph-and-gist-mode

Conversation

@skuzi

@skuzi skuzi commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Changes

① Multi-paragraph text

loadText() now preserves newlines (blank line = paragraph break) for every mode except racing (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.

  • Verified: a 3-paragraph text renders as 6 visual lines; typing the whole passage (incl. newlines) completes at 100%.

② Long / multi-screen

  • Verified: a 2377-char / 12-paragraph text is scrollable (1603px content in a 487px viewport); after typing 70% the view auto-scrolled (scrollTop 891) with the cursor kept in view.

③ 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 audio task graded on meaning coverage, not verbatim match:

  • Accuracy/error stat cards are hidden (they compare to the transcript and would read misleadingly); only Speed / Time show.
  • includeTranscript emits a Key Points: block (from config.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.
  • Verified: play 1 → "Plays used: 1 of 2", play 2 → "used all 2 plays", play 3 blocked (paused); on Finish, the accuracy/error cards are hidden and stats.txt contains Key Points: + Expected Transcription: + Submitted Transcription:.

Follow-up (deployment, not in this repo)

The base task's .codesignal/extract_solution.py must print the new Key Points: block. Snippet to add after the existing transcript block:

kp = re.search(r'Key Points:\n(.*?)\n\nExpected Transcription:', content, re.DOTALL)
if kp:
    print("Key Points:")
    print(kp.group(1).strip())
    print()

Files

  • client/text.js — preserve newlines (multi-paragraph)
  • client/games/audio-game.jsmaxPlays listen limit + gist-aware status
  • client/completion.js — gist Key Points: output + hide accuracy/error cards
  • README.md — document gradeMode, keyPoints, audio.maxPlays, multi-paragraph

🤖 Generated with Claude Code

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>
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The 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: diegochine

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main changes: multi-paragraph text support and gist mode.
Description check ✅ Passed The description directly explains the implemented multi-paragraph text, gist mode, key-point output, and playback-limit changes.

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 16eb02f and 643d889.

📒 Files selected for processing (4)
  • README.md
  • client/completion.js
  • client/games/audio-game.js
  • client/text.js

Comment thread client/completion.js
Comment on lines +29 to +41
// 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 +

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ 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 client

Repository: 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' . || true

Repository: 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'
]
PY

Repository: 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.

Comment thread client/completion.js
Comment on lines +33 to +38
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`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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.

Comment on lines +124 to +126
_maxPlays() {
const n = state.config.audio && state.config.audio.maxPlays;
return (typeof n === 'number' && n > 0) ? n : 0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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);
}
JS

Repository: 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);
}
JS

Repository: 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.

Comment on lines +135 to +145
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;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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.

Comment thread client/text.js
Comment on lines +18 to +31
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();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

npm run build

Repository: 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 || true

Repository: 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

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