Spaces:
Sleeping
Sleeping
| // 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<ApiLoginResp> { | |
| 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<ApiChatResp> { | |
| 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<ApiQuizStartResp> { | |
| 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<ApiUploadResp> { | |
| 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 }; | |
| } | |