// web/src/lib/api.ts // Aligns with api/server.py routes: // POST /api/login, /api/chat, /api/upload, /api/export, /api/summary, /api/feedback // GET /api/memoryline export type LearningMode = | "general" | "concept" | "socratic" | "exam" | "assignment" | "summary"; export type LanguagePref = "Auto" | "English" | "中文"; export type DocType = | "Syllabus" | "Lecture Slides / PPT" | "Literature Review / Paper" | "Other Course Document"; const DEFAULT_TIMEOUT_MS = 20000; function getBaseUrl() { // Vite env: VITE_API_BASE can be "", "http://localhost:8000", etc. const v = (import.meta as any)?.env?.VITE_API_BASE as string | undefined; return v && v.trim() ? v.trim() : ""; } async function fetchWithTimeout( input: RequestInfo, init?: RequestInit, timeoutMs = DEFAULT_TIMEOUT_MS ) { const controller = new AbortController(); const id = setTimeout(() => controller.abort(), timeoutMs); try { return await fetch(input, { ...init, signal: controller.signal }); } finally { clearTimeout(id); } } async function parseJsonSafe(res: Response) { const text = await res.text(); try { return text ? JSON.parse(text) : null; } catch { return { _raw: text }; } } function errMsg(data: any, fallback: string) { return data && (data.error || data.detail || data.message) ? String(data.error || data.detail || data.message) : fallback; } // -------------------- // /api/login // -------------------- export type ApiLoginReq = { name: string; user_id: string; }; export type ApiLoginResp = | { ok: true; user: { name: string; user_id: string } } | { ok: false; error: string }; export async function apiLogin(payload: ApiLoginReq): Promise { const base = getBaseUrl(); const res = await fetchWithTimeout(`${base}/api/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); const data = await parseJsonSafe(res); if (!res.ok) throw new Error(errMsg(data, `apiLogin failed (${res.status})`)); return data as ApiLoginResp; } // -------------------- // /api/chat // -------------------- export type ApiChatReq = { user_id: string; message: string; learning_mode: string; // backend expects string (not strict union) language_preference?: string; // "Auto" | "English" | "中文" doc_type?: string; // "Syllabus" | "Lecture Slides / PPT" | ... }; // ✅ allow backend to return either object refs or preformatted string refs export type ApiChatRefObj = { source_file?: string; section?: string }; export type ApiChatRefRaw = ApiChatRefObj | string; // ✅ normalize ANY ref format into {source_file, section} so App can map reliably function normalizeRefs(raw: any): ApiChatRefObj[] { const arr: any[] = Array.isArray(raw) ? raw : []; return arr .map((x) => { // Case A: already object if (x && typeof x === "object" && !Array.isArray(x)) { const a = x.source_file != null ? String(x.source_file) : ""; const b = x.section != null ? String(x.section) : ""; return { source_file: a || undefined, section: b || undefined }; } // Case B: string like "file.pdf — p3#1" (or just "file.pdf") if (typeof x === "string") { const s = x.trim(); if (!s) return null; const parts = s.split("—").map((p) => p.trim()).filter(Boolean); if (parts.length >= 2) { return { source_file: parts[0], section: parts.slice(1).join(" — ") }; } return { source_file: s, section: undefined }; } return null; }) .filter(Boolean) as ApiChatRefObj[]; } export type ApiChatResp = { reply: string; session_status_md: string; // ✅ after normalization, always object array refs: ApiChatRefObj[]; latency_ms: number; // optional tracing run id returned by backend run_id?: string | null; }; export async function apiChat(payload: ApiChatReq): Promise { const base = getBaseUrl(); const res = await fetchWithTimeout( `${base}/api/chat`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ language_preference: "Auto", doc_type: "Syllabus", ...payload, }), }, 60000 // chat can be slow ); const data = await parseJsonSafe(res); if (!res.ok) throw new Error(errMsg(data, `apiChat failed (${res.status})`)); // backend returns { reply, session_status_md, refs, latency_ms, run_id? } // but refs may be ApiChatRefObj[] OR string[] const out = data as any; return { reply: String(out?.reply ?? ""), session_status_md: String(out?.session_status_md ?? ""), refs: normalizeRefs(out?.refs ?? out?.references), latency_ms: Number(out?.latency_ms ?? 0), run_id: out?.run_id ?? null, }; } // -------------------- // /api/quiz/start // -------------------- export type ApiQuizStartReq = { user_id: string; language_preference?: string; // "Auto" | "English" | "中文" doc_type?: string; // default: "Literature Review / Paper" (backend default ok) learning_mode?: string; // default: "quiz" }; export type ApiQuizStartResp = { reply: string; session_status_md: string; refs: ApiChatRefObj[]; latency_ms: number; run_id?: string | null; }; export async function apiQuizStart( payload: ApiQuizStartReq ): Promise { const base = getBaseUrl(); const res = await fetchWithTimeout( `${base}/api/quiz/start`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ language_preference: "Auto", doc_type: "Literature Review / Paper", learning_mode: "quiz", ...payload, }), }, 60000 ); const data = await parseJsonSafe(res); if (!res.ok) throw new Error(errMsg(data, `apiQuizStart failed (${res.status})`)); const out = data as any; return { reply: String(out?.reply ?? ""), session_status_md: String(out?.session_status_md ?? ""), refs: normalizeRefs(out?.refs ?? out?.references), latency_ms: Number(out?.latency_ms ?? 0), run_id: out?.run_id ?? null, }; } // -------------------- // /api/upload // -------------------- export type ApiUploadResp = { ok: boolean; added_chunks?: number; status_md?: string; error?: string; }; export async function apiUpload(args: { user_id: string; doc_type: string; file: File; }): Promise { const base = getBaseUrl(); const fd = new FormData(); fd.append("user_id", args.user_id); fd.append("doc_type", args.doc_type); fd.append("file", args.file); const res = await fetchWithTimeout( `${base}/api/upload`, { method: "POST", body: fd }, 120000 ); const data = await parseJsonSafe(res); if (!res.ok) throw new Error(errMsg(data, `apiUpload failed (${res.status})`)); return data as ApiUploadResp; } // -------------------- // /api/export // -------------------- export async function apiExport(payload: { user_id: string; learning_mode: string; }): Promise<{ markdown: string }> { const base = getBaseUrl(); const res = await fetchWithTimeout(`${base}/api/export`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); const data = await parseJsonSafe(res); if (!res.ok) throw new Error(errMsg(data, `apiExport failed (${res.status})`)); return data as { markdown: string }; } // -------------------- // /api/summary // -------------------- export async function apiSummary(payload: { user_id: string; learning_mode: string; language_preference?: string; }): Promise<{ markdown: string }> { const base = getBaseUrl(); const res = await fetchWithTimeout(`${base}/api/summary`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ language_preference: "Auto", ...payload }), }); const data = await parseJsonSafe(res); if (!res.ok) throw new Error(errMsg(data, `apiSummary failed (${res.status})`)); return data as { markdown: string }; } // -------------------- // /api/feedback // -------------------- export type ApiFeedbackReq = { user_id: string; rating: "helpful" | "not_helpful"; // run id so backend can attach feedback to tracing run run_id?: string | null; assistant_message_id?: string; assistant_text: string; user_text?: string; comment?: string; tags?: string[]; refs?: string[]; learning_mode?: string; doc_type?: string; timestamp_ms?: number; }; export async function apiFeedback( payload: ApiFeedbackReq ): Promise<{ ok: boolean }> { const base = getBaseUrl(); const res = await fetchWithTimeout(`${base}/api/feedback`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); const data = await parseJsonSafe(res); if (!res.ok) throw new Error(errMsg(data, `apiFeedback failed (${res.status})`)); return data as { ok: boolean }; } // -------------------- // /api/memoryline // -------------------- export async function apiMemoryline( user_id: string ): Promise<{ next_review_label: string; progress_pct: number }> { const base = getBaseUrl(); const res = await fetchWithTimeout( `${base}/api/memoryline?user_id=${encodeURIComponent(user_id)}`, { method: "GET" } ); const data = await parseJsonSafe(res); if (!res.ok) throw new Error(errMsg(data, `apiMemoryline failed (${res.status})`)); return data as { next_review_label: string; progress_pct: number }; }