diff --git a/app/editor/[id]/page.jsx b/app/editor/[id]/page.jsx index 170c387..28d133a 100644 --- a/app/editor/[id]/page.jsx +++ b/app/editor/[id]/page.jsx @@ -2,9 +2,13 @@ // 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 { + IconChevronDown, IconRefresh, IconCheck, IconCircleCheck, IconAlertTriangle, + IconPlus, IconPencil, IconArrowLeft, +} from "@tabler/icons-react"; import QuestionCard from "@/components/QuestionCard"; import CanvasExportDialog from "@/components/CanvasExportDialog"; -import { QUESTION_TYPES, blankQuestion, totalPoints } from "@/lib/schema"; +import { QUESTION_TYPES, blankQuestion, totalPoints, newId } from "@/lib/schema"; import { exportTxt, exportDoc, exportClipboard, exportPrint } from "@/lib/exporter"; export default function EditorPage() { @@ -23,6 +27,8 @@ export default function EditorPage() { const [canvasOpen, setCanvasOpen] = useState(false); const [error, setError] = useState(""); const [profile, setProfile] = useState({}); + const [dragIndex, setDragIndex] = useState(null); + const [overIndex, setOverIndex] = useState(null); const toastTimer = useRef(null); useEffect(() => { @@ -30,19 +36,17 @@ export default function EditorPage() { .then(async (r) => { const data = await r.json(); if (!r.ok) throw new Error(data.error || "Could not load this assignment."); + // Guarantee every question carries a stable id (older saves may lack one), + // so React keys and drag-reorder identity stay unique. + data.questions = (data.questions || []).map((q) => (q && q.id ? q : { ...q, id: newId() })); setA(data); }) .catch((e) => setLoadErr(String(e.message || e))); - fetch("/api/settings") - .then((r) => r.json()) - .then((s) => setProfile(s?.profile || {})) - .catch(() => {}); + fetch("/api/settings").then((r) => r.json()).then((s) => setProfile(s?.profile || {})).catch(() => {}); }, [id]); useEffect(() => { - function onBeforeUnload(e) { - if (dirty) { e.preventDefault(); e.returnValue = ""; } - } + function onBeforeUnload(e) { if (dirty) { e.preventDefault(); e.returnValue = ""; } } window.addEventListener("beforeunload", onBeforeUnload); return () => window.removeEventListener("beforeunload", onBeforeUnload); }, [dirty]); @@ -53,17 +57,10 @@ export default function EditorPage() { toastTimer.current = setTimeout(() => setToast(""), 2400); } - function patch(p) { - setA((cur) => ({ ...cur, ...p })); - setDirty(true); - } + 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 }; - }); + setA((cur) => { const questions = [...cur.questions]; questions[i] = q; return { ...cur, questions }; }); setDirty(true); } @@ -78,6 +75,19 @@ export default function EditorPage() { setDirty(true); } + function reorder(from, to) { + if (from === to || from == null || to == null) return; + setA((cur) => { + const questions = [...cur.questions]; + const [moved] = questions.splice(from, 1); + questions.splice(to, 0, moved); + return { ...cur, questions }; + }); + setDirty(true); + } + + function endDrag() { setDragIndex(null); setOverIndex(null); } + function deleteQuestion(i) { if (!confirm("Delete question " + (i + 1) + "?")) return; setA((cur) => ({ ...cur, questions: cur.questions.filter((_, j) => j !== i) })); @@ -85,48 +95,31 @@ export default function EditorPage() { } async function save(silent) { - setSaving(true); - setError(""); + setSaving(true); setError(""); try { - const res = await fetch("/api/assignments/" + id, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(a), - }); + 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); + setA(data); setDirty(false); if (!silent) showToast("Saved"); - } catch (e) { - setError(String(e.message || e)); - } finally { - setSaving(false); - } + } catch (e) { setError(String(e.message || e)); } + finally { setSaving(false); } } async function regenerateQuestion(i, note) { const q = a.questions[i]; - setBusyQ(q.id); - setError(""); + setBusyQ(q.id); setError(""); try { const data = await postJson("/api/generate", { - stage: "question", - source: a.source?.text || "", + 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 }, + 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); + setQuestion(i, { ...data.question, points: q.points }); showToast("Question " + (i + 1) + " regenerated"); - } catch (e) { - setError(String(e.message || e)); - } finally { - setBusyQ(null); - } + } catch (e) { setError(String(e.message || e)); } + finally { setBusyQ(null); } } async function addQuestion(type, withAI) { @@ -136,89 +129,72 @@ export default function EditorPage() { setDirty(true); return; } - setBusyQ("__new__"); - setError(""); + setBusyQ("__new__"); setError(""); try { const data = await postJson("/api/generate", { - stage: "question", - source: a.source?.text || "", + 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 })), + 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); - } + } catch (e) { setError(String(e.message || e)); } + finally { setBusyQ(null); } } async function reverify() { - setVerifying(true); - setError(""); + setVerifying(true); setError(""); try { const data = await postJson("/api/generate", { - stage: "verify", - source: a.source?.text || "", + 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: "" } } - ), + 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); - } + 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 === "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 (kind === "copy") { exportClipboard(a, opts).then(() => showToast("Copied to clipboard")); } + } catch (e) { setError(String(e.message || e)); } } if (loadErr) { return ( -
{loadErr}
- +{loadErr}
+ ++
{[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}