Repository navigation
Digital Learning companion - #1197
Open
sahasraa09 wants to merge 18 commits into
Open
sahasraa09 wants to merge 18 commits into
sahasraa09 wants to merge 18 commits into
Conversation
A persistent animated animal sidekick that grows from real learning progress.
Backend
- New companion module: controller, service, repository, validators, classes
- GET /api/companion/me — current state with live progress
- POST /api/companion/me — select or switch animal (panda/fox/penguin/dog/cat)
- CompanionService._computeStage returns proper stages 0-5
- CompanionRepository.upsert uses findOneAndUpdate({returnDocument: 'after'})
- SelectAnimalBody class with class-validator decorators
- CompanionController uses @jsoncontroller (auto body-parser) + IUser
- #companion/*.js import path alias
Frontend
- CompanionWidget component with always-mounted canvas + overlay picker
- companionRenderer.js — faithful prototype-embed port (320x360 canvas, requestAnimationFrame loop preserved)
- companionRenderer.d.ts — TS types for renderer
- companion-store (Zustand): companion, hasSelected, isLoading, fetchCompanion, selectAnimal
- companion.ts types: CompanionAnimal, CompanionMood, ICompanion
- StudentDashboard renders <CompanionWidget />
Scripts
- scripts/switch-test.js — end-to-end switch-animal test
- scripts/inspect-body.js — body-parsing debug helper
Switch-animal fix
- userId stored as IUser object in Mongo: fixed via _userId() helper
- Inline TS interface as @Body() doesn't deserialize: fixed via SelectAnimalBody class
- @controller vs @jsoncontroller (body parser): fixed by using @jsoncontroller
- Defensive animal whitelist at widget + factory + scene call sites
- store: don't wipe hasSelected on transient backend errors (would otherwise make users re-pick and overwrite their animal) - service: _getRealProgress paginates and filters out 100%-completed enrollments (was capped at 10 enrollments and counted completed courses, inflating the highest-progress stat) - service: _getRealQuizScore handles both string and ObjectId userId representations via \, matching SubmissionRepository (was throwing inside \ for string-stored userIds, silently returning 0 and never granting the graduation cap) - repository: atomic upsert via \, eliminating the read-then-write race where a concurrent create() call could clobber the original createdAt timestamp
- widget: poll the companion endpoint every 30s after mount so mood/progress/idle stay current without a page reload. The store API contract documented this interval but the call site only fetched once. Cleans up the interval on unmount. - service: rename _getIdleDays -> _daysSinceEnrollment and update its callers. The old name was a misnomer that survived from the prototype - it measures account age, not activity. A learner who enrolled two years ago and just finished a lesson today reads as 'idle 730'. The JSON wire field stays 'idleDays' to avoid breaking the frontend, but the server-side name now matches the semantics. A TODO tracks wiring this to real lastActiveAt. - controller: throw UnauthorizedError when _userId can't resolve a valid id from the authenticated user. Previously it returned '' which would silently target empty userIds in upserts/queries. - renderer: add an inline note explaining that the 'let animal, mood, prog, idleDays, forced, quizScore' declarations look like module-level state (they're not indented) but are actually lexically inside the createCompanionRenderer factory closure. A future contributor could mistake them for constants and hoist them, silently introducing cross-widget collisions.
…, visibility-aware poll - widget: remove debug console.log left from prototype (it fired on every prop change and polluted prod consoles). - renderer: drop 3 console.warn fallbacks for 'unknown animal' in the same loop. The fallback still happens silently (the SCENES lookup was always working), so the warnings were pure noise. - widget: catch uncaught errors in selectAnimal click handlers so a failed selection doesn't leave the user stuck on the picker with no feedback. Now closes the picker regardless of outcome; the store records the error and a future inline UI can surface it. - widget: pause the 30s poll while the tab is hidden (Page Visibility API). On tab focus, fire one immediate catch-up fetch so the widget reflects current state without waiting up to 30s. - store: dedupe concurrent fetchCompanion calls via an in-flight promise. React Strict Mode and the new 30s poll could otherwise race two GET /api/companion/me requests and have whichever resolves last clobber the other's state.
Required for the local smoke test to work end-to-end. The companion feature commits don't depend on these env tweaks — they work against any properly configured backend. The dev env we inherited tries to talk to production Firebase and forces TLS on a local Mongo container that has no certs; both are dev-only adjustments. - backend/MongoDatabase.ts: comment out the SSL/TLS block. The local Mongo container (vibe-mongo) ships without certificates, and ECONNRESET fires during the TLS handshake. Re-enable for prod. - frontend/firebase.ts: add connectAuthEmulator() guarded by import.meta.env.DEV so the frontend talks to the local auth emulator (port 9099) during development instead of production Firebase (where test users don't exist). Comment explains the intent so it doesn't get carried into production by accident.
…unmount - Widget: remove debug console.log from CompanionCanvas mount; gate the defensive animal-fallback warn behind import.meta.env.DEV so it never appears in production consoles. - Store: switch catch clauses from 'err: any' to 'err: unknown' and add a per-fetch AbortController so visibility-driven unmounts don't setState after teardown. Aborted requests are swallowed silently and don't surface as errors or clear companion state. - api-client: thread AbortSignal through GET/POST so callers can opt in. - Service: replace two 'as any' casts in _getRealProgress and _daysSinceEnrollment with explicit, narrow return shapes — the cast was masking a typed envelope we already know the shape of. - Repository: drop unnecessary 'as any' on insertOne(doc) — CompanionDoc matches OptionalUnlessRequiredIdShape. - audit-current.js: new sharp, evidence-based audit that strips comments before structural checks and validates each claim with concrete regex evidence instead of vibes.
smoke-companion.js exercises the full happy-path through a real Firebase Auth emulator + backend (signup → /companion/me → pick animal → re-read → bad-animal 400). Exits non-zero on any failure. list-routes.js prints every @controller base + route so future smoke tests can find their paths without grepping. .gitignore gets *.smoke-state.json so test artifacts stay local.
Trivial ESLint nits caught by the post-fix lint pass: - CompanionWidget: setInterval handle is never reassigned, so const. - companion-store: 'get' was destructured but never used after the AbortController refactor.
Three pre-existing bugs that blocked the dashboard happy-path: 1. POST /api/auth/signup returned 500 when the Firebase Auth client SDK had already created the user (which is the normal flow: AuthPage calls createUserWithEmail first, then POSTs to /api/auth/signup). Fix: catch auth/email-already-exists and fetch the existing Firebase user record so we can still create the Mongo user. 2. currentUserChecker leaked Firebase Admin SDK errors as HTTP 500 instead of 401. The shared error handler only maps UnauthorizedError to 401 — anything else falls through to the generic Error → 500 branch. Now we translate auth/* codes to a friendly UnauthorizedError before they bubble. Also tidied authorizationChecker's catch (no more console.log in production paths, type-safe error narrowing). 3. api-client crashed on 204 No Content because response.json() throws on empty bodies. /api/companion/me returns 204 when the user hasn't picked an animal yet — the dashboard now sees the picker instead of a JSON parse error. Also: non-JSON success bodies are surfaced as strings rather than thrown, and 4xx/5xx bodies are parsed first so .message is still useful. Verified end-to-end with scripts/smoke-full.js (11 assertions: signup, signup-recovery, no-token 401, bogus-token 401, pick, re-read, bad animal 400).
When the Firebase Auth emulator is restarted (e.g. docker compose up after a reboot), all Firebase Auth users are wiped but the Mongo \users\ collection persists. A returning user signs in with their existing email, gets a brand-new localId from the emulator, and any protected request fails with " Failed to retrieve user from Firebase:
…ository
EnrollmentRepository.getEnrollments returns the enrollment docs as a bare
array, but the companion service was reading result.enrollments — which
was always undefined — and then returning 0 from the catch. As a result,
realProgress was permanently 0 for every user, and the companion never
grew past stage 0 / mood neutral regardless of enrollment state.
Same shape mismatch existed in _daysSinceEnrollment.
Fix: treat the result as Array<{...}> directly. Drive pagination by
page.length < PAGE_SIZE instead of totalDocuments.
Verified via scripts/walkthrough-companion-growth.cjs:
stage 0 / mood neutral at 0% / no quiz
stage 2 / mood studying at 50% / quiz 65
stage 0 / mood neutral at 100% (filtered out by pct < 100 guard)
stage 2 / mood studying at 50% / quiz 80
Also adds backend/scripts/seed-test-course.cjs which inserts a minimal
public course + version + 2 items + 2 itemsGroups into the dev DB so
this can be reproduced without authoring real course content.
AuthPage calls createUserWithEmail() so the subsequent /auth/signup POST (which only writes to the app DB) lands against an existing Firebase account. Without this, repeated signup attempts for the same email throw auth/email-already-exists on the backend.
scripts/probe-204.js was a throwaway used to confirm the 204-on-no-companion behavior that led to the api-client empty-body fix. No need to keep it in the tree.
POST /auth/login previously fetched Google prod's signInWithPassword with appConfig.firebase.apiKey, which is unset in dev (.env has no FIREBASE_API_KEY line). The call hit https://identitytoolkit.googleapis.com with key=undefined and Google returned 'API key not valid', causing the frontend to collapse the failed login into 'Login failed'. This extracts URL construction into buildSignInWithPasswordUrl() and routes to the emulator at http://<FIREBASE_AUTH_EMULATOR_HOST>/identitytoolkit.googleapis.com when appConfig.isDevelopment && FIREBASE_AUTH_EMULATOR_HOST is set. Production path now requires an explicit API key and throws if missing, turning a silent 4xx inside a 200 response into a real 500 with a clear message. Verified end-to-end via curl: 200 + valid idToken signed for demo-test. Bug discovered 2026-07-08.
* frontend/src/lib/firebase.ts: add `export const provider = new
GoogleAuthProvider()`. `loginWithGoogle` referenced it on line 58
and src/utils/auth.ts imported it, but the constant was never
created. Result: any page that touched the auth tree crashed with
`does not provide an export named 'provider'`, blank React tree.
* frontend/vite.config.ts: /api proxy was pointing at the previous
backend port (4001). Backend moved to 3141 in
backend/.env#APP_PORT, but the proxy was never updated. Result:
every /api/* call hit ECONNREFUSED.
Note: the CORS origin mismatch (browser Origin=127.0.0.1:5173,
backend allowed only localhost:5173) is fixed by editing
backend/.env (gitignored) to add the 127.0.0.1 origin to
APP_ORIGINS. Cannot be committed because backend/.env is
gitignored. Default fallback in src/config/app.ts already lists
both origins when APP_ORIGINS is unset.
After these two committed fixes plus the .env tweak, browser
signup + login works end-to-end: frontend (5173) -> vite proxy ->
backend (3141) -> auth emulator (9099) -> mongo (27017).
Verified 2026-07-08 against demo-test project.
- newJourney: server-authoritative flag fires when progress drops ≥15pts from prev ≥20%, message persists until user makes forward progress - Add CompanionMood type, fix store structural error, fix toPrototypeMood - Fix sleeping eyes (soft lashes + blush), add neutral mood smile - Fix idleDays empty-array guard to prevent Infinity - CompanionService now queries 'enrollments' (plural, correct collection) - Fix currentUserChecker auth: verifyToken() → getCurrentUserFromToken() - All 6 stages + 7 moods + newJourney verified in live browser
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR introduces the ViBe Digital Learning Companion, an interactive virtual companion system that enhances learner engagement by evolving alongside the learner's learning journey.
The companion provides visual feedback, motivational messages, mood changes, and growth stages to create a more engaging and personalized learning experience.
Features
🐾 Multiple Companion Animals
Users can switch between available companion animals at any time.
🌱 Companion Growth System
The companion evolves through six growth stages:
📚 Progress-based Evolution
The companion's growth is determined by the learner's progress across both enrolled course sections and completed course sections.
As learners continue progressing through their enrolled courses and complete more learning sections, the companion automatically evolves into the next growth stage.
😊 Dynamic Mood States
Implemented multiple mood states:
Each mood includes unique expressions and contextual motivational messages.
💬 Motivational Messages
Added personalized messages that change based on:
🔄 Companion Selection
Learners can switch between five different companion animals while preserving their growth progression.
🎨 Companion Artwork
Created and integrated custom companion assets for:
Screenshots
📊 Dashboard Preview
The Digital Learning Companion integrated into the learner dashboard, displaying the companion, motivational message, and current growth stage.
Companion Animals
Users can choose from five different companion animals while maintaining their companion progress.
Growth Stages
The companion evolves through six growth stages based on learner progress.
Mood States
The companion dynamically changes its mood and motivational messages based on learner activity and progress.
Testing
Performed both automated and manual testing to validate the Digital Learning Companion feature.
Automated Testing
Manual Testing
Impact
This feature introduces a gamified learning experience that motivates learners to continue progressing through their courses by rewarding engagement with an evolving digital companion.