import { useEffect, useRef, useState } from "react"; import { apiGet, apiUpload } from "../../lib/api"; import { ModelSelector } from "../ModelSelector"; interface ParserOption { id: string; name: string; description: string; } interface FileUploadPanelProps { sessionId: number; parsedData: Record; onParsedDataUpdate: (dataType: string, data: unknown) => void; onGoToStep2: () => void; } interface UploadZone { type: "questions" | "answers"; label: string; description: string; placeholder: string; icon: string; } const zones: UploadZone[] = [ { type: "questions", label: "考試題目", description: "上傳考試試卷(任意格式)", placeholder: "例如:國中三年級數學期中考,共 20 題選擇題和 5 題計算題", icon: "📝", }, { type: "answers", label: "答案", description: "上傳作答一覽表(PDF / CSV / 圖片)", placeholder: "例如:A 卷的考生作答一覽表(CSV 第 1 列為題號標題,第 2 列為標準答案,第 3 列起為學生)", icon: "✅", }, ]; export function FileUploadPanel({ sessionId, parsedData, onParsedDataUpdate, onGoToStep2, }: FileUploadPanelProps) { const [files, setFiles] = useState>({ questions: [], answers: [], }); const [descriptions, setDescriptions] = useState>({ questions: "", answers: "", }); const [analyzing, setAnalyzing] = useState>({}); const [errors, setErrors] = useState>({}); const [model, setModel] = useState("gpt-5.4"); const [parsers, setParsers] = useState([]); const [parserByZone, setParserByZone] = useState>({ questions: "auto", answers: "auto", }); const fileInputRefs = useRef>({}); useEffect(() => { apiGet<{ parsers: ParserOption[] }>("/api/parsers") .then((res) => setParsers(res.parsers)) .catch(() => {}); }, []); const handleFileSelect = (type: string, selected: FileList | null) => { if (!selected) return; setFiles((prev) => ({ ...prev, [type]: Array.from(selected) })); setErrors((prev) => ({ ...prev, [type]: "" })); }; const handleDrop = (type: string, e: React.DragEvent) => { e.preventDefault(); handleFileSelect(type, e.dataTransfer.files); }; const handleAnalyze = async (type: string) => { const typeFiles = files[type]; if (!typeFiles.length) return; setAnalyzing((prev) => ({ ...prev, [type]: true })); setErrors((prev) => ({ ...prev, [type]: "" })); try { const formData = new FormData(); formData.append("data_type", type); formData.append("description", descriptions[type] || ""); formData.append("model", model); formData.append("parser", parserByZone[type] || "auto"); for (const f of typeFiles) { formData.append("files", f); } const res = await apiUpload<{ data: unknown }>(`/api/sessions/${sessionId}/upload`, formData); if (type === "answers") { const data = res.data as { student_answers?: unknown; teacher_answers?: unknown; }; if (data.student_answers) onParsedDataUpdate("student_answers", data.student_answers); if (data.teacher_answers) onParsedDataUpdate("teacher_answers", data.teacher_answers); onParsedDataUpdate("answers", res.data); } else { onParsedDataUpdate(type, res.data); } } catch (err: unknown) { setErrors((prev) => ({ ...prev, [type]: err instanceof Error ? err.message : "Analysis failed", })); } finally { setAnalyzing((prev) => ({ ...prev, [type]: false })); } }; const hasAnyParsedData = Object.keys(parsedData).length > 0; const isAnyAnalyzing = Object.values(analyzing).some(Boolean); return (
{/* Model selector */}
{/* Info note */}

支援任意格式檔案(PDF、圖片、文字檔等)。若檔案中包含圖片、圖表或示意圖,AI 會自動將其轉換為文字描述,以確保後續分析完整準確。

{/* Upload zones */}
{zones.map((zone) => { const zoneData = parsedData[zone.type] as Record | undefined; const isAnalyzing = analyzing[zone.type]; const hasFiles = files[zone.type].length > 0; const hasParsed = !!zoneData; return (
e.preventDefault()} onDrop={(e) => handleDrop(zone.type, e)} >
{zone.icon}

{zone.label}

{zone.description}

{/* Per-zone description */}