SarahXia0405's picture
Update web/src/lib/api.ts
8c39f5c verified
// 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 };
}