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:
- 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. - 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. Greensecondary-*for completed/positive, amberaccent-*for at-risk/attention. No raw hex, no raw Tailwind palettes. - Type: max 3 sizes per screen;
Headingfor titles,Textvariants for the rest. Neverfont-extrabold. - Spacing scale
1,2,3,4,6,8,12only. Radius up toxl. Shadowsmat rest,mdon hover,lgfor modals. - Every string via
t()(i18n namespacestudyPlan). 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-3xlcentred): - Header row:
Heading h1plan name + a small streakChip(right). No exam toggle here (that is Practice Hub's concern). - Plan summary
Card: gradeBadge, subjectChips, oneProgressRingor bar ("syllabus 25% covered"). - "Today" section:
Text variant="label"eyebrow, then max 3 sessionCards. Each: subject colour bar, subject name + icon, topic title, duration, session-type tag (Badge: lesson/practice/revision), primaryButton"Start". - Footer actions: secondary
Button"This week" → Weekly view; ghostButton"Preferences" → drawer. - States: loading →
Skeleton; no plan yet →EmptyState(planningillustration) + 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 summaryCardwith 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_preferencesEmptyStatepattern the Practice Hub uses (settingsillustration) 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]),FIELDconst 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 —
CheckboxMon-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 (Tabsor a horizontal day selector). Selected day shows its 0-3 sessionCards (same card as Dashboard). - Interactions: tap a session →
Dialogwith "Move to..." (Selectof days) and "Skip" (soft). Top-right: ghost "Preferences", secondary "Regenerate". - States: empty week →
EmptyState; over-scheduled day → amberaccenthint 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 (
SegmentedToggleLight/Medium/Intensive), study days (CheckboxMon-Sun), daily time (RadioGroupMorning/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/MasteryProviderseams; 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 thestudy_planscheduling 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_masteryfor progress; the onboarding profile for grade/subjects/goal;practice_sessionsfor the doing. Do not invent a mastery store, a plan table, or a session table. chapter_idaligns with the RAG corpus chapter key (and, where an exam concept exists, withlearner_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)¶
- 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".
- 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 →
RagCorpusUnitProviderover 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/studyPlanis actually Practice Hub; it was renamed end to end topractice_hub/practice-hub/PracticeHub. The new weekly planner is the real Study Plan and now owns the cleanstudy_plan/study-plan/StudyPlannames. - Shared ranking engine (
Ranker/seams/strategies/assembler/goal_resolver), reused by both features, was renamed to the neutralservices/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, FEb2c-quota/hierarchical-feature-flags/useB2CChatGating), thepreferences['study_plan']JSONB storage key, andexam_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 atprofile.preferences['study_plan']. No new tables. Practice Hub keepsGET /practice-hub,PATCH /practice-hub/exam-target. - Unit source: exam-family learners are scheduled from
ExamCorpusUnitProvider(real JEE/NEET data); NCERT / My Space viaRagCorpusUnitProviderovercurriculum_embeddings(empty-state until seeded, per §C.4/§C.5).
Related¶
- Practice Hub engine + seams (kwilo-docs
features/b2c-practice-hub-engine.md) - Study Plan vision (kwilo-docs
features/b2c-study-plan-vision.md)