Spaces:
Runtime error
Runtime error
File size: 6,988 Bytes
f002ae5 bc45e54 f002ae5 bc45e54 f002ae5 9f97e28 bc45e54 f002ae5 9f97e28 f002ae5 9f97e28 bc45e54 9f97e28 f002ae5 bc45e54 f002ae5 bc45e54 f002ae5 bc45e54 f002ae5 bc45e54 f002ae5 9f97e28 f002ae5 9f97e28 bc45e54 9f97e28 bc45e54 f002ae5 bc45e54 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 | import { useState, useRef, useEffect } from "react";
import type { StudentReport } from "../../App";
interface ReportViewerProps {
reports: StudentReport[];
isGenerating: boolean;
onBack: () => void;
onExportAllHtml: () => void;
onSaveToDb?: () => Promise<{ students_saved: number }>;
}
export function ReportViewer({
reports,
isGenerating,
onBack,
onExportAllHtml,
onSaveToDb,
}: ReportViewerProps) {
const [activeIndex, setActiveIndex] = useState(0);
const [saveState, setSaveState] = useState<"idle" | "saving" | "saved" | "error">("idle");
const [saveMsg, setSaveMsg] = useState("");
const iframeRef = useRef<HTMLIFrameElement>(null);
const handleSaveToDb = async () => {
if (!onSaveToDb) return;
setSaveState("saving");
setSaveMsg("");
try {
const res = await onSaveToDb();
setSaveState("saved");
setSaveMsg(`已儲存 ${res.students_saved} 位學生成績`);
} catch (e) {
setSaveState("error");
setSaveMsg(e instanceof Error ? e.message : "儲存失敗");
}
};
// Auto-switch to the report currently being generated
useEffect(() => {
const generatingIdx = reports.findIndex((r) => r.status === "generating");
if (generatingIdx >= 0) {
setActiveIndex(generatingIdx);
} else {
// When generation finishes, show the last completed one
let lastDone = -1;
for (let j = reports.length - 1; j >= 0; j--) {
if (reports[j].status === "done") { lastDone = j; break; }
}
if (lastDone >= 0) setActiveIndex(lastDone);
}
}, [reports]);
const activeReport = reports[activeIndex];
const doneCount = reports.filter((r) => r.status === "done").length;
const errorCount = reports.filter((r) => r.status === "error").length;
const handleDownloadSingle = () => {
if (!activeReport?.html) return;
const blob = new Blob([activeReport.html], { type: "text/html" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `classlens-${activeReport.studentName}.html`;
a.click();
URL.revokeObjectURL(url);
};
return (
<div className="space-y-4">
{/* Progress bar */}
{isGenerating && (
<div className="card p-4">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-semibold text-[var(--color-text)]">
生成進度
</span>
<span className="text-sm text-[var(--color-text-muted)]">
{doneCount + errorCount} / {reports.length}
</span>
</div>
<div className="w-full h-2 bg-[var(--color-border)] rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-success)] transition-all duration-500"
style={{
width: `${((doneCount + errorCount) / reports.length) * 100}%`,
}}
/>
</div>
</div>
)}
{/* Action bar */}
<div className="flex flex-wrap items-center gap-3">
<button onClick={onBack} className="btn btn-outline text-sm">
← 返回選擇學生
</button>
<div className="flex-1" />
{activeReport?.status === "done" && (
<button onClick={handleDownloadSingle} className="btn btn-outline text-sm">
下載此報告
</button>
)}
{doneCount > 1 && (
<button onClick={onExportAllHtml} className="btn btn-outline text-sm">
下載全部報告
</button>
)}
{onSaveToDb && !isGenerating && doneCount > 0 && (
<button
onClick={handleSaveToDb}
disabled={saveState === "saving" || saveState === "saved"}
className="btn btn-primary text-sm disabled:opacity-60"
>
{saveState === "saving" && "儲存中..."}
{saveState === "saved" && "✅ 已儲存"}
{(saveState === "idle" || saveState === "error") && "儲存至資料庫"}
</button>
)}
</div>
{saveMsg && (
<p
className={`text-sm text-right ${
saveState === "error" ? "text-red-400" : "text-[var(--color-success)]"
}`}
>
{saveMsg}
</p>
)}
{/* Student tabs */}
<div className="flex gap-2 overflow-x-auto pb-2">
{reports.map((r, i) => (
<button
key={r.studentIndex}
onClick={() => setActiveIndex(i)}
className={`shrink-0 px-4 py-2 rounded-xl text-sm font-semibold transition-all ${
i === activeIndex
? "bg-[var(--color-primary)] text-white shadow-md"
: "bg-[var(--color-surface)] text-[var(--color-text-muted)] hover:text-[var(--color-text)] border border-[var(--color-border)]"
}`}
>
<span className="mr-1.5">
{r.status === "done" && "✅"}
{r.status === "generating" && "⏳"}
{r.status === "pending" && "⬜"}
{r.status === "error" && "❌"}
</span>
{r.studentName}
</button>
))}
</div>
{/* Report content */}
<div className="card overflow-hidden" style={{ height: "70vh" }}>
{activeReport?.status === "done" && (
<iframe
ref={iframeRef}
srcDoc={activeReport.html}
title={`Report - ${activeReport.studentName}`}
className="w-full h-full border-0"
sandbox="allow-scripts allow-same-origin"
/>
)}
{activeReport?.status === "generating" && (
<div className="w-full h-full flex flex-col items-center justify-center gap-4">
<div className="w-10 h-10 border-3 border-[var(--color-primary)] border-t-transparent rounded-full animate-spin" />
<p className="text-[var(--color-text-muted)] text-lg">
正在為 <strong className="text-[var(--color-text)]">{activeReport.studentName}</strong> 生成報告...
</p>
</div>
)}
{activeReport?.status === "pending" && (
<div className="w-full h-full flex items-center justify-center">
<p className="text-[var(--color-text-muted)]">
等待生成...
</p>
</div>
)}
{activeReport?.status === "error" && (
<div className="w-full h-full flex flex-col items-center justify-center gap-3">
<span className="text-4xl">❌</span>
<p className="text-[var(--color-text)] font-semibold">
{activeReport.studentName} 報告生成失敗
</p>
<p className="text-sm text-[var(--color-text-muted)] max-w-md text-center">
{activeReport.error}
</p>
</div>
)}
</div>
</div>
);
}
|