Skip to content

Study Plan — Design & Technical Spec (handoff)

Status: proposal / pre-build. Hand this to a design-focused agent for the UI, and to a backend/frontend agent for the wiring. Not yet implemented.

0. Context: two separate features

Kwilo B2C has two distinct learning surfaces. Keep them separate; do not merge.

  • Practice Hub (shipped) — the doing. Pick a chapter, get a graded practice session, mastery updates. JEE/NEET today, works standalone with no plan.
  • Study Plan (this spec) — the planning. Turn a syllabus (NCERT, or a learner's own uploaded material) into a weekly, realistic schedule of sessions, adjustable and regenerable.

They connect through exactly two seams, not a shared UI:

  1. Shared mastery. Both read/write the same kwiloai-memory learner_mastery (the M2 write-path, already shipped). Practising from either surface updates the plan's progress and re-ranks it.
  2. Handoff. A Study Plan session's primary action deep-links into the Practice Hub player for that chapter. The plan decides what/when; Practice Hub owns how.

Naming: "Study Plan" is the learner-facing name for this feature. "Practice Hub" is the separate practice feature. Do not reuse Practice Hub's exam-prep framing here.

Single source of truth: Learning Preferences

Grade/stage, subjects, and goal already live in Learning Preferences (Settings tab), persisted at profile.preferences['onboarding'], read via GET /api/v1/onboarding/profile ({ education_stage, onboarding_subjects, onboarding_goal }). That tab is the sole persistent editor of those values (it also backs the tutor and home-context).

Study Plan consumes those preferences; it never re-collects or forks them. The plan setup asks only for the scheduling inputs preferences do not hold (time available, study days, time of day, exam date). To change subjects/grade/goal, the learner is sent to Learning Preferences, one editor, no drift.


Part A — Competitor insights (what to adopt / avoid)

Synthesised from established patterns in Duolingo, Brilliant, Sololearn, Khan Academy, Physics Wallah, Motion, Structured, and Notion-style planners.

Onboarding / plan creation - Adopt Duolingo's "one question per screen, big tap targets, pre-selected defaults" wizard. Never show two decisions at once. (Brilliant and Duolingo both do this; it is the single biggest cognitive-load win.) - Adopt the "commitment" framing (Duolingo's daily-goal step): ask hours/week as a light choice with a consequence preview ("~25 min a day, 5 days a week"), not a raw number. - Avoid Byju's/Unacademy over-asking at signup (too many fields, drop-off). Cap at 4 steps.

Presenting the generated plan - Adopt the "Today first, week second" hierarchy (Structured, Motion, Duolingo path). The default screen answers "what do I do right now" in one glance; the full week is one tap away, never the landing view. - Adopt colour-per-subject as the primary wayfinding cue (Google Calendar, Notion, Physics Wallah). Subject colour bar on every session card; nothing else competes for colour. - Adopt max 3 session cards on "Today" (Structured caps the visible list). More than that reads as a backlog and demotivates.

Regeneration / reschedule - Adopt Motion's "auto-reschedule with a visible before/after" — never silently reshuffle. Show a compact diff ("10 sessions/week → 8", "45 min → 25 min") and require confirm. This is the trust mechanic. - Adopt "skip" as a soft action that reflows the rest (Structured/Motion), not a hard delete.

Progress / what-next (adaptive, calm) - Adopt Khan Academy's mastery bar + Duolingo's single ring over dense analytics. One progress signal per surface (syllabus % covered), not a dashboard. - Adopt streaks sparingly (Duolingo) — a small pill, never the hero. Our audience includes parents; over-gamification reads as unserious. - Avoid leaderboards/social for MVP (distraction, moderation load, off-brand for exam prep).

Visual / density - Adopt generous whitespace + larger base type (16-18px), high contrast (accessibility, and many users are on low-end Android). - Adopt friendly illustration for empty/first-run states (we already ship the Yippy kit). - Avoid decorative icons; icons only mark actions (start, edit, skip).


Part B — Design spec (for the design agent)

Design system (non-negotiable, use @kwilo/ui)

  • Components: Button, Heading, Text, Card, EmptyState, YippyIllustration, SegmentedToggle, ProgressRing, Skeleton, Badge, Chip, Dialog, AlertDialog, Tabs, Select, RadioGroup, Checkbox. No raw HTML for anything the DS provides.
  • Colour: 60-30-10. Neutral surfaces, one primary (purple primary-*) for the single main action per screen, per-subject colours only on session cards. Green secondary-* for completed/positive, amber accent-* for at-risk/attention. No raw hex, no raw Tailwind palettes.
  • Type: max 3 sizes per screen; Heading for titles, Text variants for the rest. Never font-extrabold.
  • Spacing scale 1,2,3,4,6,8,12 only. Radius up to xl. Shadow sm at rest, md on hover, lg for modals.
  • Every string via t() (i18n namespace studyPlan). Copy: active voice, sentence case, no em dashes, no exclamation marks, learner/trainer vocabulary. Empty states = explanation + motivation + action.
  • Illustrations: reuse YippyIllustration (planning, idea, under-construction, settings, growth).

Screens (5)

Page-folder pattern: apps/web/src/pages/b2c/StudyPlan*/index.tsx + components/. Page files stay under 300 lines; split into components.

B.1 Dashboard (/study-plan)

  • Purpose: answer "what do I do right now" in one glance.
  • Layout (mobile-first, max-w-3xl centred):
  • Header row: Heading h1 plan name + a small streak Chip (right). No exam toggle here (that is Practice Hub's concern).
  • Plan summary Card: grade Badge, subject Chips, one ProgressRing or bar ("syllabus 25% covered").
  • "Today" section: Text variant="label" eyebrow, then max 3 session Cards. Each: subject colour bar, subject name + icon, topic title, duration, session-type tag (Badge: lesson/practice/revision), primary Button "Start".
  • Footer actions: secondary Button "This week" → Weekly view; ghost Button "Preferences" → drawer.
  • States: loading → Skeleton; no plan yet → EmptyState (planning illustration) + primary "Create study plan"; all done today → EmptyState (growth) "You are done for today".
  • Interaction: "Start" → deep-link to Practice Hub player for that chapter (handoff). On return, session marks complete from mastery.

B.2 Plan setup (/study-plan/create)

  • Reads Learning Preferences first. On entry, fetch GET /api/v1/onboarding/profile. Grade/subjects/goal are shown as a read-only summary Card with an "Edit in Learning Preferences" link, they are not re-asked.
  • If preferences are incomplete (no stage/subjects): do not build a mini-wizard here. Show the same no_preferences EmptyState pattern the Practice Hub uses (settings illustration) with a primary CTA to Learning Preferences, then return.
  • Collect only the scheduling inputs preferences do not hold. Keep it to one short screen (RHF + Zod; if it grows past one screen, use the multi-step pattern with FormProvider + trigger([...stepFields]), FIELD const from schema keyof, no Zustand for form state):
  • Time available — SegmentedToggle (Light ~3h / Medium ~6h / Intensive ~10h+) with a consequence line ("about 25 min a day, 5 days a week").
  • Study days — Checkbox Mon-Sun (or a "weekends only" quick pick).
  • Deadline — only if goal == complete_syllabus (or the learner is exam-prep): a date field; otherwise "ongoing weekly routine", no date.
  • Primary Button "Generate plan" → loading → Weekly view. Because grade/subjects/goal come from preferences, a returning learner with saved scheduling can regenerate in one tap with no form at all (offer "Generate from my preferences" on the Dashboard empty state).
  • Copy: helper text under each control, never a wall of text.

B.3 Weekly Plan view (/study-plan/week)

  • Layout: "This week" Heading + date range. A 7-day strip (Tabs or a horizontal day selector). Selected day shows its 0-3 session Cards (same card as Dashboard).
  • Interactions: tap a session → Dialog with "Move to..." (Select of days) and "Skip" (soft). Top-right: ghost "Preferences", secondary "Regenerate".
  • States: empty week → EmptyState; over-scheduled day → amber accent hint on the day chip.

B.4 Plan settings drawer (opens over Dashboard/Weekly)

  • Edits scheduling only (the plan-specific params), 3 compact sections: study intensity (SegmentedToggle Light/Medium/Intensive), study days (Checkbox Mon-Sun), daily time (RadioGroup Morning/Evening/Flexible).
  • A separate row links out to Learning Preferences: "Subjects and goal → manage in Learning Preferences". Do not edit stage/subjects/goal here (single source of truth). Changing them there and returning re-derives the plan on next regenerate.
  • Bottom: primary Button "Apply and regenerate" → opens the regeneration preview. Helper: "We keep the same syllabus coverage."

B.5 Regeneration preview modal

  • AlertDialog (this is a confirm, not a form). Short message + a compact before/after diff (sessions/week, avg length, days). Primary "Confirm changes", secondary "Cancel". Never regenerate without this.

Interaction principles

One main action per screen. Defaults everywhere. Max 3-4 elements per card. Progressive disclosure: any advanced option (per-topic intensity, gamification) lives behind an "Advanced" link, never in the main flow.


Part C — Technical wiring (proposal)

C.1 Feature boundary

Study Plan is a new, separate feature that reuses engine internals, it is not a mode of Practice Hub.

  • New routes under /study-plan*, new nav entry (distinct from Practice Hub).
  • New backend service study_plan_scheduler (the time dimension the current ranking engine lacks).
  • Reuses: the ranking engine's UnitProvider/ImportanceStrategy/MasteryProvider seams; the RAG corpus; the question-generation agent; the Practice Hub player; learner_mastery.

C.2 What already exists to reuse (verified)

Capability Where Reuse as
RAG ingestion + chapter-sliced retrieval apps/rag (curriculum_embeddings, search(chapter=,subject=,class_level=)) unit source + content for generation
Doc-grounded question generation MockTestService._generate_questions_via_agent(), agent_client.run_agent("mock-test"/"assignment") fill practice for a plan session
Mastery write/read learner_mastery, submit_attempt, InteractionCounterMasteryProvider (M2) plan progress + re-rank
Practice player PracticeSession/PracticeAttempt, practice-session service + /practice-session API, apps/web PracticeSession page the "Start" handoff target
Ranking engine apps/backend/src/services/study_plan/ (Ranker, seams) rank chapters before scheduling
NCERT source apps/rag/src/crawlers/ncert.py (exists, not seeded) seed the K-12 library (later phase)

C.3 New pieces

Backend - RagCorpusUnitProvider(UnitProvider) — return distinct chapters from curriculum_embeddings by (board, class, subject) or by an uploaded document_id. Importance for non-exam streams = syllabus order (sequence), not weightage (honest; see trade-off). - StudyPlanScheduler — input: ranked units + scheduling prefs (hoursPerWeek, studyDays, optional examDate) + per-week overrides; output: a derived schedule (sessions on dates), computed per request and not persisted, mirroring the existing stateless plan engine (which persists nothing and re-derives every load). Respect prerequisite bands from the ranker; back-load revision. Pure, deterministic, testable. - QuestionGenerationService — extract _generate_questions_via_agent() so it takes variable count + question mix, grounded in a chapter's RAG chunks. Persist generated questions (cache per (chapter, difficulty) so a session is stable/resumable and to bound LLM cost) into the exam-question store or a generated_questions table. Data model: reuse, no new tables. The existing plan engine persists nothing and re-derives every load; follow that. Nothing here needs a table.

State Home (reused)
Grade / subjects / goal Learning Preferences, profile.preferences['onboarding'] (JSONB)
Scheduling params (hours/week, study days, day time, intensity, exam date, source, source_document_id) new block profile.preferences['study_plan'] (same JSONB store, symmetric with onboarding)
The weekly schedule itself derived per request by StudyPlanScheduler, not stored
Per-item overrides (skipped / moved), keyed by stable (iso_week, chapter_id) not a volatile session id profile.preferences['study_plan']['overrides'] (JSONB)
Completion / progress learner_mastery (a chapter is "done enough" when its mastery clears the threshold)
Actual practice when "Start" is tapped existing practice_sessions / practice_attempts via the Practice Hub create flow (the handoff)
Cached generated questions the existing question store the practice path already reads (tag by source); a small cache, not a Study Plan table
  • Endpoints (snake_case in, camelCase out via serialization_alias, response_model_by_alias=True): GET /study-plan (derive + return the current schedule, reading both preferences blocks + learner_mastery), PUT /study-plan/settings (write the study_plan scheduling block), POST /study-plan/regenerate-preview (diff old vs new schedule for the confirm modal), PATCH /study-plan/overrides (record a skip/move). All stateless except the two JSONB writes.
  • Reuse learner_mastery for progress; the onboarding profile for grade/subjects/goal; practice_sessions for the doing. Do not invent a mastery store, a plan table, or a session table.
  • chapter_id aligns with the RAG corpus chapter key (and, where an exam concept exists, with learner_mastery.concept_id) so progress joins without a mapping layer.

Frontend (apps/web) - Service folder src/services/study-plan-planner/ (schemas.ts + index.ts + hooks.ts) using getRequest/useApiQuery/useApiMutation; Zod schemas, .nullable() not .optional(); query-key constants. - Zustand store src/stores/studyPlanStore.ts for correlated UI state (selected day, drawer open, pending regenerate diff). Wizard form stays in RHF, not the store. - Reuse @kwilo/ui + the Practice Hub's TodayCard/ProgressRing where they fit; new WeeklyPlan, PlanWizard, PreferencesDrawer, RegenerateModal. - "Start" builds a Practice Hub player href for the chapter (the handoff), reusing the existing practice-session create flow.

C.4 The two honest constraints (design + product must account for)

  1. No weightage outside exams. NCERT chapters have no past-paper frequency, so ranking is syllabus-order × mastery-gap, not "most tested". The plan's promise is "cover your syllabus, prioritised by your weak spots".
  2. Generated questions cost/latency/trust. Cache aggressively per chapter; gate generation behind the existing AIQuotaService; add answer-validation guardrails. Real past questions (Practice Hub) stay more trustworthy; Study Plan practice is generated.

C.5 Phasing

  • Slice 1 (My Space source): learner uploads material → already ingested + chapter-tagged → RagCorpusUnitProvider over that document → scheduler → sessions → generated practice. No seeding needed; immediately differentiated ("practise your own notes").
  • Slice 2 (NCERT library): run the crawler, ingest Classes 1-12, same path with board/class/subject as the unit key.
  • Slice 3: adaptivity polish — regenerate weights recent mastery more; spaced-revision scheduling.

Implementation decisions (issue #1227)

Resolved during build; supersedes any ambiguity above.

  • Naming collision fully resolved (full rename). The shipped feature whose code identifier was study_plan / studyPlan is actually Practice Hub; it was renamed end to end to practice_hub / practice-hub / PracticeHub. The new weekly planner is the real Study Plan and now owns the clean study_plan / study-plan / StudyPlan names.
  • Shared ranking engine (Ranker/seams/strategies/assembler/goal_resolver), reused by both features, was renamed to the neutral services/plan_engine/ (BE) so neither feature "owns" it.
  • Frozen identifiers (NOT renamed): the persisted Pro-quota / feature-lock key literal "study_plan" (B2CQuotaFeature.STUDY_PLAN, B2C_PRO_LOCKED_FEATURES, ai_quota lock JSON, FE b2c-quota / hierarchical-feature-flags / useB2CChatGating), the preferences['study_plan'] JSONB storage key, and exam_target_override. These are stored contracts; renaming would orphan data / break the paywall.
  • Routes: Practice Hub at /practice (FE) and /practice-hub (API); Study Plan at /studyplan, /studyplan/create, /studyplan/week (FE) and /study-plan (API).
  • API contract (camelCase out): GET /study-plan, POST /study-plan (create), PUT /study-plan/settings, POST /study-plan/regenerate-preview, PATCH /study-plan/overrides. Scheduling block persisted at profile.preferences['study_plan']. No new tables. Practice Hub keeps GET /practice-hub, PATCH /practice-hub/exam-target.
  • Unit source: exam-family learners are scheduled from ExamCorpusUnitProvider (real JEE/NEET data); NCERT / My Space via RagCorpusUnitProvider over curriculum_embeddings (empty-state until seeded, per §C.4/§C.5).
  • Practice Hub engine + seams (kwilo-docs features/b2c-practice-hub-engine.md)
  • Study Plan vision (kwilo-docs features/b2c-study-plan-vision.md)