Download src/components/tuition/chapter-dashboard/classSessionData.ts from asnannp/DocDoeAI: direct link, hf CLI and curl.
- Browser
- Download file 33.8 kB
-
https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/tuition/chapter-dashboard/classSessionData.ts
- Command line
-
hf download hf://spaces/asnannp/DocDoeAI/src/components/tuition/chapter-dashboard/classSessionData.ts
-
curl -L -o classSessionData.ts https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/tuition/chapter-dashboard/classSessionData.ts
33.8 kB
| 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()}`; | |
| } | |