chih.yikuan
Add invite code gate page before login
459bded
Raw
History Blame Contribute Delete
7.87 kB
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 { 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;
}
export default function App() {
const [invited, setInvited] = useState(() => localStorage.getItem("invite_code") === INVITE_CODE);
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [currentStep, setCurrentStep] = useState<1 | 2 | 3>(1);
const [sessionId, setSessionId] = useState<number | null>(null);
const [parsedData, setParsedData] = useState<Record<string, unknown>>({});
const [promptContent, setPromptContent] = useState("");
const [reportHtml, setReportHtml] = useState("");
const [isGenerating, setIsGenerating] = useState(false);
// Check for saved JWT on mount
useEffect(() => {
const token = getToken();
if (token) {
apiGet<User>("/api/auth/me")
.then(setUser)
.catch(() => clearToken())
.finally(() => setLoading(false));
} else {
setLoading(false);
}
}, []);
// Load default prompt
useEffect(() => {
if (user && !promptContent) {
apiGet<{ content: string }>("/api/prompts/default")
.then((res) => setPromptContent(res.content))
.catch(() => {});
}
}, [user]);
// 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]);
const handleLogout = () => {
setUser(null);
setSessionId(null);
setParsedData({});
setPromptContent("");
setReportHtml("");
setCurrentStep(1);
};
const handleParsedDataUpdate = useCallback((dataType: string, data: unknown) => {
setParsedData((prev) => ({ ...prev, [dataType]: data }));
}, []);
const handleGoToStep2 = useCallback(() => {
setCurrentStep(2);
}, []);
const handleRunSummary = useCallback(async (model: string) => {
if (!sessionId || !promptContent.trim()) return;
setIsGenerating(true);
try {
const res = await apiPost<{ report_id: number; html_content: string }>(
`/api/sessions/${sessionId}/generate-report`,
{ prompt_content: promptContent, model }
);
setReportHtml(res.html_content);
setCurrentStep(3);
} catch (err) {
alert(err instanceof Error ? err.message : "Report generation failed");
} finally {
setIsGenerating(false);
}
}, [sessionId, promptContent]);
const handleExportPdf = useCallback(async () => {
// Dynamic import to avoid loading html2pdf.js unless needed
const html2pdf = (await import("html2pdf.js")).default;
const container = document.createElement("div");
container.innerHTML = reportHtml;
document.body.appendChild(container);
await html2pdf()
.set({ margin: 10, filename: "classlens-report.pdf", html2canvas: { scale: 2 } })
.from(container)
.save();
document.body.removeChild(container);
}, [reportHtml]);
const handleExportPng = useCallback(async () => {
const html2canvas = (await import("html2canvas")).default;
const container = document.createElement("div");
container.innerHTML = reportHtml;
container.style.width = "1200px";
container.style.position = "absolute";
container.style.left = "-9999px";
document.body.appendChild(container);
const canvas = await html2canvas(container, { scale: 2 });
document.body.removeChild(container);
const link = document.createElement("a");
link.download = "classlens-report.png";
link.href = canvas.toDataURL("image/png");
link.click();
}, [reportHtml]);
const handleStepClick = (step: 1 | 2 | 3) => {
if (step <= currentStep) setCurrentStep(step);
};
// Loading state
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center bg-[var(--color-background)]">
<div className="w-8 h-8 border-2 border-[var(--color-primary)] border-t-transparent rounded-full animate-spin" />
</div>
);
}
// Invite code gate
if (!invited) {
return (
<InviteGate
onSuccess={() => {
localStorage.setItem("invite_code", INVITE_CODE);
setInvited(true);
}}
correctCode={INVITE_CODE}
/>
);
}
// Login gate
if (!user) {
return <LoginForm onLogin={setUser} />;
}
return (
<div className="min-h-screen">
<Header userEmail={user.email} onLogout={handleLogout} />
<div className="pt-20">
<StepIndicator currentStep={currentStep} onStepClick={handleStepClick} />
<main className="max-w-6xl mx-auto px-4 pb-12">
{currentStep === 1 && (
<div className="space-y-6 animate-fade-in-up">
<div className="text-center mb-8">
<h2 className="font-display text-2xl font-bold text-[var(--color-text)]">
上傳考試資料
</h2>
<p className="text-[var(--color-text-muted)] mt-2">
上傳考試題目、學生答案和標準答案,分別解析後確認資料再前往下一步
</p>
</div>
{sessionId && (
<FileUploadPanel
sessionId={sessionId}
parsedData={parsedData}
onParsedDataUpdate={handleParsedDataUpdate}
onGoToStep2={handleGoToStep2}
/>
)}
</div>
)}
{currentStep === 2 && (
<div className="space-y-6 animate-fade-in-up">
<div className="text-center mb-4">
<h2 className="font-display text-2xl font-bold text-[var(--color-text)]">
編輯分析提示詞
</h2>
<p className="text-[var(--color-text-muted)] mt-2">
編輯提示詞來自訂分析報告的內容與格式
</p>
</div>
<ParsedDataSummary parsedData={parsedData} />
{sessionId && (
<PromptEditor
promptContent={promptContent}
onPromptChange={setPromptContent}
sessionId={sessionId}
onRunSummary={handleRunSummary}
isGenerating={isGenerating}
/>
)}
</div>
)}
{currentStep === 3 && (
<div className="animate-fade-in-up">
<div className="text-center mb-4">
<h2 className="font-display text-2xl font-bold text-[var(--color-text)]">
分析報告
</h2>
</div>
<ReportViewer
reportHtml={reportHtml}
onBack={() => setCurrentStep(2)}
onExportPdf={handleExportPdf}
onExportPng={handleExportPng}
/>
</div>
)}
</main>
<footer className="py-8 text-center text-sm text-[var(--color-text-muted)]">
<p>&copy; 2026 ClassLens &bull; AI-Powered Teaching Assistant</p>
</footer>
</div>
</div>
);
}