math: dollar delimiters end-to-end (exact answers, escaping examples, context rendering)

- switch the math authoring/rendering contract from \(...\)/\[...\] to the
  standard $...$ inline / $$...$$ display: prompts (20-question-tools,
  40-formatting), singleDollarTextMath on the renderer, legacy reader kept,
  and escape-debris recovery extended to dollar spans (mirroring micromark
  delimiter-run rules)
- exact answer membership now precedes debris cleanup in
  coerceCorrectAnswer (calculus incident: an exact sentence-final-period
  label was stripped as model debris, then rejected); option keys are
  passed at the boundary/grading seams, the dock no longer pre-coerces
- a complete, phase-correct JSON-escaping example rides the probe/quiz
  tool descriptions (serialized from one canonical body; quiz leads with
  type+nodeId), plus the one-sentence JSON-escaping rule in 40-formatting
- ask option descriptions render markdown/LaTeX in the pending dock;
  settled cards keep the details line and option descriptions
  retroactively, read from the persisted call arguments (chat passes
  part.input to AnsweredCard)

1566/1566 tests on both navigator configs; typecheck, lint, production
build, standalone packaging, and Electron-runtime smoke all green.
This commit is contained in:
amosblomqvist
2026-10-13 06:24:33 +03:00
parent a395f37727
commit bf29a2070f
19 changed files with 628 additions and 93 deletions
+26 -1
View File
@@ -7,6 +7,7 @@ import { ThinkingBlock } from "@/components/thinking-block";
import { cn } from "@/lib/utils";
import { MessageResponse } from "@/components/ai-elements/message";
import type { QuestionAnswerData } from "@/src/core";
import { parseAskInput, parseQuizInput } from "@/src/core";
/**
* Static rendering of an answered question (tool part in `output-available`).
@@ -18,7 +19,7 @@ import type { QuestionAnswerData } from "@/src/core";
* 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 }) {
export function AnsweredCard({ answer, input }: { answer: QuestionAnswerData; input?: unknown }) {
const [whyOpen, setWhyOpen] = useState(false); // the tucked explanation (predictions, owner 2026-10-03)
if (answer.dismissed) {
return (
@@ -32,6 +33,15 @@ export function AnsweredCard({ answer }: { answer: QuestionAnswerData }) {
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 (
<>
@@ -78,6 +88,13 @@ export function AnsweredCard({ answer }: { answer: QuestionAnswerData }) {
<div className="mb-3 font-medium">
<MessageResponse>{answer.questionText}</MessageResponse>
</div>
{parsedDetails && (
// The details line, same paint as the pending dock (owner,
// 2026-10-12): full markdown, LaTeX included.
<div className="mb-3 text-sm text-muted-foreground">
<MessageResponse>{parsedDetails}</MessageResponse>
</div>
)}
{((answer.options?.length ?? 0) > 0 || answer.other) && (
<div className="flex flex-col gap-1.5 text-sm">
@@ -115,6 +132,14 @@ export function AnsweredCard({ answer }: { answer: QuestionAnswerData }) {
)}
<span className={cn(!sel && "text-muted-foreground")}>
<MessageResponse>{opt.label}</MessageResponse>
{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.
<MessageResponse className="lh-prose block text-xs text-muted-foreground">
{descByIndex.get(opt.index)}
</MessageResponse>
)}
</span>
</div>
);