[Spike] Duolingo-style progression visualization — optional, menu-launched, sounds, younger audience #330

Closed
opened 2026-07-02 00:52:03 +00:00 by spikerj · 4 comments
Owner

Spike under epic #323.

Concept: we have a progression system (per-user lesson completion, prerequisites, auto-awarded skills, tree-of-knowledge dashboard) but no visualization that makes it engaging for a younger audience. Duolingo's UI is the reference: simple, playful, sounds, immediate feedback. This must be purely optional — launched from the menu, never forced into the lesson flow; the existing playground experience is unchanged for students who don't opt in.

Existing data to build on (no new progression backend expected):

  • GET /api/Lessons/progress — completed lesson numbers per user (UserLessonProgress: attempts, best execution time, completion timestamps)
  • Skills: SkillDefinition (bronze/silver/gold tiers) + EarnedSkill, auto-awards via CheckAutoAwardsCommandHandler (e.g. lessons_completed_in_range)
  • Existing visuals: lesson sidebar checkmarks/locks/difficulty stars, skill-earned toast stack in language-runner.html, tree-of-knowledge dashboard (category cards, proficiency rings, progress bars)
  • Audio prior art in the codebase: dungeon-crawler/audio-system.ts, github-snake, chess — there is no lesson-flow audio today

Questions the spike must answer:

  • Visualization form — path/map (Duolingo-style winding trail per language curriculum) vs constellation/tree extension of tree-of-knowledge; how prerequisites and the completion-ratio locks (#329) render on it
  • Feedback moments — lesson complete, skill earned, streak milestones: celebration animation + optional sound; where sounds live (small local assets, no CDN per platform policy) and a global mute/reduced-motion story (accessibility)
  • Streaks — do we add streak tracking (new backend: last-activity-day per user) or defer? Recommendation required
  • Entry point — menu placement (Learn menu item, e.g. "My Journey"), route (public with auth-gated data, like /tree-of-knowledge), and whether anonymous users see the visualization of the free/earned slice (ties into #329)
  • Audience fit — visual language appropriate for younger students without alienating adults; respects existing theme
  • Effort estimate — library needs (animation: CSS/Web Animations vs Lottie; audio: plain HTMLAudio vs WebAudio) with the platform's no-third-party-CDN constraint

Deliverable: design doc + clickable prototype (Storybook story or dev-tools route) + implementation sub-tickets. No production wiring in the spike.

Spike under epic #323. **Concept:** we have a progression system (per-user lesson completion, prerequisites, auto-awarded skills, tree-of-knowledge dashboard) but no visualization that makes it *engaging* for a younger audience. Duolingo's UI is the reference: simple, playful, sounds, immediate feedback. This must be **purely optional** — launched from the menu, never forced into the lesson flow; the existing playground experience is unchanged for students who don't opt in. **Existing data to build on (no new progression backend expected):** - `GET /api/Lessons/progress` — completed lesson numbers per user (`UserLessonProgress`: attempts, best execution time, completion timestamps) - Skills: `SkillDefinition` (bronze/silver/gold tiers) + `EarnedSkill`, auto-awards via `CheckAutoAwardsCommandHandler` (e.g. `lessons_completed_in_range`) - Existing visuals: lesson sidebar checkmarks/locks/difficulty stars, skill-earned toast stack in `language-runner.html`, tree-of-knowledge dashboard (category cards, proficiency rings, progress bars) - Audio prior art in the codebase: `dungeon-crawler/audio-system.ts`, github-snake, chess — there is **no** lesson-flow audio today **Questions the spike must answer:** - [ ] **Visualization form** — path/map (Duolingo-style winding trail per language curriculum) vs constellation/tree extension of tree-of-knowledge; how prerequisites and the completion-ratio locks (#329) render on it - [ ] **Feedback moments** — lesson complete, skill earned, streak milestones: celebration animation + optional sound; where sounds live (small local assets, no CDN per platform policy) and a global mute/reduced-motion story (accessibility) - [ ] **Streaks** — do we add streak tracking (new backend: last-activity-day per user) or defer? Recommendation required - [ ] **Entry point** — menu placement (Learn menu item, e.g. "My Journey"), route (public with auth-gated data, like `/tree-of-knowledge`), and whether anonymous users see the visualization of the free/earned slice (ties into #329) - [ ] **Audience fit** — visual language appropriate for younger students without alienating adults; respects existing theme - [ ] **Effort estimate** — library needs (animation: CSS/Web Animations vs Lottie; audio: plain HTMLAudio vs WebAudio) with the platform's no-third-party-CDN constraint **Deliverable:** design doc + clickable prototype (Storybook story or dev-tools route) + implementation sub-tickets. No production wiring in the spike.
spikerj added the enhancement label 2026-07-02 00:52:03 +00:00
Author
Owner

Spike deliverable — "My Journey" progression visualization

Recon done against the live code. Recommendations below answer every checklist box; two decisions flagged for your sign-off at the end. No production wiring in this spike — deliverable is this doc + a dev-tools prototype route + the sub-tickets filed below.

What already exists (build-on, don't rebuild)

  • Progress data: GET /api/Lessons/progress → completed lesson numbers per user (UserLessonProgress: attempts, best exec time, completion timestamps). No new progression backend needed for v1.
  • Skills: SkillDefinition (bronze/silver/gold) + EarnedSkill, auto-award via CheckAutoAwardsCommandHandler (lessons_completed_in_range). Skill-earned toast stack already lives in language-runner.html.
  • Dashboard pattern: tree-of-knowledge.component.ts — signals-based, HttpClient to /activity/*, proficiency rings + category cards, parent/child compare. /tree-of-knowledge-style route (public shell, auth-gated data) is the template to copy.
  • Audio prior art: dungeon-crawler/audio-system.ts is a THREE.js AudioLoader/AudioListener system pulling assets/mysterious-maze/*.wav|mp3. Confirms the local-assets, no-CDN policy — but it's far too heavy to reuse for lesson feedback (drags in three). Lesson flow has zero audio today.
  • Menu home: Learn dropdown (menu-bar.component.html) already hosts auth-gated /reading-journey (auto_stories) and /geography (public). "My Journey" slots in here cleanly.

1. Visualization form → winding path/map, per-language

A Duolingo-style vertical winding trail of lesson "stops," one track per language curriculum (C, C++, C#, Python, JavaScript — matches the standardized playground set). Rationale over a constellation/tree:

  • Lessons are an ordered sequence with prerequisites — a linear-ish trail reads as "where am I / what's next" instantly for a young audience; a tree reads as a reference map (which is exactly what tree-of-knowledge already is, so we'd be duplicating it).
  • Keeps tree-of-knowledge as the analytical dashboard and My Journey as the playful view — no overlap.

Node states: completed (filled, checkmark, language color), current (pulsing ring, "START"), available (outline), locked-prereq (padlock, greyed), locked-horizon (the #329 completion-ratio lock — distinct "create an account to continue" styling, not a plain padlock). Prereq edges render as the connecting trail segment; a horizon lock renders as a "gate" band across the trail with a sign-in CTA. This is the single integration point with #329 and is guarded behind a feature flag so the two spikes ship independently.

Rendering: inline SVG trail + positioned DOM nodes (Angular @for over computed node models). No canvas/WebGL — keeps it accessible (real focusable buttons), themeable via existing CSS vars, and cheap.

2. Feedback moments → CSS/Web Animations + tiny WebAudio blips

Three moments: lesson complete, skill earned (reuse existing toast, add optional sound), streak milestone (only if we adopt streaks — see #3).

  • Animation: Web Animations API + CSS keyframes (node fill sweep, checkmark draw, confetti burst as CSS particles). No Lottie — avoids a runtime dep and JSON asset weight for effects this simple.
  • Audio: a small standalone JourneyAudioService using a single AudioContext to synthesize short blips (success arpeggio, lock thud, level-up) — zero audio assets to ship, zero CDN, ~1KB of code. If we later want richer sound, drop .wavs into assets/journey/ following the dungeon-crawler pattern. Do not reuse the THREE-based AudioSystem.
  • Global controls (accessibility, required): a JourneyPrefsService persisting sound: on/off and motion: full/reduced to localStorage, initialized from window.matchMedia('(prefers-reduced-motion: reduce)') — note: the repo has no prefers-reduced-motion handling anywhere today, so this establishes the pattern. Reduced motion → no confetti/pulse, instant state changes; muted → no AudioContext calls. Toggles live in the My Journey header.

3. Streaks → defer to a fast-follow, not v1

Streaks need a new backend field (last-activity-day per user + a daily rollover job) and a whole "don't lose your streak" pressure loop that deserves its own product decision (and arguably conflicts with a low-pressure kids' tone). Recommendation: ship the path + feedback without streaks. File it as a separate ticket (done below, #NEW-streaks) so the visualization can render a streak flame later without rework — the node model reserves the slot.

4. Entry point → Learn ▸ My Journey, route /my-journey

  • Menu: new mat-menu-item in the Learn dropdown, emoji_events/map icon, gated like siblings (@if (isLoggedIn) for the data; the route itself renders a public shell).
  • Route: /my-journey (public component, auth-gated data fetch) mirroring /tree-of-knowledge.
  • Anonymous: with #329, anonymous users see the trail of the free/earned slice with the horizon gate rendered inline ("you've unlocked 6 of N — make an account to keep going"). Without #329 merged, anonymous simply sees the sign-in CTA. Flag-gated so it degrades gracefully.

5. Audience fit

Playful but theme-respecting: reuse existing language brand colors + CSS custom props, rounded friendly nodes, micro-animations. No mascot, no cartoon restyle of the whole app — the playground stays untouched. Adults reading the same trail just see a clean progress map. Everything opt-in via the menu; nothing injected into the lesson flow.

6. Effort estimate

  • Frontend (bulk): ~M. New feature-my-journey lib (component + node-model computed from /Lessons/progress + curriculum order), JourneyAudioService (S), JourneyPrefsService + reduced-motion (S), menu + route wiring (XS), i18n keys en/es (S), Storybook/dev-tools prototype (S), unit tests (S).
  • Backend: ~XS for v1 — none required if curriculum order + progress already expose enough; a thin GET /api/Lessons/journey (curriculum order + completion + horizon in one call) is a nice-to-have to avoid client-side stitching. Streaks = separate M if adopted.
  • Deps added: none. WAAPI + AudioContext are platform built-ins.

Decisions I need from you

  1. Streaks in or out of v1? My rec: out (fast-follow ticket filed).
  2. Thin /api/Lessons/journey aggregation endpoint, or stitch client-side from the existing /progress + curriculum? My rec: thin endpoint — one round trip, keeps horizon logic (#329) server-authoritative.

Implementation sub-tickets filed below; will link once created.

## Spike deliverable — "My Journey" progression visualization Recon done against the live code. Recommendations below answer every checklist box; two decisions flagged for your sign-off at the end. No production wiring in this spike — deliverable is this doc + a dev-tools prototype route + the sub-tickets filed below. ### What already exists (build-on, don't rebuild) - **Progress data:** `GET /api/Lessons/progress` → completed lesson numbers per user (`UserLessonProgress`: attempts, best exec time, completion timestamps). No new progression backend needed for v1. - **Skills:** `SkillDefinition` (bronze/silver/gold) + `EarnedSkill`, auto-award via `CheckAutoAwardsCommandHandler` (`lessons_completed_in_range`). Skill-earned toast stack already lives in `language-runner.html`. - **Dashboard pattern:** `tree-of-knowledge.component.ts` — signals-based, `HttpClient` to `/activity/*`, proficiency rings + category cards, parent/child compare. `/tree-of-knowledge`-style route (public shell, auth-gated data) is the template to copy. - **Audio prior art:** `dungeon-crawler/audio-system.ts` is a THREE.js `AudioLoader`/`AudioListener` system pulling `assets/mysterious-maze/*.wav|mp3`. Confirms the **local-assets, no-CDN** policy — but it's far too heavy to reuse for lesson feedback (drags in `three`). Lesson flow has **zero audio today.** - **Menu home:** `Learn` dropdown (`menu-bar.component.html`) already hosts auth-gated `/reading-journey` (`auto_stories`) and `/geography` (`public`). "My Journey" slots in here cleanly. --- ### 1. Visualization form → **winding path/map, per-language** A Duolingo-style vertical winding trail of lesson "stops," one track per language curriculum (C, C++, C#, Python, JavaScript — matches the standardized playground set). Rationale over a constellation/tree: - Lessons are an **ordered sequence with prerequisites** — a linear-ish trail reads as "where am I / what's next" instantly for a young audience; a tree reads as a reference map (which is exactly what tree-of-knowledge already is, so we'd be duplicating it). - Keeps tree-of-knowledge as the analytical dashboard and My Journey as the *playful* view — no overlap. **Node states:** `completed` (filled, checkmark, language color), `current` (pulsing ring, "START"), `available` (outline), `locked-prereq` (padlock, greyed), `locked-horizon` (the #329 completion-ratio lock — distinct "create an account to continue" styling, not a plain padlock). Prereq edges render as the connecting trail segment; a horizon lock renders as a "gate" band across the trail with a sign-in CTA. This is the single integration point with #329 and is guarded behind a feature flag so the two spikes ship independently. **Rendering:** inline SVG trail + positioned DOM nodes (Angular `@for` over computed node models). No canvas/WebGL — keeps it accessible (real focusable buttons), themeable via existing CSS vars, and cheap. ### 2. Feedback moments → **CSS/Web Animations + tiny WebAudio blips** Three moments: **lesson complete**, **skill earned** (reuse existing toast, add optional sound), **streak milestone** (only if we adopt streaks — see #3). - **Animation:** Web Animations API + CSS keyframes (node fill sweep, checkmark draw, confetti burst as CSS particles). **No Lottie** — avoids a runtime dep and JSON asset weight for effects this simple. - **Audio:** a small standalone `JourneyAudioService` using a single `AudioContext` to synthesize short blips (success arpeggio, lock thud, level-up) — **zero audio assets to ship, zero CDN**, ~1KB of code. If we later want richer sound, drop `.wav`s into `assets/journey/` following the dungeon-crawler pattern. Do **not** reuse the THREE-based `AudioSystem`. - **Global controls (accessibility, required):** a `JourneyPrefsService` persisting `sound: on/off` and `motion: full/reduced` to `localStorage`, initialized from `window.matchMedia('(prefers-reduced-motion: reduce)')` — note: the repo has **no** `prefers-reduced-motion` handling anywhere today, so this establishes the pattern. Reduced motion → no confetti/pulse, instant state changes; muted → no `AudioContext` calls. Toggles live in the My Journey header. ### 3. Streaks → **defer to a fast-follow, not v1** Streaks need a new backend field (last-activity-day per user + a daily rollover job) and a whole "don't lose your streak" pressure loop that deserves its own product decision (and arguably conflicts with a low-pressure kids' tone). **Recommendation: ship the path + feedback without streaks.** File it as a separate ticket (done below, #NEW-streaks) so the visualization can render a streak flame later without rework — the node model reserves the slot. ### 4. Entry point → **`Learn ▸ My Journey`, route `/my-journey`** - Menu: new `mat-menu-item` in the `Learn` dropdown, `emoji_events`/`map` icon, gated like siblings (`@if (isLoggedIn)` for the data; the route itself renders a public shell). - Route: `/my-journey` (public component, auth-gated data fetch) mirroring `/tree-of-knowledge`. - **Anonymous:** with #329, anonymous users see the trail of the free/earned slice with the horizon gate rendered inline ("you've unlocked 6 of N — make an account to keep going"). Without #329 merged, anonymous simply sees the sign-in CTA. Flag-gated so it degrades gracefully. ### 5. Audience fit Playful but theme-respecting: reuse existing language brand colors + CSS custom props, rounded friendly nodes, micro-animations. No mascot, no cartoon restyle of the whole app — the playground stays untouched. Adults reading the same trail just see a clean progress map. Everything opt-in via the menu; nothing injected into the lesson flow. ### 6. Effort estimate - **Frontend (bulk):** ~M. New `feature-my-journey` lib (component + node-model computed from `/Lessons/progress` + curriculum order), `JourneyAudioService` (S), `JourneyPrefsService` + reduced-motion (S), menu + route wiring (XS), i18n keys en/es (S), Storybook/dev-tools prototype (S), unit tests (S). - **Backend:** ~XS for v1 — none required if curriculum order + progress already expose enough; a thin `GET /api/Lessons/journey` (curriculum order + completion + horizon in one call) is a nice-to-have to avoid client-side stitching. Streaks = separate M if adopted. - **Deps added:** none. WAAPI + `AudioContext` are platform built-ins. --- ### Decisions I need from you 1. **Streaks in or out of v1?** My rec: **out** (fast-follow ticket filed). 2. **Thin `/api/Lessons/journey` aggregation endpoint, or stitch client-side** from the existing `/progress` + curriculum? My rec: **thin endpoint** — one round trip, keeps horizon logic (#329) server-authoritative. Implementation sub-tickets filed below; will link once created.
Author
Owner

Sub-tickets filed:

  • #333 — frontend: winding-path visualization, audio + prefs services, menu/route, prototype (the bulk of the work)
  • #334 — backend: optional thin GET /api/Lessons/journey aggregation (dropped if we stitch client-side)
  • #335 — fast-follow: daily streaks + streak-milestone feedback (deferred from v1)

Spike is complete pending your two decisions above (streaks in/out, aggregation endpoint vs client-side stitch). #333 can start immediately once you bless it; #334 and #335 gate on those two decisions.

Sub-tickets filed: - **#333** — frontend: winding-path visualization, audio + prefs services, menu/route, prototype (the bulk of the work) - **#334** — backend: optional thin `GET /api/Lessons/journey` aggregation (dropped if we stitch client-side) - **#335** — fast-follow: daily streaks + streak-milestone feedback (deferred from v1) Spike is complete pending your two decisions above (streaks in/out, aggregation endpoint vs client-side stitch). #333 can start immediately once you bless it; #334 and #335 gate on those two decisions.
Author
Owner

Decisions locked (@spikerj):

  1. Streaks — IN v1. #335 folds into the frontend build (#333); backend streak fields move into #334. #335 closed as folded.
  2. Data source — thin GET /api/Lessons/journey endpoint (#334), keeping the #329 horizon server-authoritative.

Starting implementation: #334 (backend) first, then #333 (frontend, streaks included).

**Decisions locked** (@spikerj): 1. **Streaks — IN v1.** #335 folds into the frontend build (#333); backend streak fields move into #334. #335 closed as folded. 2. **Data source — thin `GET /api/Lessons/journey` endpoint (#334)**, keeping the #329 horizon server-authoritative. Starting implementation: #334 (backend) first, then #333 (frontend, streaks included).
Author
Owner

Spike delivered and implemented. Duolingo-style optional progression visualization shipped via #333 (frontend feature-my-journey, PR #98) and #334 (backend /journey aggregation + streaks, PR #55), both merged to master. Closing the spike.

Spike delivered and implemented. Duolingo-style optional progression visualization shipped via #333 (frontend `feature-my-journey`, PR #98) and #334 (backend `/journey` aggregation + streaks, PR #55), both merged to `master`. Closing the spike.
Sign in to join this conversation.