import type { StudioToolId } from "@/components/dashboard/studioTools"; import { generateBrainAnswerCorrection, generateBrainExamAnswer, generateBrainFlashcards, generateBrainLastNightPlan, generateBrainNotes, generateBrainQuiz, generateBrainVideoPlan, generateStudyPath, } from "@/lib/api/brain"; import { getStudyPath, getWhatToStudy } from "@/lib/api/intelligence"; import { normalizeExamAnswer, normalizeFlashcards, normalizeNotes, normalizePYQAnalysis, normalizeQuiz, normalizeStudyPlan, normalizeVideoPlan, } from "@/lib/api/normalizers"; import { analyzeStructuredPreviousPapers, listPreviousPapers } from "@/lib/api/previous-papers"; import { createVideoScenePlan } from "@/lib/api/video"; import type { BrainAnswerCorrectionOutput, BrainGeneratePayload, ExamModeAnswer, ExamModeFourMarkAnswer, ExamModeTwoMarkAnswer, StudentLevel, StudyPathPlanType, VideoScene, } from "@/lib/api/types"; type SourceValidationError = { valid: false; error: string; }; type SourceValidationResult = { valid: true } | SourceValidationError; type SourceWithStatus = { id: string; status?: "uploaded" | "processing" | "ready" | "failed"; }; export type AnswerCorrectionInput = { question: string; studentAnswer: string; marks: "1" | "2" | "3" | "4" | "5" | "6" | "not_sure"; subject?: string; chapter?: string; }; function validateSourceForTool(documents: SourceWithStatus[], sourceIds: string[]): SourceValidationResult { if (sourceIds.length === 0) { return { valid: false, error: "Choose a source first so I can make this from your material." }; } const selectedDocuments = documents.filter((doc) => sourceIds.includes(doc.id)); if (selectedDocuments.length === 0) { return { valid: false, error: "Selected source not found. Please choose a valid source." }; } const processingSources = selectedDocuments.filter((doc) => doc.status === "processing" || doc.status === "uploaded"); if (processingSources.length > 0) { return { valid: false, error: "This source is still being read. I'll unlock source-based tools when it's ready." }; } const failedSources = selectedDocuments.filter((doc) => doc.status === "failed"); if (failedSources.length > 0) { return { valid: false, error: "I couldn't read this file properly. Try retrying extraction or upload a clearer file." }; } // If any source has no status, treat it as potentially not ready const unknownStatusSources = selectedDocuments.filter((doc) => !doc.status); if (unknownStatusSources.length > 0 && unknownStatusSources.length === selectedDocuments.length) { return { valid: false, error: "Source status unknown. Please wait for the source to finish processing." }; } return { valid: true }; } export type StudioRunSection = { title: string; items: string[]; variant?: "default" | "caution" | "action" | "keywords"; }; export type StudioRunResult = { toolId: StudioToolId; title: string; summary: string; sections: StudioRunSection[]; status: "ready" | "empty" | "fallback"; generationTimeMs?: number | null; cacheHit?: boolean; raw?: unknown; evidenceLabel?: string | null; }; type RunStudioToolInput = { toolId: StudioToolId; documentId: string; sourceIds?: string[]; sourceTitle: string; language?: string; studentLevel?: StudentLevel; weakAreas?: string[]; goal?: string; timeLeft?: string; topic?: string; studyProfileId?: string | null; quizQuestionCount?: number; documents?: SourceWithStatus[] | undefined; answerCorrection?: AnswerCorrectionInput; }; const toolTitles: Record = { "smart-notes": "Teach this syllabus topic", "pyq-intelligence": "Show exam pattern from uploaded PYQs", "ai-quiz": "Test me on this topic", flashcards: "Lock exam keywords", "study-videos": "Teach with a visual lesson", "exam-answer": "Make board-answer version", "answer-correction": "Correct my answer", "last-night-prep": "Tell me what to study tonight", "study-plan": "Build my chapter roadmap", "what-to-study": "What To Study First", "check-level": "Check My Level", "exam-war-mode": "Exam War Mode", }; function baseResult(input: RunStudioToolInput, summary: string, sections: StudioRunSection[], raw?: unknown, evidenceLabel?: string | null): StudioRunResult { return { toolId: input.toolId, title: `${toolTitles[input.toolId]}: ${input.sourceTitle}`, summary, sections, status: "ready", raw, evidenceLabel: evidenceLabel ?? null, }; } function answerToLine(answer: ExamModeAnswer | ExamModeTwoMarkAnswer | ExamModeFourMarkAnswer | { question?: string; answer?: string }) { if ("question" in answer && "answer" in answer) return `${answer.question}: ${answer.answer}`; return "Mark-wise answer outline"; } function planTypeForTool(toolId: StudioToolId): StudyPathPlanType { if (toolId === "last-night-prep") return "1h"; if (toolId === "exam-war-mode") return "5h"; return "7d"; } function evidenceLabelFromRaw(raw: unknown): string | null { if (raw && typeof raw === "object" && "evidence_label" in raw) { const label = (raw as { evidence_label?: unknown }).evidence_label; return typeof label === "string" ? label : null; } return null; } // The title must describe what the analysis actually did, never a span the data // does not establish. The analyzer is upload-gated: it compares only the user's // uploaded and verified papers and refuses pattern analysis below 2 years // (backend/app/services/pyq_pattern_analyzer.py). "10-year" must not appear // unless a genuine verified evidence label (8+ distinct years) is present. function pyqTitleFromEvidence(label: string | null, paperCount: number) { const text = label?.toLowerCase() ?? ""; const hasVerifiedEvidence = text.includes("verified") && !text.includes("not verified") && !text.includes("unverified"); if (hasVerifiedEvidence) return "Verified multi-year PYQ signals"; if (paperCount === 1) return "Signals from this paper"; return "Signals across uploaded papers"; } function uploadedPaperEvidenceLabel(paperCount: number) { return `Based on ${paperCount} uploaded question paper${paperCount !== 1 ? "s" : ""}`; } export async function runStudioTool(input: RunStudioToolInput): Promise { const language = input.language ?? "English"; const sourceIds = input.sourceIds?.length ? input.sourceIds : [input.documentId]; // Validate sources if documents are provided if (input.documents && input.documents.length > 0) { const validation = validateSourceForTool(input.documents, sourceIds); if (!validation.valid) { return { toolId: input.toolId, title: `${toolTitles[input.toolId]}: ${input.sourceTitle}`, summary: validation.error, sections: [{ title: "Error", items: [validation.error], variant: "caution" as const }], status: "empty", }; } } const topic = input.topic || input.sourceTitle.replace(/^\d+\s+selected\s+sources$/i, "selected sources"); const brainPayload: BrainGeneratePayload = { topic, subject: undefined, source_id: input.documentId, source_ids: sourceIds, study_profile_id: input.studyProfileId ?? null, level: input.studentLevel ?? "intermediate", goal: input.goal ?? "good marks", language_preference: language, options: { tool: input.toolId, weak_areas: input.weakAreas ?? [] }, }; if (input.toolId === "smart-notes") { const result = await generateBrainNotes({ ...brainPayload, options: { mode: "smart_notes" } }); const notes = normalizeNotes(result.output); const sections: StudioRunSection[] = [ { title: "Key points", items: notes.key_points }, { title: "Exam focus", items: notes.exam_focus ?? notes.exam_keywords ?? [] }, ...(notes.scoring_keywords?.length ? [{ title: "Scoring keywords", items: notes.scoring_keywords, variant: "keywords" as const }] : []), ...(notes.common_mistakes?.length ? [{ title: "Common mistakes", items: notes.common_mistakes }] : []), { title: "Last-minute revision", items: notes.last_minute_revision ?? [] }, ...(notes.ten_minute_revision?.length ? [{ title: "10-min revision", items: notes.ten_minute_revision }] : []), ...(notes.caution_note ? [{ title: "Caution", items: [notes.caution_note], variant: "caution" as const }] : []), ...(notes.next_study_action ? [{ title: "Next move", items: [notes.next_study_action], variant: "action" as const }] : []), ]; return baseResult( input, notes.source_summary ?? notes.summary ?? notes.student_level_summary ?? "DocDoe created source-based revision notes.", sections, result, result.evidence_label, ); } if (input.toolId === "ai-quiz") { const questionCount = input.quizQuestionCount ?? 5; const result = await generateBrainQuiz({ ...brainPayload, options: { ...(brainPayload.options ?? {}), difficulty: "exam", num_questions: questionCount, question_count: questionCount, }, }); const questions = normalizeQuiz(result.output); const traps = questions.map((q) => q.common_trap).filter((t): t is string => Boolean(t)); const quizSections: StudioRunSection[] = [ { title: "Practice questions", items: questions.map((question) => question.question) }, ...(traps.length ? [{ title: "Common traps", items: traps, variant: "caution" as const }] : []), ]; const quizResult = baseResult( input, questions.length > 0 ? result.cache_hit ? "DocDoe reused your recent quiz instantly." : questionCount >= 10 ? `DocDoe created a ${questionCount}-question practice set from your selected source.` : "DocDoe created a short practice quiz from the active source." : "DocDoe could not create quiz questions from this source yet.", quizSections, result, result.evidence_label, ); return { ...quizResult, generationTimeMs: result.generation_time_ms ?? null, cacheHit: Boolean(result.cache_hit), }; } if (input.toolId === "flashcards") { const result = await generateBrainFlashcards({ ...brainPayload, options: { card_count: 8 } }); const cards = normalizeFlashcards(result.output); const hooks = cards.map((c) => c.memory_hook).filter((h): h is string => Boolean(h)); const examUses = cards.map((c) => c.exam_use).filter((u): u is string => Boolean(u)); const flashSections: StudioRunSection[] = [ { title: "Cards", items: cards.map((card) => `${card.front} — ${card.back}`) }, ...(hooks.length ? [{ title: "Memory hooks", items: hooks }] : []), ...(examUses.length ? [{ title: "Exam use", items: examUses }] : []), ]; return baseResult( input, cards.length > 0 ? "DocDoe created active-recall cards for quick revision." : "DocDoe could not create flashcards from this source yet.", flashSections, result, result.evidence_label, ); } if (input.toolId === "exam-answer") { const result = await generateBrainExamAnswer(brainPayload); const output = normalizeExamAnswer(result.output); const answers = [ ...(output?.one_mark ?? []), ...(output?.two_mark ?? []), ...(output?.four_mark ?? []), ...result.output.six_mark, ...(output?.one_mark_answers ?? []), ...(output?.two_mark_answers ?? []), ...(output?.four_mark_answers ?? []), ]; const examSections: StudioRunSection[] = [ { title: "Mark-wise answers", items: answers.map(answerToLine).slice(0, 6) }, { title: "Keywords", items: output?.keywords_to_use ?? output?.important_keywords ?? result.output.keywords_to_underline ?? [] }, ...(result.output.scoring_keywords?.length ? [{ title: "Scoring keywords", items: result.output.scoring_keywords, variant: "keywords" as const }] : []), { title: "Common mistakes", items: output?.mistakes_to_avoid ?? [result.output.common_mistake] }, { title: "Examiner tip", items: [result.output.examiner_tip, ...(result.output.answer_writing_formula ?? [])] }, ...(result.output.caution_note ? [{ title: "Caution", items: [result.output.caution_note], variant: "caution" as const }] : []), ...(result.output.next_study_action ? [{ title: "Next move", items: [result.output.next_study_action], variant: "action" as const }] : []), ]; return baseResult( input, "DocDoe shaped this into mark-wise exam answers.", examSections, result, result.evidence_label, ); } if (input.toolId === "answer-correction") { const correction = input.answerCorrection; if (!correction?.question.trim() || !correction.studentAnswer.trim()) { return { toolId: input.toolId, title: `${toolTitles[input.toolId]}: ${input.sourceTitle}`, summary: "Paste the question and your answer before correction.", sections: [{ title: "Missing input", items: ["Paste the question and your answer before correction."], variant: "caution" as const }], status: "empty", }; } const result = await generateBrainAnswerCorrection({ ...brainPayload, topic: correction.question, subject: correction.subject || brainPayload.subject, options: { ...(brainPayload.options ?? {}), mode: "answer_correction", question: correction.question, student_answer: correction.studentAnswer, marks: correction.marks, subject: correction.subject, chapter: correction.chapter, }, }); const output = result.output as BrainAnswerCorrectionOutput; const isSourceMismatch = output.source_truth.toLowerCase().includes("not found"); const correctionSections: StudioRunSection[] = [ { title: "Syllabus position", items: [output.syllabus_position, output.mark_scheme_assumption, output.score] }, { title: "What you wrote correctly", items: output.what_correct }, { title: "Marks lost", items: output.marks_lost, variant: isSourceMismatch ? "caution" : "default" }, { title: "Missing keywords", items: output.missing_keywords, variant: "keywords" }, { title: "Corrected board-answer version", items: [output.corrected_board_answer] }, { title: "How to improve next time", items: output.how_to_improve }, { title: "Quick retry task", items: [output.quick_retry_task], variant: "action" }, ...(output.source_evidence?.length ? [{ title: "Source evidence", items: output.source_evidence, variant: "keywords" as const }] : []), { title: "Source truth", items: [output.source_truth], variant: isSourceMismatch ? "caution" : "default" }, ]; return baseResult( input, isSourceMismatch ? output.source_truth : `${output.score}. DocDoe found the marks lost and rewrote the answer in board format.`, correctionSections, result, result.evidence_label, ); } if (input.toolId === "pyq-intelligence") { const papers = await listPreviousPapers(); if (papers.length === 0) { return { toolId: input.toolId, title: "Upload question papers to unlock PYQ signals", summary: "DocDoe can explain this material, but PYQ trends need actual previous papers. Add past papers for this subject to compare patterns.", sections: [ { title: "Upload question paper", items: ["Upload question papers to unlock PYQ signals."] }, { title: "Continue with material-only questions", items: ["Likely questions from this material"] }, ], status: "empty", evidenceLabel: "Based on uploaded material only", }; } const result = await analyzeStructuredPreviousPapers({ paper_ids: papers.map((paper) => paper.id), subject: papers[0]?.subject ?? "Study", syllabus: papers[0]?.syllabus ?? "Indian Board Exams", target_exam: "Board exam", }); const analysis = normalizePYQAnalysis(result); const pyqSections: StudioRunSection[] = [ { title: "Repeated topics", items: analysis?.repeated_topics.map((topic) => `${topic.topic}: ${topic.reason}`) ?? [], }, { title: "High probability", items: analysis?.high_probability_topics.map((topic) => `${topic.topic}: ${topic.reason}`) ?? [], }, { title: "Study priority", items: analysis?.study_priority ?? [] }, ...(analysis?.one_paper_limit_note ? [{ title: "Note", items: [analysis.one_paper_limit_note], variant: "caution" as const }] : []), ...(analysis?.next_study_action ? [{ title: "Next move", items: [analysis.next_study_action], variant: "action" as const }] : []), ...(analysis?.caution_note ? [{ title: "Caution", items: [analysis.caution_note], variant: "caution" as const }] : []), ]; const evidenceLabel = evidenceLabelFromRaw(result) ?? uploadedPaperEvidenceLabel(papers.length); const pyqResult = baseResult( input, analysis?.summary ?? "DocDoe checked available previous papers for patterns.", pyqSections, result, evidenceLabel, ); return { ...pyqResult, title: pyqTitleFromEvidence(evidenceLabel, papers.length), evidenceLabel, }; } if (input.toolId === "what-to-study") { const result = await getWhatToStudy(input.documentId, input.studentLevel ?? "intermediate", "good marks"); return baseResult( input, "DocDoe prioritized the chapter from your source, goal, and current level.", [ { title: "Study first", items: result.must_study_topics.map((topic) => `${topic.topic}: ${topic.reason}`) }, { title: "Quick wins", items: result.easy_marks.map((topic) => `${topic.topic}: ${topic.reason}`) }, { title: "Skip for now", items: result.skip_for_now_topics.map((topic) => `${topic.topic}: ${topic.reason}`) }, ], result, result.evidence_label ?? null, ); } if (input.toolId === "study-plan" || input.toolId === "last-night-prep" || input.toolId === "exam-war-mode") { if (input.toolId === "last-night-prep") { const result = await generateBrainLastNightPlan({ ...brainPayload, time_left: input.timeLeft ?? "5 hours", }); const plan = result.output; const lnpSections: StudioRunSection[] = [ ...(plan.do_first?.length ? [{ title: "Do first", items: plan.do_first }] : []), { title: "Time blocks", items: plan.time_blocks.map((block) => `${block.label}: ${block.minutes} min`) }, ...(plan.if_30_minutes?.length ? [{ title: "If 30 minutes", items: plan.if_30_minutes }] : []), ...(plan.if_2_hours?.length ? [{ title: "If 2 hours", items: plan.if_2_hours }] : []), ...(plan.if_half_day?.length ? [{ title: "If half day", items: plan.if_half_day }] : []), { title: "Must study", items: plan.must_study }, { title: "Skip for now", items: plan.skip_now }, ...(plan.skip_or_skim?.length ? [{ title: "Skip or skim", items: plan.skip_or_skim }] : []), ...(plan.quick_recall_questions?.length ? [{ title: "Quick recall", items: plan.quick_recall_questions }] : []), { title: "Final checklist", items: plan.final_checklist }, ...(plan.final_check_before_sleep?.length ? [{ title: "Final check before sleep", items: plan.final_check_before_sleep }] : []), ...(plan.caution_note ? [{ title: "Caution", items: [plan.caution_note], variant: "caution" as const }] : []), { title: "Trust note", items: [result.trust_note ?? plan.trust_note ?? "Using your selected source and study goal."] }, ]; return baseResult( input, plan.calming_note ?? "DocDoe built a calm last-night plan with scoring priorities.", lnpSections, result, result.evidence_label, ); } const planType = planTypeForTool(input.toolId); const goal = input.toolId === "exam-war-mode" ? "pass" : "good marks"; const result = sourceIds.length > 1 ? await generateStudyPath({ source_id: input.documentId, source_ids: sourceIds, topic, goal, level: input.studentLevel ?? "intermediate", language_preference: language, use_my_profile: true, }) : await getStudyPath(input.documentId, planType, input.studentLevel ?? "intermediate", goal); const plan = "study_timeline" in result ? { blocks: result.study_timeline.map((item) => ({ time_block: `${item.duration_minutes} min`, topic: item.title, reason: item.reason, task: item.task, output_expected: item.expected_output, revision_checkpoint: item.actions.join(", "), })), } : normalizeStudyPlan(result); const studyPathEvidenceLabel = "evidence_label" in result && typeof (result as { evidence_label?: unknown }).evidence_label === "string" ? (result as { evidence_label: string }).evidence_label : null; return baseResult( input, `DocDoe built a ${planType} plan with checkpoints and revision blocks.`, [ { title: "Time blocks", items: plan?.blocks.map((block) => `${block.time_block}: ${block.task}`) ?? [], }, { title: "Why this order", items: plan?.blocks.map((block) => `${block.topic}: ${block.reason}`) ?? [], }, ], result, studyPathEvidenceLabel, ); } if (input.toolId === "study-videos") { const brainPlan = await generateBrainVideoPlan({ topic, video_mode: "exam_focus", teaching_style: "visual_tutor", language_preference: language, format: "16:9", source_id: input.documentId, source_ids: sourceIds, level: input.studentLevel ?? "intermediate", goal: input.goal ?? "good marks", }); if (brainPlan.planning_only || !brainPlan.render_supported) { return baseResult( input, "DocDoe created a study video plan. This long video is planned first. Rendering support will be added later.", [ { title: "Duration", items: [brainPlan.target_duration_range, `${brainPlan.estimated_duration_minutes} minutes estimated`] }, { title: "Scene plan", items: brainPlan.scenes.map((scene) => `${scene.title}: ${scene.on_screen_text}`) }, { title: "Trust note", items: [brainPlan.trust_note] }, ], brainPlan, brainPlan.evidence_label, ); } const result = await createVideoScenePlan({ document_id: input.documentId, title: input.sourceTitle, language, duration_minutes: 5, visual_style: "clean_explainer", video_format: "16:9", }); const plan = normalizeVideoPlan(result); const scenes = "scenes" in (plan ?? {}) ? (plan as { scenes: VideoScene[] }).scenes : []; return baseResult( input, "DocDoe created a teachable scene plan. Long videos can take more time to prepare.", [{ title: "Scene plan", items: scenes.map((scene) => `${scene.screen_text}: ${scene.visual_hint}`) }], result, result.evidence_label ?? null, ); } return { toolId: input.toolId, title: `${toolTitles[input.toolId]}: ${input.sourceTitle}`, summary: "Choose this tool from Studio to continue.", sections: [{ title: "Next step", items: ["DocDoe is ready when you are."] }], status: "fallback", }; }