// 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 }; } // -------------------- // /api/tts (text-to-speech) – returns audio/mpeg // -------------------- export async function apiTts(payload: { user_id: string; text: string; voice?: string; }): Promise { const base = getBaseUrl(); const res = await fetchWithTimeout( `${base}/api/tts`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ voice: "nova", ...payload }), }, 60000 ); if (!res.ok) { const data = await parseJsonSafe(res); throw new Error(errMsg(data, `TTS failed (${res.status})`)); } return res.blob(); } // -------------------- // /api/podcast – returns audio/mpeg // -------------------- export async function apiPodcast(payload: { user_id: string; source: "summary" | "conversation"; voice?: string; }): Promise { const base = getBaseUrl(); const res = await fetchWithTimeout( `${base}/api/podcast`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ voice: "nova", ...payload }), }, 120000 ); if (!res.ok) { const data = await parseJsonSafe(res); throw new Error(errMsg(data, `Podcast failed (${res.status})`)); } return res.blob(); } // -------------------- // 教师 Agent API(AI 智能建课) // -------------------- const TEACHER_TIMEOUT_MS = 90000; export type TeacherStatus = { weaviate_configured: boolean; features: string[]; }; export async function apiTeacherStatus(): Promise { const base = getBaseUrl(); const res = await fetchWithTimeout(`${base}/api/teacher/status`, {}, 10000); const data = await parseJsonSafe(res); if (!res.ok) throw new Error(errMsg(data, "Teacher status failed")); return data as TeacherStatus; } export async function apiTeacherCourseDescription(payload: { topic: string; outline_hint?: string | null; reply_language?: string | null; history?: Array<[string, string]> | null; userMessage?: string | null; }): Promise<{ description: string; weaviate_used: boolean }> { const base = getBaseUrl(); const res = await fetchWithTimeout( `${base}/api/teacher/course-description`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }, TEACHER_TIMEOUT_MS ); const data = await parseJsonSafe(res); if (!res.ok) throw new Error(errMsg(data, "Course description failed")); return data as { description: string; weaviate_used: boolean }; } export async function apiTeacherDocSuggestion(payload: { topic: string; current_doc_excerpt?: string | null; doc_type?: string; reply_language?: string | null; history?: Array<[string, string]> | null; }): Promise<{ suggestion: string; weaviate_used: boolean }> { const base = getBaseUrl(); const res = await fetchWithTimeout( `${base}/api/teacher/doc-suggestion`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }, TEACHER_TIMEOUT_MS ); const data = await parseJsonSafe(res); if (!res.ok) throw new Error(errMsg(data, "Doc suggestion failed")); return data as { suggestion: string; weaviate_used: boolean }; } export async function apiTeacherAssignmentQuestions(payload: { topic: string; week_or_module?: string | null; question_type?: string; reply_language?: string | null; history?: Array<[string, string]> | null; }): Promise<{ suggestion: string; weaviate_used: boolean }> { const base = getBaseUrl(); const res = await fetchWithTimeout( `${base}/api/teacher/assignment-questions`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }, TEACHER_TIMEOUT_MS ); const data = await parseJsonSafe(res); if (!res.ok) throw new Error(errMsg(data, "Assignment questions failed")); return data as { suggestion: string; weaviate_used: boolean }; } export async function apiTeacherAssessmentAnalysis(payload: { assessment_summary: string; course_topic_hint?: string | null; reply_language?: string | null; history?: Array<[string, string]> | null; }): Promise<{ analysis: string; weaviate_used: boolean }> { const base = getBaseUrl(); const res = await fetchWithTimeout( `${base}/api/teacher/assessment-analysis`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }, TEACHER_TIMEOUT_MS ); const data = await parseJsonSafe(res); if (!res.ok) throw new Error(errMsg(data, "Assessment analysis failed")); return data as { analysis: string; weaviate_used: boolean }; } // -------------------- Courseware (vision, activities, copilot, qa-optimize, content) -------------------- export async function apiCoursewareVision(payload: { course_info: string; syllabus: string; history?: Array<[string, string]> | null; }): Promise<{ content: string; weaviate_used: boolean }> { const base = getBaseUrl(); const res = await fetchWithTimeout( `${base}/api/courseware/vision`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }, TEACHER_TIMEOUT_MS ); const data = await parseJsonSafe(res); if (!res.ok) throw new Error(errMsg(data, "Course vision failed")); return data as { content: string; weaviate_used: boolean }; } export async function apiCoursewareActivities(payload: { topic: string; learning_objectives?: string | null; rag_context_override?: string | null; history?: Array<[string, string]> | null; }): Promise<{ content: string; weaviate_used: boolean }> { const base = getBaseUrl(); const res = await fetchWithTimeout( `${base}/api/courseware/activities`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }, TEACHER_TIMEOUT_MS ); const data = await parseJsonSafe(res); if (!res.ok) throw new Error(errMsg(data, "Activities failed")); return data as { content: string; weaviate_used: boolean }; } export async function apiCoursewareCopilot(payload: { current_content: string; student_profiles?: Array<{ name?: string; progress?: string; behavior?: string }> | null; history?: Array<[string, string]> | null; }): Promise<{ content: string; weaviate_used: boolean }> { const base = getBaseUrl(); const res = await fetchWithTimeout( `${base}/api/courseware/copilot`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }, TEACHER_TIMEOUT_MS ); const data = await parseJsonSafe(res); if (!res.ok) throw new Error(errMsg(data, "Copilot failed")); return data as { content: string; weaviate_used: boolean }; } export async function apiCoursewareQAOptimize(payload: { quiz_summary: string; course_topic?: string | null; history?: Array<[string, string]> | null; }): Promise<{ content: string; weaviate_used: boolean }> { const base = getBaseUrl(); const res = await fetchWithTimeout( `${base}/api/courseware/qa-optimize`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }, TEACHER_TIMEOUT_MS ); const data = await parseJsonSafe(res); if (!res.ok) throw new Error(errMsg(data, "QA optimize failed")); return data as { content: string; weaviate_used: boolean }; } export async function apiCoursewareContent(payload: { topic: string; duration?: string | null; outline_points?: string | null; history?: Array<[string, string]> | null; }): Promise<{ content: string; weaviate_used: boolean }> { const base = getBaseUrl(); const res = await fetchWithTimeout( `${base}/api/courseware/content`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }, TEACHER_TIMEOUT_MS ); const data = await parseJsonSafe(res); if (!res.ok) throw new Error(errMsg(data, "Content generation failed")); return data as { content: string; weaviate_used: boolean }; } // -------------------- AI Courseware structured APIs (Plan / Prepare / Reflect / Improve) -------------------- export type AiMeta = { model: string; model_version?: string | null; prompt_version: string; temperature: number; tokens_used: number; latency_ms: number; }; // 2.1 Generate Syllabus Preview export type AiSyllabusContext = { courseName: string; learningOutcome: string; studentLevel: string; teachingFocus: string; courseLength: number; }; export type AiSyllabusGenerateReq = { requestId: string; context: AiSyllabusContext; }; export type AiWeekSyllabus = { weekNumber: number; title: string; learningObjectives: string[]; topics: string[]; }; export type AiSyllabusGenerateResp = { data: { syllabus: AiWeekSyllabus[] }; meta: AiMeta; }; export async function apiAiSyllabusGenerate( payload: AiSyllabusGenerateReq ): Promise { const base = getBaseUrl(); const res = await fetchWithTimeout( `${base}/ai/courseware/syllabus/generate`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }, TEACHER_TIMEOUT_MS ); const data = await parseJsonSafe(res); if (!res.ok) throw new Error(errMsg(data, "AI syllabus generate failed")); return data as AiSyllabusGenerateResp; } // 2.2 Generate Lesson Flow export type AiModuleContext = { title: string; learningObjectives: string[]; topics: string[]; durationMinutes: number; }; export type AiFlowGenerateReq = { requestId: string; moduleContext: AiModuleContext; systemPrompts?: string[] | null; }; export type AiLessonStep = { type: string; title: string; estimated_duration: number; ai_understanding: string; }; export type AiFlowGenerateResp = { data: { steps: AiLessonStep[] }; meta: AiMeta; }; export async function apiAiFlowGenerate( payload: AiFlowGenerateReq ): Promise { const base = getBaseUrl(); const res = await fetchWithTimeout( `${base}/ai/courseware/flow/generate`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }, TEACHER_TIMEOUT_MS ); const data = await parseJsonSafe(res); if (!res.ok) throw new Error(errMsg(data, "AI flow generate failed")); return data as AiFlowGenerateResp; } // 2.3 Regenerate Partial Flow export type AiSimpleStep = { id: string; title: string; duration: number; }; export type AiCurrentFlow = { lockedSteps: AiSimpleStep[]; unlockedSteps: AiSimpleStep[]; }; export type AiFlowPartialReq = { requestId: string; prompt: string; currentFlow: AiCurrentFlow; }; export type AiCopilotProposedStep = { type: string; title: string; estimated_duration: number; ai_understanding: string; }; export type AiFlowPartialResp = { data: { explanation: string; proposedSteps: AiCopilotProposedStep[]; }; meta: AiMeta; }; export async function apiAiFlowRegeneratePartial( payload: AiFlowPartialReq ): Promise { const base = getBaseUrl(); const res = await fetchWithTimeout( `${base}/ai/courseware/flow/regenerate-partial`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }, TEACHER_TIMEOUT_MS ); const data = await parseJsonSafe(res); if (!res.ok) throw new Error(errMsg(data, "AI flow partial regenerate failed")); return data as AiFlowPartialResp; } // 2.4 Generate/Polish Lesson Plan Detail export type AiPlanDetailReq = { requestId: string; finalizedSteps: any[]; }; export type AiLessonSection = { section_id: string; type: string; content: string; }; export type AiPlanDetailResp = { data: { sections: AiLessonSection[] }; meta: AiMeta; }; export async function apiAiPlanDetailGenerate( payload: AiPlanDetailReq ): Promise { const base = getBaseUrl(); const res = await fetchWithTimeout( `${base}/ai/courseware/plan/detail/generate`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }, TEACHER_TIMEOUT_MS ); const data = await parseJsonSafe(res); if (!res.ok) throw new Error(errMsg(data, "AI plan detail generate failed")); return data as AiPlanDetailResp; } // 2.5 Generate Reflection Report export type AiTeachAnnotation = { category: string; selectedText?: string | null; feedback?: string | null; }; export type AiQuizAggregations = { averageScore?: number | null; lowestTopic?: string | null; }; export type AiReflectionReq = { requestId: string; teachAnnotations: AiTeachAnnotation[]; quizAggregations?: AiQuizAggregations | null; }; export type AiReflectionUnderstanding = { status: string; summary: string; bulletPoints?: string[] | null; }; export type AiReflectionEngagement = { status: string; summary?: string | null; }; export type AiReflectionDifficulty = { status: string; challengingTopics?: any[] | null; }; export type AiReflectionMisconception = { status: string; issues?: any[] | null; }; export type AiNextLessonSuggestion = { actionText: string; deepLinkType?: string | null; }; export type AiReflectionResp = { data: { understanding: AiReflectionUnderstanding; engagement: AiReflectionEngagement; difficulty: AiReflectionDifficulty; misconceptions: AiReflectionMisconception; nextLessonSuggestions: AiNextLessonSuggestion[]; }; meta: AiMeta; }; export async function apiAiReflectionGenerate( payload: AiReflectionReq ): Promise { const base = getBaseUrl(); const res = await fetchWithTimeout( `${base}/ai/courseware/reflection/generate`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }, TEACHER_TIMEOUT_MS ); const data = await parseJsonSafe(res); if (!res.ok) throw new Error(errMsg(data, "AI reflection generate failed")); return data as AiReflectionResp; } // 2.6 Generate Improvement Proposals export type AiImprovementReq = { requestId: string; reflectionReports: any[]; }; export type AiImprovementProposal = { title: string; priority: string; affectedWeeks?: string | null; evidence?: string | null; rootCause?: string | null; proposedSolution?: string | null; expectedImpact?: string | null; }; export type AiImprovementResp = { data: { proposals: AiImprovementProposal[] }; meta: AiMeta; }; export async function apiAiImprovementGenerate( payload: AiImprovementReq ): Promise { const base = getBaseUrl(); const res = await fetchWithTimeout( `${base}/ai/courseware/improvement/generate`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }, TEACHER_TIMEOUT_MS ); const data = await parseJsonSafe(res); if (!res.ok) throw new Error(errMsg(data, "AI improvement generate failed")); return data as AiImprovementResp; }