claudqunwang's picture
Add structured AI courseware APIs
2a4f012
// 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 };
}
// --------------------
// /api/tts (text-to-speech) – returns audio/mpeg
// --------------------
export async function apiTts(payload: {
user_id: string;
text: string;
voice?: string;
}): Promise<Blob> {
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<Blob> {
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<TeacherStatus> {
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<AiSyllabusGenerateResp> {
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<AiFlowGenerateResp> {
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<AiFlowPartialResp> {
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<AiPlanDetailResp> {
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<AiReflectionResp> {
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<AiImprovementResp> {
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;
}