"use client"; /** * The problem-set list (practice slice 5): rows of sets — title, date, * status, problem/graded counts — click through to the exam page; a * delete affordance in the delete-session dialog's shape; "New problem * set" opens the creation dialog. Polls /api/practice/sets (2s, the * graph-view reveal-poll idiom) while ANY set is still generating, so * statuses live-update; a settled list stops polling. */ import { useCallback, useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import { createPortal } from "react-dom"; import { PlusIcon, TrashIcon } from "lucide-react"; import { TopBar } from "@/components/top-bar"; import type { ProblemSet } from "@/src/core"; import { PracticeDialog } from "@/components/practice-dialog"; /** A set's status word — plain and honest, no invented progress. */ export function setStatusWord(set: Pick): string { return set.status === "partitioning" ? "partitioning" : set.status === "generating" ? "generating" : set.status; } /** The list's pure body — exported for SSR pins. Rows are buttons * (click-through); the delete affordance is a nested icon button. */ export function PracticeListBody({ sets, onOpen, onDelete, onNew, }: { sets: ProblemSet[]; onOpen: (id: string) => void; onDelete: (set: ProblemSet) => void; onNew: () => void; }) { return (
{/* The top bar (owner, UI round 2026-09-29): the new-session pattern — h-15, lh-drag (the Electron window-drag surface), the sidebar reopen toggle first, "Practice" beside it, the page's one action at the right edge (lh-no-drag — it must stay clickable inside the drag band). */}
Practice
{sets.length === 0 && (
No problem sets yet — create one from a session.
)}
{sets.map((set) => { const ready = set.problems.filter( (p) => p.status === "ready", ).length; const graded = set.problems.filter((p) => p.grade).length; return (
); })}
); } /** The delete confirm — the delete-session dialog's shape (no mode * checkbox: a set has no preserve-graph question). Pure body + portal * wrapper. */ export function DeleteSetBody({ title, busy, failure, onClose, onRun, }: { title: string; busy: boolean; failure: string | null; onClose: () => void; onRun: () => void; }) { return (
e.stopPropagation()} className="w-[32rem] max-w-[calc(100vw-2rem)] rounded-xl border bg-card p-5 shadow-xl" >

Delete problem set?

{title}

The set and its problems are removed. Nothing in the graph or your sessions changes.

{failure && (

{failure}

)}
); } export function PracticeListPage({ initialSets, }: { initialSets: ProblemSet[]; }) { const router = useRouter(); const [sets, setSets] = useState(initialSets); const [creating, setCreating] = useState(false); const [deleting, setDeleting] = useState(null); const [deleteBusy, setDeleteBusy] = useState(false); const [deleteFailure, setDeleteFailure] = useState(null); const refresh = useCallback(async () => { try { const res = await fetch("/api/practice/sets", { cache: "no-store" }); if (!res.ok) return; const d = (await res.json()) as { sets: ProblemSet[] }; setSets(d.sets); } catch { // a transient failure on the poll keeps the last snapshot } }, []); // The reveal poll: while any set is partitioning/generating, statuses // live-update; a settled list goes quiet. const anyRunning = sets.some( (s) => s.status === "partitioning" || s.status === "generating", ); useEffect(() => { if (!anyRunning) return; const t = setInterval(() => void refresh(), 2000); return () => clearInterval(t); }, [anyRunning, refresh]); const runDelete = useCallback(async () => { if (!deleting || deleteBusy) return; setDeleteBusy(true); setDeleteFailure(null); try { const res = await fetch(`/api/practice/sets/${deleting.id}`, { method: "DELETE", }); if (!res.ok) { const body = (await res.json().catch(() => ({}))) as { error?: string }; setDeleteFailure(body.error ?? `Delete failed (${res.status})`); setDeleteBusy(false); return; } setDeleting(null); setDeleteBusy(false); await refresh(); } catch { setDeleteFailure("Delete failed — network error. Try again."); setDeleteBusy(false); } }, [deleting, deleteBusy, refresh]); return ( <> router.push(`/practice/${id}`)} onDelete={(set) => setDeleting(set)} onNew={() => setCreating(true)} /> {creating && setCreating(false)} />} {deleting && createPortal(
{ if (!deleteBusy) setDeleting(null); }} > setDeleting(null)} onRun={() => void runDelete()} />
, document.body, )} ); }