// 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" | "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 = { course_name: string; learning_outcome: string; student_level: string; teaching_focus: string; course_length: number; }; export type AiSyllabusGenerateReq = { request_id: string; context: AiSyllabusContext; system_prompts?: string[] | null; }; export type AiWeekSyllabus = { week_number: number; title: string; learning_objectives: string[]; topics: string[]; }; export type AiSyllabusGenerateResp = { data: { course_overview?: string | null; assessment?: string | null; teaching_approach?: string | null; workload_expectation?: string | null; academic_integrity_policy?: string | null; 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 Regenerate Partial Syllabus export type AiSyllabusLockedSections = { course_overview?: string | null; assessment?: string | null; teaching_approach?: string | null; workload_expectation?: string | null; academic_integrity_policy?: string | null; syllabus?: Partial[] | null; }; export type AiSyllabusUnlockedSections = { syllabus?: Partial[]; assessment?: string | null; course_overview?: string | null; teaching_approach?: string | null; workload_expectation?: string | null; academic_integrity_policy?: string | null; [key: string]: any; }; export type AiCurrentSyllabus = { locked_sections: AiSyllabusLockedSections; unlocked_sections: AiSyllabusUnlockedSections; }; export type AiSyllabusPartialReq = { request_id: string; prompt: string; current_syllabus: AiCurrentSyllabus; context: AiSyllabusContext; }; export type AiSyllabusPartialResp = { data: { explanation: string; // Must have exactly the same top-level keys as current_syllabus.unlocked_sections. proposed_draft: Record; }; meta: AiMeta; }; export async function apiAiSyllabusRegeneratePartial( payload: AiSyllabusPartialReq ): Promise { const base = getBaseUrl(); const res = await fetchWithTimeout( `${base}/ai/courseware/syllabus/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 syllabus partial regenerate failed")); return data as AiSyllabusPartialResp; } // 2.3 Generate Lesson Flow export type AiModuleContext = { title: string; learning_objectives: string[]; topics: string[]; duration_minutes: number; }; export type AiFlowGenerateReq = { request_id: string; module_context: AiModuleContext; system_prompts?: string[] | null; }; export type AiLessonStep = { type: string; title: string; estimated_duration: number; ai_understanding: string; teacher_activity?: string | null; student_activity?: string | null; guiding_questions?: string | null; required_materials?: string | null; }; 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 = { locked_steps: AiSimpleStep[]; unlocked_steps: AiSimpleStep[]; }; export type AiFlowPartialReq = { request_id: string; prompt: string; current_flow: AiCurrentFlow; }; export type AiCopilotProposedStep = { type: string; title: string; estimated_duration: number; ai_understanding: string; teacher_activity?: string | null; student_activity?: string | null; guiding_questions?: string | null; required_materials?: string | null; }; export type AiFlowPartialResp = { data: { explanation: string; proposed_steps: 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 = { request_id: string; finalized_steps: any[]; }; export type AiLessonSection = { section_id: string; type: string; title: string; content: string; }; export type AiPlanDetailResp = { data: { course_name?: string | null; 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 = { dimension: string; level: string; selected_text?: string | null; feedback?: string | null; }; export type AiQuizAggregations = { average_score?: number | null; lowest_topic?: string | null; }; export type AiReflectionReq = { request_id: string; lesson_steps?: { step_number: number; title: string; type: string; duration: number; teacher_activity?: string | null; student_activity?: string | null; guiding_questions?: string | null; }[]; teach_annotations: AiTeachAnnotation[]; quiz_aggregations?: AiQuizAggregations | null; }; export type AiReflectionDetails = { description?: string | null; bullet_points?: string[] | null; challenging_topics?: string[] | null; issues?: string[] | null; }; export type AiReflectionField = { status: string; summary: string; details: AiReflectionDetails; }; export type AiUpperSection = { description?: string | null; required: boolean; fields: { understanding: AiReflectionField; engagement: AiReflectionField; difficulty: AiReflectionField; misconceptions: AiReflectionField; }; }; export type AiNextLessonSuggestion = { text: string; deep_link_type?: string | null; }; export type AiLowerSection = { summary: string; description?: string | null; overall_summary?: string | null; strengths: string[]; next_lesson_suggestions: AiNextLessonSuggestion[]; }; export type AiReflectionResp = { data: { overall_status: string; upper_section: AiUpperSection; lower_section: AiLowerSection; }; 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 = { request_id: string; reflection_reports: any[]; }; export type AiImprovementProposal = { title: string; priority: string; affected_weeks?: string | null; evidence?: string | null; root_cause?: string | null; proposed_solution?: string | null; expected_impact?: 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; }