"use client";
/**
* The problem-set creation dialog (practice slice 5, 2026-09-29;
* cluster-picker fix same evening): the three parameter dials + the
* CLUSTER picker → POST /api/practice/sets → router.push to the new
* exam page. In the delete-session dialog's shape (portaled z-50, pure
* body exported for SSR pins).
*
* GRAPH-DOMAIN, NOT CHAT-DOMAIN (owner ruling 2026-09-29): problem sets
* take NODES. The picker lists the birth-session clusters the graph
* view renders (GET /api/graph/clusters); picking one resolves to its
* member node ids, and the cluster's label rides along as the set's
* title. This dialog is the ONLY creation entry for now — the graph-view
* cluster entry comes later with the free-form selection UI.
*
* Honest failures inline: 400 empty_selection (the vanished-nodes edge
* — the cluster's propositions are gone) renders the honest line;
* 503 (models/key refusal) renders the server's line + an "Open
* settings" deep link (the zero-keys precedent — setup owns the fix,
* the dialog owns the pointer).
*/
import { useCallback, useEffect, useState } from "react";
import { createPortal } from "react-dom";
import { useRouter } from "next/navigation";
import { useSettingsDialog } from "@/components/settings/settings-dialog";
import type { GraphClusterGroup } from "@/src/core";
/** The three dials — key order matches PracticeParams; anchors are the
* honest display (a raw 0..1 number tells the learner nothing). */
export const PRACTICE_DIALS = [
{ key: "searchSolutionRatio", label: "Search ↔ Solve", anchors: "flashcard · competition" },
{ key: "cueDistance", label: "Cue distance", anchors: "near the lesson · far transfer" },
{ key: "presentation", label: "Presentation", anchors: "theory · application" },
] as const;
export interface PracticeFailure {
status: number;
error: string;
code?: string;
}
/** The dialog's pure body — exported for SSR pins (the delete-session /
* settings body pattern). The wrapper owns state + fetch + portal. */
export function PracticeDialogBody({
values,
onDial,
clusters,
selectedKey,
onSelect,
busy,
failure,
onOpenSettings,
onClose,
onRun,
}: {
values: Record<(typeof PRACTICE_DIALS)[number]["key"], number>;
onDial: (key: (typeof PRACTICE_DIALS)[number]["key"], v: number) => void;
clusters: GraphClusterGroup[];
selectedKey: string | null;
onSelect: (key: string) => void;
busy: boolean;
failure: PracticeFailure | null;
onOpenSettings: () => void;
onClose: () => void;
onRun: () => void;
}) {
const canRun = !busy && selectedKey !== null;
return (
No clusters yet — a cluster appears once a session births 3+ propositions.
)}
{/* The three dials (SettingSlider's markup, anchors as display). */}
{PRACTICE_DIALS.map((dial) => (
))}
{failure && (
{failure.code === "empty_selection" ? "Nothing practicable is left in that cluster — the graph changed." : failure.error}
{failure.status === 503 && (
)}