DocDoeAI / src /lib /api /brain.ts
asnannp's picture
deploy: sync backend to Space root (learn-lesson HF cache fix)
65d68f5
Raw History Blame Contribute Delete
60.6 kB
import {
ApiError,
apiGet,
apiPost,
} from "@/lib/api/client";
import { isLocalResourceId, withDemoFallback } from "@/lib/api/mode";
import type {
ApiDocument,
BrainAskPayload,
BrainAskResponse,
BrainGenerationResponse,
BrainGeneratePayload,
BrainLastNightPlanOutput,
BrainAnswerCorrectionOutput,
BrainPyqAnalysisOutput,
BrainSourcesListResponse,
BrainSourceResponse,
BrainVideoPlanPayload,
BrainVideoPlanResponse,
BrainExamAnswerOutput,
DocumentChunk,
FlashcardResponseItem,
NotesOutput,
QuizQuestionResponse,
StudyPathGeneratePayload,
BrainStudyPathResponse,
StudyProfilePayload,
StudyProfileResponse,
StudyRequestAnalyzePayload,
StudyRequestAnalysis,
StudentLevel,
TextSourcePayload,
VideoMode,
} from "@/lib/api/types";
import { demoUser } from "@/lib/frontend-demo-data";
import {
getLocalDocuments,
retrieveLocalDocumentChunks,
saveLocalDocument,
} from "@/lib/frontend-local-store";
const OFFLINE_EVIDENCE_LABEL = "Offline preview — not connected to DocDoe AI";
const OFFLINE_TRUST_NOTE =
"Offline preview only. Connect to the DocDoe backend for source-grounded AI answers.";
function safeTitle(value: string) {
return value
.trim()
.replace(/\s+/g, " ")
.replace(/\b\w/g, (letter) => letter.toUpperCase());
}
function containsAny(text: string, patterns: string[]) {
return patterns.some((pattern) => text.includes(pattern));
}
function inferSubject(text: string) {
const subjects = [
"physics",
"chemistry",
"biology",
"maths",
"mathematics",
"computer science",
"english",
"social science",
];
const found = subjects.find((subject) => text.includes(subject));
if (!found) return null;
return found === "maths" ? "Maths" : safeTitle(found);
}
function inferTopic(rawText: string, lowerText: string, subject: string | null) {
const knownTopics = [
"electromagnetic induction",
"photosynthesis",
"organic chemistry",
"trigonometry",
"python basics",
"current electricity",
"life processes",
"chemical reactions",
"faraday's law",
"lenz's law",
];
const known = knownTopics.find((topic) => lowerText.includes(topic));
if (known) return safeTitle(known);
const afterSubject = subject ? rawText.split(new RegExp(subject, "i"))[1]?.trim() : "";
const candidate = (afterSubject || rawText)
.replace(/\b(exam|tomorrow|today|need|want|a\+|full score|good marks|pass|study|prepare|class|kerala|cbse|sslc|jee|neet)\b/gi, " ")
.replace(/[^\w\s+.-]/g, " ")
.replace(/\s+/g, " ")
.trim();
if (candidate.length > 3 && !/^(asdf|test|hello|hi)$/i.test(candidate)) {
return safeTitle(candidate.split(" ").slice(0, 5).join(" "));
}
return null;
}
function inferAnalysis(payload: StudyRequestAnalyzePayload): StudyRequestAnalysis {
const rawText = payload.raw_text.trim();
const lowerText = rawText.toLowerCase();
const subject = inferSubject(lowerText);
const topic = inferTopic(rawText, lowerText, subject);
const exam = containsAny(lowerText, ["kerala", "+2", "hse"])
? "Kerala +2"
: containsAny(lowerText, ["sslc"])
? "SSLC"
: containsAny(lowerText, ["cbse"])
? "CBSE"
: containsAny(lowerText, ["jee"])
? "JEE"
: containsAny(lowerText, ["neet"])
? "NEET"
: null;
const grade = containsAny(lowerText, ["+2", "class 12", "12th"]) ? "12" : containsAny(lowerText, ["+1", "class 11", "11th"]) ? "11" : null;
const board = exam?.includes("Kerala") ? "Kerala HSE" : exam === "CBSE" ? "CBSE" : null;
const goal = containsAny(lowerText, ["a+", "a plus"]) ? "A+" : containsAny(lowerText, ["full score", "rank", "top score"]) ? "Full score" : containsAny(lowerText, ["pass"]) ? "Pass" : "Good marks";
const timeLeft = containsAny(lowerText, ["tomorrow"]) ? "tomorrow" : containsAny(lowerText, ["today", "1 hour"]) ? "1 hour" : containsAny(lowerText, ["3 hours"]) ? "3 hours" : containsAny(lowerText, ["5 hours"]) ? "5 hours" : containsAny(lowerText, ["7 days", "week"]) ? "7 days" : null;
const level: StudentLevel | null = containsAny(lowerText, ["beginner", "basics", "from zero"])
? "beginner"
: containsAny(lowerText, ["advanced", "full-score", "rank"])
? "advanced"
: null;
const language = containsAny(lowerText, ["malayalam"]) ? "Malayalam + English" : containsAny(lowerText, ["hindi"]) ? "Hindi + English" : null;
const primaryNeed = containsAny(lowerText, ["video"]) ? "study_video" : containsAny(lowerText, ["quiz"]) ? "quiz_me" : containsAny(lowerText, ["last night", "tomorrow"]) ? "last_night_plan" : "what_to_study";
const missingFields = [
exam ? null : "exam",
subject ? null : "subject",
topic ? null : "topic",
timeLeft ? null : "time_left",
level ? null : "level",
language ? null : "language_preference",
].filter((field): field is string => Boolean(field));
const confidence = Math.max(0.35, Math.min(0.92, 1 - missingFields.length * 0.11));
return {
raw_text: rawText,
exam,
board,
grade,
subject,
chapter: topic,
topic,
goal,
time_left: timeLeft,
level,
language_preference: language,
primary_need: primaryNeed,
confidence,
missing_fields: missingFields,
warnings: [
...(confidence < 0.55 ? ["DocDoe needs a clearer exam, subject, or chapter before building your path."] : []),
"PYQ data is not available yet. DocDoe will use your source, topic, goal, and time.",
],
syllabus_status: payload.syllabus_text?.trim() ? "provided" : "not_provided",
pyq_status: "unavailable",
};
}
export function brainSourceToApiDocument(source: BrainSourceResponse): ApiDocument {
return {
id: source.id,
user_id: source.user_id,
title: source.title,
file_name: source.file_name,
file_type: source.file_type || source.source_type,
subject: source.subject,
chapter: source.chapter,
syllabus: source.syllabus,
status: source.status,
extracted_text: source.preview_text,
chunk_count: source.chunk_count,
created_at: source.created_at,
};
}
function apiDocumentToBrainSource(document: ApiDocument, sourceType = "syllabus_text"): BrainSourceResponse {
return {
id: document.id,
user_id: document.user_id,
title: document.title,
source_type: sourceType,
status: document.status,
file_name: document.file_name,
file_type: document.file_type,
subject: document.subject,
chapter: document.chapter,
syllabus: document.syllabus,
word_count: Math.round((document.extracted_text_length ?? 0) / 5),
chunk_count: document.chunk_count,
preview_text: null, // Use /documents/{id}/preview endpoint for preview text
created_at: document.created_at,
updated_at: document.created_at,
};
}
function localTextSource(payload: TextSourcePayload): BrainSourceResponse {
const now = new Date().toISOString();
const document: ApiDocument = {
id: `doc-local-text-${Date.now()}`,
user_id: demoUser.id,
title: payload.title || payload.chapter || "Study source",
file_name: payload.title || payload.chapter || "Study source",
file_type: payload.source_type,
subject: payload.subject ?? "Study",
chapter: payload.chapter ?? payload.title,
syllabus: payload.syllabus ?? "Study plan",
status: "ready",
extracted_text: payload.text,
chunk_count: Math.max(1, Math.ceil(payload.text.length / 420)),
created_at: now,
};
return apiDocumentToBrainSource(saveLocalDocument(document), payload.source_type);
}
function fallbackProfile(payload: StudyProfilePayload): StudyProfileResponse {
const now = new Date().toISOString();
return {
id: `sp-local-${Date.now()}`,
user_id: demoUser.id,
exam: payload.exam ?? null,
board: payload.board ?? null,
grade: payload.grade ?? null,
subject: payload.subject ?? null,
chapter: payload.chapter ?? null,
topic: payload.topic ?? null,
goal: payload.goal ?? null,
time_left: payload.time_left ?? null,
level: payload.level ?? "intermediate",
language_preference: payload.language_preference ?? "English",
primary_need: payload.primary_need ?? "what_to_study",
weak_areas: payload.weak_areas ?? [],
onboarding_completed: payload.onboarding_completed ?? false,
extra: payload.extra ?? {},
created_at: now,
updated_at: now,
};
}
function planDurationMinutes(timeLeft: string | null | undefined) {
if (!timeLeft) return 300;
const value = timeLeft.toLowerCase();
if (value.includes("1")) return 60;
if (value.includes("3")) return 180;
if (value.includes("5") || value.includes("tomorrow")) return 300;
if (value.includes("7")) return 420;
return 600;
}
function fallbackStudyPath(payload: StudyPathGeneratePayload): BrainStudyPathResponse {
const topic = payload.topic || payload.syllabus_text?.split(/\s+/).slice(0, 4).join(" ") || "your chapter";
const duration = planDurationMinutes(payload.time_left);
const blocks = [
{
title: `${topic} meaning`,
reason_type: "concept",
task: "Read the core definition, then write it once in your own words.",
expected_output: "One crisp definition and three keywords.",
actions: ["Read", "Underline keywords", "Write a 2-mark answer"],
},
{
title: "Scoring points",
reason_type: "exam",
task: "Convert the chapter into mark-wise bullet answers.",
expected_output: "1-mark, 2-mark, and 4-mark answer frames.",
actions: ["Make notes", "Practice answer", "Self-check"],
},
{
title: "Final active recall",
reason_type: "memory",
task: "Close the source and answer five quick questions.",
expected_output: "A short checklist of weak points to revise.",
actions: ["Quiz", "Revise", "Repeat mistakes"],
},
];
return {
title: `${safeTitle(topic)} study path`,
exam: payload.exam ?? null,
subject: payload.subject ?? null,
topic,
goal: payload.goal ?? "Good marks",
time_left: payload.time_left ?? "5 hours",
level: payload.level ?? "intermediate",
readiness_score: 0.66,
next_best_action: `Start with ${topic}, then generate notes and a short quiz.`,
study_timeline: blocks.map((block, index) => ({
order: index + 1,
title: block.title,
duration_minutes: Math.max(20, Math.round(duration / blocks.length)),
reason: index === 0
? `This locks the meaning before exam answers.`
: index === 1
? `Marks come from structure, exact keywords, and clear points.`
: `Active recall shows what still needs revision.`,
reason_type: block.reason_type,
task: block.task,
expected_output: block.expected_output,
actions: block.actions,
source_basis: payload.source_ids?.length ? "selected_sources" : payload.source_id ? "selected_source" : "study_profile",
})),
weak_areas: ["concept clarity", "answer writing"],
easy_marks: [`1-mark definition of ${topic}`, "Keyword-only short answers"],
danger_areas: ["Writing long paragraphs instead of mark-wise points"],
revision_keywords: ["definition", "keywords", "diagram", "example"],
source_basis: payload.source_ids?.length ? "user_sources" : payload.source_id ? "user_source" : "study_profile",
trust_notes: [
OFFLINE_TRUST_NOTE,
payload.source_ids?.length
? `Using ${payload.source_ids.length} selected sources for this path.`
: payload.source_id
? "Using your selected source for this path."
: "No source uploaded yet. DocDoe is using your study setup.",
],
evidence_label: OFFLINE_EVIDENCE_LABEL,
analysis: {},
};
}
const TOPIC_KNOWLEDGE: Record<string, { definition: string; keyPoints: string[]; formula?: string; diagram?: string; examTip: string; realLife: string; mistakes: string[] }> = {
"electromagnetic induction": {
definition: "Electromagnetic induction is the production of an electromotive force (EMF) across an electrical conductor in a changing magnetic field. When the magnetic flux through a circuit changes, an EMF is induced in it.",
keyPoints: [
"Faraday's First Law: Whenever the magnetic flux linked with a circuit changes, an EMF is induced in the circuit.",
"Faraday's Second Law: The magnitude of induced EMF is equal to the rate of change of magnetic flux, e = -dΦ/dt.",
"Lenz's Law: The direction of induced EMF opposes the change in flux that causes it (conservation of energy).",
"Motional EMF: When a conductor moves in a magnetic field, EMF = Bvl (B = field, v = velocity, l = length).",
"Self-inductance (L): EMF induced in a coil due to change in its own current, e = -L(dI/dt).",
"Mutual inductance (M): EMF induced in one coil due to change in current in a nearby coil.",
],
formula: "e = -N(dΦ/dt) where Φ = B·A·cos(θ)",
diagram: "Draw a coil connected to a galvanometer, with a bar magnet moving toward it. Label: coil turns (N), magnetic field lines, direction of induced current, galvanometer deflection.",
examTip: "In Kerala +2 boards, Faraday's law appears as a 4-mark or 5-mark question almost every year. Always write the mathematical expression with the negative sign (Lenz's law).",
realLife: "Electric generators at power stations use electromagnetic induction — rotating coils in magnetic fields produce the electricity that powers your home.",
mistakes: ["Forgetting the negative sign in Faraday's law (it represents Lenz's law)", "Confusing magnetic flux (Φ = BA cos θ) with magnetic field (B)", "Not drawing the diagram for AC generator questions"],
},
"faraday": {
definition: "Faraday's Law of Electromagnetic Induction states that the induced EMF in a circuit is directly proportional to the rate of change of magnetic flux through the circuit. Mathematically, e = -N(dΦ/dt), where N is the number of turns and Φ is the magnetic flux.",
keyPoints: [
"First Law: A change in magnetic flux through a circuit induces an EMF.",
"Second Law: The induced EMF equals the negative rate of change of flux linkage: e = -N(dΦ/dt).",
"The negative sign comes from Lenz's Law — the induced current opposes the change causing it.",
"Magnetic flux Φ = B·A·cos(θ), where B is field strength, A is area, θ is angle between B and normal to A.",
"More turns (N) = larger induced EMF. Faster flux change = larger EMF.",
"This is the principle behind generators, transformers, and induction cooktops.",
],
formula: "e = -N(dΦ/dt), where Φ = BAcos(θ)",
examTip: "For a 5-mark answer: (1) State the law, (2) Write the formula, (3) Explain each symbol, (4) Mention Lenz's law connection, (5) Give one application.",
realLife: "When you swipe a credit card, the changing magnetic stripe induces tiny currents in the reader — that's Faraday's law in action.",
mistakes: ["Writing e = N(dΦ/dt) without the minus sign", "Not specifying what Φ represents", "Confusing EMF with current"],
},
"lenz": {
definition: "Lenz's Law states that the direction of the induced current is such that it opposes the change in magnetic flux that produced it. This is a consequence of the conservation of energy.",
keyPoints: [
"The induced current creates its own magnetic field that opposes the original change.",
"If flux is increasing → induced current creates opposing field. If decreasing → supporting field.",
"Lenz's law explains the negative sign in Faraday's law: e = -N(dΦ/dt).",
"Without Lenz's law, energy could be created from nothing — violating conservation of energy.",
"The opposing nature means you must do work to maintain the change, converting mechanical → electrical energy.",
],
examTip: "Board exams often ask: 'Explain the significance of the negative sign in Faraday's law.' Your answer should clearly connect it to Lenz's law and conservation of energy.",
realLife: "When you drop a magnet through a copper tube, it falls slowly because the induced currents oppose its motion — that's Lenz's law slowing it down.",
mistakes: ["Saying Lenz's law opposes the flux instead of opposing the CHANGE in flux", "Not connecting it to energy conservation"],
},
"ac generator": {
definition: "An AC generator (alternator) converts mechanical energy into alternating current electrical energy using the principle of electromagnetic induction. A coil rotating in a uniform magnetic field produces a sinusoidal EMF.",
keyPoints: [
"Main parts: Armature coil, field magnets, slip rings, brushes, and axle.",
"Working principle: When the coil rotates in the magnetic field, the flux changes continuously, inducing an alternating EMF.",
"EMF equation: e = NBAω sin(ωt), where ω = angular velocity.",
"Peak EMF (e₀) = NBAω occurs when the coil is parallel to the field (θ = 90°).",
"EMF is zero when the coil is perpendicular to the field (θ = 0°, 180°).",
"One full rotation = one complete AC cycle.",
],
formula: "e = NBAω sin(ωt) = e₀ sin(ωt)",
diagram: "Draw a rectangular coil ABCD between two pole pieces N and S. Show slip rings R₁, R₂ connected to the coil ends, brushes B₁, B₂ pressing against rings, and external load. Label the axis of rotation.",
examTip: "AC generator is a strong practice topic. Draw and label the diagram clearly, then explain the working step by step.",
realLife: "Hydroelectric dams use massive AC generators where water turbines spin the coil to produce electricity for cities.",
mistakes: ["Confusing slip rings (AC) with split-ring commutator (DC)", "Forgetting to label the diagram", "Not writing the condition for maximum and zero EMF"],
},
"self inductance": {
definition: "Self-inductance is the property of a coil by which it opposes any change in current flowing through it by inducing an EMF in itself. The self-induced EMF is e = -L(dI/dt), where L is the inductance in Henry.",
keyPoints: [
"When current through a coil changes, the magnetic flux linked with the coil changes, inducing a back EMF.",
"Self-inductance L = NΦ/I for a solenoid: L = μ₀n²Al (n = turns/length, A = area, l = length).",
"Energy stored in an inductor: U = ½LI².",
"SI unit of inductance is Henry (H). 1 H = 1 V·s/A.",
"Self-inductance is the electrical analogue of inertia in mechanics.",
],
formula: "L = μ₀n²Al, e = -L(dI/dt), U = ½LI²",
examTip: "When asked about self-inductance, always mention the analogy with inertia — examiners look for conceptual understanding.",
realLife: "The spark you see when unplugging an appliance is caused by self-inductance — the coil tries to maintain current flow.",
mistakes: ["Confusing self-inductance with mutual inductance", "Forgetting that L depends on the geometry of the coil, not the current"],
},
"mutual inductance": {
definition: "Mutual inductance is the property by which a change in current in one coil induces an EMF in a neighboring coil. If current I₁ in coil 1 produces flux Φ₂ in coil 2, then M = N₂Φ₂/I₁.",
keyPoints: [
"Mutual inductance depends on: geometry, number of turns, distance between coils, and the medium.",
"The induced EMF in coil 2: e₂ = -M(dI₁/dt).",
"M₁₂ = M₂₁ = M (reciprocity theorem).",
"For two coaxial solenoids: M = μ₀n₁n₂Al.",
"Transformers work on the principle of mutual inductance.",
],
formula: "M = μ₀n₁n₂Al, e₂ = -M(dI₁/dt)",
examTip: "Mutual inductance questions often connect to transformers. Mention the transformer principle when discussing M.",
realLife: "Wireless phone chargers use mutual inductance — a coil in the pad induces current in a coil inside your phone.",
mistakes: ["Assuming M depends on current (it depends only on geometry)", "Not mentioning the reciprocity theorem"],
},
"eddy currents": {
definition: "Eddy currents are loops of electric current induced within conductors by a changing magnetic field. They flow in closed loops within the conductor, perpendicular to the magnetic field.",
keyPoints: [
"Caused by changing magnetic flux through a bulk conductor (not just a wire loop).",
"They produce heating (I²R losses) and opposing magnetic effects.",
"Applications: electromagnetic braking, induction heating, speedometers, metal detectors.",
"To reduce eddy current losses, cores are laminated (thin insulated sheets).",
"Eddy currents follow Lenz's law — they oppose the change causing them.",
],
examTip: "Usually a 1-mark or 2-mark question. Know 3 applications and the lamination technique.",
realLife: "Induction cooktops heat pans using eddy currents — the changing magnetic field from the coil induces currents in the pan's metal base.",
mistakes: ["Not mentioning lamination as the method to reduce eddy currents", "Confusing eddy currents with regular induced currents in a wire"],
},
"transformer": {
definition: "A transformer is a device that transfers electrical energy between two circuits through mutual inductance. It can step up or step down AC voltage. Vs/Vp = Ns/Np = Ip/Is.",
keyPoints: [
"Works only with AC (needs changing flux for induction).",
"Step-up: Ns > Np → voltage increases, current decreases.",
"Step-down: Ns < Np → voltage decreases, current increases.",
"Ideal transformer: VpIp = VsIs (power conserved).",
"Core is laminated soft iron to reduce eddy current losses.",
"Energy losses: copper loss (I²R), eddy currents, hysteresis, flux leakage.",
],
formula: "Vs/Vp = Ns/Np, efficiency = (Ps/Pp) × 100%",
examTip: "Always mention the ideal transformer equation AND the energy losses. Examiners give marks for mentioning at least 3 types of losses.",
realLife: "The large boxes on electric poles are transformers stepping down 11kV to 230V for your home.",
mistakes: ["Saying transformers work with DC", "Forgetting to mention lamination for reducing losses"],
},
};
function findTopicMatch(question: string, sourceContext?: string): { definition: string; keyPoints: string[]; formula?: string; diagram?: string; examTip: string; realLife: string; mistakes: string[] } | null {
const q = question.toLowerCase();
for (const [key, data] of Object.entries(TOPIC_KNOWLEDGE)) {
if (q.includes(key)) return data;
}
if (q.includes("emf") || q.includes("flux")) return TOPIC_KNOWLEDGE["electromagnetic induction"];
if (q.includes("generator") || q.includes("alternator")) return TOPIC_KNOWLEDGE["ac generator"];
if (q.includes("inductor") || q.includes("self induct") || q.includes("back emf")) return TOPIC_KNOWLEDGE["self inductance"];
if (q.includes("mutual") || q.includes("two coil")) return TOPIC_KNOWLEDGE["mutual inductance"];
if (q.includes("eddy")) return TOPIC_KNOWLEDGE["eddy currents"];
if (q.includes("step up") || q.includes("step down") || q.includes("transformer")) return TOPIC_KNOWLEDGE["transformer"];
if (sourceContext) {
const sc = sourceContext.toLowerCase();
for (const [key, data] of Object.entries(TOPIC_KNOWLEDGE)) {
if (sc.includes(key)) return data;
}
if (sc.includes("electromagnetic") || sc.includes("induction")) return TOPIC_KNOWLEDGE["electromagnetic induction"];
}
return null;
}
function classifyQuestion(question: string): "explain" | "formula" | "diagram" | "exam_answer" | "mistakes" | "real_life" | "summarize" | "general" {
const q = question.toLowerCase();
if (q.includes("formula") || q.includes("equation") || q.includes("derive") || q.includes("mathematical")) return "formula";
if (q.includes("diagram") || q.includes("draw") || q.includes("label") || q.includes("figure")) return "diagram";
if (q.includes("mistake") || q.includes("error") || q.includes("wrong") || q.includes("avoid")) return "mistakes";
if (q.includes("mark") || q.includes("exam") || q.includes("answer format") || q.includes("board")) return "exam_answer";
if (q.includes("real") || q.includes("life") || q.includes("example") || q.includes("daily") || q.includes("application")) return "real_life";
if (q.includes("summar") || q.includes("brief") || q.includes("short") || q.includes("points") || q.includes("key")) return "summarize";
if (q.includes("explain") || q.includes("what is") || q.includes("define") || q.includes("concept") || q.includes("important") || q.includes("simply")) return "explain";
return "general";
}
function fallbackAsk(payload: BrainAskPayload): BrainAskResponse {
const sourceIds = payload.source_ids?.length ? payload.source_ids : payload.source_id ? [payload.source_id] : [];
const chunks: DocumentChunk[] = sourceIds
.filter((id) => isLocalResourceId(id))
.flatMap((id) => retrieveLocalDocumentChunks(id, payload.question, 2).chunks);
const primaryChunk = chunks[0];
const sourceTitles = sourceIds.map((id) => getLocalDocuments().find((doc) => doc.id === id)?.title).filter((title): title is string => Boolean(title));
const topic = findTopicMatch(payload.question, sourceTitles.join(" "));
const questionType = classifyQuestion(payload.question);
let answer: string;
let sections: Array<{ title: string; body: string }>;
let suggestedActions: string[];
if (topic) {
switch (questionType) {
case "explain":
answer = topic.definition;
sections = [
{ title: "Key concepts", body: topic.keyPoints.slice(0, 4).join("\n") },
...(topic.formula ? [{ title: "Formula", body: topic.formula }] : []),
{ title: "Exam tip", body: topic.examTip },
];
suggestedActions = ["Ask for diagram help", "Generate a quiz on this", "Explain with real-life example"];
break;
case "formula":
answer = topic.formula
? `The key formula is: ${topic.formula}. ${topic.keyPoints.find((p) => p.toLowerCase().includes("formula") || p.toLowerCase().includes("equation")) || topic.keyPoints[1] || ""}`
: topic.definition;
sections = [
...(topic.formula ? [{ title: "Formula", body: topic.formula }] : []),
{ title: "How to apply", body: topic.keyPoints.filter((p) => p.includes("=") || p.includes("where")).join("\n") || topic.keyPoints.slice(0, 3).join("\n") },
{ title: "Common mistakes", body: topic.mistakes.join("\n") },
];
suggestedActions = ["Show me worked examples", "What are common numerical problems?", "Explain the derivation"];
break;
case "diagram":
answer = topic.diagram || `For ${payload.question.slice(0, 60)}, you'll need a clear labelled diagram. ${topic.keyPoints[0]}`;
sections = [
...(topic.diagram ? [{ title: "Diagram instructions", body: topic.diagram }] : []),
{ title: "Key labels to include", body: topic.keyPoints.slice(0, 3).join("\n") },
{ title: "Examiner note", body: topic.examTip },
];
suggestedActions = ["What marks does the diagram carry?", "Explain the working from the diagram", "Common diagram mistakes"];
break;
case "exam_answer":
answer = `Here's how to structure your exam answer: Start with the definition, then the mathematical expression, followed by key points. ${topic.examTip}`;
sections = [
{ title: "Definition (1-2 marks)", body: topic.definition },
...(topic.formula ? [{ title: "Formula (1 mark)", body: topic.formula }] : []),
{ title: "Key points to include", body: topic.keyPoints.slice(0, 4).join("\n") },
{ title: "Avoid these mistakes", body: topic.mistakes.join("\n") },
];
suggestedActions = ["Write a full 5-mark answer", "What keywords get marks?", "Show marking scheme"];
break;
case "mistakes":
answer = `Here are the most common mistakes students make: ${topic.mistakes[0]}. Knowing these can help you avoid losing marks.`;
sections = [
{ title: "Common mistakes", body: topic.mistakes.map((m, i) => `${i + 1}. ${m}`).join("\n") },
{ title: "How to avoid them", body: topic.examTip },
{ title: "Quick check", body: topic.keyPoints[0] },
];
suggestedActions = ["Give me a quiz to test this", "What are examiner expectations?", "Show the correct approach"];
break;
case "real_life":
answer = topic.realLife;
sections = [
{ title: "Real-world connection", body: topic.realLife },
{ title: "The science behind it", body: topic.keyPoints.slice(0, 2).join("\n") },
{ title: "Why this matters for exams", body: "Real-life examples are often asked as 1-mark or 2-mark questions. Examiners love when you connect theory to applications." },
];
suggestedActions = ["Give more applications", "Now explain the theory", "Create flashcards"];
break;
case "summarize":
answer = `Quick summary: ${topic.definition.split(". ").slice(0, 2).join(". ")}.`;
sections = [
{ title: "Key points", body: topic.keyPoints.join("\n") },
...(topic.formula ? [{ title: "Important formula", body: topic.formula }] : []),
{ title: "Exam tip", body: topic.examTip },
];
suggestedActions = ["Explain in more detail", "Create flashcards from this", "Give me practice questions"];
break;
default:
answer = topic.definition;
sections = [
{ title: "Key concepts", body: topic.keyPoints.slice(0, 3).join("\n") },
...(topic.formula ? [{ title: "Formula", body: topic.formula }] : []),
{ title: "Exam focus", body: topic.examTip },
];
suggestedActions = ["Explain deeper", "Show diagram", "Write exam answer"];
}
} else if (primaryChunk?.chunk_text) {
answer = `From your material: ${primaryChunk.chunk_text}`;
sections = [
{ title: "From your source", body: primaryChunk.chunk_text },
{ title: "Study approach", body: "Read this section carefully, underline keywords, and try to write the main idea in one sentence.\nThen check if you can recall it without looking." },
];
suggestedActions = ["Summarize this section", "Create a quiz from this", "Explain this simply"];
} else {
const questionTopic = payload.question.replace(/[?.,!]/g, "").trim();
answer = `DocDoe needs a live backend connection to answer "${questionTopic.slice(0, 80)}".`;
sections = [
{
title: "Connect the knowledge brain",
body: "Sign in and ensure the DocDoe backend is running. The AI engine retrieves your uploaded sources, applies exam intelligence, and generates grounded answers.",
},
{
title: "Source-grounded mode",
body: "Upload a textbook chapter, notes, or question paper. DocDoe retrieves relevant chunks and answers from your material — not generic templates.",
},
{
title: "Offline preview limits",
body: "Without the backend, only cached physics scaffolds are available. Real production answers require the connected AI pipeline.",
},
];
suggestedActions = ["Upload your textbook PDF", "Select an uploaded source", "Retry after signing in"];
}
return {
answer,
sections,
citations: chunks.length > 0
? chunks.slice(0, 5).map((chunk) => ({
chunk_id: chunk.id,
chunk_index: chunk.chunk_index,
heading: chunk.heading,
page_number: chunk.page_number,
snippet: chunk.chunk_text.slice(0, 140),
source_id: chunk.document_id,
source_title: sourceTitles[0] ?? null,
}))
: sourceTitles.map((title) => ({
chunk_id: null,
chunk_index: null,
heading: title,
page_number: null,
snippet: null,
source_id: null,
source_title: title,
})),
suggested_actions: suggestedActions,
trust_note: sourceIds.length
? `${OFFLINE_TRUST_NOTE} Showing local source excerpt.`
: topic
? `${OFFLINE_TRUST_NOTE} Showing cached topic scaffold.`
: OFFLINE_TRUST_NOTE,
evidence_label: OFFLINE_EVIDENCE_LABEL,
};
}
function fallbackExamAnswer(payload: BrainGeneratePayload): BrainGenerationResponse<BrainExamAnswerOutput> {
const topic = payload.topic || "this topic";
const output: BrainExamAnswerOutput = {
one_mark: [{ question: `Define ${topic}.`, answer: `${topic} is the core idea from this chapter, written in one crisp line.` }],
two_mark: [{ question: `Explain ${topic} in two points.`, answer: "Write the definition, then add one keyword-based example." }],
four_mark: [{ question: `Write a 4-mark answer on ${topic}.`, answer: "Definition, principle, keyword explanation, and one example.", diagram_needed: true }],
six_mark: [{ question: `Prepare a long answer for ${topic}.`, answer: "Start with definition, add law/principle, explain steps, include diagram, finish with application.", diagram_needed: true }],
keywords_to_underline: ["definition", "principle", "diagram", "example"],
diagram_required: true,
common_mistake: "Writing long story answers instead of clean mark-wise points.",
examiner_tip: "Marks come from exact keywords, neat structure, and a labelled diagram when needed.",
answer_writing_formula: ["Definition", "Principle", "Steps", "Diagram", "Application"],
trust_note: payload.source_ids?.length ? `Using ${payload.source_ids.length} selected sources.` : "Using your selected topic and source.",
};
return {
type: "exam_answer",
topic,
output,
trust_note: output.trust_note,
evidence_label: OFFLINE_EVIDENCE_LABEL,
};
}
function fallbackAnswerCorrection(payload: BrainGeneratePayload): BrainGenerationResponse<BrainAnswerCorrectionOutput> {
const question = String(payload.options?.question || payload.topic || "this question");
const marks = Number(payload.options?.marks || 5) || 5;
const output: BrainAnswerCorrectionOutput = {
syllabus_position: payload.subject ? `${payload.subject} -> ${question}` : question,
mark_scheme_assumption: `Assuming this is a ${marks}-mark answer.`,
score: `Estimated score: not available offline/${marks}`,
what_correct: ["Offline preview cannot grade the pasted answer reliably."],
marks_lost: ["Connect to the live backend to calculate marks lost from the answer."],
missing_keywords: ["exact keywords", "marking points", "board format"],
corrected_board_answer: "Connect to DocDoe AI to rewrite this in board-answer format.",
how_to_improve: ["Use the live backend for source-grounded answer correction."],
quick_retry_task: "Connect to the backend and run Correct my answer again.",
source_truth: OFFLINE_TRUST_NOTE,
source_evidence: [],
assumed_marks: marks,
official_scheme_available: false,
};
return {
type: "answer_correction",
topic: question,
output,
trust_note: OFFLINE_TRUST_NOTE,
evidence_label: OFFLINE_EVIDENCE_LABEL,
};
}
function fallbackNotes(payload: BrainGeneratePayload): BrainGenerationResponse<NotesOutput> {
const topic = payload.topic || "this topic";
return {
type: "notes",
topic,
output: {
title: `${topic} scoring notes`,
summary: `DocDoe shaped ${topic} into exam-focused notes.`,
student_level_summary: `This is tuned for a ${payload.level ?? "intermediate"} student.`,
must_learn_first: [`Meaning of ${topic}`, "One clean example", "A short exam answer frame"],
key_points: [`Meaning of ${topic}`, "Important keywords", "Exam answer structure", "One diagram, formula, or example if the chapter supports it"],
important_definitions: [`Define ${topic} in one crisp line.`],
exam_keywords: ["definition", "principle", "example", "diagram"],
scoring_keywords: ["definition", "principle", "example", "diagram"],
memory_tricks: ["Definition first, example second, keywords always."],
exam_focus: ["Write short point-wise answers.", "Underline exact terms."],
possible_exam_questions: [`Define ${topic}.`, `Write a short note on ${topic}.`, `Give one example or application of ${topic}.`],
common_mistakes: ["Writing a long paragraph without keywords.", "Skipping the example or diagram when the question asks for it."],
last_minute_revision: ["Revise definition", "Practice one 4-mark answer"],
ten_minute_revision: ["Read the definition aloud.", "Write three keywords without looking.", "Answer one short question.", "Circle the mistake you usually make."],
quick_check: [`Define ${topic}.`, "List three keywords."],
next_study_action: `Turn ${topic} into a 5-question quiz, then revise only the wrong answers.`,
},
trust_note: payload.source_ids?.length
? `${OFFLINE_TRUST_NOTE} Using ${payload.source_ids.length} selected sources.`
: OFFLINE_TRUST_NOTE,
evidence_label: OFFLINE_EVIDENCE_LABEL,
};
}
function fallbackQuiz(payload: BrainGeneratePayload): BrainGenerationResponse<{ quiz_title: string; questions: QuizQuestionResponse[] }> {
const topic = payload.topic || "this topic";
const requestedCount = Math.max(1, Math.min(Number(payload.options?.num_questions ?? payload.options?.question_count ?? 5) || 5, 15));
const baseQuestions: QuizQuestionResponse[] = [
{
question: `What is the core meaning of ${topic}?`,
type: "mcq",
options: [
"A crisp definition of the main idea",
"A random example without keywords",
"A long story answer",
"A diagram label only",
],
answer: "A crisp definition of the main idea",
explanation: "Direct definitions are valuable when phrased clearly.",
difficulty: "easy",
common_trap: "Starting with examples before defining the term.",
exam_tip: "Write the definition first, then one keyword.",
},
{
question: `Which keywords should you underline for ${topic}?`,
type: "mcq",
options: [
"Exact terms from the chapter",
"Only filler sentences",
"Unrelated examples",
"Personal opinions",
],
answer: "Exact terms from the chapter",
explanation: "Keywords help examiners find the scoring points fast.",
difficulty: "medium",
common_trap: "Using casual words instead of textbook terms.",
exam_tip: "Underline exact terms after writing the answer.",
},
{
question: `Write a two-point answer for ${topic}.`,
type: "mcq",
options: [
"Definition plus one clear exam point",
"Only the chapter title",
"Three unrelated facts",
"A diagram without labels",
],
answer: "Definition plus one clear exam point",
explanation: "Two-mark answers need structure more than length.",
difficulty: "medium",
common_trap: "Writing one long sentence with no separate points.",
exam_tip: "Use two bullets for two marks.",
},
{
question: `What diagram or example can support ${topic}?`,
type: "mcq",
options: [
"The simplest labelled diagram or class example",
"An unlabelled rough sketch",
"An unrelated picture",
"No support at all",
],
answer: "The simplest labelled diagram or class example",
explanation: "Visual support helps when the chapter has a process or setup.",
difficulty: "medium",
common_trap: "Drawing without labels.",
exam_tip: "Labels often carry marks in exams.",
},
{
question: `What is a common mistake in ${topic}?`,
type: "mcq",
options: [
"Skipping keywords and writing a story answer",
"Using point-wise structure",
"Underlining exact terms",
"Adding a neat labelled diagram",
],
answer: "Skipping keywords and writing a story answer",
explanation: "Short, keyword-rich answers score better.",
difficulty: "hard",
common_trap: "Trying to write everything you remember.",
exam_tip: "Write what earns marks first.",
},
];
const questions = Array.from({ length: requestedCount }, (_, index) => {
const base = baseQuestions[index % baseQuestions.length];
return index < baseQuestions.length
? base
: {
...base,
question: `${base.question} Practice ${index + 1}.`,
};
});
return {
type: "quiz",
topic,
output: {
quiz_title: `${topic} quick test`,
questions,
},
trust_note: payload.source_ids?.length
? `${OFFLINE_TRUST_NOTE} Using ${payload.source_ids.length} selected sources.`
: OFFLINE_TRUST_NOTE,
evidence_label: OFFLINE_EVIDENCE_LABEL,
is_fallback: true,
generation_time_ms: 0,
cache_hit: false,
};
}
function fallbackFlashcards(payload: BrainGeneratePayload): BrainGenerationResponse<{ cards: FlashcardResponseItem[] }> {
const topic = payload.topic || "this topic";
return {
type: "flashcards",
topic,
output: {
cards: [
{ front: `Define ${topic}`, back: "A crisp one-line definition with exact keywords.", hint: "Start with the core process.", tag: "definition", memory_hook: "One line first, details later." },
{ front: "Exam answer order", back: "Definition > principle > keywords > example or diagram.", hint: "Structure scores marks.", tag: "answer_writing", memory_hook: "D-P-K-E." },
{ front: "What should you underline?", back: "Only the exact scoring words: definition terms, laws, formulas, labels, and named examples.", hint: "Think like an examiner.", tag: "keywords" },
{ front: "Common mistake", back: "Writing a long story answer without point-wise structure.", hint: "Length is not the same as marks.", tag: "avoid" },
{ front: "Two-mark frame", back: `Point 1: Define ${topic}. Point 2: Add one keyword-based example or application.`, hint: "Two points for two marks.", tag: "2_mark" },
{ front: "Four-mark frame", back: "Definition, principle, two key points, then example or diagram.", hint: "Use four scoring beats.", tag: "4_mark" },
{ front: "Final recall check", back: `Close the notes and say: meaning of ${topic}, three keywords, one example, one mistake to avoid.`, hint: "No looking.", tag: "recall" },
{ front: "When stuck", back: "Ask DocDoe for a simpler explanation, then quiz only the weak part.", hint: "Recover fast.", tag: "next_step" },
],
},
trust_note: payload.source_ids?.length
? `${OFFLINE_TRUST_NOTE} Using ${payload.source_ids.length} selected sources.`
: OFFLINE_TRUST_NOTE,
evidence_label: OFFLINE_EVIDENCE_LABEL,
};
}
function fallbackLastNightPlan(payload: BrainGeneratePayload): BrainGenerationResponse<BrainLastNightPlanOutput> {
const topic = payload.topic || "your chapter";
const output: BrainLastNightPlanOutput = {
time_blocks: [
{ label: "Chapter pass", minutes: 45 },
{ label: "Definitions + keywords", minutes: 35 },
{ label: "Exam answer practice", minutes: 60 },
{ label: "Quick self-test", minutes: 30 },
{ label: "Final revision", minutes: 20 },
],
must_study: [`Core definition of ${topic}`, "Important keywords", "Mark-wise answer frames"],
skip_now: ["Deep extra reading", "Unseen long derivations unless your teacher marked them"],
easy_marks: ["Direct definitions", "Diagram labels", "Short keyword answers"],
revision_keywords: ["definition", "formula", "diagram", "keywords"],
final_checklist: ["Definition is crisp", "Keywords are ready", "One answer practiced", "Sleep plan is calm"],
answer_writing_formula: ["Definition", "Keyword", "Point", "Example"],
calming_note: "You do not need to master everything tonight. Lock the scoring parts first.",
trust_note: "PYQ data is not available yet. DocDoe is using your source, topic, goal, and time.",
};
return {
type: "last_night_plan",
topic,
output,
trust_note: output.trust_note,
evidence_label: OFFLINE_EVIDENCE_LABEL,
};
}
function durationForVideoMode(mode: VideoMode) {
if (mode === "quick_concept") return { range: "5 minutes", minutes: 5, planningOnly: false };
if (mode === "exam_focus") return { range: "10 minutes", minutes: 10, planningOnly: false };
if (mode === "deep_masterclass") return { range: "30 minutes", minutes: 30, planningOnly: false };
return { range: "30 minutes", minutes: 30, planningOnly: false };
}
function fallbackVideoPlan(payload: BrainVideoPlanPayload): BrainVideoPlanResponse {
const duration = durationForVideoMode(payload.video_mode);
return {
title: `${payload.topic} - ${payload.video_mode === "quick_concept" ? "Quick Concept" : payload.video_mode === "exam_focus" ? "Exam Focus" : payload.video_mode === "deep_masterclass" ? "Deep Masterclass" : "Full Chapter War Mode"}`,
video_mode: payload.video_mode,
target_duration_range: duration.range,
estimated_duration_minutes: duration.minutes,
render_supported: !duration.planningOnly,
planning_only: duration.planningOnly,
scenes: [
{
scene_type: "intro",
title: `What is ${payload.topic}?`,
narration: `Start with the simplest meaning of ${payload.topic}, then connect it to the exam question style.`,
visual_direction: payload.teaching_style,
on_screen_text: payload.topic,
duration_seconds: 45,
subtitle_segments: [`DocDoe explains ${payload.topic} from the basics.`],
exam_tip: "Define the concept in one line before expanding.",
pyq_connection: null,
},
{
scene_type: "exam_focus",
title: "Scoring points",
narration: "Turn the concept into short mark-wise answers.",
visual_direction: "clean_explainer",
on_screen_text: "Definition -> Keywords -> Example",
duration_seconds: 80,
subtitle_segments: ["Use definition, keywords, and one clean example."],
exam_tip: "Underline keywords in the final answer.",
pyq_connection: null,
},
{
scene_type: "worked_example",
title: "One exam-style example",
narration: "Show one short answer and point out exactly where marks are earned.",
visual_direction: "split_answer_breakdown",
on_screen_text: "Where the marks are",
duration_seconds: 95,
subtitle_segments: ["Definition earns the first mark.", "Keywords and example complete the answer."],
exam_tip: "Practice one answer immediately after watching.",
pyq_connection: null,
},
{
scene_type: "recall",
title: "Pause and recall",
narration: "Ask the student to pause, close notes, and recall the definition, three keywords, and one mistake.",
visual_direction: "clean_recall_screen",
on_screen_text: "Pause: 3 keywords + 1 mistake",
duration_seconds: 60,
subtitle_segments: ["Pause the video.", "Say three keywords before continuing."],
exam_tip: "Active recall beats rereading.",
pyq_connection: null,
},
],
narration_style: payload.teaching_style,
visual_style: payload.teaching_style === "anime_tutor" ? "anime_visual_explainer" : "clean_explainer",
exam_focus: payload.video_mode !== "quick_concept",
answer_cards: [
{ label: "Exam keywords", items: ["definition", "keywords", "diagram", "example"] },
{ label: "Answer frame", items: ["Start with the definition", "Add the principle or formula", "Use two short points", "End with an example"] },
],
quiz_moments: ["Pause and answer one 2-mark question.", "Recall three keywords without looking.", "Spot the common mistake before the final recap."],
revision_keywords: ["definition", "keywords", "example", "diagram", "common mistake"],
duration_validation: {},
trust_note: OFFLINE_TRUST_NOTE,
evidence_label: OFFLINE_EVIDENCE_LABEL,
};
}
export function analyzeStudyRequest(payload: StudyRequestAnalyzePayload) {
if (!payload.raw_text.trim()) {
throw new ApiError({
message: "Please describe what you want to study.",
status: 400,
body: null,
});
}
return withDemoFallback(
() => apiPost<StudyRequestAnalysis, StudyRequestAnalyzePayload>("/study/analyze-request", payload),
() => inferAnalysis(payload),
);
}
export function saveStudyProfile(payload: StudyProfilePayload) {
return withDemoFallback(
() => apiPost<StudyProfileResponse, StudyProfilePayload>("/study-profile", payload),
() => fallbackProfile(payload),
);
}
export function getStudyProfile() {
return withDemoFallback(
() => apiGet<StudyProfileResponse>("/study-profile/me"),
() => fallbackProfile({}),
);
}
export function createTextSource(payload: TextSourcePayload) {
return withDemoFallback(
() => apiPost<BrainSourceResponse, TextSourcePayload>("/sources/text", payload),
() => localTextSource(payload),
);
}
export async function listBrainSourcesAsDocuments() {
return withDemoFallback(
async () => {
const result = await apiGet<BrainSourcesListResponse>("/sources");
return result.sources.map(brainSourceToApiDocument);
},
() => getLocalDocuments(),
);
}
export function generateStudyPath(payload: StudyPathGeneratePayload) {
return withDemoFallback(
() => apiPost<BrainStudyPathResponse, StudyPathGeneratePayload>("/study-path/generate", payload, { timeoutMs: 60000 }),
() => fallbackStudyPath(payload),
);
}
export function askDocDoe(payload: BrainAskPayload, options?: { signal?: AbortSignal }) {
return withDemoFallback(
() => apiPost<BrainAskResponse, BrainAskPayload>("/ask", payload, { timeoutMs: 60000, signal: options?.signal }),
() => fallbackAsk(payload),
);
}
/**
* Stream /ask tokens via SSE for real-time StudyCast feel.
* Uses fetch + reader (EventSource can't easily do POST+auth).
*/
export async function streamAskDocDoe(
payload: BrainAskPayload,
callbacks: {
onDelta: (token: string) => void;
onDone: (meta: { model: string; isFallback?: boolean }) => void;
onError: (message: string) => void;
},
signal?: AbortSignal,
): Promise<void> {
const authToken = (await import("@/lib/auth/auth-client")).getAuthToken();
const streamSignal = AbortSignal.any([
AbortSignal.timeout(120_000), ...(signal ? [signal] : []),
]);
let response: Response;
try {
response = await fetch(`${(await import("@/lib/api/client")).getApiBaseUrl()}/ask/stream`, {
method: "POST",
signal: streamSignal,
headers: {
"Content-Type": "application/json",
Accept: "text/event-stream",
...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
},
body: JSON.stringify(payload),
});
} catch {
if (streamSignal.aborted) {
callbacks.onError(signal?.aborted ? "Response stopped." : "The response timed out. Please retry.");
return;
}
callbacks.onError("Ask stream unreachable.");
return;
}
if (!response.ok) {
callbacks.onError("Could not stream ask answer.");
return;
}
const reader = response.body?.getReader();
if (!reader) {
callbacks.onError("Stream unsupported.");
return;
}
const decoder = new TextDecoder();
let buffer = "";
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
callbacks.onError("The response ended before it was complete. Please retry.");
return;
}
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() ?? "";
for (const line of lines) {
if (!line.startsWith("data: ")) continue;
const raw = line.slice(6).trim();
if (!raw) continue;
try {
const ev = JSON.parse(raw) as Record<string, unknown>;
if (ev.error) {
callbacks.onError(String(ev.error));
return;
}
if (ev.delta) callbacks.onDelta(String(ev.delta));
if (ev.done) {
callbacks.onDone({ model: String(ev.model ?? "docdoe"), isFallback: !!ev.is_fallback });
return;
}
} catch {}
}
}
} catch {
callbacks.onError(streamSignal.aborted
? (signal?.aborted ? "Response stopped." : "The response timed out. Please retry.")
: "Ask stream lost. Please retry.");
} finally {
await reader.cancel().catch(() => undefined);
reader.releaseLock();
}
}
export function generateBrainExamAnswer(payload: BrainGeneratePayload) {
return withDemoFallback(
() => apiPost<BrainGenerationResponse<BrainExamAnswerOutput>, BrainGeneratePayload>("/generate/exam-answer", payload, { timeoutMs: 60000 }),
() => fallbackExamAnswer(payload),
);
}
export function generateBrainAnswerCorrection(payload: BrainGeneratePayload) {
return withDemoFallback(
() => apiPost<BrainGenerationResponse<BrainAnswerCorrectionOutput>, BrainGeneratePayload>("/generate/answer-correction", payload, { timeoutMs: 90000 }),
() => fallbackAnswerCorrection(payload),
);
}
export function generateBrainPyqAnalysis(payload: BrainGeneratePayload) {
const output: BrainPyqAnalysisOutput = {
summary: "Upload PYQ papers to analyze repeated patterns.",
data_source_label: "No uploaded PYQ evidence",
total_questions_found: 0,
years_seen: [],
year_count: 0,
is_full_ten_year_analysis: false,
coverage_note: "Upload PYQ papers to analyze repeated patterns.",
pattern_note: "No uploaded PYQ questions matched this request.",
repeated_topics: [],
marks_distribution: [],
answer_type_distribution: [],
sample_questions: [],
evidence_question_ids: [],
confidence: 0,
next_step: "Upload previous year question papers for this subject or chapter.",
};
return withDemoFallback(
() => apiPost<BrainGenerationResponse<BrainPyqAnalysisOutput>, BrainGeneratePayload>("/generate/pyq-analysis", payload, { timeoutMs: 60000 }),
() => ({
type: "pyq_analysis",
topic: payload.topic ?? "",
output,
trust_note: output.coverage_note,
evidence_label: "No uploaded PYQ evidence",
is_fallback: true,
}),
);
}
export function generateBrainNotes(payload: BrainGeneratePayload) {
return withDemoFallback(
() => apiPost<BrainGenerationResponse<NotesOutput>, BrainGeneratePayload>("/generate/notes", payload, { timeoutMs: 60000 }),
() => fallbackNotes(payload),
);
}
export function generateBrainQuiz(payload: BrainGeneratePayload) {
const requestedCount = Number(payload.options?.num_questions ?? payload.options?.question_count ?? 5) || 5;
const timeoutMs = requestedCount >= 10 ? 240000 : 90000;
return withDemoFallback(
() => apiPost<BrainGenerationResponse<{ quiz_title: string; questions: QuizQuestionResponse[] }>, BrainGeneratePayload>("/generate/quiz", payload, { timeoutMs }),
() => fallbackQuiz(payload),
);
}
export function generateBrainFlashcards(payload: BrainGeneratePayload) {
return withDemoFallback(
() => apiPost<BrainGenerationResponse<{ cards: FlashcardResponseItem[] }>, BrainGeneratePayload>("/generate/flashcards", payload, { timeoutMs: 60000 }),
() => fallbackFlashcards(payload),
);
}
export function generateBrainExamMode(payload: BrainGeneratePayload) {
return withDemoFallback(
() => apiPost<BrainGenerationResponse<BrainExamAnswerOutput>, BrainGeneratePayload>("/generate/exam-mode", payload, { timeoutMs: 60000 }),
() => fallbackExamAnswer(payload),
);
}
export function generateBrainLastNightPlan(payload: BrainGeneratePayload) {
return withDemoFallback(
() => apiPost<BrainGenerationResponse<BrainLastNightPlanOutput>, BrainGeneratePayload>("/generate/last-night-plan", payload, { timeoutMs: 60000 }),
() => fallbackLastNightPlan(payload),
);
}
export function generateBrainVideoPlan(payload: BrainVideoPlanPayload) {
return withDemoFallback(
() => apiPost<BrainVideoPlanResponse, BrainVideoPlanPayload>("/video-generator/plan", payload, { timeoutMs: 180000 }),
() => fallbackVideoPlan(payload),
);
}
export type DeepChapterOutput = {
syllabus_position: string;
chapter_title: string;
what_this_chapter_is_about: string;
why_students_struggle: string;
learning_path: string[];
prerequisite_topics: string[];
deep_concept_sections: Array<{
name: string;
simple_explanation: string;
deep_explanation: string;
real_example: string;
equation: string;
why_it_matters: string;
exam_use: string;
common_mistake: string;
practice_question: string;
}>;
derivations: Array<{
name: string;
syllabus_position: string;
prerequisite: string;
symbols: string[];
assumptions: string[];
steps: string[];
final_formula: string;
unit_check: string;
common_mistakes: string[];
board_answer_format: string;
}>;
formulas: Array<{ name: string; formula: string; meaning: string; unit: string; when_to_use: string; condition: string }>;
numerical_patterns: Array<{ pattern_name: string; formula_used: string; method: string; example: string; common_mistakes: string[] }>;
diagrams_needed: string[];
pyq_patterns: Array<{ question: string; marks: string; year: string; topic: string }>;
pyq_note: string;
board_answer_keywords: string[];
common_mistakes: string[];
important_questions: string[];
practice_questions: string[];
quick_revision_box: string[];
next_best_action: string;
source_used: string;
};
export type VisualLessonOutput = {
lesson_title: string;
syllabus_position: string;
learning_goal: string;
visual_style: string;
concept_flow: string[];
derivation_frames: Array<{
frame_number: number;
teaching_purpose: string;
visual_type: string;
visual_description: string;
on_screen_text: string;
narration: string;
why_this_helps: string;
common_misunderstanding_fixed: string;
exam_connection: string;
duration_seconds: number;
}>;
diagram_frames: Array<{
frame_number: number;
teaching_purpose: string;
visual_type: string;
visual_description: string;
on_screen_text: string;
narration: string;
why_this_helps: string;
exam_connection: string;
duration_seconds: number;
}>;
numerical_frames: Array<{
frame_number: number;
teaching_purpose: string;
visual_type: string;
visual_description: string;
on_screen_text: string;
narration: string;
why_this_helps: string;
exam_connection: string;
duration_seconds: number;
}>;
recap_frames: Array<{
frame_number: number;
teaching_purpose: string;
visual_type: string;
visual_description: string;
on_screen_text: string;
narration: string;
why_this_helps: string;
exam_connection: string;
duration_seconds: number;
}>;
practice_frames: Array<{
frame_number: number;
teaching_purpose: string;
visual_type: string;
visual_description: string;
on_screen_text: string;
narration: string;
why_this_helps: string;
exam_connection: string;
duration_seconds: number;
}>;
total_estimated_seconds: number;
total_frames: number;
narration_script: string;
};
export type StudyIntentOutput = {
raw_text: string;
board: string;
class_level: string;
stream: string;
subject: string;
chapter: string;
topic: string;
goal: string;
urgency: string;
language: string;
requested_mode: string;
confidence: number;
missing_fields: string[];
clarification_question: string;
};
export function generateBrainDeepChapter(payload: BrainGeneratePayload) {
return withDemoFallback(
() => apiPost<BrainGenerationResponse<DeepChapterOutput>, BrainGeneratePayload>("/generate/deep-chapter", payload, { timeoutMs: 90000 }),
() => ({ type: "deep_chapter", topic: payload.topic ?? "", output: {} as DeepChapterOutput, trust_note: "Offline preview.", is_fallback: true }),
);
}
export function generateBrainVisualLesson(payload: BrainGeneratePayload) {
return withDemoFallback(
() => apiPost<BrainGenerationResponse<VisualLessonOutput>, BrainGeneratePayload>("/generate/visual-lesson", payload, { timeoutMs: 90000 }),
() => ({ type: "visual_lesson", topic: payload.topic ?? "", output: {} as VisualLessonOutput, trust_note: "Offline preview.", is_fallback: true }),
);
}