DocDoeAI / src /components /study-chat /study-workflow.ts
asnannp's picture
deploy: sync backend to Space root (learn-lesson HF cache fix)
65d68f5
Raw History Blame Contribute Delete
8.76 kB
import {
askDocDoe,
generateBrainDeepChapter,
generateBrainExamAnswer,
generateBrainFlashcards,
generateBrainLastNightPlan,
generateBrainNotes,
generateBrainPyqAnalysis,
generateBrainQuiz,
generateBrainVisualLesson,
generateStudyPath,
} from "@/lib/api/brain";
import type { ApiDocument, BrainGeneratePayload } from "@/lib/api/types";
import { buildStructuredStudyResponse, shouldUseStructuredStudyResponse } from "@/lib/phase3-learning";
import type { StudyIntent } from "@/lib/study-intents";
import { askModeForIntent, type StudyBrainContext } from "./study-brain-context";
import type { StudyCard, StudyCardMeta } from "./types";
export type RunStudyGenerationInput = {
intent: StudyIntent;
prompt: string;
topic?: string | null;
sourceIds: string[];
selectedDocuments: ApiDocument[];
brainContext?: StudyBrainContext;
/**
* When true, source IDs are stripped before calling the API.
* Use for "general_answer" mode where the user asked a topic
* question without explicitly requesting their uploaded material.
*/
generalMode?: boolean;
};
function revisionTimeLeftForPrompt(prompt: string) {
const value = prompt.toLowerCase();
if (/\b(30\s*min|half\s*hour)\b/.test(value)) return "30_minutes";
if (/\b(1\s*hour|one\s*hour|60\s*min)\b/.test(value)) return "1_hour";
if (/\b(2\s*hours?|two\s*hours?)\b/.test(value)) return "2_hours";
if (/\b(3\s*hours?|three\s*hours?)\b/.test(value)) return "3_hours";
if (/\b(5\s*hours?|five\s*hours?|half\s*day)\b/.test(value)) return "5_hours";
if (/\b(tonight|one\s+night|overnight|cram|last\s+night)\b/.test(value)) return "tonight";
if (/\b(tomorrow|next\s+day|1\s*day|one\s*day)\b/.test(value)) return "tomorrow";
return "tonight";
}
function generationPayload(
generationTopic: string,
sourceIds: string[],
brainContext?: StudyBrainContext,
extra: Partial<BrainGeneratePayload> = {},
): BrainGeneratePayload {
return {
topic: generationTopic,
source_id: sourceIds[0] ?? null,
source_ids: sourceIds,
subject: brainContext?.subject ?? null,
study_profile_id: brainContext?.studyProfileId ?? null,
level: brainContext?.level ?? null,
language_preference: brainContext?.languagePreference ?? null,
...extra,
};
}
const GENERAL_EVIDENCE_LABEL = "No source attached - general answer";
export function metaFromResponse(response: {
trust_note?: string | null;
evidence_label?: string | null;
is_fallback?: boolean;
suggested_actions?: string[];
citations?: unknown[];
}, sourceIds: string[] = []): StudyCardMeta {
const trustNote = response.trust_note ?? null;
const evidenceLabel = response.evidence_label ?? null;
const citations = Array.isArray(response.citations) ? response.citations : [];
const looksSourceBacked = evidenceLabel ? /uploaded|source|material|paper/i.test(evidenceLabel) : false;
const saysNoSource = trustNote ? /no source attached|not source-grounded|generic/i.test(trustNote) : false;
const shouldUseGeneralLabel = sourceIds.length === 0 && citations.length === 0 && (saysNoSource || looksSourceBacked);
return {
trustNote,
evidenceLabel: shouldUseGeneralLabel ? GENERAL_EVIDENCE_LABEL : evidenceLabel,
isFallback: Boolean(response.is_fallback),
suggestedActions: response.suggested_actions ?? [],
};
}
export async function runStudyGeneration({
intent,
prompt,
topic,
sourceIds: rawSourceIds,
selectedDocuments,
brainContext,
generalMode = false,
}: RunStudyGenerationInput): Promise<StudyCard> {
const sourceIds = generalMode ? [] : rawSourceIds;
const generationTopic = topic?.trim() || prompt;
if (sourceIds.length === 0 && shouldUseStructuredStudyResponse(prompt, intent)) {
const data = buildStructuredStudyResponse(prompt);
return {
kind: "structured_study_response",
data,
topic: data.topic || generationTopic,
meta: {
trustNote: "Frontend fallback. Backend structured chapter contract is still a TODO.",
evidenceLabel: "No source attached - Study Chat fallback",
isFallback: true,
},
};
}
if (intent === "analyze_pyq" || intent === "predict_questions") {
const response = await generateBrainPyqAnalysis(
generationPayload(generationTopic, sourceIds, brainContext, {
subject: selectedDocuments[0]?.subject ?? brainContext?.subject ?? null,
options: { raw_text: prompt },
}),
);
return {
kind: "pyq_analysis",
data: response.output,
topic: response.topic || generationTopic,
meta: metaFromResponse(response, sourceIds),
};
}
if (intent === "generate_notes" || intent === "show_formulas") {
const response = await generateBrainNotes(
generationPayload(generationTopic, sourceIds, brainContext, {
options: { mode: intent === "show_formulas" ? "formulas" : "smart_notes" },
}),
);
return {
kind: "notes",
data: response.output,
topic: response.topic || generationTopic,
meta: metaFromResponse(response, sourceIds),
};
}
if (intent === "generate_quiz") {
const response = await generateBrainQuiz(
generationPayload(generationTopic, sourceIds, brainContext, {
options: { num_questions: 8 },
}),
);
return {
kind: "quiz",
data: response.output,
topic: response.topic || generationTopic,
meta: metaFromResponse(response, sourceIds),
};
}
if (intent === "generate_flashcards") {
const response = await generateBrainFlashcards(
generationPayload(generationTopic, sourceIds, brainContext, {
options: { card_count: 10 },
}),
);
return {
kind: "flashcards",
data: response.output,
topic: response.topic || generationTopic,
meta: metaFromResponse(response, sourceIds),
};
}
if (intent === "generate_exam_answer") {
const response = await generateBrainExamAnswer(
generationPayload(generationTopic, sourceIds, brainContext),
);
return {
kind: "exam_answer",
data: response.output,
topic: response.topic || generationTopic,
meta: metaFromResponse(response, sourceIds),
};
}
if (intent === "create_revision_plan") {
const response = await generateBrainLastNightPlan(
generationPayload(generationTopic, sourceIds, brainContext, {
time_left: revisionTimeLeftForPrompt(prompt),
}),
);
return {
kind: "revision_plan",
data: response.output,
topic: response.topic || generationTopic,
meta: metaFromResponse(response, sourceIds),
};
}
if (intent === "continue_learning") {
const response = await generateStudyPath({
raw_text: prompt,
source_id: sourceIds[0] ?? null,
source_ids: sourceIds,
subject: brainContext?.subject ?? selectedDocuments[0]?.subject ?? null,
topic: generationTopic,
goal: "School exam tuition path",
time_left: "next exam",
use_my_profile: true,
level: brainContext?.level ?? null,
language_preference: brainContext?.languagePreference ?? null,
});
return {
kind: "study_path",
data: response,
topic: response.topic || generationTopic,
meta: metaFromResponse(response, sourceIds),
};
}
if (intent === "create_video_plan" || intent === "render_video") {
const response = await generateBrainVisualLesson(
generationPayload(generationTopic, sourceIds, brainContext, {
options: { raw_text: prompt, mode: "visual_lesson" },
}),
);
return {
kind: "visual_lesson",
data: response.output,
topic: response.topic || generationTopic,
meta: metaFromResponse(response, sourceIds),
};
}
if (intent === "teach_topic" || intent === "ask_doubt" || intent === "explain_source" || intent === "summarize_source" || intent === "use_syllabus_chapter") {
const response = await generateBrainDeepChapter(
generationPayload(generationTopic, sourceIds, brainContext, {
options: { raw_text: prompt },
}),
);
return {
kind: "deep_chapter",
data: response.output,
topic: response.topic || generationTopic,
meta: metaFromResponse(response, sourceIds),
};
}
const response = await askDocDoe({
question: prompt,
source_id: sourceIds[0] ?? null,
source_ids: sourceIds,
mode: askModeForIntent(intent, prompt),
level: brainContext?.level ?? null,
language_preference: brainContext?.languagePreference ?? null,
study_profile_id: brainContext?.studyProfileId ?? null,
});
return {
kind: "explanation",
data: response,
topic: generationTopic,
intent,
meta: metaFromResponse(response, sourceIds),
};
}