import type { SourceRef, TuitionSessionData } from "./sessionData"; import { getPhysicsLessonSeed, NOT_CURATED_MESSAGE, type PhysicsFormulaSeed, type TuitionLessonSeed, type VisualBoardSeed, } from "@/lib/teaching/physicsLessonSeeds"; import { objectivesForLessonSeed, type LessonObjective, } from "@/lib/teaching/lessonObjectives"; import { buildLongFormClassRuntime, type LongFormClassRuntime, } from "@/lib/tuition/longFormClassRuntime"; import { getSslcCoverage, getSslcStudyPack, sslcVideoSrc, studentReadySampleQuestions, type SslcStudyPack, } from "@/lib/tuition/sslcStudyPack"; import type { StudyIntent } from "@/lib/study-intents"; import { getTeachLessonNotes, TEACH_METHOD } from "@/lib/tuition/teachLessonNotes"; import "@/content/sslc/register-all-teach-notes"; export type TuitionLessonStepId = | "recall" | "teach" | "video" | "visual-board" | "docdoe-trick" | "key-notes" | "exam-answer" | "your-turn" | "correction" | "quiz" | "save-revision"; export type TuitionLessonStepDefinition = { id: TuitionLessonStepId; label: string; }; export type TuitionClassSession = { id: string; chapterId: string; missionId: string; subject: string; chapter: string; missionTitle: string; goal: string; examUse: string; estimatedMinutes: number; sourceRefs: SourceRef[]; /** Stable, source-derived objectives displayed and retained with the class. */ objectives: LessonObjective[]; recall: { question: string; expectedIdea: string; correctFeedback: string; incorrectFeedback: string; }; teach: { title: string; explanation: string; keyConcepts: string[]; teacherNote: string; }; visualBoard: { title: string; mindMap: string[]; flow: string[]; diagram: string[]; conclusion: string; }; trick: { title: string; acronym: string; memoryLine: string; meanings: string[]; examUsage: string; }; keyNotes: { points: string[]; keywords: string[]; commonMistake: string; }; examAnswer: { question: string; twoMarkAnswer: string; fourMarkAnswer: string[]; keywords: string[]; structure: string[]; }; practice: { question: string; hint: string; }; quiz: { questions: TuitionClassQuizQuestion[]; }; recap: { learned: string[]; memoryLine: string; examReadiness: string[]; weakAreas: string[]; nextRevisionDate: string; }; saveRevision: { savedTopics: string[]; reason: string; revisionDate: string; weekendPyq: string; }; // ── Phase 6D optional rich content (populated only from a lesson seed) ── /** Structured visual-board frames (rendered as board-step cards). */ boardFrames?: VisualBoardSeed["frames"]; /** Physics formulas with symbols/units, shown in the Key Notes step. */ formulas?: PhysicsFormulaSeed[]; /** Short Malayalam / Manglish helper support for the current concept. */ langSupport?: { malayalam?: string; manglish?: string }; /** Correction rubric (scoring points + keywords) for the Your Turn answer. */ correction?: { maxMarks: number; scoringPoints: Array<{ point: string; marks: number; requiredKeywords: string[]; minimumMatches?: number; }>; commonMissingParts: string[]; }; /** When set, the mission is source-backed but has no curated seed yet. */ notCurated?: { message: string }; /** * Phase 6L long-form runtime (populated only from a curated lesson seed). * Turns the same seed into a 30-50 minute teacher-led session: session plan, * multi-segment teaching, board frames with narration, method cards, * notebook-grade notes, grouped exam practice and a revision pack. */ runtime?: LongFormClassRuntime; chapterMedia?: { videoStatus: "available" | "in_production" | "planned"; videoSrc: string | null; captionsSrc: string | null; }; studyPack?: SslcStudyPack | null; teachNotes?: import("@/lib/tuition/teachLessonNotes").TeachLessonNotes | null; }; export type TuitionClassQuizQuestion = { id: string; prompt: string; answer: string; type: "mcq" | "short" | "reason" | "exam-style"; options?: string[]; /** Shown after the quiz is submitted (from the curated seed). */ explanation?: string; difficulty?: "easy" | "medium" | "exam"; /** Exact earlier mission assessed by a chapter-test question. */ conceptMissionId?: string; conceptLabel?: string; }; // Step ids stay stable for saved-progress backward compatibility. // Product labels match the continuous class language. // "recap" was folded into save-revision (Recap/Revision); "video" is the new step 3. export const tuitionLessonSteps = [ { id: "recall", label: "Session Plan" }, { id: "teach", label: "Teach" }, { id: "video", label: "Video" }, { id: "visual-board", label: "Visual Board" }, { id: "docdoe-trick", label: "Methods/Formulas" }, { id: "key-notes", label: "Key Notes" }, { id: "exam-answer", label: "Exam Answer" }, { id: "your-turn", label: "Your Turn" }, { id: "correction", label: "Accurate Correction" }, { id: "quiz", label: "Quiz/Flashcards" }, { id: "save-revision", label: "Recap/Revision" }, ] as const satisfies readonly TuitionLessonStepDefinition[]; function attachChapterNotebook(session: TuitionClassSession): TuitionClassSession { const notes = getTeachLessonNotes(session.chapterId); if (!notes) return session; session.teachNotes = notes; session.teach = { ...session.teach, title: notes.title, teacherNote: notes.method, keyConcepts: notes.goals, }; // A readable notebook cannot make missing reviewed questions ready. return session; } const DEFAULT_KEYWORDS = [ "direct answer", "exam keywords", "clear points", "final result", ]; function cleanMissionTitle( title: string | undefined, fallback: string, ): string { return title?.replace(/^M\d+:\s*/i, "").trim() || fallback; } function getEstimatedMinutes(value: string | undefined): number { const match = value?.match(/\d+/); return match ? Number(match[0]) : 45; } function sentenceFromPoints(points: string[]): string { return points.filter(Boolean).slice(0, 2).join(" "); } function getKeywords(session: TuitionSessionData): string[] { const quizKeywords = session.quiz.keywords?.filter(Boolean) ?? []; const noteKeywords = session.notes.map((note) => note.title).filter(Boolean); const weakKeywords = session.weakAreas .map((area) => area.label) .filter(Boolean); const merged = [...quizKeywords, ...noteKeywords, ...weakKeywords]; return Array.from(new Set(merged)).slice(0, 8); } function getCommonMistake(session: TuitionSessionData): string { const warning = session.notes.find((note) => note.tone === "warning"); return ( warning?.body || session.feedback.mistake || "Students often miss one exam keyword even when the idea is correct." ); } function getRenaissanceTrick( topic: string, ): TuitionClassSession["trick"] | null { if (!/renaissance|italy|humanism/i.test(topic)) return null; return { title: "DocDoe Trick: C-T-K-P", acronym: "C-T-K-P", memoryLine: "Cities Trade Knowledge with Patrons.", meanings: [ "C = City-states", "T = Trade and wealth", "K = Greek and Roman knowledge", "P = Patronage from rich supporters", ], examUsage: "For 2 marks, write any two points. For 4 marks, write all four points with one explanation each.", }; } function buildGenericTrick( title: string, points: string[], ): TuitionClassSession["trick"] { const firstFour = points.filter(Boolean).slice(0, 4); const letters = firstFour .map((point) => point.trim()[0]?.toUpperCase()) .filter(Boolean); const acronym = letters.length >= 2 ? letters.join("-") : "P-K-A"; return { title: `DocDoe Trick: ${acronym}`, acronym, memoryLine: `Remember ${acronym} before writing the answer.`, meanings: firstFour.map( (point, index) => `${letters[index] ?? index + 1} = ${point}`, ), examUsage: "Write the direct point first, then add one short explanation for each mark.", }; } function buildQuiz( session: TuitionSessionData, keywords: string[], ): TuitionClassQuizQuestion[] { const questions: TuitionClassQuizQuestion[] = []; const answerOutline = session.quiz.answerOutline || session.feedback.why || session.lessonTakeaway; if (session.quiz.question) { questions.push({ id: session.quiz.id ?? "mission-question", prompt: session.quiz.question, answer: answerOutline, type: session.quiz.options.length > 0 ? "mcq" : "exam-style", options: session.quiz.options.map( (option) => `${option.key}. ${option.text}`, ), }); } questions.push( { id: "meaning-check", prompt: `What is the main idea of ${cleanMissionTitle(session.missionTitle, session.chapterSubtitle)}?`, answer: session.lessonTakeaway, type: "short", }, { id: "reason-check", prompt: `Name two reasons or points connected to ${session.chapterSubtitle}.`, answer: session.notes .slice(0, 2) .map((note) => note.body) .join(" "), type: "reason", }, { id: "keyword-check", prompt: "Which keyword is important for the exam answer?", answer: keywords[0] ?? DEFAULT_KEYWORDS[0], type: "short", }, ); return questions.slice(0, 4); } function seedSourceRefs(seed: TuitionLessonSeed): SourceRef[] { return seed.sourceRefs.map((ref) => ({ file_name: ref.sourceBook, book: ref.chapterTitle, printed_page: ref.pageRange ? Number.parseInt(ref.pageRange.split("-")[0] ?? "", 10) || undefined : undefined, })); } function seedQuizType( type: TuitionLessonSeed["quiz"][number]["type"], ): TuitionClassQuizQuestion["type"] { if (type === "mcq") return "mcq"; if (type === "short") return "short"; if (type === "numerical") return "exam-style"; return "reason"; } function attachSslcChapterMedia( session: TuitionClassSession, options: { includeSampleQuiz?: boolean } = {}, ): TuitionClassSession { const coverage = getSslcCoverage(session.chapterId); const pack = getSslcStudyPack(session.chapterId); const sampleQuestions = studentReadySampleQuestions(pack); const videoSrc = coverage?.videoStatus === "available" ? sslcVideoSrc(session.chapterId) : null; const notes = pack?.shortNotes?.length ? Array.from( new Set([...(session.keyNotes.points ?? []), ...pack.shortNotes]), ).slice(0, 12) : session.keyNotes.points; const extraQuiz = options.includeSampleQuiz === false ? [] : sampleQuestions.slice(0, 4).map((q) => ({ id: q.id, prompt: q.question, answer: q.answer, type: "exam-style" as const, explanation: "Sample practice from the 2025 chapter pack — not a board-certified PYQ.", })); return { ...session, chapterMedia: coverage ? { videoStatus: coverage.videoStatus, videoSrc, captionsSrc: coverage.captionsPublicPath, } : session.chapterMedia, studyPack: pack ? { ...pack, sampleQuestions } : (session.studyPack ?? null), keyNotes: { ...session.keyNotes, points: notes, commonMistake: pack?.commonMistakes?.[0] ?? session.keyNotes.commonMistake, }, quiz: { questions: [ ...session.quiz.questions, ...extraQuiz.filter( (q) => !session.quiz.questions.some((s) => s.prompt === q.prompt), ), ], }, }; } export function classSessionFromStudyPack( chapterId: string, missionId = "M1", ): TuitionClassSession | null { if (missionId.trim().toUpperCase() !== "M1") return null; const pack = getSslcStudyPack(chapterId); const coverage = getSslcCoverage(chapterId); if (!pack || !coverage) return null; const sampleQuestions = studentReadySampleQuestions(pack); const defs = pack.definitions .slice(0, 6) .map((d) => `${d.term}: ${d.definition}`); const sample = sampleQuestions[0]; const session: TuitionClassSession = { id: `${chapterId}-${missionId}`, chapterId, missionId, subject: coverage.subject, chapter: coverage.name, missionTitle: pack.title, goal: pack.shortNotes[0] || pack.title, examUse: "Sample practice from the 2025 study pack — not board-certified.", estimatedMinutes: coverage.videoStatus === "available" ? 40 : 25, sourceRefs: [{ file_name: pack.source, book: pack.title }], objectives: [], recall: { question: pack.definitions[0] ? `What is ${pack.definitions[0].term}?` : `What is the key idea of ${pack.title}?`, expectedIdea: pack.definitions[0]?.definition || pack.shortNotes[0] || pack.title, correctFeedback: "Good. Keep the textbook wording for the exam.", incorrectFeedback: pack.commonMistakes[0] || "Re-read the short notes before writing.", }, teach: { title: pack.title, explanation: pack.shortNotes.slice(0, 4).join(" "), keyConcepts: pack.definitions.slice(0, 6).map((d) => d.term), teacherNote: pack.source, }, visualBoard: { title: pack.title, mindMap: pack.shortNotes.slice(0, 6), flow: defs, diagram: pack.formulas.slice(0, 4), conclusion: pack.commonMistakes[0] || "Write mark-wise points.", }, trick: { title: "Sample pack", acronym: "", memoryLine: pack.shortNotes[0] || pack.title, meanings: pack.shortNotes.slice(0, 4), examUsage: "Sample practice only.", }, keyNotes: { points: pack.shortNotes.slice(0, 10), keywords: pack.definitions.slice(0, 8).map((d) => d.term), commonMistake: pack.commonMistakes[0] || "Skipping exam keywords.", }, examAnswer: { question: sample?.question || `Explain ${pack.title}.`, twoMarkAnswer: sample?.answer || pack.shortNotes[0] || "", fourMarkAnswer: pack.shortNotes.slice(0, 4), keywords: pack.definitions.slice(0, 5).map((d) => d.term), structure: [ "Direct definition.", "Two supporting points.", "Label it sample practice.", ], }, practice: { question: sample?.question || pack.shortNotes[0] || pack.title, hint: "Sample practice — use the notes above.", }, quiz: { questions: sampleQuestions.slice(0, 6).map((q) => ({ id: q.id, prompt: q.question, answer: q.answer, type: "exam-style" as const, explanation: "Sample practice from the 2025 chapter pack — not a board-certified PYQ.", })), }, recap: { learned: pack.shortNotes.slice(0, 4), memoryLine: pack.shortNotes[0] || pack.title, examReadiness: [ "Sample questions done", "Rewatch the chapter video if it is available", ], weakAreas: pack.commonMistakes.slice(0, 3), nextRevisionDate: "Friday", }, saveRevision: { savedTopics: pack.definitions.slice(0, 3).map((d) => d.term), reason: "Saved from the 2025 study pack.", revisionDate: "Friday", weekendPyq: "Sample practice only.", }, ...(sampleQuestions.length > 0 ? {} : { notCurated: { message: "Practice questions for this lesson are still being reviewed. DocDoe will make the lesson available when the questions are ready.", }, }), }; return attachSslcChapterMedia(attachChapterNotebook(session)); } /** Map a curated Physics lesson seed to the 11-step class session (rich). */ export function classSessionFromSeed( seed: TuitionLessonSeed, ): TuitionClassSession { return attachSslcChapterMedia(classSessionFromSeedInner(seed), { // Curated lessons already assess their own taught concepts. Chapter-wide // samples can ask about topics that this lesson has not introduced yet. includeSampleQuiz: false, }); } function classSessionFromSeedInner( seed: TuitionLessonSeed, ): TuitionClassSession { const twoMark = seed.examAnswers.find((answer) => answer.marks <= 2) ?? seed.examAnswers[0]; const fourMark = seed.examAnswers.find((answer) => answer.marks >= 3) ?? twoMark; const examKeywords = Array.from( new Set(seed.examAnswers.flatMap((answer) => answer.keywords)), ); const session: TuitionClassSession = { id: `${seed.chapterId}-${seed.missionId}`, chapterId: seed.chapterId, missionId: seed.missionId, subject: seed.subject, chapter: seed.chapterTitle, missionTitle: seed.missionTitle, goal: seed.goal, examUse: `${Array.from(new Set([twoMark?.marks ?? 2, fourMark?.marks ?? 4])).join("/")}-mark board answer practice`, estimatedMinutes: seed.estimatedMinutes, sourceRefs: seedSourceRefs(seed), objectives: objectivesForLessonSeed(seed), recall: { question: seed.recall.prompt, expectedIdea: seed.recall.expectedPoints.join("; "), correctFeedback: `Good. ${seed.recall.expectedPoints[0] ?? "Keep this idea for the class."}`, incorrectFeedback: `No problem. Remember: ${seed.recall.misconceptionCheck}`, }, teach: { title: seed.missionTitle, explanation: seed.teach.teacherIntro, keyConcepts: seed.teach.explanationBlocks.map((block) => block.heading), teacherNote: seed.teach.explanationBlocks .map((block) => block.explanation) .join(" "), }, visualBoard: { title: seed.visualBoard.boardTitle, mindMap: seed.visualBoard.frames.map((frame) => frame.title), flow: seed.visualBoard.frames.flatMap((frame) => frame.boardText), diagram: seed.visualBoard.frames .map((frame) => frame.diagramDescription) .filter((value): value is string => Boolean(value)), conclusion: seed.visualBoard.frames[seed.visualBoard.frames.length - 1] ?.studentFocus ?? seed.goal, }, boardFrames: seed.visualBoard.frames, trick: { title: seed.docDoeTrick.title, acronym: seed.docDoeTrick.title, memoryLine: seed.docDoeTrick.trick, meanings: [seed.docDoeTrick.whyItWorks], examUsage: seed.docDoeTrick.whyItWorks, }, keyNotes: { points: seed.keyNotes, keywords: examKeywords.length > 0 ? examKeywords : seed.tags, commonMistake: seed.examAnswers.flatMap((answer) => answer.commonMistakes)[0] ?? "Watch the exam keywords.", }, formulas: seed.formulas.length > 0 ? seed.formulas.map((formula) => ({ ...formula, commonMistake: formula.commonMistake ?? "Check the formula symbols, SI units, and final unit before submitting.", })) : undefined, examAnswer: { question: (twoMark ?? fourMark)?.question ?? `Explain ${seed.missionTitle}.`, twoMarkAnswer: twoMark?.boardStyleRewrite ?? (twoMark?.answerPoints ?? []).join(" "), fourMarkAnswer: ( fourMark?.answerPoints ?? twoMark?.answerPoints ?? [] ).slice(0, 5), keywords: examKeywords, structure: [ "Start with a direct answer.", "Add the exam keywords.", "Keep one line per point.", "End with the result.", ], }, practice: { question: seed.yourTurn.prompt, hint: seed.yourTurn.hint, }, correction: { maxMarks: seed.correctionRubric.maxMarks, scoringPoints: seed.correctionRubric.scoringPoints, commonMissingParts: seed.correctionRubric.commonMissingParts, }, quiz: { questions: seed.quiz.map((question) => ({ id: question.id, prompt: question.question, answer: question.answer, type: seedQuizType(question.type), options: question.options, explanation: question.explanation, difficulty: question.difficulty, conceptMissionId: question.conceptMissionId, conceptLabel: question.conceptLabel, })), }, recap: { learned: seed.recap.summary, memoryLine: seed.docDoeTrick.trick, examReadiness: seed.recap.mustRemember, weakAreas: seed.revisionSave.weakAreaTags, nextRevisionDate: "Friday", }, saveRevision: { savedTopics: seed.revisionSave.weakAreaTags, reason: `Saved from today's ${seed.subject} class — key ideas, definitions and common mistakes.`, revisionDate: "Friday", weekendPyq: `${seed.chapterTitle} practice can appear in weekend exam practice.`, }, langSupport: seed.teach.malayalamSupport || seed.teach.manglishSupport ? { malayalam: seed.teach.malayalamSupport, manglish: seed.teach.manglishSupport, } : undefined, runtime: buildLongFormClassRuntime(seed), }; const notes = getTeachLessonNotes(seed.chapterId, seed.missionId); const sections = notes?.sections ?? seed.teach.explanationBlocks.map((block, index) => ({ id: `${seed.missionId}-explanation-${index + 1}`, title: block.heading, markdown: block.explanation, boardPoints: session.runtime?.teachSegments[index]?.boardPoints ?? block.keyTerms, })); session.teachNotes = { chapterId: seed.chapterId, missionId: seed.missionId, scope: "mission", title: seed.missionTitle, method: notes?.method ?? TEACH_METHOD, goals: [seed.goal], // Keep the original authored explanation when there is no explicit // notebook mapping. Never substitute a neighbouring mission or chapter. sections: sections.map((section, index) => ({ ...section, markdown: [ index === 0 && !section.markdown.includes(seed.teach.teacherIntro) ? seed.teach.teacherIntro : "", section.markdown, index === sections.length - 1 && seed.teach.analogy && !section.markdown.includes(seed.teach.analogy) ? `Example: ${seed.teach.analogy}` : "", ].filter(Boolean).join("\n\n"), })), }; return session; } /** * Safe fallback for a Physics mission that is source-backed but has no curated * lesson seed yet. Never fabricates content and never uses Humanism content. */ export function notCuratedPhysicsClassSession( session: TuitionSessionData, ): TuitionClassSession { const title = cleanMissionTitle( session.missionTitle, session.lessonBoard.title || session.chapterSubtitle, ); const message = NOT_CURATED_MESSAGE; const line = [message]; return { id: `${session.chapterId ?? "chapter"}-${session.missionId ?? "mission"}`, chapterId: session.chapterId ?? "chapter", missionId: session.missionId ?? "mission", subject: session.subject ?? "Physics", chapter: session.chapterLabel, missionTitle: title, goal: session.missionGoal || title, examUse: "Lesson seed pending review", estimatedMinutes: getEstimatedMinutes(session.estimatedTime), sourceRefs: session.source_refs, objectives: [], notCurated: { message }, recall: { question: message, expectedIdea: "", correctFeedback: message, incorrectFeedback: message, }, teach: { title, explanation: message, keyConcepts: [], teacherNote: message, }, visualBoard: { title, mindMap: line, flow: line, diagram: [], conclusion: message, }, trick: { title: "Pending", acronym: "", memoryLine: message, meanings: [], examUsage: message, }, keyNotes: { points: line, keywords: [], commonMistake: message }, examAnswer: { question: message, twoMarkAnswer: message, fourMarkAnswer: line, keywords: [], structure: [], }, practice: { question: message, hint: "" }, quiz: { questions: [] }, recap: { learned: line, memoryLine: message, examReadiness: [], weakAreas: [], nextRevisionDate: "", }, saveRevision: { savedTopics: [], reason: message, revisionDate: "", weekendPyq: "", }, }; } export function buildTuitionClassSession( session: TuitionSessionData, ): TuitionClassSession { // Phase 6D: a curated Physics lesson seed wins — the class uses real seed // content across all 11 steps. A Physics mission without a seed shows a safe // "not curated yet" state (never Humanism content, never fabricated). const coverage = getSslcCoverage(session.chapterId); // A chapter-level study pack is the M1 fallback only. Reusing it for an // unknown mission would make an unavailable lesson look like a real class. const missionId = session.missionId ?? "M1"; const packSession = /^M1$/i.test(missionId.trim()) ? classSessionFromStudyPack(session.chapterId ?? "", missionId) : null; const seed = getPhysicsLessonSeed(session.chapterId, session.missionId); if (seed) { const seedName = seed.chapterTitle.trim().toLowerCase(); const covName = (coverage?.name ?? "").trim().toLowerCase(); const namesMatch = !covName || seedName === covName || covName.includes(seedName) || seedName.includes(covName); if (namesMatch) return classSessionFromSeed(seed); if (packSession) return packSession; // A stale seed must never win over the authoritative chapter identity. // Keep the route usable, but make the missing curation explicit instead of // showing a different chapter's lesson under this task. return notCuratedPhysicsClassSession(session); } if (packSession) return packSession; // Any catalogued chapter without a matching reviewed lesson must fail // honestly. Falling through to the old generic demo content can put a // different academic topic under a real student's assigned mission. if ( coverage || ["physics", "chemistry", "biology", "maths"].includes( (session.subject ?? "").toLowerCase(), ) ) { return notCuratedPhysicsClassSession(session); } const title = cleanMissionTitle( session.missionTitle, session.lessonBoard.title || session.chapterSubtitle, ); const keyPoints = session.notes.map((note) => note.body).filter(Boolean); const notes = keyPoints.length > 0 ? keyPoints.slice(0, 5) : session.lessonBoard.points.slice(0, 5); const keywords = getKeywords(session); const safeKeywords = keywords.length > 0 ? keywords : DEFAULT_KEYWORDS; const answerQuestion = session.quiz.question || `Explain ${title}.`; const fourMarkPoints = notes.length >= 4 ? notes.slice(0, 4) : [...notes, session.feedback.why].filter(Boolean).slice(0, 4); const trick = getRenaissanceTrick(`${title} ${session.missionGoal} ${answerQuestion}`) ?? buildGenericTrick(title, notes); const weakAreas = session.weakAreas.map((area) => area.label); const sourceRefs = session.source_refs.length > 0 ? session.source_refs : (session.quiz.source_refs ?? []); return { id: `${session.chapterId ?? "chapter"}-${session.missionId ?? "mission"}`, chapterId: session.chapterId ?? "chapter", missionId: session.missionId ?? "mission", subject: session.subject ?? "Tuition", chapter: session.chapterLabel, missionTitle: title, goal: session.missionGoal, examUse: session.quiz.marks ? `${session.quiz.marks}-mark answer practice` : "Board answer practice", estimatedMinutes: getEstimatedMinutes(session.estimatedTime), sourceRefs, objectives: [], recall: { question: /renaissance/i.test(`${title} ${answerQuestion}`) ? "What does Renaissance mean?" : `What do you remember from the previous class about ${session.chapterSubtitle}?`, expectedIdea: /renaissance/i.test(`${title} ${answerQuestion}`) ? "Renaissance means rebirth or revival." : session.lessonTakeaway, correctFeedback: /renaissance/i.test(`${title} ${answerQuestion}`) ? "Good. Renaissance means rebirth - the revival of art, literature, and knowledge." : `Good. Keep this idea: ${session.lessonTakeaway}`, incorrectFeedback: /renaissance/i.test(`${title} ${answerQuestion}`) ? "No problem. Remember this: Renaissance means rebirth. It refers to the revival of old knowledge, art, and literature." : `No problem. Remember this before we continue: ${session.lessonTakeaway}`, }, teach: { title, explanation: session.missionGoal || `${title} is important because it connects the textbook idea to the way marks are awarded in the exam.`, keyConcepts: notes.slice(0, 4), teacherNote: sentenceFromPoints(notes) || session.lessonTakeaway, }, visualBoard: { title, mindMap: notes.slice(0, 5), flow: [ session.chapterSubtitle, ...notes.slice(0, 3), session.feedback.toRevise || session.lessonTakeaway, ].filter(Boolean), diagram: session.lessonBoard.points.slice(0, 4), conclusion: session.feedback.why || `Therefore, ${title.toLowerCase()} is the main idea to remember from today's class.`, }, trick, keyNotes: { points: notes, keywords: safeKeywords, commonMistake: getCommonMistake(session), }, examAnswer: { question: answerQuestion, twoMarkAnswer: sentenceFromPoints(fourMarkPoints.slice(0, 2)) || session.lessonTakeaway, fourMarkAnswer: fourMarkPoints, keywords: safeKeywords, structure: [ "Start with a direct answer.", "Add clear points.", "Use exam keywords.", "End with the final result.", ], }, practice: { question: `Write a ${session.quiz.marks && session.quiz.marks > 2 ? session.quiz.marks : 4}-mark answer: ${answerQuestion}`, hint: `Use these keywords: ${safeKeywords.slice(0, 4).join(", ")}.`, }, quiz: { questions: buildQuiz(session, safeKeywords), }, recap: { learned: notes.slice(0, 4), memoryLine: trick.memoryLine, examReadiness: [ "2-mark answer: ready", "4-mark answer: practise once more with keywords", ], weakAreas: weakAreas.length > 0 ? weakAreas : [getCommonMistake(session)], nextRevisionDate: "Friday", }, saveRevision: { savedTopics: weakAreas.length > 0 ? weakAreas : safeKeywords.slice(0, 2), reason: "Saved from today's answer practice and quiz checks.", revisionDate: "Friday", weekendPyq: "Included in weekend exam practice.", }, }; } export type TuitionTutorContext = { subject: string; chapter: string; topic: string; objective: string; currentStep: TuitionLessonStepId; sourceRef: string; sourceLabel: string; relevantContext: string; }; export type TuitionLanguagePreference = | "English" | "English + Malayalam" | "Malayalam"; export function tuitionLanguagePresentation( session: TuitionClassSession, preference: TuitionLanguagePreference, ): { primary: string; supplemental: string | null } { const english = session.teach.explanation; const malayalam = session.langSupport?.malayalam || session.langSupport?.manglish || null; if (preference === "English") return { primary: english, supplemental: null }; if (preference === "Malayalam") return { primary: malayalam || english, supplemental: null }; return { primary: english, supplemental: malayalam }; } export function buildTuitionTutorContext( session: TuitionClassSession, currentStep: TuitionLessonStepId, question: string, ): TuitionTutorContext { const source = session.sourceRefs[0]; const sourceLabel = source?.book || source?.file_name || "Curated lesson source"; const page = source?.printed_page ?? source?.pdf_page; const sourceRef = page ? `${sourceLabel}, page ${page}` : sourceLabel; const asksMinuteConversion = /(?:divide|convert).{0,20}60|60.{0,20}(?:second|minute)/i.test(question) && /frequency|period/i.test(`${session.missionTitle} ${session.teach.title}`); const relevantContext = asksMinuteConversion ? "Frequency in hertz is per second; 1 minute = 60 seconds, so a per-minute count is divided by 60." : session.formulas?.[0]?.meaning || session.teach.keyConcepts[0] || session.teach.explanation; return { subject: session.subject, chapter: session.chapter, topic: session.missionTitle, objective: session.objectives[0]?.statement || session.goal, currentStep, sourceRef, sourceLabel, relevantContext, }; } export function getTuitionStudyChatHandoffHref( session: TuitionClassSession, prompt?: string, currentStep: TuitionLessonStepId = "teach", ): string { const resolvedPrompt = prompt ?? `I have a doubt from today's tuition class: ${session.chapter} - ${session.missionTitle}`; const context = buildTuitionTutorContext( session, currentStep, resolvedPrompt, ); const handoffIntent: StudyIntent = "ask_doubt"; const params = new URLSearchParams({ intent: handoffIntent, prompt: resolvedPrompt, originPage: "tuition", subject: context.subject, chapter: context.chapter, topic: context.topic, objective: context.objective, currentStep: context.currentStep, relevantContext: context.relevantContext, sourceRef: context.sourceRef, sourceLabel: context.sourceLabel, activity: `tuition-${context.currentStep}`, questionLabel: resolvedPrompt, returnTo: `/tuition?start=1&chapterId=${session.chapterId}&missionId=${session.missionId}`, run: "1", }); return `/study-chat?${params.toString()}`; }