"use client"; import { useI18n } from "@/components/i18n"; import { useState } from "react"; import { ConfidenceRail } from "@/components/confidence-rail"; import { GlyphMark } from "@/components/ui/glyph-mark"; import { ThinkingBlock } from "@/components/thinking-block"; import { PathText } from "@/components/file-path-chip"; import { cn } from "@/lib/utils"; import { ToolContentResponse } from "@/components/ai-elements/message"; import type { QuestionAnswerData } from "@/src/core"; import { answerForPresentation, parseAskInput, parseQuizInput } 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: savedAnswer, input }: { answer: QuestionAnswerData; input?: unknown }) { const { t } = useI18n(); const answer = answerForPresentation(savedAnswer, input); const [whyOpen, setWhyOpen] = useState(false); // the tucked explanation (predictions, owner 2026-10-03) if (answer.dismissed) { return (
{answer.questionText} {" "}{t("— 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 ?? []); // The question's own context rides the ORIGINAL CALL, not the answer // snapshot (owner, 2026-10-12): the details line — and, for ask // questions, option descriptions — re-render from the persisted // arguments, so every existing transcript shows them too. The lean // snapshot (what the model sees, compactAnswerForModel) is untouched; // any parse failure or absent input renders exactly the older card. const askInput = answer.kind === "ask" ? parseAskInput(input) : undefined; const parsedDetails = askInput?.details ?? (answer.kind === "quiz" ? parseQuizInput(input)?.details : undefined); const descByIndex = new Map((askInput?.options ?? []).map((o, i) => [i + 1, o.description])); 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 && ( )} {/* [&>*:last-child]:mb-0 (owner round, 2026-10-07): on ask cards the thinking row is the last child — its my-3 stacked with the card's p-4 into a 28px void below (12px above); the card's own padding is the bottom rhythm when a row ends the card. */}
{/* The tool's OWN name first (owner 2026-10-09): probe-phase cards label Probe/Recall — the phase boundary visible in the transcript. No `tool` on old records: kind labels as before. */} {answer.tool === "probe" ? t("Probe") : answer.tool === "recall" ? t("Recall") : isQuiz ? answer.questionType === "predict" ? t("Prediction") : t("Check") : isExplain ? t("Explain") : t("Question")} {isQuiz && (answer.dontKnow ? ( {t("didn't know")} ) : answer.correct ? ( {t("correct")} ) : ( {t("incorrect")} ))}
{answer.questionText}
{parsedDetails && ( // The details line, same paint as the pending dock (owner, // 2026-10-12): full markdown, LaTeX included.
{parsedDetails}
)} {((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} {descByIndex.get(opt.index) && ( // Ask-option subtext (owner, 2026-10-12): same markdown path // as the pending dock — LaTeX included. Quiz options carry // none (owner ruling 2026-09-26); the map stays empty for them. {descByIndex.get(opt.index)} )}
); })} {/* 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.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 && (
{t("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 — one geometry, but NO band on the card (owner round five, 2026-10-07: the accent lift read too light); rail only, heights identical. */ )} {/* 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 && ( /* 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). */
{whyOpen && (
{t("Why:")}{" "} {/* Keep the first paragraph beside the label, not every paragraph: later paragraphs must retain their recovered document structure. */} {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 && (
{t("The answer:")}{" "} {answer.committed}
)}
); }