"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.
{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 && (