"use client"; // app/page.jsx — the Create flow: Source -> Configure -> Generate. import { useEffect, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import { ASSIGNMENT_TYPES, QUESTION_TYPES, GRADE_LEVELS, DIFFICULTIES } from "@/lib/schema"; const MAX_PASTE = 120000; // Maps a generation phase to its 0-based order index const PHASE_ORDER = ["analyze", "generate", "verify", "save"]; function ProgressStep({ phase, label, currentPhase, hasVerify }) { if (phase === "verify" && !hasVerify) return null; const cur = PHASE_ORDER.indexOf(currentPhase); const me = PHASE_ORDER.indexOf(phase); const state = me < cur ? "done" : me === cur ? "active" : ""; return (
  • {state === "done" ? "✓" : state === "active" ? : "·"} {label}{state === "active" ? "…" : ""}
  • ); } export default function CreatePage() { const router = useRouter(); const [step, setStep] = useState(0); // --- source state --- const [sourceTab, setSourceTab] = useState("paste"); const [text, setText] = useState(""); const [sourceName, setSourceName] = useState(""); const [url, setUrl] = useState(""); const [fetching, setFetching] = useState(false); const [error, setError] = useState(""); const fileRef = useRef(null); // --- config state --- const [config, setConfig] = useState({ assignmentType: "quiz", gradeLevel: "Grade 8", subject: "", questionCount: 10, difficulty: "Mixed", questionTypes: ["multiple_choice", "true_false", "short_answer", "fill_blank"], includeExplanations: true, includeRubrics: true, focusNote: "", verify: true, }); // --- provider readiness --- const [providerNote, setProviderNote] = useState(null); useEffect(() => { fetch("/api/settings") .then((r) => r.json()) .then((s) => { const cfg = s.providers?.[s.provider] || {}; if (!cfg.model) { setProviderNote({ provider: s.provider, missing: "model" }); } else if (["openai", "anthropic", "google"].includes(s.provider) && !cfg.apiKey) { setProviderNote({ provider: s.provider, missing: "key" }); } else { setProviderNote(null); } setConfig((c) => ({ ...c, verify: s.generation?.verification !== false })); }) .catch(() => {}); }, []); // --- generation state --- const [genState, setGenState] = useState(null); const generating = !!genState && !genState.error; function update(patch) { setConfig((c) => ({ ...c, ...patch })); } function toggleQType(id) { setConfig((c) => { const has = c.questionTypes.includes(id); const next = has ? c.questionTypes.filter((t) => t !== id) : [...c.questionTypes, id]; return { ...c, questionTypes: next }; }); } async function onFile(e) { setError(""); const file = e.target.files?.[0]; if (!file) return; if (!/\.(txt|md|markdown|text|csv)$/i.test(file.name)) { setError("Please choose a plain-text file (.txt or .md). For Word docs or PDFs, copy the text and paste it instead."); return; } try { const content = await file.text(); setText(content.slice(0, MAX_PASTE)); setSourceName(file.name); setSourceTab("upload"); } catch { setError("Could not read that file."); } } async function fetchUrl() { setError(""); setFetching(true); try { const res = await fetch("/api/fetch-url", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url }), }); const data = await res.json(); if (!res.ok) throw new Error(data.error || "Could not fetch that page."); setText(data.text.slice(0, MAX_PASTE)); setSourceName(data.title || url); } catch (e) { setError(String(e.message || e)); } finally { setFetching(false); } } const sourceReady = text.trim().length >= 100; const configReady = config.subject.trim().length > 0 && (["discussion", "case_study"].includes(config.assignmentType) || config.questionTypes.length > 0); async function generate() { setGenState({ phase: "analyze" }); const source = text.trim(); const cfg = { ...config, subject: config.subject.trim() }; try { let analysis = null; try { const r1 = await postJson("/api/generate", { stage: "analyze", source, config: cfg }); analysis = r1.analysis; } catch (e) { console.warn("Analysis stage failed, continuing:", e); } setGenState({ phase: "generate" }); const r2 = await postJson("/api/generate", { stage: "generate", source, analysis, config: cfg }); const assignment = r2.assignment; if (cfg.verify) { setGenState({ phase: "verify" }); try { const r3 = await postJson("/api/generate", { stage: "verify", source, config: cfg, questions: assignment.questions, }); for (const q of assignment.questions) { if (r3.verifications[q.id]) q.verification = r3.verifications[q.id]; } } catch (e) { console.warn("Verification stage failed, continuing:", e); } } setGenState({ phase: "save" }); const saved = await postJson("/api/assignments", { ...assignment, source: { type: sourceTab, name: sourceName || "Pasted text", text: source }, config: cfg, }); router.push("/editor/" + saved.id); } catch (e) { setGenState({ phase: null, error: String(e.message || e) }); } } const isDiscussionOrCase = ["discussion", "case_study"].includes(config.assignmentType); return (

    Create an assignment

    Give it your source material, set the parameters, and get a classroom-ready assignment with a verified answer key — all on your own machine.

    {providerNote && (
    {providerNote.missing === "key" ? "Your selected AI provider needs an API key before you can generate." : "No AI model is selected yet."}{" "} Open Settings to finish setup.
    )}
    {["Source", "Configure", "Generate"].map((label, i) => ( ))}
    {/* ============ STEP 1: SOURCE ============ */} {step === 0 && (

    What should the questions come from?

    Questions are grounded strictly in this material — the AI is instructed not to add outside facts.

    {[["paste", "Paste text"], ["upload", "Upload file"], ["url", "From a web page"]].map(([id, label]) => ( ))}
    {sourceTab === "upload" && (
    {sourceName && {sourceName}}
    )} {sourceTab === "url" && (
    setUrl(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && url.trim()) fetchUrl(); }} style={{ flex: 1, minWidth: 240 }} />
    )}