Download src/lib/teach-engine.ts from asnannp/DocDoeAI: direct link, hf CLI and curl.
- Browser
- Download file 26.5 kB
-
https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/lib/teach-engine.ts
- Command line
-
hf download hf://spaces/asnannp/DocDoeAI/src/lib/teach-engine.ts
-
curl -L -o teach-engine.ts https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/lib/teach-engine.ts
26.5 kB
| // βββ Intelligent Teach Engine βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // The "brain" behind /teach. Turns a free-text study request into a structured, | |
| // syllabus-aware, exam-aware, progress-aware teaching plan. | |
| // | |
| // parseStudyRequest(rawText) β deterministic intent/level/etc. | |
| // buildIntelligentTeachPlan(rawText) β full plan (match + strategy + AI ctx) | |
| // | |
| // Curriculum-first: matching and exam intelligence come from local data before | |
| // any AI guessing. Never throws; degrades to friendly suggestions on weak input. | |
| import { matchChapter } from "./curriculum/chapter-matcher"; | |
| import type { ChapterHit, ChapterMatchResult, MatchConfidence } from "./curriculum/chapter-matcher"; | |
| import { getExamProfile, pyqLabel, difficultyLabel } from "./curriculum/exam-intelligence"; | |
| import { getChapterEntry, getRecommendedNextChapters } from "./study-progress"; | |
| import type { ChapterProgressEntry, ChapterStatus } from "./study-progress"; | |
| import type { BrainGeneratePayload, StudentLevel } from "./api/types"; | |
| // βββ Public types βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export type StudyIntent = | |
| | "teach" | |
| | "revise" | |
| | "last-night" | |
| | "quiz" | |
| | "exam-answer" | |
| | "roadmap" | |
| | "video-plan" | |
| | "weak-topic-help"; | |
| export type Urgency = "high" | "normal" | "low"; | |
| export type TeachMode = | |
| | "basics-first" | |
| | "continue" | |
| | "revise" | |
| | "weak-repair" | |
| | "exam-answer-practice" | |
| | "last-night"; | |
| export type LanguagePreference = "english" | "malayalam" | "manglish" | null; | |
| export type ParsedStudyRequest = { | |
| rawText: string; | |
| level: string | null; // "+1" | "+2" | "SSLC" | |
| studentLevel: StudentLevel; // beginner | intermediate | advanced | |
| board: string | null; | |
| subjectHint: string | null; // subject slug if explicitly named | |
| intent: StudyIntent; | |
| urgency: Urgency; | |
| baseMode: TeachMode; // mode before progress is considered | |
| languagePreference: LanguagePreference; | |
| weakTopicSignal: boolean; | |
| availableMinutes: number | null; | |
| cleanedTopic: string; | |
| }; | |
| export type NextActionKind = | |
| | "start" | |
| | "continue" | |
| | "mark-complete" | |
| | "revise" | |
| | "quiz" | |
| | "last-night" | |
| | "exam-answer" | |
| | "next-chapter" | |
| | "prerequisite"; | |
| export type NextAction = { | |
| kind: NextActionKind; | |
| label: string; | |
| hint?: string; | |
| href?: string; | |
| primary?: boolean; | |
| }; | |
| export type AiPromptContext = { | |
| topic: string; // precise chapter name when matched, else cleaned topic | |
| subject?: string; | |
| curriculumLevel?: string; // "+1" | "+2" | "SSLC" | |
| studentLevel: StudentLevel; | |
| board: string; | |
| intent: StudyIntent; | |
| urgency: Urgency; | |
| mode: TeachMode; | |
| languagePreference: LanguagePreference; | |
| timeLeftMinutes: number | null; | |
| examImportance?: string; | |
| pyqFrequency?: string; | |
| estimatedMinutes?: number; | |
| formulas: string[]; | |
| definitions: string[]; | |
| commonMistakes: string[]; | |
| prerequisites: string[]; | |
| relatedChapters: string[]; | |
| progressStatus: string; | |
| quizBest: number | null; | |
| styleDirectives: string[]; | |
| /** Ready-to-prepend instruction block carried to the AI via the `goal` field. */ | |
| promptPreamble: string; | |
| }; | |
| export type IntelligentTeachPlan = { | |
| matchedChapter: ChapterHit | null; | |
| confidence: number; | |
| confidenceLabel: MatchConfidence; | |
| alternatives: ChapterHit[]; | |
| matchReason: string; | |
| intent: StudyIntent; | |
| urgency: Urgency; | |
| recommendedMode: TeachMode; | |
| modeLabel: string; | |
| lessonStrategy: string; | |
| priorityFocus: string[]; | |
| prerequisiteWarnings: string[]; | |
| examGuidance: string; | |
| revisionGuidance: string; | |
| weakTopicAdvice: string; | |
| nextActions: NextAction[]; | |
| aiPromptContext: AiPromptContext; | |
| parsed: ParsedStudyRequest; | |
| }; | |
| // βββ Detection helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function has(text: string, patterns: (string | RegExp)[]): boolean { | |
| return patterns.some((p) => | |
| typeof p === "string" ? text.includes(p) : p.test(text), | |
| ); | |
| } | |
| function detectLevel(t: string): string | null { | |
| if (has(t, [/\bsslc\b/, /\bclass\s*10\b/, /\b10th\b/, /\bstd\s*10\b/, /\btenth\b/, /\bx\b(?!i)/])) return "SSLC"; | |
| if (has(t, [/\+\s*2\b/, /plus\s*two/, /\bclass\s*12\b/, /\b12th\b/, /\bstd\s*12\b/, /\bxii\b/, /second\s*year/])) return "+2"; | |
| if (has(t, [/\+\s*1\b/, /plus\s*one/, /\bclass\s*11\b/, /\b11th\b/, /\bstd\s*11\b/, /\bxi\b/, /first\s*year/])) return "+1"; | |
| return null; | |
| } | |
| function detectBoard(t: string): string | null { | |
| if (has(t, ["kerala", "scert", "dhse", "hse"])) return "Kerala SCERT"; | |
| if (has(t, ["cbse", "ncert"])) return "CBSE"; | |
| if (has(t, ["icse"])) return "ICSE"; | |
| return null; | |
| } | |
| function detectSubject(t: string): string | null { | |
| if (has(t, [/\bphysics\b/, /\bphy\b/])) return "physics"; | |
| if (has(t, [/\bchemistry\b/, /\bchem\b/])) return "chemistry"; | |
| if (has(t, [/\bmaths?\b/, /\bmathematics\b/])) return "maths"; | |
| if (has(t, [/\bbotany\b/])) return "botany"; | |
| if (has(t, [/\bzoology\b/])) return "zoology"; | |
| if (has(t, [/\bbiology\b/, /\bbio\b/])) return "biology"; | |
| return null; | |
| } | |
| function detectLanguage(raw: string): LanguagePreference { | |
| if (/[ΰ΄-ΰ΅Ώ]/.test(raw)) return "malayalam"; // Malayalam script | |
| const t = raw.toLowerCase(); | |
| if (has(t, ["in malayalam", "malayalathil", "malayalam"])) return "malayalam"; | |
| if (has(t, ["manglish", "padikkanam", "padikkano", "venam", "enthaanu", "parayamo", "ariyanam"])) return "manglish"; | |
| if (has(t, ["in english", "english"])) return "english"; | |
| return null; | |
| } | |
| function detectUrgency(t: string): Urgency { | |
| if (has(t, [ | |
| "last night", "lastnight", "tonight", "tomorrow", "exam today", "few hours", | |
| "1 hour", "one hour", "2 hours", "two hours", "cram", "urgent", "right now", | |
| "exam is near", "exam near", "day before", | |
| ])) return "high"; | |
| if (has(t, ["no rush", "no hurry", "later", "sometime", "whenever", "slowly", "take time"])) return "low"; | |
| return "normal"; | |
| } | |
| function detectWeakSignal(t: string): boolean { | |
| return has(t, [ | |
| "weak in", "weak at", "not understand", "dont understand", "don't understand", | |
| "didnt understand", "didn't understand", "struggling", "struggle", "confused", | |
| "confusing", "difficult for me", "hard for me", "tough for me", "scared of", | |
| "afraid of", "cant solve", "can't solve", "not getting", "bad at", "poor in", | |
| "need help with", "find it hard", | |
| ]); | |
| } | |
| function detectIntent(t: string): StudyIntent { | |
| if (has(t, ["exam answer", "write answer", "model answer", "answer writing", "how to answer", "answer practice", "full marks"])) return "exam-answer"; | |
| if (has(t, ["roadmap", "study plan", "where to start", "how to learn", "how do i learn", "learn from zero", "path to", "guide to learn"])) return "roadmap"; | |
| if (has(t, ["video", "animation", "animated", "visual lesson", "watch"])) return "video-plan"; | |
| if (has(t, ["quiz", "mcq", "mcqs", "test me", "practice questions", "practise questions", "questions", "objective"])) return "quiz"; | |
| if (has(t, ["last night", "lastnight", "tonight", "cram", "few hours before"])) return "last-night"; | |
| if (has(t, ["revise", "revision", "recap", "review", "quick read", "go through again"])) return "revise"; | |
| if (detectWeakSignal(t)) return "weak-topic-help"; | |
| return "teach"; | |
| } | |
| function detectStudentLevel(t: string): StudentLevel { | |
| if (has(t, ["from basics", "from scratch", "from zero", "beginner", "basic", "never studied", "first time", "easy way", "simple"])) return "beginner"; | |
| if (has(t, ["advanced", "in depth", "in-depth", "deep", "jee", "neet", "competitive", "tough questions", "expert"])) return "advanced"; | |
| return "intermediate"; | |
| } | |
| function detectMinutes(t: string): number | null { | |
| const hr = t.match(/(\d+(?:\.\d+)?)\s*(?:hours?|hrs?|h)\b/); | |
| if (hr) return Math.round(parseFloat(hr[1]) * 60); | |
| if (/half\s*(?:an\s*)?hour/.test(t)) return 30; | |
| const mn = t.match(/(\d+)\s*(?:minutes?|mins?|min)\b/); | |
| if (mn) return parseInt(mn[1], 10); | |
| return null; | |
| } | |
| function baseModeFor(intent: StudyIntent): TeachMode { | |
| switch (intent) { | |
| case "last-night": return "last-night"; | |
| case "revise": return "revise"; | |
| case "weak-topic-help": return "weak-repair"; | |
| case "exam-answer": return "exam-answer-practice"; | |
| default: return "basics-first"; | |
| } | |
| } | |
| // βββ Parser βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * Deterministic, curriculum-first parse of a free-text study request. | |
| * Detects level, board, subject, intent, urgency, mode, language, weak signal, | |
| * available time, and a cleaned topic string. Never throws. | |
| */ | |
| export function parseStudyRequest(rawText: string): ParsedStudyRequest { | |
| const raw = (rawText ?? "").trim(); | |
| const t = raw.toLowerCase(); | |
| const level = detectLevel(t); | |
| const board = detectBoard(t); | |
| const subjectHint = detectSubject(t); | |
| let intent = detectIntent(t); | |
| const urgency = detectUrgency(t); | |
| const weakTopicSignal = detectWeakSignal(t); | |
| const languagePreference = detectLanguage(raw); | |
| const studentLevel = detectStudentLevel(t); | |
| const availableMinutes = detectMinutes(t); | |
| // Urgency overrides a plain teach/revise request into last-night triage. | |
| if (urgency === "high" && (intent === "teach" || intent === "revise")) { | |
| intent = "last-night"; | |
| } | |
| // A weak signal beats a generic teach intent. | |
| if (weakTopicSignal && intent === "teach") { | |
| intent = "weak-topic-help"; | |
| } | |
| const baseMode = baseModeFor(intent); | |
| // Cleaned topic: drop obvious level/board tokens for a tidier display string. | |
| const cleanedTopic = raw | |
| .replace(/\+\s*[12]\b|plus\s*(one|two)|class\s*1[012]\b|sslc|cbse|scert|dhse|kerala/gi, " ") | |
| .replace(/\s+/g, " ") | |
| .trim(); | |
| return { | |
| rawText: raw, | |
| level, | |
| studentLevel, | |
| board, | |
| subjectHint, | |
| intent, | |
| urgency, | |
| baseMode, | |
| languagePreference, | |
| weakTopicSignal, | |
| availableMinutes, | |
| cleanedTopic, | |
| }; | |
| } | |
| // βββ Mode resolution (intent + progress) ββββββββββββββββββββββββββββββββββββββ | |
| function resolveMode(intent: StudyIntent, status: ChapterStatus): TeachMode { | |
| if (intent === "last-night") return "last-night"; | |
| if (intent === "exam-answer") return "exam-answer-practice"; | |
| if (intent === "weak-topic-help") return "weak-repair"; | |
| if (status === "weak") return "weak-repair"; | |
| if (intent === "revise") { | |
| return status === "not_started" ? "basics-first" : "revise"; | |
| } | |
| // teach / quiz / roadmap / video-plan follow the progress signal | |
| switch (status) { | |
| case "not_started": return "basics-first"; | |
| case "started": return "continue"; | |
| case "completed": | |
| case "revised": return "revise"; | |
| default: return "basics-first"; | |
| } | |
| } | |
| const MODE_LABEL: Record<TeachMode, string> = { | |
| "basics-first": "Teach from basics", | |
| continue: "Continue where you left off", | |
| revise: "Quick revision", | |
| "weak-repair": "Weak-topic repair", | |
| "exam-answer-practice": "Exam answer practice", | |
| "last-night": "Last-night revision", | |
| }; | |
| const MODE_STRATEGY: Record<TeachMode, string> = { | |
| "basics-first": "Start from fundamentals and build up step by step with simple examples.", | |
| continue: "Pick up from where you left off and reinforce the core ideas.", | |
| revise: "Fast recap of key points, formulas, and the traps examiners use.", | |
| "weak-repair": "Slow, careful rebuild that targets exactly where students get stuck.", | |
| "exam-answer-practice": "Focus on clear answer structure, scoring keywords, and presentation.", | |
| "last-night": "Priority triage - cover core ideas first, then revise weak spots.", | |
| }; | |
| // βββ Plan builder βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const BOARD_DEFAULT = "Kerala SCERT / DHSE"; | |
| function statusText(status: ChapterStatus): string { | |
| switch (status) { | |
| case "not_started": return "not started"; | |
| case "started": return "in progress"; | |
| case "completed": return "completed"; | |
| case "revised": return "revised"; | |
| case "weak": return "needs revision"; | |
| } | |
| } | |
| function buildExamGuidance(hit: ChapterHit): string { | |
| const c = hit.chapter; | |
| const profile = getExamProfile(c); | |
| const freq = pyqLabel(c); | |
| const diff = difficultyLabel(c); | |
| const marks = c.examWeightageMarks ? `~${c.examWeightageMarks} marks` : null; | |
| const parts: string[] = []; | |
| parts.push(`${freq}.`); | |
| if (marks) parts.push(`Practice with a ${marks} answer structure.`); | |
| parts.push(`${diff} difficulty.`); | |
| parts.push(profile.action + "."); | |
| return parts.join(" "); | |
| } | |
| function buildRevisionGuidance(status: ChapterStatus, studyCount: number, quizScore: number | null): string { | |
| let base: string; | |
| switch (status) { | |
| case "not_started": | |
| base = "You haven't started this yet β read it once fully, then revise the key points."; | |
| break; | |
| case "started": | |
| base = studyCount > 1 | |
| ? `You've opened this ${studyCount} times. One focused revision will lock it in.` | |
| : "You've started this β a focused revision will make it stick."; | |
| break; | |
| case "completed": | |
| base = "Completed already. A quick revision before the exam keeps it fresh."; | |
| break; | |
| case "revised": | |
| base = "Already revised once. Skim the formulas and common mistakes again."; | |
| break; | |
| case "weak": | |
| base = "You marked this weak β revise slowly and redo the worked examples."; | |
| break; | |
| } | |
| if (quizScore !== null && quizScore < 50) { | |
| base += ` Your last quiz was ${quizScore}% β revise before moving on.`; | |
| } else if (quizScore !== null && quizScore >= 80) { | |
| base += ` Your last quiz was ${quizScore}% β you're in good shape.`; | |
| } | |
| return base; | |
| } | |
| function buildWeakTopicAdvice(hit: ChapterHit, weakSignal: boolean, status: ChapterStatus, entry: ChapterProgressEntry | null): string { | |
| if (!weakSignal && status !== "weak") return ""; | |
| const mistakes = hit.chapter.commonMistakes?.slice(0, 2) ?? []; | |
| const yourWeak = entry?.weakTopics?.slice(0, 2) ?? []; | |
| const parts: string[] = []; | |
| parts.push("Let's rebuild this slowly."); | |
| if (yourWeak.length) { | |
| parts.push(`You flagged: ${yourWeak.join("; ")}.`); | |
| } | |
| if (mistakes.length) { | |
| parts.push(`Students usually slip on: ${mistakes.join("; ")}.`); | |
| } | |
| parts.push("We'll go concept by concept with extra examples."); | |
| return parts.join(" "); | |
| } | |
| function buildPriorityFocus(hit: ChapterHit, mode: TeachMode): string[] { | |
| const c = hit.chapter; | |
| const focus: string[] = []; | |
| if (c.pyqFrequency === "very_high") focus.push("Highest revision priority"); | |
| else if (c.pyqFrequency === "high") focus.push("Frequent-practice topic"); | |
| if (c.lastNightPriority >= 4) focus.push("Study first tonight"); | |
| if (c.formulas && c.formulas.length > 0) focus.push(`${c.formulas.length} key formula${c.formulas.length !== 1 ? "s" : ""}`); | |
| if (c.commonMistakes && c.commonMistakes.length > 0) focus.push("Watch for common mistakes"); | |
| if (mode === "exam-answer-practice") focus.push("Answer structure and presentation"); | |
| if (mode === "last-night") focus.push("Core points first"); | |
| if (mode === "weak-repair") focus.push("Foundations before problems"); | |
| return focus.length ? focus : ["Core concepts and definitions"]; | |
| } | |
| function resolvePrerequisiteWarnings(hit: ChapterHit, override?: ChapterProgressEntry | null): string[] { | |
| const prereqs = hit.chapter.prerequisites ?? []; | |
| if (prereqs.length === 0) return []; | |
| const warnings: string[] = []; | |
| for (const name of prereqs) { | |
| // If caller supplied a single override, we can't resolve other chapters' | |
| // status reliably β fall back to a gentle reminder. | |
| if (override !== undefined) { | |
| warnings.push(`Make sure you know ${name} first.`); | |
| continue; | |
| } | |
| const pre = matchChapter(name, { | |
| level: hit.level, | |
| subjectSlug: hit.subjectSlug, | |
| strictLevel: true, | |
| limit: 1, | |
| }).best; | |
| if (!pre) { | |
| warnings.push(`Make sure you know ${name} first.`); | |
| continue; | |
| } | |
| const entry = getChapterEntry(pre.level, pre.subjectSlug, pre.chapter.slug); | |
| const st = entry?.status ?? "not_started"; | |
| if (st === "not_started") warnings.push(`You haven't studied ${name} yet β it's a prerequisite.`); | |
| else if (st === "weak") warnings.push(`Revise ${name} first β you marked it weak.`); | |
| } | |
| return warnings; | |
| } | |
| function buildNextActions( | |
| hit: ChapterHit, | |
| status: ChapterStatus, | |
| intent: StudyIntent, | |
| urgency: Urgency, | |
| quizScore: number | null, | |
| hasPrereqWarning: boolean, | |
| ): NextAction[] { | |
| const actions: NextAction[] = []; | |
| const lowQuiz = quizScore !== null && quizScore < 50; | |
| if (hasPrereqWarning) { | |
| actions.push({ | |
| kind: "prerequisite", | |
| label: "Revise prerequisite first", | |
| hint: "Build the foundation before this chapter", | |
| }); | |
| } | |
| if (lowQuiz || status === "weak") { | |
| actions.push({ kind: "revise", label: "Revise this chapter", primary: true, hint: "Strengthen weak spots" }); | |
| actions.push({ kind: "quiz", label: "Retake quiz" }); | |
| actions.push({ kind: "last-night", label: "Last-night plan" }); | |
| } else if (status === "not_started") { | |
| actions.push({ kind: "start", label: "Start learning", primary: true }); | |
| actions.push({ kind: "quiz", label: "Take a quiz" }); | |
| if (urgency === "high") actions.push({ kind: "last-night", label: "Last-night plan" }); | |
| } else if (status === "started") { | |
| actions.push({ kind: "continue", label: "Continue studying", primary: true }); | |
| actions.push({ kind: "mark-complete", label: "Mark completed" }); | |
| actions.push({ kind: "quiz", label: "Take a quiz" }); | |
| } else { | |
| // completed / revised | |
| actions.push({ kind: "quiz", label: "Test yourself", primary: true }); | |
| actions.push({ kind: "revise", label: "Revise in 20 min" }); | |
| } | |
| if (intent === "exam-answer") { | |
| actions.unshift({ kind: "exam-answer", label: "Practice exam answers", primary: true, hint: "Model answers + marks" }); | |
| } | |
| // Next chapter suggestion (only when this one is done or being tested). | |
| if (status === "completed" || status === "revised") { | |
| try { | |
| const rec = getRecommendedNextChapters(hit.level, hit.subjectSlug, 1)[0]; | |
| if (rec && rec.chapter.slug !== hit.chapter.slug) { | |
| actions.push({ | |
| kind: "next-chapter", | |
| label: `Next: ${rec.chapter.name}`, | |
| hint: rec.reason, | |
| href: `/teach?q=${encodeURIComponent(rec.chapter.name)}`, | |
| }); | |
| } | |
| } catch { | |
| // recommendations are best-effort | |
| } | |
| } | |
| return actions; | |
| } | |
| function buildAiContext( | |
| parsed: ParsedStudyRequest, | |
| hit: ChapterHit | null, | |
| mode: TeachMode, | |
| status: ChapterStatus, | |
| quizScore: number | null, | |
| ): AiPromptContext { | |
| const c = hit?.chapter; | |
| const board = parsed.board ?? BOARD_DEFAULT; | |
| const topic = hit ? hit.chapter.name : parsed.cleanedTopic || parsed.rawText; | |
| const subject = hit?.subjectName; | |
| const curriculumLevel = hit?.level ?? parsed.level ?? undefined; | |
| const formulas = c?.formulas?.slice(0, 8) ?? []; | |
| const definitions = c?.definitions?.slice(0, 6) ?? []; | |
| const commonMistakes = c?.commonMistakes?.slice(0, 5) ?? []; | |
| const prerequisites = c?.prerequisites ?? []; | |
| const relatedChapters = c?.relatedChapterSlugs ?? []; | |
| const styleDirectives: string[] = []; | |
| styleDirectives.push(`${board} aligned`); | |
| if (mode === "last-night") styleDirectives.push("concise, highest-yield only"); | |
| if (mode === "weak-repair") styleDirectives.push("extra-simple, step by step"); | |
| if (mode === "exam-answer-practice") styleDirectives.push("answer-writing focus"); | |
| if (mode === "basics-first") styleDirectives.push("beginner-friendly, build from zero"); | |
| if (mode === "revise") styleDirectives.push("recap, no fluff"); | |
| if (parsed.studentLevel === "advanced") styleDirectives.push("include deeper/tougher questions"); | |
| if (parsed.languagePreference === "malayalam") styleDirectives.push("explain in Malayalam"); | |
| if (parsed.languagePreference === "manglish") styleDirectives.push("explain in simple Manglish"); | |
| // Compact instruction block carried to the AI through the `goal` field. | |
| const pre: string[] = []; | |
| pre.push(`Teach "${topic}"${subject ? ` (${board} ${curriculumLevel ?? ""} ${subject})` : ""}.`.replace(/\s+/g, " ").trim()); | |
| pre.push(`Intent: ${parsed.intent}. Mode: ${MODE_LABEL[mode]}.`); | |
| if (c) { | |
| pre.push(`Exam relevance: ${pyqLabel(c)}, ${difficultyLabel(c).toLowerCase()} difficulty${c.examWeightageMarks ? `, ~${c.examWeightageMarks} marks` : ""}.`); | |
| } | |
| if (formulas.length) pre.push(`Must cover formulas: ${formulas.slice(0, 5).join("; ")}.`); | |
| if (commonMistakes.length) pre.push(`Warn about: ${commonMistakes.slice(0, 3).join("; ")}.`); | |
| if (prerequisites.length) pre.push(`Assumes prior knowledge of: ${prerequisites.join(", ")}.`); | |
| pre.push(`Student progress: ${statusText(status)}${quizScore !== null ? `, last quiz ${quizScore}%` : ""}.`); | |
| if (parsed.urgency === "high") pre.push("Exam is very soon β prioritise speed and scoring."); | |
| if (styleDirectives.length) pre.push(`Style: ${styleDirectives.join("; ")}.`); | |
| pre.push("Use Kerala exam wording. Be original and clear. Avoid generic filler."); | |
| return { | |
| topic, | |
| subject, | |
| curriculumLevel, | |
| studentLevel: parsed.studentLevel, | |
| board, | |
| intent: parsed.intent, | |
| urgency: parsed.urgency, | |
| mode, | |
| languagePreference: parsed.languagePreference, | |
| timeLeftMinutes: parsed.availableMinutes, | |
| examImportance: c ? getExamProfile(c).priority : undefined, | |
| pyqFrequency: c ? pyqLabel(c) : undefined, | |
| estimatedMinutes: c?.estimatedMinutes, | |
| formulas, | |
| definitions, | |
| commonMistakes, | |
| prerequisites, | |
| relatedChapters, | |
| progressStatus: statusText(status), | |
| quizBest: quizScore, | |
| styleDirectives, | |
| promptPreamble: pre.join(" "), | |
| }; | |
| } | |
| /** | |
| * Build the full intelligent teach plan for a study request. | |
| * | |
| * @param request Raw student text, or a pre-parsed request. | |
| * @param progressOverride Optional explicit progress entry (mainly for tests); | |
| * when omitted, progress is read from this device. | |
| */ | |
| export function buildIntelligentTeachPlan( | |
| request: string | ParsedStudyRequest, | |
| progressOverride?: ChapterProgressEntry | null, | |
| ): IntelligentTeachPlan { | |
| const parsed = typeof request === "string" ? parseStudyRequest(request) : request; | |
| const match: ChapterMatchResult = matchChapter(parsed.rawText, { | |
| level: parsed.level, | |
| subjectSlug: parsed.subjectHint, | |
| }); | |
| const hit = match.best; | |
| // Progress for the matched chapter (client-side; null on server). | |
| let entry: ChapterProgressEntry | null = null; | |
| if (progressOverride !== undefined) { | |
| entry = progressOverride; | |
| } else if (hit) { | |
| entry = getChapterEntry(hit.level, hit.subjectSlug, hit.chapter.slug); | |
| } | |
| const status: ChapterStatus = entry?.status ?? "not_started"; | |
| const quizScore = entry?.quizScore ?? null; | |
| const studyCount = entry?.studyCount ?? 0; | |
| const recommendedMode = resolveMode(parsed.intent, status); | |
| // When there's no confident match we still return a helpful, generic plan. | |
| if (!hit) { | |
| return { | |
| matchedChapter: null, | |
| confidence: match.confidence, | |
| confidenceLabel: match.confidenceLabel, | |
| alternatives: match.alternatives, | |
| matchReason: match.reason, | |
| intent: parsed.intent, | |
| urgency: parsed.urgency, | |
| recommendedMode, | |
| modeLabel: MODE_LABEL[recommendedMode], | |
| lessonStrategy: MODE_STRATEGY[recommendedMode], | |
| priorityFocus: ["Core concepts and definitions"], | |
| prerequisiteWarnings: [], | |
| examGuidance: "", | |
| revisionGuidance: "", | |
| weakTopicAdvice: parsed.weakTopicSignal | |
| ? "Tell me the exact chapter and we'll rebuild it slowly from basics." | |
| : "", | |
| nextActions: [{ kind: "start", label: "Start learning", primary: true }], | |
| aiPromptContext: buildAiContext(parsed, null, recommendedMode, status, quizScore), | |
| parsed, | |
| }; | |
| } | |
| const prerequisiteWarnings = resolvePrerequisiteWarnings(hit, progressOverride); | |
| const nextActions = buildNextActions( | |
| hit, | |
| status, | |
| parsed.intent, | |
| parsed.urgency, | |
| quizScore, | |
| prerequisiteWarnings.length > 0, | |
| ); | |
| return { | |
| matchedChapter: hit, | |
| confidence: match.confidence, | |
| confidenceLabel: match.confidenceLabel, | |
| alternatives: match.alternatives, | |
| matchReason: match.reason, | |
| intent: parsed.intent, | |
| urgency: parsed.urgency, | |
| recommendedMode, | |
| modeLabel: MODE_LABEL[recommendedMode], | |
| lessonStrategy: MODE_STRATEGY[recommendedMode], | |
| priorityFocus: buildPriorityFocus(hit, recommendedMode), | |
| prerequisiteWarnings, | |
| examGuidance: buildExamGuidance(hit), | |
| revisionGuidance: buildRevisionGuidance(status, studyCount, quizScore), | |
| weakTopicAdvice: buildWeakTopicAdvice(hit, parsed.weakTopicSignal, status, entry), | |
| nextActions, | |
| aiPromptContext: buildAiContext(parsed, hit, recommendedMode, status, quizScore), | |
| parsed, | |
| }; | |
| } | |
| // βββ AI payload mapping βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * Map a teach plan's AI context to a BrainGeneratePayload. The structured | |
| * teaching context rides along in `goal`, with `options` carrying mode/extras | |
| * the backend understands (unknown keys are safely ignored server-side). | |
| */ | |
| export function buildGenerationPayload( | |
| plan: IntelligentTeachPlan, | |
| extraOptions?: Record<string, unknown>, | |
| ): BrainGeneratePayload { | |
| const ctx = plan.aiPromptContext; | |
| return { | |
| topic: ctx.topic, | |
| subject: ctx.subject, | |
| level: ctx.studentLevel, | |
| goal: ctx.promptPreamble, | |
| language_preference: ctx.languagePreference ?? undefined, | |
| time_left: ctx.timeLeftMinutes ? `${ctx.timeLeftMinutes} minutes` : undefined, | |
| options: { | |
| mode: ctx.mode, | |
| intent: ctx.intent, | |
| urgency: ctx.urgency, | |
| curriculum_level: ctx.curriculumLevel, | |
| board: ctx.board, | |
| ...extraOptions, | |
| }, | |
| }; | |
| } | |