DocDoeAI / src /components /tuition /chapter-dashboard /classSessionData.ts
asnannp's picture
deploy: sync backend to Space root (learn-lesson HF cache fix)
66680b1
Raw History Blame Contribute Delete
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()}`;
}