fix: preserve source data and recover tool math at presentation
This commit is contained in:
@@ -34,7 +34,7 @@ import {
|
||||
type PluginConfig,
|
||||
} from "streamdown";
|
||||
import { mdComponents } from "@/components/md-blocks";
|
||||
import { transformMathDelimiters } from "@/src/core";
|
||||
import { repairMathForDisplay, transformMathDelimiters } from "@/src/core";
|
||||
|
||||
export type MessageProps = HTMLAttributes<HTMLDivElement> & {
|
||||
from: LhViewMessage["role"];
|
||||
@@ -388,6 +388,16 @@ export const MessageResponse = memo(
|
||||
|
||||
MessageResponse.displayName = "MessageResponse";
|
||||
|
||||
/** Recovery is a tool-text presentation concern, not a shape/answer-data
|
||||
* transform. Ordinary assistant/user prose keeps MessageResponse unchanged. */
|
||||
export function ToolContentResponse({ children, ...props }: MessageResponseProps) {
|
||||
return (
|
||||
<MessageResponse {...props}>
|
||||
{typeof children === "string" ? repairMathForDisplay(children) : children}
|
||||
</MessageResponse>
|
||||
);
|
||||
}
|
||||
|
||||
export type MessageToolbarProps = ComponentProps<"div">;
|
||||
|
||||
export const MessageToolbar = ({
|
||||
|
||||
@@ -5,9 +5,9 @@ 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 { ToolContentResponse } from "@/components/ai-elements/message";
|
||||
import type { QuestionAnswerData } from "@/src/core";
|
||||
import { parseAskInput, parseQuizInput } from "@/src/core";
|
||||
import { answerForPresentation, parseAskInput, parseQuizInput } from "@/src/core";
|
||||
|
||||
/**
|
||||
* Static rendering of an answered question (tool part in `output-available`).
|
||||
@@ -19,7 +19,8 @@ import { parseAskInput, parseQuizInput } 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, input }: { answer: QuestionAnswerData; input?: unknown }) {
|
||||
export function AnsweredCard({ answer: savedAnswer, input }: { answer: QuestionAnswerData; input?: unknown }) {
|
||||
const answer = answerForPresentation(savedAnswer, input);
|
||||
const [whyOpen, setWhyOpen] = useState(false); // the tucked explanation (predictions, owner 2026-10-03)
|
||||
if (answer.dismissed) {
|
||||
return (
|
||||
@@ -86,13 +87,13 @@ export function AnsweredCard({ answer, input }: { answer: QuestionAnswerData; in
|
||||
))}
|
||||
</div>
|
||||
<div className="mb-3 font-medium">
|
||||
<MessageResponse>{answer.questionText}</MessageResponse>
|
||||
<ToolContentResponse>{answer.questionText}</ToolContentResponse>
|
||||
</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>
|
||||
<ToolContentResponse>{parsedDetails}</ToolContentResponse>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -131,14 +132,14 @@ export function AnsweredCard({ answer, input }: { answer: QuestionAnswerData; in
|
||||
</span>
|
||||
)}
|
||||
<span className={cn(!sel && "text-muted-foreground")}>
|
||||
<MessageResponse>{opt.label}</MessageResponse>
|
||||
<ToolContentResponse>{opt.label}</ToolContentResponse>
|
||||
{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">
|
||||
<ToolContentResponse className="lh-prose block text-xs text-muted-foreground">
|
||||
{descByIndex.get(opt.index)}
|
||||
</MessageResponse>
|
||||
</ToolContentResponse>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -211,9 +212,9 @@ export function AnsweredCard({ answer, input }: { answer: QuestionAnswerData; in
|
||||
{whyOpen && (
|
||||
<div className="mt-1.5 rounded-lg bg-muted py-2 text-sm text-foreground/50">
|
||||
<span className="font-medium text-muted-foreground">Why: </span>
|
||||
{/* The markdown wrapper must go truly inline — a block <p> after the
|
||||
"Why:" label created the oversized leading gap (owner's fix list). */}
|
||||
<MessageResponse className="inline [&_p]:inline [&_p]:m-0">{answer.explanation}</MessageResponse>
|
||||
{/* Keep the first paragraph beside the label, not every paragraph:
|
||||
later paragraphs must retain their recovered document structure. */}
|
||||
<ToolContentResponse className="inline [&>p:first-child]:inline [&>p:first-child]:m-0">{answer.explanation}</ToolContentResponse>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -225,7 +226,7 @@ export function AnsweredCard({ answer, input }: { answer: QuestionAnswerData; in
|
||||
{isExplain && answer.committed && (
|
||||
<div className="mt-3 rounded-lg bg-muted py-2 text-sm text-muted-foreground">
|
||||
<span className="font-medium text-foreground">The answer: </span>
|
||||
<MessageResponse className="inline [&_p]:inline [&_p]:m-0">{answer.committed}</MessageResponse>
|
||||
<ToolContentResponse className="inline [&>p:first-child]:inline [&>p:first-child]:m-0">{answer.committed}</ToolContentResponse>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { MessageResponse } from "@/components/ai-elements/message";
|
||||
import { ToolContentResponse } from "@/components/ai-elements/message";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { repairMathForDisplay, type ArtifactPartProps } from "@/src/core";
|
||||
import type { ArtifactPartProps } from "@/src/core";
|
||||
|
||||
/**
|
||||
* The practice transcript cards (practice sessions, owner design
|
||||
@@ -66,7 +66,7 @@ export function ProblemCard({ part }: ArtifactPartProps) {
|
||||
<div data-testid="problem-card" className="lh-card-enter my-1 rounded-xl border bg-card p-4">
|
||||
<div className="mb-1 text-xs font-medium uppercase tracking-wide text-muted-foreground">Problem</div>
|
||||
<div className="lh-prose text-[15px]">
|
||||
<MessageResponse>{repairMathForDisplay(statement)}</MessageResponse>
|
||||
<ToolContentResponse>{statement}</ToolContentResponse>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -133,7 +133,7 @@ export function ConcludeCard({ part }: ArtifactPartProps) {
|
||||
(owner round 3): the same quiet 50% register as quiz explanations. */}
|
||||
{verdict.length > 0 && (
|
||||
<div className="text-sm text-foreground/50">
|
||||
<MessageResponse>{repairMathForDisplay(verdict)}</MessageResponse>
|
||||
<ToolContentResponse>{verdict}</ToolContentResponse>
|
||||
</div>
|
||||
)}
|
||||
{/* Then the divider, then the labeled model solution (Slice G: the
|
||||
@@ -144,7 +144,7 @@ export function ConcludeCard({ part }: ArtifactPartProps) {
|
||||
<div>
|
||||
<div className="mb-0.5 text-xs font-medium uppercase tracking-wide text-muted-foreground">Model solution</div>
|
||||
<div className="lh-prose text-[15px]">
|
||||
<MessageResponse>{repairMathForDisplay(reveal)}</MessageResponse>
|
||||
<ToolContentResponse>{reveal}</ToolContentResponse>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { ChevronRightIcon } from "lucide-react";
|
||||
import { MessageResponse } from "@/components/ai-elements/message";
|
||||
import { ToolContentResponse } from "@/components/ai-elements/message";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { joinDictation, useDictationTarget } from "@/components/dictation";
|
||||
import { useEnterSubmits } from "@/components/enter-submits";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { repairMathForDisplay } from "@/src/core";
|
||||
|
||||
/**
|
||||
* The practice dock (practice sessions, owner design 2026-10-04): an
|
||||
@@ -109,7 +108,7 @@ export function PracticeDock({ problemCallId, input, expanded, onExpand, onSubmi
|
||||
<div className="max-h-[var(--dock-max-h,40vh)] overflow-y-auto px-4 pt-3 pb-2">
|
||||
<div className="mb-1 text-xs font-medium uppercase tracking-wide text-muted-foreground">Problem</div>
|
||||
<div className="lh-prose text-[15px]">
|
||||
<MessageResponse>{typeof input.statement === "string" ? repairMathForDisplay(input.statement) : input.statement}</MessageResponse>
|
||||
<ToolContentResponse>{input.statement}</ToolContentResponse>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,10 +5,11 @@ import { ArrowDownIcon, ChevronRightIcon, PlayIcon } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { finishDictation, isDictationActive, joinDictation, useDictationTarget } from "@/components/dictation";
|
||||
import { useEnterSubmits } from "@/components/enter-submits";
|
||||
import { MessageResponse } from "@/components/ai-elements/message";
|
||||
import { ToolContentResponse } from "@/components/ai-elements/message";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
buildAnswer,
|
||||
repairMathForDisplay,
|
||||
normalizeQuizType,
|
||||
CONFIDENCE_STOPS,
|
||||
DEFAULT_CONFIDENCE_INDEX,
|
||||
@@ -166,7 +167,7 @@ export function QuestionDock({
|
||||
|
||||
/** A plain-text teaser of a question for the collapsed bar: unwrap math, flatten common LaTeX. */
|
||||
function teaserText(q: string): string {
|
||||
return q
|
||||
return repairMathForDisplay(q)
|
||||
.replace(/\\\(([\s\S]*?)\\\)/g, "$1") // \(...\) inline math
|
||||
.replace(/\\\[([\s\S]*?)\\\]/g, "$1") // \[...\] display math
|
||||
.replace(/\$\$?([^$]*)\$\$?/g, "$1") // standard $...$/$$...$$ math
|
||||
@@ -560,11 +561,11 @@ function QuestionDockForm({
|
||||
{qd.mode === "multi-select" && " · select all that apply"}
|
||||
</div>
|
||||
<div className="mb-3 font-medium">
|
||||
<MessageResponse>{qd.question}</MessageResponse>
|
||||
<ToolContentResponse>{qd.question}</ToolContentResponse>
|
||||
</div>
|
||||
{qd.context && (
|
||||
<div className="mb-3 text-sm text-muted-foreground">
|
||||
<MessageResponse>{qd.context}</MessageResponse>
|
||||
<ToolContentResponse>{qd.context}</ToolContentResponse>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -600,7 +601,7 @@ function QuestionDockForm({
|
||||
)}
|
||||
</span>
|
||||
<span>
|
||||
<MessageResponse>{opt.label}</MessageResponse>
|
||||
<ToolContentResponse>{opt.label}</ToolContentResponse>
|
||||
{kind !== "quiz" && opt.description && (
|
||||
// Option subtext renders in Source Sans 3 (owner's mockup);
|
||||
// the "I don't know" copy stays JetBrains Mono. QUIZ options render
|
||||
@@ -609,9 +610,9 @@ function QuestionDockForm({
|
||||
// records must not leak either. Same MessageResponse path as the
|
||||
// label (owner, 2026-10-12): descriptions are full markdown —
|
||||
// LaTeX included — not plain text.
|
||||
<MessageResponse className="lh-prose block text-xs text-muted-foreground">
|
||||
<ToolContentResponse className="lh-prose block text-xs text-muted-foreground">
|
||||
{opt.description}
|
||||
</MessageResponse>
|
||||
</ToolContentResponse>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user