// 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" | ... }; export type ApiChatRef = { source_file?: string; section?: string }; export type ApiChatResp = { reply: string; session_status_md: string; refs: ApiChatRef[]; latency_ms: number; // ✅ NEW: 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? } return data as ApiChatResp; } // -------------------- // /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: ApiChatRef[]; latency_ms: number; // ✅ NEW: optional tracing run id returned by backend (if enabled) 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})`)); return data as ApiQuizStartResp; } // -------------------- // /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"; // ✅ NEW: 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 }; }