import { useState, useEffect, useCallback } from "react"; import { Header } from "./components/Header"; import { InviteGate } from "./components/InviteGate"; import { LoginForm } from "./components/LoginForm"; import { StepIndicator } from "./components/StepIndicator"; import { FileUploadPanel } from "./components/step1/FileUploadPanel"; import { ParsedDataSummary } from "./components/step2/ParsedDataSummary"; import { StudentSelector } from "./components/step2/StudentSelector"; import { PromptEditor } from "./components/step2/PromptEditor"; import { ReportViewer } from "./components/step3/ReportViewer"; import { apiGet, apiPost, getToken, clearToken } from "./lib/api"; const INVITE_CODE = "taboola-npo-cz"; interface User { id: number; email: string; display_name: string; } interface StudentInfo { index: number; name: string; id: string; } export interface StudentReport { studentName: string; studentIndex: number; html: string; status: "pending" | "generating" | "done" | "error"; error?: string; } export default function App() { const [invited, setInvited] = useState(() => localStorage.getItem("invite_code") === INVITE_CODE); const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [currentStep, setCurrentStep] = useState<1 | 2 | 3>(1); const [sessionId, setSessionId] = useState(null); const [parsedData, setParsedData] = useState>({}); const [students, setStudents] = useState([]); const [selectedIndices, setSelectedIndices] = useState([]); const [reports, setReports] = useState([]); const [isGenerating, setIsGenerating] = useState(false); // Check for saved JWT on mount useEffect(() => { const token = getToken(); if (token) { apiGet("/api/auth/me") .then(setUser) .catch(() => clearToken()) .finally(() => setLoading(false)); } else { setLoading(false); } }, []); // Create session when user is authenticated and no session exists useEffect(() => { if (user && !sessionId) { apiPost<{ id: number }>("/api/sessions", { title: "New Session" }) .then((session) => setSessionId(session.id)) .catch(() => {}); } }, [user]); // Extract student list from parsed data useEffect(() => { const sa = parsedData.student_answers as { students?: { name?: string; id?: string }[] } | undefined; if (sa?.students) { setStudents( sa.students.map((s, i) => ({ index: i, name: s.name || `Student ${i + 1}`, id: s.id || "", })) ); } }, [parsedData]); const handleLogout = () => { setUser(null); setSessionId(null); setParsedData({}); setStudents([]); setSelectedIndices([]); setReports([]); setCurrentStep(1); }; const handleParsedDataUpdate = useCallback((dataType: string, data: unknown) => { setParsedData((prev) => ({ ...prev, [dataType]: data })); }, []); const handleGoToStep2 = useCallback(() => { setCurrentStep(2); }, []); // Sequential per-student report generation const handleGenerateReports = useCallback(async (model: string) => { if (!sessionId || selectedIndices.length === 0) return; const initial: StudentReport[] = selectedIndices.map((idx) => { const s = students.find((s) => s.index === idx); return { studentName: s?.name || `Student ${idx + 1}`, studentIndex: idx, html: "", status: "pending", }; }); setReports(initial); setIsGenerating(true); setCurrentStep(3); for (let i = 0; i < selectedIndices.length; i++) { const idx = selectedIndices[i]; setReports((prev) => prev.map((r, j) => (j === i ? { ...r, status: "generating" } : r)) ); try { const res = await apiPost<{ report_id: number; html_content: string; student_name: string; }>(`/api/sessions/${sessionId}/generate-student-report`, { student_index: idx, model, }); setReports((prev) => prev.map((r, j) => j === i ? { ...r, html: res.html_content, status: "done" } : r ) ); } catch (err) { const msg = err instanceof Error ? err.message : "Generation failed"; setReports((prev) => prev.map((r, j) => j === i ? { ...r, status: "error", error: msg } : r ) ); } } setIsGenerating(false); }, [sessionId, selectedIndices, students]); const handleExportAllHtml = useCallback(() => { const doneReports = reports.filter((r) => r.status === "done"); if (doneReports.length === 0) return; const combined = ` ClassLens - Student Reports ${doneReports .map( (r, i) => `
${r.html}
` ) .join("\n")} `; const blob = new Blob([combined], { type: "text/html" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "classlens-all-reports.html"; a.click(); URL.revokeObjectURL(url); }, [reports]); const handleStepClick = (step: 1 | 2 | 3) => { if (step <= currentStep) setCurrentStep(step); }; if (loading) { return (
); } if (!invited) { return ( { localStorage.setItem("invite_code", INVITE_CODE); setInvited(true); }} correctCode={INVITE_CODE} /> ); } if (!user) { return ; } return (
{currentStep === 1 && (

上傳考試資料

上傳考試題目、學生答案和標準答案,分別解析後確認資料再前往下一步

{sessionId && ( )}
)} {currentStep === 2 && (

選擇學生 & 編輯提示詞

勾選要生成報告的學生,編輯提示詞,預覽資料後生成報告

{students.length > 0 ? ( <> {sessionId && ( )} ) : (

尚未上傳學生答案資料,請先回到上一步上傳檔案

)}
)} {currentStep === 3 && (

學生分析報告

setCurrentStep(2)} onExportAllHtml={handleExportAllHtml} />
)}
); }