File size: 60,595 Bytes
65d68f5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 | 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 }),
);
}
|