Skip to content

B2C Personalized Learner Journey

A B2C user signs up, answers an onboarding survey (persona, education stage, subjects, goal), then lands on a home that today uses almost none of it. This feature promotes that scattered signal into one served LearnerProfile and feeds it to the existing surfaces, so the home and the first chat turn reflect what the user already told us. Personalization is content inside a fixed dashboard skeleton, never a new layout: same slots for everyone, only the content adapts.

How it works

One profile, three consumers:

            ┌─────────────────────────────────────┐
            │   LearnerProfile  (served, editable) │
            │  persona · stage · subjects · goal   │
            │  + interests + progress (accumulates)│
            └───────────────┬──────────────────────┘
       ┌────────────────────┼────────────────────┐
       ▼                    ▼                     ▼
  Home slots          Chat seeding         Study Plan (Slice 2)
  (profile-aware)     (pre-fill+confirm)   (generated path)

Invariant: the personalized and default states are the same component tree, only props differ. Each slot takes a possibly-empty profile and degrades gracefully. There is no if (hasProfile) return <PersonalizedHome/>.

Profile-aware home. On /learn, HeroGreeting reads the profile ("Welcome back, Aisha. Ready for your Calculus exam prep?") and SubjectScroller shows the user's subjects first. Skipped-onboarding users fall back to today's generic content with no regression; the ContinueCard slot renders a "start here" starter so the skeleton stays identical.

Chat entry is the per-intent wizards. Dashboard tiles route into the existing per-intent wizards (GetHelpPicker, UnderstandPicker, PracticePicker, RevisePicker), which already pre-fill the subject from the profile and surface weak-topic chips. Each intent has its own structured intake that drives a distinct artifact (a quiz needs count and difficulty; revision needs weak topics; get-help needs the problem), so a free-form pre-filled composer would discard that structured context. /new-chat stays the stateless "start from scratch" composer: tiles never seed it.

Behavior notes:

  • First-turn is pre-fill and confirm, never auto-send. We never fire a model call on a guess.
  • Multi-subject tiles land on an inline subject-chip choice rather than silently guessing.
  • trainer and parent personas adapt copy and tiles; artifact naming is already persona-aware via artifactKeyFor.

Built in two slices. Slice 1 (this work): profile-aware home plus backend learner_profile on the home-context endpoint, no migration needed (onboarding_goal is persisted in profiles.preferences JSONB). Slice 2 turns the Study Plan into a generated goal → milestones → sessions path, Pro-gated; see b2c-study-plan-gate.md.

Where it lives

Route Component Purpose
/learn StudentHomePage B2C home; profile-aware slots
/learn/:intent AITutorPage (via StudentIntentRoute) Chat seeded from intent + profile context
/study-plan StudyPlanComingSoon Pro-gated; becomes the generated path in Slice 2

Key files: apps/web/src/pages/student/StudentHomePage/ (home slots), apps/web/src/pages/b2c/constants.ts (artifactKeyFor), apps/web/src/pages/b2c/OnboardingWizard/index.tsx (stage / subjects / goal capture), and the backend B2C home-context endpoint that serves LearnerProfile.