"use client"; import { useState } from "react"; import { ConfidenceRail } from "@/components/confidence-rail"; import { GlyphMark } from "@/components/ui/glyph-mark"; import { ThinkingBlock } from "@/components/thinking-block"; import { cn } from "@/lib/utils"; import { MessageResponse } from "@/components/ai-elements/message"; import type { QuestionAnswerData } from "@/src/core"; /** * Static rendering of an answered question (tool part in `output-available`). * * Quiz answers are GRADED: ✓/✗ marks, the correct answers, and the * explanation. Ask answers are NOT graded — a plain record of what was said. * (The v1 bug this pins: ask answers must never get quiz grading UI.) * Explain answers are graded by the MODEL in its next reply, not by the * client — the card reveals his answer, the committed correct answer, and * the explanation, with NO verdict chips (owner ruling 2026-09-27). */ export function AnsweredCard({ answer }: { answer: QuestionAnswerData }) { const [whyOpen, setWhyOpen] = useState(false); // the tucked explanation (predictions, owner 2026-10-03) if (answer.dismissed) { return (
{answer.questionText} — skipped
); } const isQuiz = answer.kind === "quiz"; const isExplain = answer.kind === "explain"; const selectedIdx = new Set(answer.answers.map((a) => a.index)); const correctIdx = new Set(answer.correctIndices ?? []); return ( <> {/* The reading channel LIFTED (owner round, 2026-10-05): thoughts captured while reading are chronologically PRIOR to the question — rendered as its own user-side block ABOVE the card, the unified ThinkingBlock, instead of an inset inside the answer. Same muted rail as before; the label is now plain "Your thinking". */} {answer.readingThinking && ( )}
{isQuiz ? (answer.questionType === "predict" ? "Prediction" : "Quiz") : isExplain ? "Explain" : "Question"} {isQuiz && (answer.dontKnow ? ( didn't know ) : answer.correct ? ( correct ) : ( incorrect ))}
{answer.questionText}
{((answer.options?.length ?? 0) > 0 || answer.other) && (
{answer.options?.map((opt) => { const sel = selectedIdx.has(opt.index); const key = isQuiz && correctIdx.has(opt.index); return (
{isQuiz && ( // Zapf-Dingbats-derived SVG outlines — the exact LaTeX \ding{51}/ // \ding{55} hand-drawn shapes (owner, round 6); see ui/glyph-mark. {/* Owner round 8: a touch smaller than the text (size-4→size-3.5, ≈14px vs the 16px row) while centered. */} {(key || sel) && } )} {opt.label}
); })} {/* The learner's own typed option is an OPTION — same container, same selected styling as its siblings, not a loose quote floating outside the list. Quotes keep the learner's words. */} {answer.other && (
“{answer.other}”
)}
)} {answer.text &&
“{answer.text}”
} {/* The confidence record (owner round 6): the slider's rail, disc removed — the missing handle itself says "this is a record, not a control". Same paint, same ticks, same ramp, same % read-out as the live dock. Quiz answers only, and only when a confidence exists: "I don't know" sent none (its slider was grayed out), explain and ask never had one, and records from before the feature carry no field — nothing to show, nothing rendered. */} {isQuiz && answer.confidence != null && (
{answer.confidence}%
)} {answer.dontKnow && isQuiz && (
Answered “I don't know”.
)} {(answer.thinking ?? answer.note) && ( /* The SAME unified row (owner round, 2026-10-05): the quiz answer's thinking is the identical collapsible ThinkingBlock — it just happens to sit inside the quiz container. Same placement, same trigger, same 50%-italic quote block on expand. */ )} {/* 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. */
{whyOpen && (
Why: {answer.explanation}
)}
) : ( isQuiz && answer.explanation && (
Why: {/* The markdown wrapper must go truly inline — a block

after the "Why:" label created the oversized leading gap (owner's fix list). */} {answer.explanation}

) )} {/* The explain reveal (owner ruling 2026-09-27): the committed answer — one field, reasoning included — shown on submit; no verdict, the model's next reply IS the grading. */} {isExplain && answer.committed && (
The answer: {answer.committed}
)}
); }