diff --git a/components/answered-card.tsx b/components/answered-card.tsx index 5b900af..70cb9f2 100644 --- a/components/answered-card.tsx +++ b/components/answered-card.tsx @@ -145,11 +145,11 @@ export function AnsweredCard({ answer }: { answer: QuestionAnswerData }) { {/* px-3→removed: card p-4 + box px-3 double-inset sat the "Why" label deeper than the option box edges (owner, round 3). Sides flush with everything else; py-2 stays for vertical air. */} - {isQuiz && answer.explanation && answer.questionType === "predict" ? ( - /* The TUCKED explanation (owner 2026-10-03): a prediction's - verdict shows, but the explanation sits collapsed — viewable, - never center stage. The teaching that follows resolves the - prediction; THAT is the real explanation. */ + {isQuiz && answer.explanation && ( + /* The TUCKED explanation — ALL questions (owner, 2026-10-07): the + verdict shows, the explanation sits collapsed behind the Why + toggle — viewable, never center stage. Identical to the + prediction pattern it generalizes (owner, 2026-10-03). */
after the
- "Why:" label created the oversized leading gap (owner's fix list). */}
-
{children}; + if (!alert) { + // Plain quote — Streamdown's own styling, restored by hand: overriding + // `blockquote` displaced its styled component (the classes below are + // its verbatim defaults), and bare
rendered unstyled + // (owner report, 2026-10-07). + return ( ++ {children} ++ ); + } return ( ; + // 50%-italic quote block on expand. Legacy records' commit flag is + // deliberately unread (one lane now). A TRANSCRIPT row (owner round, + // 2026-10-07): mirrored to the right side, arriving expanded. + return ( ++ ); } diff --git a/components/thinking-block.tsx b/components/thinking-block.tsx index dc259df..d026972 100644 --- a/components/thinking-block.tsx +++ b/components/thinking-block.tsx @@ -1,35 +1,86 @@ "use client"; -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { ChevronDownIcon } from "lucide-react"; import { cn } from "@/lib/utils"; /** The unified user-thinking row (owner round, 2026-10-05): ONE visual * language for every surface where the learner's own thinking renders — * practice thinking traces, the reading channel lifted above the answered - * card, the quiz answer's thinking inside its container. It arrives - * COLLAPSED as a trigger row in the exact register of the agent's - * reasoning rows (muted label + chevron + hover affordance); expanding - * opens the quote block — rail, 50% opacity, italics. The italics + the - * half-opacity text are the inner-voice signature; the rail marks it as - * learner material. No shell, no border: document material, full width. + * card, the quiz answer's thinking inside its container. It arrives as + * a trigger row in the exact register of the agent's reasoning rows + * (muted label + chevron + hover affordance); expanding opens the quote + * block — rail, 50% opacity, italics. The italics + the half-opacity + * text are the inner-voice signature; the rail marks it as learner + * material. No shell, no border: document material, full width. * * The glance-level law (owner): user thinking = rail + 50% italic; - * agent thinking = no rail. */ + * agent thinking = no rail. + * + * SIDE-BASED AUTHORSHIP (owner round, 2026-10-07): transcript-level rows + * go RIGHT — mirrored rail (border-right), block hugging the right edge + * at ~85% width, text still reading left-to-right. In-card rows (inside + * the answered card, the agent's context) stay left. `mirror` selects + * the side; `defaultOpen` marks the transcript rows, which arrive + * EXPANDED (owner: clearly conveyed that the thinking was sent). + * + * ⌘T (toggle-user-thinking) sets EVERY row to one state via the module + * store below — one source of truth, no provider tree: rows subscribe on + * mount; explicit sets (and only those) stomp individual toggles. */ + +// ── The ⌘T store (module-scope, one per page load) ───────────────────────── +// null = "rows run their own defaults" (the pre-toggle state); the first ⌘T +// resolves from `true` (transcript rows default open) and then alternates. +let allUserThinkingOpen: boolean | null = null; +const userThinkingListeners = new Set<(open: boolean) => void>(); + +export function currentUserThinkingOpen(): boolean { + return allUserThinkingOpen ?? true; +} + +/** The row's starting state: an explicit ⌘T set wins over the row's own + * default (late mounts honor the last set — same path as the live sync, + * readable at render time so static SSR reflects the store too). */ +function resolveUserThinkingOpen(defaultOpen: boolean): boolean { + return allUserThinkingOpen ?? defaultOpen; +} + +export function setAllUserThinkingOpen(open: boolean) { + allUserThinkingOpen = open; + for (const l of userThinkingListeners) l(open); +} + +function subscribeUserThinking(fn: (open: boolean) => void): () => void { + userThinkingListeners.add(fn); + if (allUserThinkingOpen !== null) fn(allUserThinkingOpen); // late mounts honor the last set + return () => userThinkingListeners.delete(fn); +} export function ThinkingBlock({ label = "YOUR THINKING", text, testId, dataCommit, + mirror = false, + defaultOpen = false, }: { label?: string; text: string; testId?: string; /** The practice marker's register flag (commit vs nudge). */ dataCommit?: boolean; + /** Right-side placement (transcript rows): mirrored rail, right edge. */ + mirror?: boolean; + /** Transcript rows arrive expanded (owner, 2026-10-07). */ + defaultOpen?: boolean; }) { - const [open, setOpen] = useState(false); // arrives collapsed, always + const [open, setOpen] = useState(() => resolveUserThinkingOpen(defaultOpen)); + // ⌘T: an explicit global set overrides every row (once set, rows re-sync). + const syncedRef = useRef(false); + useEffect( + () => subscribeUserThinking((o) => { syncedRef.current = true; setOpen(o); }), + [], + ); return ( /* The rail GROUPS and the rail IS THE MISSING CHARACTER (owner rounds, 2026-10-05): the heading sits INSIDE the rail, and the @@ -44,7 +95,16 @@ export function ThinkingBlock({