File size: 4,935 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
import { askDocDoe } from "@/lib/api/brain";
import { getPublicDemoMode } from "@/lib/env";
import { hasLiveSession } from "@/lib/api/mode";
import type { BrainAskMode, BrainAskResponse, DocumentChunk, StudentLevel } from "@/lib/api/types";

export type DocDoeAnswer = {
  title: string;
  summary: string;
  points: string[];
  examTip: string;
  citations: string[];
  evidenceLabel?: string | null;
};

type AskDocDoeInput = {
  documentId: string;
  sourceTitle: string;
  sourceIds?: string[];
  sourceTitles?: string[];
  question: string;
  language?: string;
  level?: StudentLevel;
  mode?: string;
};

function createFallbackAnswer(input: AskDocDoeInput, chunks: DocumentChunk[]): DocDoeAnswer {
  const bestChunk = chunks[0]?.chunk_text;
  return {
    title: `Study insight: ${input.question.slice(0, 42)}${input.question.length > 42 ? "..." : ""}`,
    summary: bestChunk
      ? `From ${input.sourceTitle}: ${bestChunk}`
      : `Choose a ready source and DocDoe will answer from your material. For now, focus on the core idea and turn it into short exam points.`,
    points: chunks.slice(0, 3).map((chunk) => chunk.heading || chunk.chunk_text).filter(Boolean),
    examTip: "Exam tip: write the definition first, then add keywords, then finish with one clean example or diagram if needed.",
    citations: chunks.length > 0 ? chunks.map((chunk) => chunk.heading || input.sourceTitle).slice(0, 3) : [input.sourceTitle],
  };
}

function isExamPrompt(question: string) {
  const text = question.toLowerCase();
  return text.includes("mark") || text.includes("exam") || text.includes("answer") || text.includes("pyq");
}

function askModeFromPrompt(question: string, explicitMode?: string): BrainAskMode {
  if (explicitMode === "explain_simple" || explicitMode === "exam_answer" || explicitMode === "pyq_pattern" || explicitMode === "summarize" || explicitMode === "diagram_help") {
    return explicitMode;
  }
  const text = question.toLowerCase();
  if (text.includes("pyq") || text.includes("pattern")) return "pyq_pattern";
  if (isExamPrompt(question)) return "exam_answer";
  if (text.includes("summar") || text.includes("notes") || text.includes("points")) return "summarize";
  if (text.includes("diagram")) return "diagram_help";
  return "explain_simple";
}

function brainAnswerToDocDoeAnswer(input: AskDocDoeInput, response: BrainAskResponse): DocDoeAnswer {
  const sectionPoints = response.sections
    .flatMap((section) => section.body.split(/\n+/).map((line) => line.replace(/^[-*\d.\s]+/, "").trim()))
    .filter(Boolean);
  const sourceTitleById = new Map<string, string>();
  input.sourceIds?.forEach((id, index) => {
    const title = input.sourceTitles?.[index];
    if (title) sourceTitleById.set(id, title);
  });
  const citations = response.citations
    .map((citation) => citation.source_title || (citation.source_id ? sourceTitleById.get(citation.source_id) : null) || citation.heading || citation.snippet)
    .filter((citation): citation is string => Boolean(citation));

  return {
    title: response.sections[0]?.title || `DocDoe answer: ${input.question.slice(0, 42)}${input.question.length > 42 ? "..." : ""}`,
    summary: response.answer,
    points: sectionPoints.length > 0 ? sectionPoints.slice(0, 6) : response.suggested_actions.slice(0, 4),
    examTip: response.trust_note || "Use keywords from your source and keep the answer point-wise.",
    citations: citations.length > 0 ? Array.from(new Set(citations)).slice(0, 6) : input.sourceTitles?.slice(0, 5) ?? [input.sourceTitle],
    evidenceLabel: response.evidence_label ?? null,
  };
}

export async function askDocDoeFromSource(input: AskDocDoeInput): Promise<DocDoeAnswer> {
  const sourceIds = input.sourceIds?.length ? input.sourceIds : [input.documentId];
  if (getPublicDemoMode() === "live" && !hasLiveSession()) {
    // Only block if no session at all — local sources still work via fallback
    if (sourceIds.length === 0) {
      throw new Error("Sign in to use real DocDoe answers.");
    }
  }

  if (input.question.toLowerCase().includes("pyq") && !input.sourceTitle.toLowerCase().includes("pyq")) {
    const fallback = createFallbackAnswer(input, []);
    return {
      ...fallback,
      title: `Likely questions from this material`,
      summary: "No question papers uploaded yet. Here are exam-style questions from your uploaded material instead.",
      points: [
        "Upload a previous question paper to unlock PYQ pattern analysis.",
        ...fallback.points.slice(0, 3),
      ],
      evidenceLabel: "Based on uploaded material only",
    };
  }

  const response = await askDocDoe({
    question: input.question,
    mode: askModeFromPrompt(input.question, input.mode),
    source_id: input.documentId,
    source_ids: sourceIds,
    level: input.level ?? "intermediate",
    language_preference: input.language ?? "English",
  });
  return brainAnswerToDocDoeAnswer(input, response);
}