ui: conclude card — quiz-chip outcomes, verdict-first; YOUR THINKING / AGENT THINKING
Owner round 3 (2026-10-05): - CONCLUDE CARD: the stacked same-weight headings are gone. Plain 'Concluded' eyebrow + the outcome in the quiz cards' EXACT chip register. Colors per outcome: solved green (the 'correct' twin), solvedWithHints amber (succeeded with cost), surrendered red (the problem won), movedOn muted (the agent's call, not a verdict against the learner). Verdict as plain 50% prose under the heading — the 'Verdict:' label is gone — then a divider, then the labeled model solution. - ALL-CAPS: 'YOUR THINKING' and 'AGENT THINKING' — display strings only (the model-facing marker label stays sentence-case). The two rows are equal-length over the shared rail, so the chevrons align vertically. Dock field labels match.
This commit is contained in:
@@ -137,7 +137,7 @@ const defaultGetThinkingMessage = (isStreaming: boolean) => {
|
||||
if (isStreaming) {
|
||||
return <Shimmer duration={1}>Thinking...</Shimmer>;
|
||||
}
|
||||
return <p>Agent's thinking</p>;
|
||||
return <p>AGENT THINKING</p>;
|
||||
};
|
||||
|
||||
export const ReasoningTrigger = memo(
|
||||
|
||||
@@ -41,7 +41,7 @@ export function AnsweredCard({ answer }: { answer: QuestionAnswerData }) {
|
||||
ThinkingBlock, instead of an inset inside the answer. Same muted
|
||||
rail as before; the label is now plain "Your thinking". */}
|
||||
{answer.readingThinking && (
|
||||
<ThinkingBlock testId="reading-thinking-block" label="Your thinking" text={answer.readingThinking} />
|
||||
<ThinkingBlock testId="reading-thinking-block" label="YOUR THINKING" text={answer.readingThinking} />
|
||||
)}
|
||||
<div className="rounded-xl border bg-card p-4 text-card-foreground" data-testid="answered-card">
|
||||
<div className="mb-1 flex items-center gap-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
@@ -139,7 +139,7 @@ export function AnsweredCard({ answer }: { answer: QuestionAnswerData }) {
|
||||
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. */
|
||||
<ThinkingBlock testId="your-thinking" label="Your thinking" text={answer.thinking ?? answer.note ?? ""} />
|
||||
<ThinkingBlock testId="your-thinking" label="YOUR THINKING" text={answer.thinking ?? answer.note ?? ""} />
|
||||
)}
|
||||
|
||||
{/* px-3→removed: card p-4 + box px-3 double-inset sat the "Why" label deeper
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { MessageResponse } from "@/components/ai-elements/message";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ArtifactPartProps } from "@/src/core";
|
||||
|
||||
/**
|
||||
@@ -54,6 +55,19 @@ const OUTCOME_LABEL: Record<string, string> = {
|
||||
dismissed: "Dismissed",
|
||||
};
|
||||
|
||||
/* The outcome chips (owner round 3, 2026-10-05): the EXACT register of the
|
||||
* quiz cards' correct/incorrect bubbles. Colors per outcome — solved reads
|
||||
* green (the "correct" twin), solved-with-hints amber (succeeded with
|
||||
* cost), surrendered red (the problem beat the learner), moved on muted
|
||||
* (the agent's call, not a verdict against him). */
|
||||
const OUTCOME_CHIP: Record<string, string> = {
|
||||
solved: "bg-green-500/15 text-green-500",
|
||||
solvedWithHints: "bg-amber-500/15 text-amber-500",
|
||||
movedOn: "bg-muted text-muted-foreground",
|
||||
surrendered: "bg-red-500/15 text-red-500",
|
||||
dismissed: "bg-muted text-muted-foreground",
|
||||
};
|
||||
|
||||
export function ConcludeCard({ part }: ArtifactPartProps) {
|
||||
// The conclusion redesigned (owner round, 2026-10-05): the SOLUTION is
|
||||
// the hero — the modelSolution committed at pose time (part.reveal, the
|
||||
@@ -79,26 +93,36 @@ export function ConcludeCard({ part }: ArtifactPartProps) {
|
||||
|
||||
return (
|
||||
<div data-testid="conclude-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">
|
||||
{OUTCOME_LABEL[outcome] ?? "Concluded"}
|
||||
{/* The heading row (owner round 3, 2026-10-05): plain 'Concluded'
|
||||
eyebrow + the outcome in the quiz cards' EXACT chip register —
|
||||
no more two same-weight headings stacked. */}
|
||||
<div className="mb-1 flex items-center gap-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
Concluded
|
||||
{(OUTCOME_LABEL[outcome] ?? "") !== "" && outcome !== "dismissed" && (
|
||||
<span className={cn("rounded px-1.5 py-0.5", OUTCOME_CHIP[outcome] ?? OUTCOME_CHIP.movedOn)}>
|
||||
{OUTCOME_LABEL[outcome]}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* LABELED SECTIONS (owner round 2, 2026-10-05): 'Solved' alone over
|
||||
unlabeled prose didn't read as the model solution — the hero now
|
||||
carries its own label, and the verdict section mirrors it. Same
|
||||
register as the quiz cards' section labels. */}
|
||||
{reveal.length > 0 && (
|
||||
<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>{reveal}</MessageResponse>
|
||||
</div>
|
||||
{/* The verdict — plain prose under the heading, no 'Verdict:' label
|
||||
(owner round 3): the same quiet 50% register as quiz explanations. */}
|
||||
{verdict.length > 0 && (
|
||||
<div className="text-sm text-foreground/50">
|
||||
<MessageResponse>{verdict}</MessageResponse>
|
||||
</div>
|
||||
)}
|
||||
{verdict.length > 0 && (
|
||||
<div className="mt-3 rounded-lg bg-muted py-2 text-sm text-foreground/50">
|
||||
<span className="font-medium text-muted-foreground">Verdict: </span>
|
||||
<MessageResponse className="inline [&_p]:inline [&_p]:m-0">{verdict}</MessageResponse>
|
||||
</div>
|
||||
{/* Then the divider, then the labeled model solution (Slice G: the
|
||||
hero is the pose's commitment, verbatim). */}
|
||||
{reveal.length > 0 && (
|
||||
<>
|
||||
<div className="my-3 border-t" />
|
||||
<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>{reveal}</MessageResponse>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -108,5 +132,5 @@ export function ThinkingCard({ data }: { data: PracticeThinkingData }) {
|
||||
// The unified row (owner rounds, 2026-10-05): one style, one label —
|
||||
// collapsed arrival, "Your thinking", 50%-italic quote block on expand.
|
||||
// Legacy records' commit flag is deliberately unread (one lane now).
|
||||
return <ThinkingBlock testId="thinking-card" label="Your thinking" text={data.text} />;
|
||||
return <ThinkingBlock testId="thinking-card" label="YOUR THINKING" text={data.text} />;
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ export function PracticeDock({ problemCallId, input, expanded, onExpand, onSubmi
|
||||
className="m-1.5 rounded-xl border border-border/70 bg-background/60 transition-colors focus-within:border-primary/50"
|
||||
>
|
||||
<div className="px-2.5 pt-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
Your thinking
|
||||
YOUR THINKING
|
||||
</div>
|
||||
<textarea
|
||||
value={thinking}
|
||||
|
||||
@@ -772,7 +772,7 @@ function QuestionDockForm({
|
||||
className="rounded-xl border border-border/70 bg-background/60 transition-colors focus-within:border-primary/50"
|
||||
>
|
||||
<div className="px-2.5 pt-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
{qd.mode === "text" ? "Your answer" : "Your thinking"}
|
||||
{qd.mode === "text" ? "Your answer" : "YOUR THINKING"}
|
||||
</div>
|
||||
{qd.mode === "text" ? (
|
||||
<textarea
|
||||
|
||||
@@ -18,7 +18,7 @@ import { cn } from "@/lib/utils";
|
||||
* agent thinking = no rail. */
|
||||
|
||||
export function ThinkingBlock({
|
||||
label = "Your thinking",
|
||||
label = "YOUR THINKING",
|
||||
text,
|
||||
testId,
|
||||
dataCommit,
|
||||
|
||||
@@ -142,7 +142,7 @@ describe("render smoke tests", () => {
|
||||
// The learner's voice: a labeled "Your thinking" composer-twin lives
|
||||
// INSIDE the dock — never called optional, inviting the dump.
|
||||
assert.ok(html.includes('data-testid="thinking-field"'), "thinking field present");
|
||||
assert.match(html, /Your thinking/, "labeled as the learner's voice");
|
||||
assert.match(html, /YOUR THINKING/, "labeled as the learner's voice (all caps, owner round 3)");
|
||||
assert.match(html, /Why this answer\? What are you unsure about\?/, "placeholder invites, not apologizes");
|
||||
assert.ok(!html.includes("optional"), "the field is never called optional");
|
||||
assert.match(html, /Submit answer/, "send becomes Submit answer");
|
||||
@@ -171,7 +171,7 @@ describe("render smoke tests", () => {
|
||||
assert.ok(html.includes('role="radio"'), "Other row keeps the option role");
|
||||
assert.match(html, /aria-checked="false"/, "circle starts unfilled");
|
||||
assert.ok(html.includes('data-testid="thinking-field"'), "thinking field coexists with Other");
|
||||
assert.match(html, /Your thinking/, "ask select-mode also gets the learner's voice");
|
||||
assert.match(html, /YOUR THINKING/, "ask select-mode also gets the learner's voice");
|
||||
});
|
||||
|
||||
it("multi-select ask renders checkbox rows and the select-all hint", () => {
|
||||
@@ -287,7 +287,7 @@ describe("render smoke tests", () => {
|
||||
});
|
||||
const html = renderToStaticMarkup(<AnsweredCard answer={answer} />);
|
||||
assert.ok(html.includes('data-testid="your-thinking"'), "the thinking row exists");
|
||||
assert.match(html, /Your thinking/, "labeled as the learner's voice");
|
||||
assert.match(html, /YOUR THINKING/, "labeled as the learner's voice (all caps, owner round 3)");
|
||||
// COLLAPSED ARRIVAL (owner round 2, 2026-10-05): the SSR output is the
|
||||
// trigger row only — the thinking text is one click away, never loud.
|
||||
assert.ok(html.includes('data-testid="your-thinking-trigger"'), "the trigger row");
|
||||
@@ -595,7 +595,7 @@ describe("predict vs check rendering", () => {
|
||||
const cardAt = html.indexOf("answered-card");
|
||||
assert.ok(blockAt !== -1, "the lifted block renders");
|
||||
assert.ok(blockAt < cardAt, "it sits ABOVE the answered card");
|
||||
assert.ok(html.includes("Your thinking"), "the plain label");
|
||||
assert.ok(html.includes("YOUR THINKING"), "the plain label");
|
||||
assert.ok(!html.includes("Your thinking while reading"), "the old verbose label is gone");
|
||||
assert.ok(!html.includes("your-reading-thinking"), "the in-card inset is gone");
|
||||
// COLLAPSED ARRIVAL: SSR shows the triggers, not the texts.
|
||||
|
||||
@@ -55,7 +55,7 @@ describe("PracticeDock", () => {
|
||||
assert.match(html, /Problem/);
|
||||
// The measured column cap (the old fixed 40vh is only the fallback).
|
||||
assert.match(html, /--dock-max-h/);
|
||||
assert.match(html, /Your thinking/);
|
||||
assert.match(html, /YOUR THINKING/);
|
||||
assert.match(html, /data-testid="practice-send"/);
|
||||
assert.ok(!html.includes("practice-nudge") && !html.includes("practice-commit"), "the two-lane buttons are gone");
|
||||
// The buttons carry their lane in the disabled gate; both start
|
||||
@@ -114,11 +114,14 @@ describe("the practice transcript cards", () => {
|
||||
assert.match(html, /data-testid="conclude-card"/);
|
||||
assert.match(html, /Solved with hints/);
|
||||
assert.match(html, /Pair them; each pair is even\./, "the committed solution, verbatim");
|
||||
assert.match(html, /Model solution/); // the labeled hero (owner round 2)
|
||||
assert.match(html, /Verdict:/);
|
||||
assert.match(html, /Concluded/); // the plain eyebrow (owner round 3)
|
||||
assert.match(html, /Solved/); // the outcome rides the quiz chip
|
||||
assert.match(html, /Model solution/); // the labeled hero after the divider
|
||||
assert.ok(html.indexOf("Solved") < html.indexOf("Model solution"), "chip at the top");
|
||||
assert.match(html, /The pairing came after R2\./);
|
||||
// The verdict row renders AFTER the solution (the hero first).
|
||||
assert.ok(html.indexOf("Pair them; each pair is even.") < html.indexOf("The pairing came after R2."));
|
||||
// VERDICT FIRST (owner round 3): verdict under the heading row, then
|
||||
// the divider, then the labeled model solution.
|
||||
assert.ok(html.indexOf("The pairing came after R2.") < html.indexOf("Pair them; each pair is even."));
|
||||
assert.doesNotMatch(html, /<button/, "nothing collapsible");
|
||||
});
|
||||
|
||||
@@ -144,11 +147,11 @@ describe("the practice transcript cards", () => {
|
||||
|
||||
it("ThinkingCard: one label, collapsed arrival (legacy commit flags ignored)", () => {
|
||||
const trace = renderToStaticMarkup(<ThinkingCard data={{ problemCallId: "c", commit: false, text: "stuck at step two" }} />);
|
||||
assert.match(trace, /Your thinking/);
|
||||
assert.match(trace, /YOUR THINKING/);
|
||||
assert.match(trace, /aria-expanded="false"/);
|
||||
assert.ok(!trace.includes("stuck at step two"), "the text is one click away");
|
||||
const legacy = renderToStaticMarkup(<ThinkingCard data={{ problemCallId: "c", commit: true, text: "It is 42." }} />);
|
||||
assert.match(legacy, /Your thinking/);
|
||||
assert.match(legacy, /YOUR THINKING/);
|
||||
assert.doesNotMatch(legacy, /Your solution/);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user