Skip to content

Digital Learning companion - #1197

Open
sahasraa09 wants to merge 18 commits into
vicharanashala:mainfrom
sahasraa09:digital-virtual-companion
Open

sahasraa09 wants to merge 18 commits into
vicharanashala:mainfrom
sahasraa09:digital-virtual-companion

Conversation

@sahasraa09

Copy link
Copy Markdown

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

  • Panda
  • Fox
  • Penguin
  • Dog
  • Cat

Users can switch between available companion animals at any time.

🌱 Companion Growth System

The companion evolves through six growth stages:

  • Baby
  • Toddler
  • Child
  • Teen
  • Young Adult
  • Adult

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

  • Happy
  • Sad
  • Angry
  • Sleeping
  • Celebrating

Each mood includes unique expressions and contextual motivational messages.

💬 Motivational Messages

Added personalized messages that change based on:

  • Companion growth stage
  • Companion mood
  • Learner progress

🔄 Companion Selection

Learners can switch between five different companion animals while preserving their growth progression.

🎨 Companion Artwork

Created and integrated custom companion assets for:

  • Five companion animals
  • Six growth stages
  • Multiple mood variations
  • Background scenes and animations

Screenshots

📊 Dashboard Preview

The Digital Learning Companion integrated into the learner dashboard, displaying the companion, motivational message, and current growth stage.

Screenshot 2026-07-24 160319

Companion Animals

Users can choose from five different companion animals while maintaining their companion progress.

companions_overview_collage

Growth Stages

The companion evolves through six growth stages based on learner progress.

companions_growth_stages_collage

Mood States

The companion dynamically changes its mood and motivational messages based on learner activity and progress.

companions_mood_grid

Testing

Performed both automated and manual testing to validate the Digital Learning Companion feature.

Automated Testing

  • ✅ Executed test scripts to verify companion growth progression across all six stages.
  • ✅ Validated growth calculation based on enrolled course sections and completed course sections.
  • ✅ Verified companion mood transitions (Happy, Sad, Angry, Sleeping, and Celebrating).
  • ✅ Tested motivational message updates for different growth stages, moods, and learner progress.
  • ✅ Verified companion behavior for course enrollment and course completion scenarios.

Manual Testing

  • ✅ Verified companion rendering and integration on the learner dashboard.
  • ✅ Tested companion selection across all five available companion animals.
  • ✅ Verified UI responsiveness and overall user experience across different companion states.

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.

sahasraa09 and others added 18 commits July 6, 2026 22:45
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
@github-actions github-actions Bot added frontend Changes to the frontend of the project dependencies Modifications to any frontend or backend external dependencies backend labels Jul 29, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend dependencies Modifications to any frontend or backend external dependencies frontend Changes to the frontend of the project

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant