Download src/components/study-chat/chat-intent.ts from asnannp/DocDoeAI: direct link, hf CLI and curl.
- Browser
- Download file 22.2 kB
-
https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/study-chat/chat-intent.ts
- Command line
-
hf download hf://spaces/asnannp/DocDoeAI/src/components/study-chat/chat-intent.ts
-
curl -L -o chat-intent.ts https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/study-chat/chat-intent.ts
22.2 kB
| /** | |
| * Classifies a user message into a response mode so the chat can: | |
| * - route greetings/casual messages to chat AI without requiring sources | |
| * - answer generally for topic questions (API without source IDs) | |
| * - show a clean upload CTA for doc-specific requests with no source | |
| * - show clarify options for genuinely ambiguous short messages | |
| * - ground answers in selected sources when present | |
| */ | |
| import type { ApiDocument } from "@/lib/api/types"; | |
| import { defaultStudyIntent, type StudyIntent } from "@/lib/study-intents"; | |
| import type { ChatMessage } from "./types"; | |
| // ββ Types βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export type ResponseMode = | |
| | "casual" | |
| | "general_answer" | |
| | "needs_source" | |
| | "source_grounded" | |
| | "tool_ready" | |
| | "clarify"; | |
| export type AnswerMode = | |
| | "casual_chat" | |
| | "general_ai_answer" | |
| | "web_search_answer" | |
| | "source_grounded_answer" | |
| | "source_required_upload_prompt" | |
| | "tool_generation" | |
| | "clarification"; | |
| export type AnswerModeResult = { | |
| mode: AnswerMode; | |
| intent: string; | |
| requiresSource: boolean; | |
| shouldUseWeb: boolean; | |
| canAnswerGenerally: boolean; | |
| topic: string | null; | |
| toolType: StudyIntent | null; | |
| userFacingNextStep: string | null; | |
| }; | |
| export type AnswerModeSource = { | |
| title?: string | null; | |
| file_name?: string | null; | |
| }; | |
| export type ChatIntentResult = { | |
| /** Best-guess StudyIntent for downstream routing. */ | |
| intent: StudyIntent; | |
| responseMode: ResponseMode; | |
| answerMode?: AnswerMode; | |
| shouldUseWeb?: boolean; | |
| userFacingNextStep?: string | null; | |
| requiresSource: boolean; | |
| canAnswerGenerally: boolean; | |
| /** | |
| * Pre-built reply text for local UX prompts such as needs_source. | |
| * Null means the API should generate the answer. | |
| */ | |
| immediateReply: string | null; | |
| topic: string | null; | |
| }; | |
| // ββ Patterns ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const GREETING_RE = | |
| /^\s*(hi+|hey+|hello+|yo+|sup|bro|howdy|hola|namaste|good\s+(morning|evening|afternoon|day|night)|start|begin|let'?s\s+go|ready)\W*$/i; | |
| // Matches "hi + something" β user starts with a greeting word then asks a question | |
| const GREETING_PREFIX_RE = | |
| /^\s*(hi+|hey+|hello+|yo+|bro|sup)\b[\s,!.]*/i; | |
| const CAPABILITY_RE = | |
| /\b(what\s+can\s+you\s+do|what\s+do\s+you\s+do|who\s+are\s+you|what(?:'s|\s+is)\s+your\s+name|are\s+you\s+(an?\s+)?(ai|bot|chatbot|robot|assistant)|what\s+are\s+you|how\s+(do|to|did|does|can|should|would|could|might|shall)\s+(i\s+)?use|how\s+(does|do|would|should)\s+(this|it|docdoe|the\s+app)\s+work|help\s+me\s+study|what\s+is\s+this\s+(app|tool)|your\s+features?|your\s+capabilities?|introduce\s+yourself|how\s+does\s+docdoe\s+work)\b/i; | |
| const CASUAL_INTRO_RE = | |
| /^\s*(hi+|hey+|hello+|yo+|bro)\b[\s,!.]*(my\s+name\s+is|i'?m|i\s+am)\b/i; | |
| // Patterns that explicitly reference an already-uploaded or "this" document. | |
| // Does NOT match generic notes/quiz/explain requests that happen to lack a source. | |
| const DOC_REF_RE = | |
| /\b(this|that|the|my|uploaded|selected)\s+(pdf|doc(?:ument)?|file|chapter|notes?|textbook|paper|source|material|page)\b|\b(from|based\s+on|in|using|explain|summarize|summarise|extract\s+from)\s+(this|that|my|the\s+uploaded|the\s+selected)\b/i; | |
| const PYQ_RE = | |
| /\b(pyq|previous[\s-]year|previous\s+question\s+papers?|past\s+papers?|10[\s-]year\s+(?:pyq|pattern|paper)|analyze\s+papers?|paper\s+signal|exam\s+pattern|repeated?\s+questions?|predict\s+questions?)\b/i; | |
| const UNCLEAR_RE = | |
| /^\s*(ok(?:ay)?|yeah|yep|yes|no|hmm?|uh|sure|got\s*it|that\s+one|do\s+it|go\s+ahead|continue|next|done|fine|proceed|thanks?|thank\s+you|thx|ty|cool|nice|great|good)\s*[.!?]?\s*$/i; | |
| const NOTES_RE = | |
| // allow any adjective(s) between the verb and "notes": exam notes, study | |
| // notes, scoring notes, short notes, chapter notes, etc. | |
| /\b(make|generate|create|write|give\s+me|build|prepare)\s+(me\s+)?(some\s+|a\s+)?(?:[a-z-]+\s+){0,3}notes?\b|\bnotes?\s+(on|for|about|of)\b/i; | |
| const QUIZ_RE = | |
| /\b(make|create|generate|give\s+me|build)\s+(a\s+|some\s+)?(quiz|practice\s+(quiz|test)|test)\b|\b(test\s+me|quiz\s+me|ask\s+me\s+questions?)\b/i; | |
| const FLASHCARD_RE = | |
| /\b(make|create|generate|give\s+me)\s+(some\s+)?(flash\s?cards?|memory\s+cards?|revision\s+cards?)\b|\b(flash\s?cards?|memory\s+cards?|revision\s+cards?)\s+(on|for|about|of)\b/i; | |
| const VIDEO_RE = | |
| /\b(make|create|build|generate)\s+(a\s+)?(study\s+)?video\b|(video\s+(plan|for|on|about|of))\b/i; | |
| const STUDY_PLAN_RE = | |
| /\b(study\s+plan|revision\s+plan|last\s+night|one\s+night|overnight|tonight|tomorrow|cram|crash\s+course|schedule|timetable|what\s+should\s+i\s+study|learn\b.*\bnight|finish\b.*\bnight|complete\b.*\bnight)\b/i; | |
| const EXAM_ANSWER_RE = | |
| /\b(exam\s+mode|exam\s+answer|board\s+style|marks?\s+answer|[1-9]\s*[- ]?mark|five\s+mark|answer\s+format)\b/i; | |
| const EXPLICIT_WEB_REQUEST_RE = | |
| /\b(?:search|browse|look\s+up|check)\s+(?:(?:the|on)\s+)?(?:internet|web|online)\b|\b(?:internet|web|online)\s+search\b/i; | |
| const CURRENT_INFO_TOPIC_RE = | |
| /\b(?:latest|current|today(?:'s)?|recent|updated|new)\b.{0,80}\b(?:news|notices?|announcements?|results?|exam\s+dates?|(?:official|board|exam)\s+(?:timetables?|schedules?)|syllabus|prices?|rates?|status|weather|scores?|rankings?|releases?|deadlines?|admissions?|scholarships?|polic(?:y|ies)|rules?|guidelines?)\b|\b(?:news|notices?|announcements?|results?|exam\s+dates?|(?:official|board|exam)\s+(?:timetables?|schedules?)|syllabus|prices?|rates?|status|weather|scores?|rankings?|releases?|deadlines?|admissions?|scholarships?|polic(?:y|ies)|rules?|guidelines?)\b.{0,80}\b(?:today|latest|current|recent|updated)\b/i; | |
| const CURRENT_ROLE_RE = | |
| /\b(?:who\s+is|who's|name)\s+(?:the\s+)?(?:current|latest)\s+(?:president|prime\s+minister|minister|chief\s+minister|governor|chief\s+executive|ceo|chairperson|head)\b/i; | |
| const YEARLY_UPDATE_RE = | |
| /\b20\d{2}\s+(?:update|notice|announcement|result|syllabus|timetable|schedule)\b/i; | |
| function asksForLiveWebInformation(message: string, hasSource: boolean) { | |
| if (EXPLICIT_WEB_REQUEST_RE.test(message)) return true; | |
| if (hasSource) return false; | |
| return CURRENT_INFO_TOPIC_RE.test(message) || CURRENT_ROLE_RE.test(message) || YEARLY_UPDATE_RE.test(message); | |
| } | |
| const PLACEHOLDER_TOPIC_RE = | |
| /\b(topic\s+i\s+type|topic\s+you\s+type|topic\s+first|the\s+topic|break\s+it\s+into\s+scenes|matched\s+to\s+your\s+material)\b/i; | |
| // ββ Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function cleanTopicCandidate(value: string) { | |
| return value | |
| .replace(/[?.!,]+$/g, "") | |
| // strip leading request filler ("make a", "give me some", "please a", etc.) | |
| .replace(/^((?:please|make|create|generate|build|write|prepare|give\s+me|a|an|the|some)\s+)+/gi, "") | |
| .replace(/\b(fully|completely|properly|fast|quickly|from\s+scratch|from\s+zero)\b/gi, " ") | |
| .replace(/\b(?:in|within|by|before|for)\s+(?:one\s+night|tonight|tomorrow|the\s+exam|my\s+exam|exam)\b.*$/i, " ") | |
| .replace(/\b(?:one\s+night|overnight|tonight|tomorrow|cram|crash\s+course)\b.*$/i, " ") | |
| .replace(/\s+/g, " ") | |
| .trim(); | |
| } | |
| function extractTopic(message: string): string | null { | |
| const patterns: RegExp[] = [ | |
| // Tool requests with a preposition: "<tool words> ... on/for/about <topic>". | |
| // Runs first so "study video for calculus" β "calculus" (not "video for calculus"). | |
| /(?:notes?|quiz(?:zes)?|flash\s?cards?|memory\s+cards?|revision\s+cards?|video(?:\s+plan)?|answer|questions?|test|summary)\s+(?:me\s+)?(?:on|for|about|of)\s+(.+)/i, | |
| // Direct teach/learn verbs. | |
| /(?:i\s+(?:want|need|have)\s+to\s+learn\s+|i\s+must\s+learn\s+|help\s+me\s+(?:learn|understand)\s+|teach\s+(?:me\s+)?|explain\s+(?:me\s+)?|what\s+is\s+|what\s+are\s+|define\s+|tell\s+me\s+about\s+|about\s+|learn\s+|finish\s+|complete\s+|revise\s+|study\s+|summarize\s+|summarise\s+|prove\s+|derive\s+|derivation\s+of\s+)(.+)/i, | |
| // Trailing form: "<topic> notes/quiz/flashcards/...". | |
| /^(.+?)\s+(?:notes?|quiz|flash\s?cards?|explained?|summary|definition|meaning)\s*$/i, | |
| ]; | |
| const STOP_WORDS = new Set(["make", "create", "generate", "build", "write", "give", "me", "some", "a", "an", "study", "video", "memory", "card", "cards"]); | |
| for (const p of patterns) { | |
| const m = message.match(p); | |
| if (m?.[1]) { | |
| const topic = cleanTopicCandidate(m[1]); | |
| const lc = topic.toLowerCase(); | |
| if ( | |
| topic.length > 2 && | |
| topic.length < 120 && | |
| !STOP_WORDS.has(lc) && | |
| !/^(this|that|my|the|it|everything|all of it)$/i.test(topic) | |
| ) { | |
| return topic; | |
| } | |
| } | |
| } | |
| return null; | |
| } | |
| function isDocRef(message: string): boolean { | |
| return DOC_REF_RE.test(message); | |
| } | |
| function sourceTitleMentioned(message: string, selectedSources: readonly AnswerModeSource[]): boolean { | |
| const lower = message.toLowerCase(); | |
| return selectedSources.some((source) => { | |
| const title = (source.title || source.file_name || "") | |
| .toLowerCase() | |
| .replace(/\.[a-z0-9]+$/i, "") | |
| .trim(); | |
| return title.length >= 4 && lower.includes(title); | |
| }); | |
| } | |
| function toolTypeForMessage(lower: string): StudyIntent | null { | |
| if (PYQ_RE.test(lower)) return "analyze_pyq"; | |
| if (NOTES_RE.test(lower)) return "generate_notes"; | |
| if (QUIZ_RE.test(lower)) return "generate_quiz"; | |
| if (FLASHCARD_RE.test(lower)) return "generate_flashcards"; | |
| if (VIDEO_RE.test(lower)) return "create_video_plan"; | |
| if (STUDY_PLAN_RE.test(lower)) return "create_revision_plan"; | |
| if (EXAM_ANSWER_RE.test(lower)) return "generate_exam_answer"; | |
| return null; | |
| } | |
| function uploadPromptForTool(toolType: StudyIntent | null): string { | |
| if (toolType === "analyze_pyq" || toolType === "predict_questions") { | |
| return "Upload previous question papers first. I'll find repeated topics, marks weightage, trends, and likely questions from those papers."; | |
| } | |
| if (toolType === "generate_notes" || toolType === "summarize_source") { | |
| return "Tell me the topic, paste text, or upload a chapter. I can create scoring notes from any of those."; | |
| } | |
| if (toolType === "create_video_plan" || toolType === "render_video") { | |
| return "Tell me the topic first, or select a source if you want the video matched to your material."; | |
| } | |
| return "Tell me the topic, goal, or time left. Uploading is optional unless you want source-matched output."; | |
| } | |
| function mapAnswerModeToResponseMode(mode: AnswerMode): ResponseMode { | |
| switch (mode) { | |
| case "casual_chat": | |
| return "casual"; | |
| case "source_required_upload_prompt": | |
| return "needs_source"; | |
| case "source_grounded_answer": | |
| return "source_grounded"; | |
| case "tool_generation": | |
| return "tool_ready"; | |
| case "clarification": | |
| return "clarify"; | |
| case "web_search_answer": | |
| case "general_ai_answer": | |
| default: | |
| return "general_answer"; | |
| } | |
| } | |
| function promptForMode(result: AnswerModeResult): string | null { | |
| if (result.mode === "source_required_upload_prompt") { | |
| return result.userFacingNextStep; | |
| } | |
| return null; | |
| } | |
| export function detectAnswerMode({ | |
| message, | |
| selectedSources = [], | |
| activeTool = defaultStudyIntent, | |
| conversationContext = [], | |
| webSearchEnabled = false, | |
| }: { | |
| message: string; | |
| selectedSources?: readonly AnswerModeSource[]; | |
| activeTool?: StudyIntent; | |
| conversationContext?: readonly ChatMessage[]; | |
| webSearchEnabled?: boolean; | |
| }): AnswerModeResult { | |
| const trimmed = message.trim(); | |
| // Pure greeting ("hi", "hello bro") or "Hi, my name is X" stay casual. | |
| if (GREETING_RE.test(trimmed) || CASUAL_INTRO_RE.test(trimmed)) { | |
| return { | |
| mode: "casual_chat", | |
| intent: "casual", | |
| requiresSource: false, | |
| shouldUseWeb: false, | |
| canAnswerGenerally: false, | |
| topic: null, | |
| toolType: null, | |
| userFacingNextStep: null, | |
| }; | |
| } | |
| // "Hi teach me +2 matrix maths" β a greeting prefix in front of a REAL ask. | |
| // Strip the greeting and process the actual request, otherwise a friendly | |
| // "Hi" would downgrade a genuine study question to the casual welcome card. | |
| let working = trimmed; | |
| const greetingPrefixMatch = GREETING_PREFIX_RE.exec(trimmed); | |
| if (greetingPrefixMatch) { | |
| const rest = trimmed.slice(greetingPrefixMatch[0].length).trim(); | |
| if (rest.length >= 2 && !GREETING_RE.test(rest)) { | |
| working = rest; | |
| } | |
| } | |
| const lower = working.toLowerCase(); | |
| const hasSource = selectedSources.length > 0; | |
| const topic = extractTopic(working); | |
| const explicitSourceRequest = isDocRef(working) || sourceTitleMentioned(working, selectedSources); | |
| const toolType = toolTypeForMessage(lower); | |
| const asksForCurrentInfo = asksForLiveWebInformation(lower, hasSource); | |
| if (CAPABILITY_RE.test(working)) { | |
| return { | |
| mode: "casual_chat", | |
| intent: "help", | |
| requiresSource: false, | |
| shouldUseWeb: false, | |
| canAnswerGenerally: false, | |
| topic: null, | |
| toolType: null, | |
| userFacingNextStep: null, | |
| }; | |
| } | |
| if (UNCLEAR_RE.test(working)) { | |
| const lastAssistant = [...conversationContext].reverse().find( | |
| (m) => m.role === "assistant" && m.card && m.card.kind !== "loading" && m.card.kind !== "error", | |
| ); | |
| if (lastAssistant && /^(yes|yep|yeah|sure|do it|go ahead|continue|next|proceed)$/i.test(working)) { | |
| return { | |
| mode: "general_ai_answer", | |
| intent: "continuation", | |
| requiresSource: false, | |
| shouldUseWeb: false, | |
| canAnswerGenerally: true, | |
| topic: null, | |
| toolType: activeTool, | |
| userFacingNextStep: null, | |
| }; | |
| } | |
| if (/^(ok(?:ay)?|thanks?|thank you|cool|nice|got it|fine)$/i.test(working)) { | |
| return { | |
| mode: "casual_chat", | |
| intent: "casual", | |
| requiresSource: false, | |
| shouldUseWeb: false, | |
| canAnswerGenerally: false, | |
| topic: null, | |
| toolType: null, | |
| userFacingNextStep: null, | |
| }; | |
| } | |
| return { | |
| mode: "clarification", | |
| intent: "ambiguous", | |
| requiresSource: false, | |
| shouldUseWeb: false, | |
| canAnswerGenerally: false, | |
| topic: null, | |
| toolType: activeTool, | |
| userFacingNextStep: "Tell me the topic, or upload/select a source if you want textbook-matched output.", | |
| }; | |
| } | |
| if (explicitSourceRequest && !hasSource) { | |
| return { | |
| mode: "source_required_upload_prompt", | |
| intent: toolType ? "tool_request" : "source_specific", | |
| requiresSource: true, | |
| shouldUseWeb: false, | |
| canAnswerGenerally: false, | |
| topic, | |
| toolType, | |
| userFacingNextStep: toolType ? uploadPromptForTool(toolType) : "Upload/select the PDF or source first, then I'll explain it clearly.", | |
| }; | |
| } | |
| // A live/current-information request is an evidence route, not a study-plan | |
| // generator merely because the sentence also contains "timetable" or | |
| // "schedule". Put it ahead of tool detection so official notices and exam | |
| // dates reach web retrieval. Personal requests such as "update my plan" and | |
| // "what should I study today?" do not satisfy the narrow web predicate. | |
| if (asksForCurrentInfo) { | |
| return { | |
| mode: "web_search_answer", | |
| intent: "current_info", | |
| requiresSource: false, | |
| shouldUseWeb: webSearchEnabled, | |
| canAnswerGenerally: true, | |
| topic: topic ?? working.slice(0, 80), | |
| toolType: null, | |
| userFacingNextStep: webSearchEnabled ? null : "I can answer generally, but live internet search is not connected yet.", | |
| }; | |
| } | |
| if (toolType) { | |
| if (toolType === "create_video_plan" && topic && PLACEHOLDER_TOPIC_RE.test(topic) && !hasSource) { | |
| return { | |
| mode: "clarification", | |
| intent: "tool_request", | |
| requiresSource: false, | |
| shouldUseWeb: false, | |
| canAnswerGenerally: false, | |
| topic: null, | |
| toolType, | |
| userFacingNextStep: uploadPromptForTool(toolType), | |
| }; | |
| } | |
| // PYQ: always run the tool β it returns REAL downloadable Kerala previous | |
| // papers (no source needed). If a source IS attached it also analyzes it. | |
| if (toolType === "analyze_pyq" || toolType === "predict_questions") { | |
| return { | |
| mode: "tool_generation", | |
| intent: "tool_request", | |
| requiresSource: false, | |
| shouldUseWeb: false, | |
| canAnswerGenerally: true, | |
| topic, | |
| toolType, | |
| userFacingNextStep: null, | |
| }; | |
| } | |
| if (!topic && !hasSource && !asksForCurrentInfo) { | |
| if (toolType === "create_revision_plan") { | |
| return { | |
| mode: "tool_generation", | |
| intent: "tool_request", | |
| requiresSource: false, | |
| shouldUseWeb: false, | |
| canAnswerGenerally: true, | |
| topic: "your topic", | |
| toolType, | |
| userFacingNextStep: null, | |
| }; | |
| } | |
| return { | |
| mode: "clarification", | |
| intent: "tool_request", | |
| requiresSource: false, | |
| shouldUseWeb: false, | |
| canAnswerGenerally: false, | |
| topic: null, | |
| toolType, | |
| userFacingNextStep: "Tell me the topic first. You can type it, paste text, or upload a source if you want textbook-matched output.", | |
| }; | |
| } | |
| return { | |
| mode: "tool_generation", | |
| intent: "tool_request", | |
| requiresSource: explicitSourceRequest || (!topic && hasSource), | |
| shouldUseWeb: asksForCurrentInfo && webSearchEnabled, | |
| canAnswerGenerally: Boolean(topic) || !explicitSourceRequest, | |
| topic: topic ?? working.slice(0, 80), | |
| toolType, | |
| userFacingNextStep: asksForCurrentInfo && !webSearchEnabled | |
| ? "I can answer generally, but live internet search is not connected yet." | |
| : null, | |
| }; | |
| } | |
| if (explicitSourceRequest && hasSource) { | |
| return { | |
| mode: "source_grounded_answer", | |
| intent: "source_specific", | |
| requiresSource: true, | |
| shouldUseWeb: false, | |
| canAnswerGenerally: false, | |
| topic, | |
| toolType: null, | |
| userFacingNextStep: null, | |
| }; | |
| } | |
| return { | |
| mode: "general_ai_answer", | |
| intent: "general_topic", | |
| requiresSource: false, | |
| shouldUseWeb: false, | |
| canAnswerGenerally: true, | |
| topic: topic ?? working.slice(0, 80), | |
| toolType: activeTool === defaultStudyIntent ? "teach_topic" : activeTool, | |
| userFacingNextStep: null, | |
| }; | |
| } | |
| // ββ Main export βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function buildCasualReply( | |
| intent: string, | |
| selectedSources: ApiDocument[], | |
| message: string, | |
| ): string { | |
| const hasSource = selectedSources.length > 0; | |
| const sourceHint = hasSource | |
| ? `You have ${selectedSources.length} source${selectedSources.length > 1 ? "s" : ""} ready.` | |
| : "Upload a textbook, notes, or question paper, or just ask me any topic."; | |
| // Identity / name question | |
| if ( | |
| /\b(what(?:'s|\s+is)\s+your\s+name|who\s+are\s+you|are\s+you\s+(an?\s+)?(ai|bot|chatbot|robot)|what\s+are\s+you)\b/i.test(message) | |
| ) { | |
| return `I'm **DocDoe Knowledge Brain** β an exam intelligence system for Indian board and competitive exams.\n\nI retrieve from your uploaded sources, apply mark-scheme logic, and generate explanations, notes, quizzes, flashcards, PYQ analysis, study paths, and video plans.\n\n${sourceHint}`; | |
| } | |
| // General greeting | |
| if (/^\s*(hi+|hey+|hello+|yo+|sup|bro|howdy|hola|ready|start)\b/i.test(message)) { | |
| return hasSource | |
| ? `${selectedSources.length} source${selectedSources.length > 1 ? "s are" : " is"} active. Ask a question, or generate notes, a quiz, flashcards, a revision plan, or a video plan from your material.` | |
| : "DocDoe Knowledge Brain is ready. Ask any topic for a grounded explanation, or upload a textbook, notes, or previous paper for source-matched exam output."; | |
| } | |
| // Capability / help | |
| if (intent === "help") { | |
| return `DocDoe provides seven study workflows: chapter explanations, scoring notes, practice quizzes, flashcards, PYQ pattern analysis, revision plans, and study video plans.\n\nAttach a source for textbook-matched answers, or ask a topic for general exam intelligence.`; | |
| } | |
| // Generic acknowledgement (ok, thanks, etc.) | |
| return `Understood. What should we work on next β explain a topic, generate notes, run a quiz, or attach a source?`; | |
| } | |
| export function detectStudyChatIntent( | |
| message: string, | |
| selectedSources: ApiDocument[], | |
| activeTool: StudyIntent, | |
| conversationContext: ChatMessage[], | |
| ): ChatIntentResult { | |
| const detection = detectAnswerMode({ | |
| message, | |
| selectedSources, | |
| activeTool, | |
| conversationContext, | |
| webSearchEnabled: false, | |
| }); | |
| const responseMode = mapAnswerModeToResponseMode(detection.mode); | |
| // For casual mode: always build an immediateReply so the welcome card renders | |
| // without hitting the backend. For source prompts, use the structured CTA. | |
| let immediateReply: string | null = null; | |
| if (detection.mode === "casual_chat") { | |
| immediateReply = buildCasualReply(detection.intent, selectedSources, message); | |
| } else { | |
| immediateReply = promptForMode(detection); | |
| } | |
| // When no tool is detected, a fresh general/source answer must NOT inherit a | |
| // stale tool from the previous turn (e.g. asking a topic right after a PYQ | |
| // request). Default to teach_topic / explain_source instead of activeTool. | |
| const intent = | |
| detection.toolType ?? | |
| (detection.mode === "source_grounded_answer" || detection.mode === "source_required_upload_prompt" | |
| ? "explain_source" | |
| : "teach_topic"); | |
| return { | |
| intent, | |
| responseMode, | |
| answerMode: detection.mode, | |
| shouldUseWeb: detection.shouldUseWeb, | |
| userFacingNextStep: detection.userFacingNextStep, | |
| requiresSource: detection.requiresSource, | |
| canAnswerGenerally: detection.canAnswerGenerally, | |
| immediateReply, | |
| topic: detection.topic, | |
| }; | |
| } | |