Skip to content

Exercise UX fixes (issue #2885 items 4, audio freeze, page update)#2889

Merged
lifeart merged 6 commits intomasterfrom
feat/exercise-ux-fixes-2885
Apr 18, 2026
Merged

Exercise UX fixes (issue #2885 items 4, audio freeze, page update)#2889
lifeart merged 6 commits intomasterfrom
feat/exercise-ux-fixes-2885

Conversation

@lifeart
Copy link
Copy Markdown
Collaborator

@lifeart lifeart commented Apr 17, 2026

Summary

Addresses the exercise-screen items from issue #2885 that weren't in the landing PR #2832.

1. Remove Interact→Solve auto-transition (item 4)

  • exerciseSequenceTask stops after INTERACT; Solve is entered manually.
  • Listen→Interact auto-transition is kept.
  • Solve button lights up in the existing "next" style the moment every answer option has been heard (allOptionsHeard).
  • New native tooltip on Solve: «Нажмите, когда будете готовы перейти к решению» / "Click when you are ready to start solving".

2. Fix silent words and mid-task audio freeze

User report: after 3+ words, audio "freezes" until the mouse moves; individual words can also come out silent.

Root cause in services/audio.ts playTask: the playback loop used a wall-clock setTimeout to wait for each word. Browsers (Safari, and Chrome under timer throttling) suspend idle AudioContexts. When that happens, source.start(0) queues playback instead of emitting sound, yet the wall-clock timer keeps firing — so the loop advances right over silent clips and only the user's mouse gesture resumes the context.

Fix:

  • Resume a suspended AudioContext before starting each native source.
  • Await the AudioBufferSourceNode.onended event (with duration + 1s safety timeout) instead of trusting setTimeout to match real playback.
  • Tone-based signal exercises (no onended) still use the timeout — that path was never reported as affected.

3. Persist exercise completion across navigation

User report: the green check disappears on the next visit to a subgroup after completing an exercise.

Root cause in controllers/group/series/subgroup/exercise.ts enableNextExercise: only mutated isManuallyCompleted on the in-memory record. tasksManager.completedExerciseIds — the durable source used by the subgroup's availability calc task — was populated solely from loadTodayCompletedExercises on app boot / login and never on in-session completion.

Fix: add the just-completed exercise id to completedExerciseIds (new Set, so @tracked picks it up) as part of enableNextExercise.

Files

  • app/components/task-player/index.gts — drop trailing setMode(MODES.TASK); pass @interactReady={{this.allOptionsHeard}} to ExerciseSteps.
  • app/components/exercise-steps/index.gts — accept @interactReady, style Solve as "next", add title for the hint.
  • app/services/audio.ts — resume AudioContext if suspended; await onended for native sources.
  • app/controllers/group/series/subgroup/exercise.ts — add completed id to tasksManager.completedExerciseIds.
  • translations/en-us.yaml, translations/ru-ru.yaml — add control_exercises.solve_hint.
  • tests/unit/components/task-player/heard-words-test.js — update to assert listen→interact only (no TASK).

Still out of scope

  • Item 3 (user instructions «Как заниматься с помощью сайта») — needs design input on placement (floating button on every exercise screen, modal, or dedicated route).
  • Item 5 (CRM volunteer presentation ideas) — content-driven, partially covered by landing PR Update landing per latest BrainUp presentation #2832.

Test plan

  • Start a Words exercise → Listen auto-starts → Interact auto-starts.
  • Hear every option in Interact → Solve button gets the "next" glow but does not auto-advance.
  • Click Solve manually → Task mode runs as before.
  • Hover Solve before all words heard → tooltip visible.
  • Long task (≥4 words) in Safari → every word plays, no silent gaps, no freeze until mouse movement.
  • Complete an exercise → return to subgroup → leave subgroup → come back → completed exercise still shows green check.
  • pnpm test — unit tests for task-player still green.

🤖 Generated with Claude Code

Addresses item 4 of issue #2885: users reported the implicit jump from
Repeat (Interact) into Solve (Task) was confusing and hid the moment
when the step actually changed.

The Listen → Interact auto-transition is kept. Solve is now entered
only when the user clicks the Solve button; it lights up in the
"next" style as soon as every option has been heard, and a title
tooltip hints that the step can be started when ready.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

Frontend test coverage: 71.55% (+0.13% compared to 71.42% on base)

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 17, 2026

Gradle Unit and Integration Test Results

523 tests  ±0   521 ✔️ ±0   37s ⏱️ +2s
116 suites ±0       2 💤 ±0 
116 files   ±0       0 ±0 

Results for commit ae0d611. ± Comparison against base commit 27cb347.

♻️ This comment has been updated with latest results.

lifeart and others added 2 commits April 18, 2026 00:06
Addresses the audio bug in issue #2885: users reported that after 3+
words, audio "freezes" until the mouse moves, and individual words can
go silent.

Root cause: the playback loop used a wall-clock setTimeout to wait for
each word to finish. Browsers (Safari, and Chrome under timer
throttling) suspend idle AudioContexts. When that happens,
source.start(0) queues playback but never produces sound until a user
gesture resumes the context — yet the wall-clock timer keeps firing,
so the loop advances right over the silent clips.

Fix:
- Resume a suspended AudioContext before starting each native source,
  so queued playback does not pile up.
- Await the AudioBufferSourceNode onended event (with duration+1s as a
  safety fallback) instead of trusting setTimeout to match real
  playback duration.

The Tone-based path (signal exercises, no onended) still uses timeout;
this path was never reported as affected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses the "fix update page after task done" item of issue #2885:
after completing an exercise, the green check disappeared if the user
navigated away and came back.

Root cause: enableNextExercise only mutated isManuallyCompleted on the
in-memory record. tasksManager.completedExerciseIds was the durable
source used by the subgroup's exerciseAvailabilityCalculationTask, but
it was populated solely from loadTodayCompletedExercises on app boot
and login — never when the user finished an exercise.

Fix: add the just-completed exercise's id to completedExerciseIds as
part of enableNextExercise. A fresh Set is assigned so @Tracked picks
up the change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lifeart lifeart changed the title Remove Interact->Solve auto-transition (issue #2885 item 4) Exercise UX fixes (issue #2885 items 4, audio freeze, page update) Apr 18, 2026
@github-actions
Copy link
Copy Markdown

Frontend test coverage: 71.34% (-0.17% compared to 71.51% on base)

lifeart and others added 3 commits April 18, 2026 00:14
Tests cover the three behavior changes in the previous commits:

- enableNextExercise: marks current completed, enables next sibling,
  adds id to tasksManager.completedExerciseIds (new Set, coerced to
  string), preserves prior ids, skips update when id is null.
- playTask: awaits the source onended event for AudioBufferSourceNode
  (so it advances in real time, not on wall-clock); falls back to
  duration+1s timeout when onended never fires.
- ExerciseSteps: Solve button gets the "next" style when
  @interactReady is true during Interact, stays default otherwise,
  exposes the ready-when-you-are hint via title attribute.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- audio.ts: wrap source.start(0) in try/catch so a synchronous throw
  (closed context, re-started source) bails out of the word-loop
  instead of stranding it in the duration+1s safety net.
- audio-test.js: replace real-AudioContext probes with prototype-injected
  fake sources to avoid CI flake on headless Chrome suspended contexts.
  Tighten fallback lower bound from >=1000ms to >=1050ms so the extra
  second of margin is actually pinned. Add a test for the sync-start
  error path.
- heard-words-test.js: remove the tautological exerciseSequenceTask
  module — it re-defined and asserted the same fixture; the real body
  is already covered in component-test.js:272-347.
- exercise-test.js: combine null and undefined into one id-guard test
  so `!= null` semantics are pinned, not the accidental choice of one
  literal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Solve-NEXT test: walk through LISTEN first via a @Tracked state
  so setLastMode populates `modes` with both LISTEN and INTERACT.
  Without this, modeForTask stays DISABLED and taskBtnClass returns
  STATE_LOCKED regardless of interactReady.
- Title test: match the repo's existing ember-intl test pattern —
  setupIntl in this env returns the `t:<key>` placeholder when
  translations aren't loaded (see doctor-feedback/component-test.gjs).

Verified locally: tests 50-53 in the exercise-steps module all pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

Frontend test coverage: 72.47% (+1.00% compared to 71.47% on base)

@sonarqubecloud
Copy link
Copy Markdown

@lifeart lifeart marked this pull request as ready for review April 18, 2026 14:00
@lifeart lifeart merged commit 92b150e into master Apr 18, 2026
10 checks passed
@lifeart lifeart deleted the feat/exercise-ux-fixes-2885 branch April 18, 2026 14:00
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