DocDoeAI / src /lib /tuition /longFormClassRuntime.ts
asnannp's picture
deploy: sync backend to Space root (learn-lesson HF cache fix)
ab01cae
Raw History Blame Contribute Delete
15.1 kB
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,
};
}