import type { TuitionLessonSeed } from "@/lib/teaching/physicsLessonSeeds"; /** * Phase 6L — long-form class runtime. * * Transforms a curated TuitionLessonSeed into a rich 30-50 minute class * runtime for the Tuition UI. Fully deterministic: no AI calls, no invented * content. Every field below is derived from seed fields that already passed * validation (source-backed, honest). Where the seed genuinely lacks data, * the runtime records a "needs curation" warning instead of faking text. */ export type LongFormTeachSegment = { title: string; durationMinutes: number; teacherScript: string[]; boardPoints: string[]; examples: string[]; malayalamSupport?: string; manglishSupport?: string; checkpointQuestion?: string; }; export type LongFormBoardFrame = { title: string; durationMinutes: number; boardDrawingSteps: string[]; diagramDescription: string; labels: string[]; teacherNarration: string[]; studentShouldNotice: string[]; }; export type LongFormMethodCard = { title: string; method: string; whenToUse: string; mistakeAvoided: string; example: string; }; export type LongFormExamPractice = { marks: number; question: string; answerStructure: string[]; keywords: string[]; commonMissingPoints: string[]; improvedAnswer: string; }; export type LongFormPracticeItem = { type: "short" | "numerical" | "diagram" | "reasoning"; question: string; hint: string; expectedAnswer: string[]; rubric: string[]; }; export type LongFormClassRuntime = { missionId: string; estimatedMinutes: number; sessionPlan: { title: string; learningGoals: string[]; prerequisites: string[]; chapterPosition: string; examRelevance: string[]; classFlow: string[]; byTheEnd: string[]; timing: Array<{ block: string; minutes: number }>; }; teachSegments: LongFormTeachSegment[]; visualBoardFrames: LongFormBoardFrame[]; tricksAndMethods: LongFormMethodCard[]; detailedNotes: { definitions: string[]; formulas: string[]; examKeywords: string[]; commonMistakes: string[]; boardAnswerPoints: string[]; copyToNotebook: string[]; malayalamQuickHelp?: string[]; manglishQuickHelp?: string[]; }; examPractice: LongFormExamPractice[]; practiceSet: LongFormPracticeItem[]; revisionPack: { flashcards: Array<{ front: string; back: string; requiredKeywords?: string[] }>; onePageRevision: string[]; lastMinuteMistakes: string[]; }; /** Deterministic teacher narration — placeholder for future audio/video generation. */ narration: { lessonMediaPlaceholder: string; teacherScript: string[]; }; /** "Needs curation" gaps — shown honestly, never filled with fake text. */ warnings: string[]; }; const NEEDS_CURATION = "This section needs curation for this mission."; function unique(values: string[]): string[] { return Array.from(new Set(values.map((v) => v.trim()).filter(Boolean))); } /** Deterministic integer split of `total` into `parts` chunks (≥1 each). */ function splitMinutes(total: number, parts: number): number[] { if (parts <= 0) return []; const base = Math.max(1, Math.floor(total / parts)); const result = Array.from({ length: parts }, () => base); let remainder = Math.max(0, total - base * parts); for (let i = 0; remainder > 0 && i < parts; i += 1, remainder -= 1) result[i] += 1; return result; } function timingPlan(estimatedMinutes: number): Array<{ block: string; minutes: number }> { // Deterministic proportional split of the whole class. const teach = Math.max(10, Math.round(estimatedMinutes * 0.3)); const board = Math.max(8, Math.round(estimatedMinutes * 0.2)); const methods = Math.max(4, Math.round(estimatedMinutes * 0.1)); const notes = Math.max(5, Math.round(estimatedMinutes * 0.12)); const exam = Math.max(6, Math.round(estimatedMinutes * 0.14)); const plan = 3; const used = plan + teach + board + methods + notes + exam; const practice = Math.max(4, estimatedMinutes - used); return [ { block: "Session Plan", minutes: plan }, { block: "Teach", minutes: teach }, { block: "Visual Board", minutes: board }, { block: "DocDoe Methods", minutes: methods }, { block: "Key Notes", minutes: notes }, { block: "Exam Answers", minutes: exam }, { block: "Practice + Correction + Quiz + Revision", minutes: practice }, ]; } function buildTeachSegments(seed: TuitionLessonSeed, warnings: string[]): LongFormTeachSegment[] { const blocks = seed.teach.explanationBlocks; if (blocks.length === 0) { warnings.push(`Teach segments: ${NEEDS_CURATION}`); return []; } const minutes = splitMinutes(Math.max(10, Math.round(seed.estimatedMinutes * 0.3)), blocks.length); const checkpoints = seed.quiz.map((q) => q.question); return blocks.map((block, index) => { const boardPoints = unique([ ...block.keyTerms, ...seed.keyNotes.filter((note) => block.keyTerms.some((term) => note.toLowerCase().includes(term.toLowerCase().slice(0, 6)))), ]); return { title: block.heading, durationMinutes: minutes[index] ?? 1, teacherScript: [ index === 0 ? seed.teach.teacherIntro : "", block.explanation, ].filter(Boolean), boardPoints: boardPoints.length > 0 ? boardPoints : block.keyTerms, examples: index === blocks.length - 1 && seed.teach.analogy ? [seed.teach.analogy] : [], malayalamSupport: index === 0 ? seed.teach.malayalamSupport : undefined, manglishSupport: index === 0 ? seed.teach.manglishSupport : undefined, checkpointQuestion: checkpoints[index], }; }); } function buildBoardFrames(seed: TuitionLessonSeed, warnings: string[]): LongFormBoardFrame[] { const frames = seed.visualBoard.frames; if (frames.length === 0) { warnings.push(`Visual board: ${NEEDS_CURATION}`); return []; } const minutes = splitMinutes(Math.max(8, Math.round(seed.estimatedMinutes * 0.2)), frames.length); return frames.map((frame, index) => ({ title: frame.title, durationMinutes: minutes[index] ?? 1, boardDrawingSteps: [frame.teacherAction, ...frame.boardText], diagramDescription: frame.diagramDescription ?? "", labels: unique(frame.boardText.flatMap((line) => line.split(/[→=:;,]/).map((part) => part.trim()).filter((part) => part.length > 2 && part.length <= 40))).slice(0, 6), teacherNarration: [frame.teacherAction, frame.studentFocus], studentShouldNotice: [frame.studentFocus], })); } function buildMethods(seed: TuitionLessonSeed): LongFormMethodCard[] { const methods: LongFormMethodCard[] = []; const mistakes = unique(seed.examAnswers.flatMap((a) => a.commonMistakes)); const keywords = unique(seed.examAnswers.flatMap((a) => a.keywords)); // 1) The seed's own DocDoe trick — always first. methods.push({ title: seed.docDoeTrick.title, method: seed.docDoeTrick.trick, whenToUse: "Use while learning the concept and again during last-minute revision.", mistakeAvoided: mistakes[0] ?? "Forgetting the core idea under exam pressure.", example: seed.docDoeTrick.whyItWorks, }); // 2) Formula/rule selection method (only when the seed has formulas). if (seed.formulas.length > 0) { const formula = seed.formulas[0]; methods.push({ title: "Formula selection method", method: `Write the formula first: ${formula.formula}. ${formula.commonUseCase}`, whenToUse: "At the start of every numerical or formula-based answer.", mistakeAvoided: formula.numericalPattern ?? "Substituting values before writing the formula loses the method mark.", example: formula.symbols.map((s) => `${s.symbol} = ${s.meaning}${s.unit ? ` (${s.unit})` : ""}`).join("; "), }); } // 3) Exam keyword method (from the seed's exam answers). if (keywords.length > 0) { methods.push({ title: "Exam keyword method", method: `Before writing, list the keywords: ${keywords.slice(0, 5).join(", ")}. Tick each one as you use it.`, whenToUse: "In every written answer — keywords carry the marks.", mistakeAvoided: "Writing the right idea but missing the exact keyword the examiner looks for.", example: seed.examAnswers[0]?.boardStyleRewrite ?? seed.examAnswers[0]?.answerPoints[0] ?? "", }); } // 4) Mistake-avoidance method (from common mistakes). if (mistakes.length > 0) { methods.push({ title: "Mistake-avoidance method", method: `Check your answer against the known traps: ${mistakes.slice(0, 3).join("; ")}.`, whenToUse: "After writing, before moving on — a 20-second self-check.", mistakeAvoided: mistakes[0], example: seed.correctionRubric.commonMissingParts.join("; ") || mistakes[0], }); } // 5) Board-drawing memory method (from the visual board). if (seed.visualBoard.frames.length > 0) { const first = seed.visualBoard.frames[0]; methods.push({ title: "Board memory method", method: `Redraw the board from memory: ${seed.visualBoard.frames.map((f) => f.title).join(" → ")}.`, whenToUse: "During revision — drawing recalls more than re-reading.", mistakeAvoided: "Recognising the diagram in the book but failing to reproduce it in the exam.", example: first.teacherAction, }); } return methods.slice(0, 5); } function buildDetailedNotes(seed: TuitionLessonSeed): LongFormClassRuntime["detailedNotes"] { const keywords = unique(seed.examAnswers.flatMap((a) => a.keywords)); const mistakes = unique([ ...seed.examAnswers.flatMap((a) => a.commonMistakes), ...seed.correctionRubric.commonMissingParts, ]); const boardAnswers = unique(seed.examAnswers.map((a) => a.boardStyleRewrite)); return { definitions: seed.keyNotes, formulas: seed.formulas.map( (f) => `${f.formula} — ${f.meaning} [${f.symbols.map((s) => `${s.symbol}=${s.meaning}${s.unit ? ` (${s.unit})` : ""}`).join(", ")}]`, ), examKeywords: keywords.length > 0 ? keywords : seed.tags, commonMistakes: mistakes, boardAnswerPoints: boardAnswers, copyToNotebook: unique([...seed.keyNotes, ...seed.revisionSave.revisionNotes]), malayalamQuickHelp: seed.teach.malayalamSupport ? [seed.teach.malayalamSupport] : undefined, manglishQuickHelp: seed.teach.manglishSupport ? [seed.teach.manglishSupport] : undefined, }; } function buildExamPractice(seed: TuitionLessonSeed): LongFormExamPractice[] { return [...seed.examAnswers] .sort((a, b) => a.marks - b.marks) .map((answer) => ({ marks: answer.marks, question: answer.question, answerStructure: answer.answerPoints, keywords: answer.keywords, commonMissingPoints: answer.commonMistakes, improvedAnswer: answer.boardStyleRewrite, })); } function practiceTypeFor(quizType: TuitionLessonSeed["quiz"][number]["type"]): LongFormPracticeItem["type"] { if (quizType === "numerical") return "numerical"; if (quizType === "diagram") return "diagram"; if (quizType === "mcq") return "short"; return "reasoning"; } function buildPracticeSet(seed: TuitionLessonSeed): LongFormPracticeItem[] { const rubric = seed.correctionRubric.scoringPoints.map( (point) => `${point.point} (${point.marks} mark${point.marks === 1 ? "" : "s"}; keywords: ${point.requiredKeywords.join(", ")})`, ); const main: LongFormPracticeItem = { type: "reasoning", question: seed.yourTurn.prompt, hint: seed.yourTurn.hint, expectedAnswer: seed.yourTurn.expectedAnswerPoints, rubric, }; const fromQuiz: LongFormPracticeItem[] = seed.quiz.map((q) => ({ type: practiceTypeFor(q.type), question: q.question, hint: q.explanation, expectedAnswer: [q.answer], rubric: [], })); return [main, ...fromQuiz]; } function buildNarration(seed: TuitionLessonSeed, segments: LongFormTeachSegment[], frames: LongFormBoardFrame[]): LongFormClassRuntime["narration"] { return { lessonMediaPlaceholder: "Video renderer coming later; this is today's lesson plan outline from the curated class content.", teacherScript: [ `Today's class: ${seed.missionTitle}. ${seed.goal}`, ...segments.flatMap((segment) => segment.teacherScript), ...frames.flatMap((frame) => frame.teacherNarration), `Recap: ${seed.recap.summary.join(" ")}`, ].filter(Boolean), }; } /** Build the long-form runtime from a curated lesson seed. Deterministic. */ export function buildLongFormClassRuntime(seed: TuitionLessonSeed): LongFormClassRuntime { const warnings: string[] = []; const teachSegments = buildTeachSegments(seed, warnings); const visualBoardFrames = buildBoardFrames(seed, warnings); const marksCovered = unique(seed.examAnswers.map((a) => `${a.marks}-mark`)); if (seed.revisionSave.flashcards.length === 0) warnings.push(`Flashcards: ${NEEDS_CURATION}`); if (seed.examAnswers.length === 0) warnings.push(`Exam practice: ${NEEDS_CURATION}`); return { missionId: seed.missionId, estimatedMinutes: seed.estimatedMinutes, sessionPlan: { title: `Today's Session Plan — ${seed.missionTitle}`, learningGoals: [seed.goal, ...seed.teach.explanationBlocks.map((block) => `Understand: ${block.heading}`)], prerequisites: seed.recall.expectedPoints, chapterPosition: `${seed.chapterTitle} — mission ${seed.missionId.replace(/^M/, "")} (${seed.contentType.replace(/_/g, " ")})`, examRelevance: marksCovered.length > 0 ? [`Board answer practice: ${marksCovered.join(", ")} questions`, ...unique(seed.examAnswers.map((a) => a.question)).slice(0, 3)] : [], classFlow: [ "Session Plan — what today covers", `Teach — ${seed.teach.explanationBlocks.map((block) => block.heading).join(", ")}`, `Visual Board — ${seed.visualBoard.frames.length} board frame${seed.visualBoard.frames.length === 1 ? "" : "s"}`, "DocDoe Methods — tricks and self-checks", "Key Notes — notebook notes", "Exam Answers — board-style writing", "Your Turn + Correction + Quiz", "Recap + Save Revision", ], byTheEnd: [ seed.goal, ...seed.recap.mustRemember.map((item) => `Remember: ${item}`), ...(seed.examAnswers[0] ? [`Write a ${seed.examAnswers[0].marks}-mark board answer with the right keywords`] : []), ], timing: timingPlan(seed.estimatedMinutes), }, teachSegments, visualBoardFrames, tricksAndMethods: buildMethods(seed), detailedNotes: buildDetailedNotes(seed), examPractice: buildExamPractice(seed), practiceSet: buildPracticeSet(seed), revisionPack: { flashcards: seed.revisionSave.flashcards, onePageRevision: unique([...seed.recap.summary, ...seed.recap.mustRemember, ...seed.revisionSave.revisionNotes]), lastMinuteMistakes: unique([ ...seed.examAnswers.flatMap((a) => a.commonMistakes), ...seed.correctionRubric.commonMissingParts, ]), }, narration: buildNarration(seed, teachSegments, visualBoardFrames), warnings, }; }