Download src/lib/api/chat.ts from asnannp/DocDoeAI: direct link, hf CLI and curl.
- Browser
- Download file 8.19 kB
-
https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/lib/api/chat.ts
- Command line
-
hf download hf://spaces/asnannp/DocDoeAI/src/lib/api/chat.ts
-
curl -L -o chat.ts https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/lib/api/chat.ts
8.19 kB
| import { apiPost, getApiBaseUrl } from "@/lib/api/client"; | |
| import { getAuthToken } from "@/lib/auth/auth-client"; | |
| export const CHAT_ORIGIN_PAGES = [ | |
| "home", | |
| "tuition", | |
| "learn-anything", | |
| "study-chat", | |
| "classes", | |
| "notes", | |
| "quizzes", | |
| "pyq-analyzer", | |
| "progress", | |
| "bookmarks", | |
| "notifications", | |
| "settings", | |
| "workspace", | |
| ] as const; | |
| export type ChatOriginPage = (typeof CHAT_ORIGIN_PAGES)[number]; | |
| export type StudyChatAcademicContext = { | |
| origin?: string | null; | |
| subject?: string | null; | |
| chapter?: string | null; | |
| topic?: string | null; | |
| objective?: string | null; | |
| current_step?: string | null; | |
| relevant_context?: string | null; | |
| source_id?: string | null; | |
| source_ref?: string | null; | |
| source_label?: string | null; | |
| activity_type?: string | null; | |
| question_id?: string | null; | |
| question_label?: string | null; | |
| return_href?: string | null; | |
| captured_at?: string | null; | |
| }; | |
| export function isChatOriginPage(value: string | null): value is ChatOriginPage { | |
| return CHAT_ORIGIN_PAGES.some((page) => page === value); | |
| } | |
| export type ChatRequest = { | |
| message: string; | |
| intent?: string; | |
| source_ids?: string[]; | |
| subject?: string; | |
| language?: string; | |
| origin_page?: ChatOriginPage; | |
| academic_context?: StudyChatAcademicContext; | |
| mode?: StudyChatMode; | |
| correction_question?: string; | |
| student_answer?: string; | |
| marks?: number; | |
| session_id?: string; | |
| idempotency_key?: string; | |
| }; | |
| export type StudyChatMode = | |
| | "general_explanation" | |
| | "selected_sources" | |
| | "pyq_verified" | |
| | "pyq_style" | |
| | "answer_correction"; | |
| export type SourceCitation = { | |
| source_id: string; | |
| source_title: string; | |
| source_version: string; | |
| chunk_id: string; | |
| chunk_index: number; | |
| page_number?: number | null; | |
| heading?: string | null; | |
| excerpt: string; | |
| confidence: number; | |
| availability?: "live" | "snapshot"; | |
| }; | |
| export type VerifiedPyqAnalysis = { | |
| available: boolean; | |
| status: "available" | "unavailable" | "awaiting_verification"; | |
| message: string; | |
| disclaimer: string; | |
| repeated_topics: Array<{ topic: string; chapter: string; count: number; years: number[] }>; | |
| marks_distribution: Array<{ marks: number; count: number; percentage: number }>; | |
| data_coverage: { papers: number; questions: number; years: number[] }; | |
| source_count: number; | |
| }; | |
| export type StudyChatEvidence = { | |
| mode: StudyChatMode; | |
| trust_state: | |
| | "general_knowledge" | |
| | "source_grounded" | |
| | "verified_pyq" | |
| | "pyq_style_only" | |
| | "deterministic_correction" | |
| | "web_grounded" | |
| | "web_unavailable"; | |
| evidence_scope: string; | |
| source_ids: string[]; | |
| citations: SourceCitation[]; | |
| pyq_classification: "verified_pyq" | "pyq_style" | "not_pyq"; | |
| pyq_analysis?: VerifiedPyqAnalysis | null; | |
| }; | |
| export type WebSource = { | |
| title: string; | |
| url: string; | |
| publisher: string; | |
| published_date?: string | null; | |
| author?: string | null; | |
| snippet?: string | null; | |
| is_official: boolean; | |
| }; | |
| export type ChatApiResponse = { | |
| answer: string; | |
| intent_detected: string; | |
| model_used: string; | |
| evidence_label?: string | null; | |
| web_sources: WebSource[]; | |
| evidence: StudyChatEvidence; | |
| correction?: Record<string, unknown> | null; | |
| }; | |
| export async function sendChatMessage( | |
| payload: ChatRequest, | |
| options?: { signal?: AbortSignal; timeoutMs?: number }, | |
| ): Promise<ChatApiResponse> { | |
| return apiPost<ChatApiResponse, ChatRequest>("/chat", payload, { | |
| signal: options?.signal, | |
| timeoutMs: options?.timeoutMs ?? 25000, | |
| }); | |
| } | |
| /** | |
| * Stream chat tokens from DocDoe via SSE. | |
| * Calls onDelta for each token, onDone when stream ends, onError on failure. | |
| */ | |
| export async function streamChatMessage( | |
| payload: ChatRequest, | |
| callbacks: { | |
| onDelta: (token: string) => void; | |
| onDone: (meta: { | |
| intent: string; | |
| model: string; | |
| evidenceLabel: string | null; | |
| webSources: WebSource[]; | |
| evidence: StudyChatEvidence; | |
| correction: Record<string, unknown> | null; | |
| }) => void; | |
| onError: (message: string) => void; | |
| }, | |
| signal?: AbortSignal, | |
| ): Promise<void> { | |
| const authToken = getAuthToken(); | |
| const streamSignal = AbortSignal.any([ | |
| AbortSignal.timeout(120_000), ...(signal ? [signal] : []), | |
| ]); | |
| let response: Response; | |
| try { | |
| response = await fetch(`${getApiBaseUrl()}/chat/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("DocDoe backend is offline or unreachable."); | |
| return; | |
| } | |
| if (!response.ok) { | |
| // Forward the real backend reason so quota/limit/auth errors are classified | |
| // correctly downstream instead of all looking like "AI service offline". | |
| let detail = ""; | |
| try { | |
| const body = (await response.clone().json()) as { error?: { message?: string }; detail?: unknown; message?: string }; | |
| detail = | |
| body?.error?.message || | |
| (typeof body?.detail === "string" ? body.detail : "") || | |
| (body?.detail && typeof body.detail === "object" && "message" in body.detail | |
| ? String((body.detail as { message?: unknown }).message ?? "") | |
| : "") || | |
| body?.message || | |
| ""; | |
| } catch { | |
| detail = ""; | |
| } | |
| if (response.status === 402) { | |
| callbacks.onError(detail || "You've reached your plan limit. Try again later or upgrade."); | |
| } else if (response.status === 429) { | |
| callbacks.onError("Too many requests. Wait a few seconds and try again."); | |
| } else if (response.status === 401 || response.status === 403) { | |
| callbacks.onError("Your session expired. Please sign in again to continue."); | |
| } else { | |
| callbacks.onError(detail || "DocDoe could not reach the AI service. Please try again."); | |
| } | |
| return; | |
| } | |
| const reader = response.body?.getReader(); | |
| if (!reader) { | |
| callbacks.onError("Streaming not supported. Please try again."); | |
| 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 event = JSON.parse(raw) as Record<string, unknown>; | |
| if (event.error) { | |
| callbacks.onError(String(event.error)); | |
| return; | |
| } | |
| if (event.delta) { | |
| callbacks.onDelta(String(event.delta)); | |
| } | |
| if (event.done) { | |
| callbacks.onDone({ | |
| intent: String(event.intent ?? ""), | |
| model: String(event.model ?? ""), | |
| evidenceLabel: | |
| typeof event.evidence_label === "string" | |
| ? event.evidence_label | |
| : null, | |
| webSources: Array.isArray(event.web_sources) | |
| ? (event.web_sources as WebSource[]) | |
| : [], | |
| evidence: event.evidence as StudyChatEvidence, | |
| correction: | |
| event.correction && typeof event.correction === "object" | |
| ? (event.correction as Record<string, unknown>) | |
| : null, | |
| }); | |
| return; | |
| } | |
| } catch { | |
| // malformed SSE line — skip | |
| } | |
| } | |
| } | |
| } catch { | |
| if (streamSignal.aborted) { | |
| callbacks.onError(signal?.aborted ? "Response stopped." : "The response timed out. Please retry."); | |
| return; | |
| } | |
| callbacks.onError("Connection lost. Please try again."); | |
| } finally { | |
| await reader.cancel().catch(() => undefined); | |
| reader.releaseLock(); | |
| } | |
| } | |