practice slice 5 — UI: list page, exam page, creation dialog, sidebar entries
components/practice-dialog.tsx: portal dialog (delete-session shape), three dial rows (SettingSlider markup, anchor text as display: flashcard-competition / near the lesson-far transfer / theory- application, defaults 0.5), session picker (generic mode, /api/chats) or preselected lock (right-click path); 400 empty_selection renders 'This session has no propositions yet', 503 renders the server line + Open settings deep link (useSettingsDialog, zero-keys precedent). Pure body exported for SSR pins. components/practice-list.tsx: rows (title, date, ready/total problems, graded count, status word; destructive tint on error), click-through, delete affordance → portaled confirm (DeleteSetBody: no mode checkbox — a set has no preserve-graph question), 2s reveal-poll while any set is partitioning/generating. components/practice-exam.tsx: numbered problems; generating stubs (BrailleSpinner + 'Generating problem…', propositions never surface); ready statements/solutions/rubrics/ feedback as MessageResponse markdown with restoreMathEscapes at the display seam; per-problem textarea + useDictation instance with the DictationBar rendered only on box focus; one Submit answers POST per set; graded reveal (verdict chip, feedback, model solution, rubric) with read-only answer; polling while not ready OR grading-live (activity feed practice rows — dropped grades terminate the poll). app/practice/page.tsx (force-dynamic, listPracticeSets) + app/practice/ [id]/page.tsx (getPracticeSet + notFound). Sidebar: quiet Practice nav row under New session (GraduationCapIcon, active on /practice) + 'Practice this session's propositions' in the session right-click below the hairline, above CoverageMenuBlock — opens the dialog preselected. tests/render-practice.test.tsx — 15 pins: dials + anchors, picker selection gating, preselected lock, empty-selection line, 503 deep link, list rows/counts/empty, delete confirm + inline failure, exam generating (no proposition text, no answer boxes, no submit), ready (markdown statement, gated submit), graded reveal + read-only + sibling answerable, failed problem/set lines, math healing. 1129/1129, tsc 0, lint 0, build 0.
This commit is contained in:
+40
-1
@@ -6,9 +6,10 @@ import { usePathname, useRouter } from "next/navigation";
|
||||
import { MenuShell } from "@/components/menu-shell";
|
||||
import { DeleteSessionDialog } from "@/components/delete-session-dialog";
|
||||
import type { ChatDeleteMode } from "@/src/core";
|
||||
import { PanelLeftCloseIcon, PanelLeftOpenIcon, PlusIcon, SearchIcon, SettingsIcon } from "lucide-react";
|
||||
import { GraduationCapIcon, PanelLeftCloseIcon, PanelLeftOpenIcon, PlusIcon, SearchIcon, SettingsIcon } from "lucide-react";
|
||||
import { ActivitySection } from "@/components/activity-dock";
|
||||
import { CoverageMenuBlock } from "@/components/extraction-coverage";
|
||||
import { PracticeDialog } from "@/components/practice-dialog";
|
||||
import { LhMark } from "@/components/lh-mark";
|
||||
import { BrailleSpinner } from "@/components/braille-spinner";
|
||||
import { KeyboardShortcuts } from "@/components/keyboard-shortcuts";
|
||||
@@ -397,6 +398,9 @@ function SessionsSidebar() {
|
||||
const ctxAnchorRef = useRef<HTMLElement | null>(null);
|
||||
const [renamingId, setRenamingId] = useState<string | null>(null);
|
||||
const [deleting, setDeleting] = useState<ChatListItem | null>(null);
|
||||
// Practice slice 5 (2026-09-29): the right-click "Practice this session's
|
||||
// propositions" opens the creation dialog PRESELECTED on that session.
|
||||
const [practiceChat, setPracticeChat] = useState<{ chatId: string; title: string } | null>(null);
|
||||
|
||||
const openContext = (e: React.MouseEvent<HTMLButtonElement>, chat: ChatListItem) => {
|
||||
e.preventDefault(); // suppress the native menu
|
||||
@@ -591,6 +595,21 @@ function SessionsSidebar() {
|
||||
<span className="sr-only">create a new session</span>
|
||||
</button>
|
||||
|
||||
{/* Practice (slice 5, 2026-09-29): the set list's entry — the same
|
||||
quiet row language as New session, active-state lit while any
|
||||
/practice route is open. GraduationCapIcon to match the dock. */}
|
||||
<button
|
||||
type="button"
|
||||
data-testid="nav-practice"
|
||||
onClick={() => router.push("/practice")}
|
||||
className={`mx-3 mb-1 flex items-center gap-1.5 rounded-md px-2 py-1.5 text-xs uppercase tracking-wider transition-colors hover:bg-accent hover:text-foreground ${
|
||||
pathname.startsWith("/practice") ? "bg-accent text-foreground" : "text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
<GraduationCapIcon className="size-3" />
|
||||
Practice
|
||||
</button>
|
||||
|
||||
<div className="mx-3 mb-1 flex items-center gap-2">
|
||||
<span className="text-xs uppercase tracking-widest text-muted-foreground/70">Sessions</span>
|
||||
<Hint label="Search sessions" action="sessions-search" side="bottom">
|
||||
@@ -717,6 +736,21 @@ function SessionsSidebar() {
|
||||
Delete…
|
||||
</button>
|
||||
<div className="my-0.5 border-t border-border/60" aria-hidden="true" />
|
||||
{/* Practice (slice 5): the session's propositions → a problem set.
|
||||
Sits ABOVE the coverage block — an action like Extract, not
|
||||
management like Rename/Delete. */}
|
||||
<button
|
||||
type="button"
|
||||
data-testid="ctx-practice"
|
||||
onClick={() => {
|
||||
const target = chats?.find((c) => c.id === ctxChatId);
|
||||
if (target) setPracticeChat({ chatId: target.id, title: target.title });
|
||||
setCtxChatId(null);
|
||||
}}
|
||||
className="flex items-center rounded-lg px-2.5 py-1.5 text-left text-foreground transition-colors hover:bg-accent"
|
||||
>
|
||||
Practice this session's propositions
|
||||
</button>
|
||||
<CoverageMenuBlock chatId={ctxChatId} />
|
||||
</MenuShell>
|
||||
)}
|
||||
@@ -728,6 +762,11 @@ function SessionsSidebar() {
|
||||
onClose={() => setDeleting(null)}
|
||||
onDeleted={onDeleted}
|
||||
/>
|
||||
{/* The preselected practice dialog (slice 5) — portaled by the
|
||||
component itself, here for state locality like the delete dialog. */}
|
||||
{practiceChat && (
|
||||
<PracticeDialog preselected={practiceChat} onClose={() => setPracticeChat(null)} />
|
||||
)}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user