"use client"; // app/editor/[id]/page.jsx — review and refine an assignment, then export it. import { useEffect, useRef, useState } from "react"; import { useParams, useRouter } from "next/navigation"; import QuestionCard from "@/components/QuestionCard"; import { QUESTION_TYPES, blankQuestion, totalPoints } from "@/lib/schema"; import { exportTxt, exportDoc, exportClipboard, exportPrint } from "@/lib/exporter"; export default function EditorPage() { const { id } = useParams(); const router = useRouter(); const [a, setA] = useState(null); const [loadErr, setLoadErr] = useState(""); const [dirty, setDirty] = useState(false); const [saving, setSaving] = useState(false); const [toast, setToast] = useState(""); const [busyQ, setBusyQ] = useState(null); const [verifying, setVerifying] = useState(false); const [addOpen, setAddOpen] = useState(false); const [exportOpen, setExportOpen] = useState(false); const [error, setError] = useState(""); const [profile, setProfile] = useState({}); const toastTimer = useRef(null); useEffect(() => { fetch("/api/assignments/" + id) .then(async (r) => { const data = await r.json(); if (!r.ok) throw new Error(data.error || "Could not load this assignment."); setA(data); }) .catch((e) => setLoadErr(String(e.message || e))); fetch("/api/settings") .then((r) => r.json()) .then((s) => setProfile(s?.profile || {})) .catch(() => {}); }, [id]); useEffect(() => { function onBeforeUnload(e) { if (dirty) { e.preventDefault(); e.returnValue = ""; } } window.addEventListener("beforeunload", onBeforeUnload); return () => window.removeEventListener("beforeunload", onBeforeUnload); }, [dirty]); function showToast(msg) { setToast(msg); clearTimeout(toastTimer.current); toastTimer.current = setTimeout(() => setToast(""), 2400); } function patch(p) { setA((cur) => ({ ...cur, ...p })); setDirty(true); } function setQuestion(i, q) { setA((cur) => { const questions = [...cur.questions]; questions[i] = q; return { ...cur, questions }; }); setDirty(true); } function moveQuestion(i, dir) { setA((cur) => { const questions = [...cur.questions]; const j = i + dir; if (j < 0 || j >= questions.length) return cur; [questions[i], questions[j]] = [questions[j], questions[i]]; return { ...cur, questions }; }); setDirty(true); } function deleteQuestion(i) { if (!confirm("Delete question " + (i + 1) + "?")) return; setA((cur) => ({ ...cur, questions: cur.questions.filter((_, j) => j !== i) })); setDirty(true); } async function save(silent) { setSaving(true); setError(""); try { const res = await fetch("/api/assignments/" + id, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(a), }); const data = await res.json(); if (!res.ok) throw new Error(data.error || "Save failed."); setA(data); setDirty(false); if (!silent) showToast("Saved"); } catch (e) { setError(String(e.message || e)); } finally { setSaving(false); } } async function regenerateQuestion(i, note) { const q = a.questions[i]; setBusyQ(q.id); setError(""); try { const data = await postJson("/api/generate", { stage: "question", source: a.source?.text || "", config: a.config || { assignmentType: a.assignmentType, gradeLevel: a.gradeLevel, subject: a.subject, difficulty: a.difficulty }, type: q.type, note, replacing: { question: q.question }, existingQuestions: a.questions.filter((_, j) => j !== i).map((x) => ({ question: x.question })), }); const next = { ...data.question, points: q.points }; setQuestion(i, next); showToast("Question " + (i + 1) + " regenerated"); } catch (e) { setError(String(e.message || e)); } finally { setBusyQ(null); } } async function addQuestion(type, withAI) { setAddOpen(false); if (!withAI) { setA((cur) => ({ ...cur, questions: [...cur.questions, blankQuestion(type)] })); setDirty(true); return; } setBusyQ("__new__"); setError(""); try { const data = await postJson("/api/generate", { stage: "question", source: a.source?.text || "", config: a.config || { assignmentType: a.assignmentType, gradeLevel: a.gradeLevel, subject: a.subject, difficulty: a.difficulty }, type, existingQuestions: a.questions.map((x) => ({ question: x.question })), }); setA((cur) => ({ ...cur, questions: [...cur.questions, data.question] })); setDirty(true); showToast("Question added"); } catch (e) { setError(String(e.message || e)); } finally { setBusyQ(null); } } async function reverify() { setVerifying(true); setError(""); try { const data = await postJson("/api/generate", { stage: "verify", source: a.source?.text || "", config: a.config || { assignmentType: a.assignmentType, gradeLevel: a.gradeLevel, subject: a.subject, difficulty: a.difficulty }, questions: a.questions, }); setA((cur) => ({ ...cur, questions: cur.questions.map((q) => data.verifications[q.id] ? { ...q, verification: data.verifications[q.id] } : { ...q, verification: { status: "unchecked", note: "" } } ), })); setDirty(true); const warns = Object.values(data.verifications).filter((v) => v.status === "warn").length; showToast(warns ? `Accuracy check done — ${warns} question${warns === 1 ? "" : "s"} flagged` : "Accuracy check done — all clear" ); } catch (e) { setError(String(e.message || e)); } finally { setVerifying(false); } } function doExport(kind, who) { setExportOpen(false); const opts = who === "packet" ? { packet: true, profile } : { teacher: who === true, profile }; try { if (kind === "txt") { exportTxt(a, opts); showToast("Downloaded .txt"); } if (kind === "doc") { exportDoc(a, opts); showToast("Downloaded Word file"); } if (kind === "print") { exportPrint(a, opts); } if (kind === "copy") { exportClipboard(a, opts).then(() => showToast("Copied to clipboard")); } } catch (e) { setError(String(e.message || e)); } } if (loadErr) { return (

Couldn’t open that assignment

{loadErr}

); } if (!a) { return (
{[0, 1, 2].map((i) => (
))}
); } const warnCount = a.questions.filter((q) => q.verification?.status === "warn").length; const uncheckedCount = a.questions.filter((q) => !q.verification || q.verification.status === "unchecked").length; return (
patch({ title: e.target.value })} aria-label="Assignment title" style={{ fontFamily: "var(--font-display)", fontSize: "1.6rem", fontWeight: 700, border: "1.5px solid transparent", background: "transparent", padding: "4px 8px", marginLeft: -8, borderRadius: 8, width: "100%", transition: "border-color 0.15s, background 0.15s", }} onFocus={(e) => { e.target.style.borderColor = "var(--line-strong)"; e.target.style.background = "var(--field-bg)"; }} onBlur={(e) => { e.target.style.borderColor = "transparent"; e.target.style.background = "transparent"; }} />

{[a.assignmentType?.replace("_", " "), a.gradeLevel, a.subject].filter(Boolean).join(" · ")} · {a.questions.length} questions · {totalPoints(a.questions)} points {a.source?.name ? <> · from {a.source.name} : null}

{exportOpen && (
Student version
Teacher version (answer key)
Complete packet — student + answer key
)}
{error &&
{error}
} {warnCount > 0 && (
{warnCount} question{warnCount === 1 ? "" : "s"} flagged by the accuracy check. Look for the ⚠ stamps below — each has a reviewer note. Edit or regenerate those questions, then re-run the check.
)} {warnCount === 0 && uncheckedCount === 0 && a.questions.length > 0 && (
✓ Every question passed the accuracy check against your source.
)}