"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 (
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) => (
i ? " done" : ""}`}
onClick={() => {
if (i === 0 || (i === 1 && sourceReady) || (i === 2 && sourceReady && configReady)) setStep(i);
}}
disabled={
generating ||
(i === 1 && !sourceReady) ||
(i === 2 && (!sourceReady || !configReady))
}
role="tab"
aria-selected={step === i}
>
{step > i ? "✓" : i + 1} {label}
))}
{/* ============ 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]) => (
{ setSourceTab(id); setError(""); }}
>
{label}
))}
{sourceTab === "upload" && (
fileRef.current?.click()}>Choose a .txt or .md file
{sourceName && {sourceName} }
)}
{sourceTab === "url" && (
setUrl(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter" && url.trim()) fetchUrl(); }}
style={{ flex: 1, minWidth: 240 }}
/>
{fetching ? <> Fetching…> : "Fetch page"}
)}
)}
{/* ============ STEP 2: CONFIGURE ============ */}
{step === 1 && (
Set up the assignment
Assignment type
{ASSIGNMENT_TYPES.map((t) => (
update({ assignmentType: t.id })}
>
{t.label}
{t.hint}
))}
Grade level
update({ gradeLevel: e.target.value })}>
{GRADE_LEVELS.map((g) => {g} )}
Subject
update({ subject: e.target.value })}
/>
{["English Language Arts","U.S. History","World History","Civics / Government","Biology","Chemistry","Physics","Earth Science","Mathematics","Geography","Economics","Health","Computer Science","Spanish","Art History"].map((s) => (
))}
{isDiscussionOrCase ? "Number of prompts/questions" : "Number of questions"} — {config.questionCount}
update({ questionCount: Number(e.target.value) })}
style={{ width: "100%", accentColor: "var(--board)" }}
/>
Difficulty
update({ difficulty: e.target.value })}>
{DIFFICULTIES.map((d) => {d} )}
{!isDiscussionOrCase && (
Question types to include
{QUESTION_TYPES.map((t) => (
toggleQType(t.id)}
/>
{t.label}
))}
{config.questionTypes.length === 0 && (
Pick at least one question type.
)}
)}
update({ includeExplanations: e.target.checked })} />
Include explanations in the answer keyWhy each answer is correct — and for multiple choice, why the others are wrong.
{!isDiscussionOrCase && (
update({ includeRubrics: e.target.checked })} />
Include rubrics for essay questionsPoint-based criteria that sum to the question total.
)}
update({ verify: e.target.checked })} />
Run the accuracy checkA second AI pass reviews every question and answer against your source. Strongly recommended — adds a little time.
Anything to focus on? (optional)
update({ focusNote: e.target.value })}
/>
setStep(0)}>← Back
setStep(2)}>
Next: Generate →
)}
{/* ============ STEP 3: GENERATE ============ */}
{step === 2 && (
Ready to generate
{ASSIGNMENT_TYPES.find((t) => t.id === config.assignmentType)?.label}
{" "}
· {config.gradeLevel} · {config.subject || "—"} · {config.questionCount} question{config.questionCount === 1 ? "" : "s"} · {config.difficulty} difficulty
Source: {sourceName || "Pasted text"} ({text.length.toLocaleString()} characters)
{!genState && (
setStep(1)}>← Back
✎ Generate assignment
)}
{genState && !genState.error && (
<>
Local models can take a few minutes for large assignments. Leave this tab open.
>
)}
{genState?.error && (
<>
Generation failed. {genState.error}
setGenState(null)}>Adjust and retry
Try again
>
)}
)}
);
}
async function postJson(url, body) {
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.error || `Request failed (${res.status}).`);
return data;
}