Download src/lib/api/brain.ts from asnannp/DocDoeAI: direct link, hf CLI and curl.
- Browser
- Download file 60.6 kB
-
https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/lib/api/brain.ts
- Command line
-
hf download hf://spaces/asnannp/DocDoeAI/src/lib/api/brain.ts
-
curl -L -o brain.ts https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/lib/api/brain.ts
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 }), | |
| ); | |
| } | |