From 6da192bb2241b9f926076f37c45f8c6f157e01c9 Mon Sep 17 00:00:00 2001 From: amosblomqvist Date: Fri, 2 Oct 2026 01:21:12 +0300 Subject: [PATCH] =?UTF-8?q?answered=20quiz=20cards=20show=20the=20confiden?= =?UTF-8?q?ce=20rail=20=E2=80=94=20the=20slider's=20record=20form?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rail paint is extracted to components/confidence-rail.tsx (ramp, tick stack, geometry laws) and shared by both surfaces so they cannot drift: the dock's live slider paints the rail under its disc, the answered card renders it disc-less — the missing handle itself says 'record, not control'. Same look otherwise: thin rail, stop color, solid ticks, % read-out on the right, dock-mirrored placement (below the options, above the thinking). Renders only where a confidence exists: quiz answers with one sent; never on 'I don't know' (none was sent), never on explain/ask, never on pre-feature records (optional field, absent). --- components/answered-card.tsx | 17 ++++++++++ components/confidence-rail.tsx | 61 ++++++++++++++++++++++++++++++++++ components/question-dock.tsx | 32 ++---------------- tests/render-cards.test.tsx | 21 ++++++++++++ 4 files changed, 101 insertions(+), 30 deletions(-) create mode 100644 components/confidence-rail.tsx diff --git a/components/answered-card.tsx b/components/answered-card.tsx index b989cee..5639fe1 100644 --- a/components/answered-card.tsx +++ b/components/answered-card.tsx @@ -1,5 +1,6 @@ "use client"; +import { ConfidenceRail } from "@/components/confidence-rail"; import { GlyphMark } from "@/components/ui/glyph-mark"; import { cn } from "@/lib/utils"; import { MessageResponse } from "@/components/ai-elements/message"; @@ -101,6 +102,22 @@ export function AnsweredCard({ answer }: { answer: QuestionAnswerData }) { )} {answer.text &&
“{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 && (
Answered “I don't know”.
)} diff --git a/components/confidence-rail.tsx b/components/confidence-rail.tsx new file mode 100644 index 0000000..daa14de --- /dev/null +++ b/components/confidence-rail.tsx @@ -0,0 +1,61 @@ +import type { CSSProperties } from "react"; +import { cn } from "@/lib/utils"; +import { CONFIDENCE_STOPS } from "@/src/core"; + +/** + * The confidence RAIL — the slider's paint, shared by two surfaces so they + * can never drift apart (owner, 2026-10-01): + * + * - the dock's live slider (question-dock) — a transparent range input + * rides on top of this paint, disc and all; + * - the ANSWERED card (owner round 6) — the same rail with NO disc, + * which itself communicates "this is a record, not a control". + * + * Geometry law: the rail spans the thumb-CENTER range (inset half a 16px + * disc per side) so the dock's 0/100 stops sit flush at the very ends. + * The static card has no thumb, but keeps the identical shape. + * + * Stack law: #141414 base (the darker main background), SOLID stop ticks + * as background layers UNDER the fill, and the fill itself TRANSPARENT + * past the value — so ticks show only on the empty side, never over the + * color. Ticks skip 0 and 100 (the rail's ends already say those). + */ + +/** The six-step ramp (owner ruling): 0 red → 100 emerald, indexed by stop. */ +export const CONFIDENCE_COLORS: readonly string[] = [ + "#ef4444", // 0 + "#f97316", // 30 + "#eab308", // 50 + "#84cc16", // 70 + "#22c55e", // 90 + "#10b981", // 100 +]; + +/** The rail's paint for a stop value (0–100). */ +export function confidenceRailStyle(value: number): CSSProperties { + const color = CONFIDENCE_COLORS[CONFIDENCE_STOPS.indexOf(value)] ?? CONFIDENCE_COLORS[2]!; + return { + backgroundColor: "#141414", + backgroundImage: [ + `linear-gradient(90deg, ${color} 0%, ${color} ${value}%, transparent ${value}%, transparent 100%)`, + ...[30, 50, 70, 90].map( + () => "linear-gradient(90deg, rgba(230,230,230,0.35), rgba(230,230,230,0.35))", + ), + ].join(", "), + backgroundSize: ["100% 100%", "1px 100%", "1px 100%", "1px 100%", "1px 100%"].join(", "), + backgroundPosition: ["0 0", ...[30, 50, 70, 90].map((p) => `${p}% 0`)].join(", "), + backgroundRepeat: "no-repeat", + }; +} + +/** The static rail — the slider's look, disc removed. */ +export function ConfidenceRail({ value, className }: { value: number; className?: string }) { + return ( +
+ ); +} diff --git a/components/question-dock.tsx b/components/question-dock.tsx index 3a53283..d652719 100644 --- a/components/question-dock.tsx +++ b/components/question-dock.tsx @@ -32,6 +32,7 @@ import { type SelectMode, type Phase3, } from "@/src/core"; +import { CONFIDENCE_COLORS, confidenceRailStyle } from "@/components/confidence-rail"; /** * The question dock — a pending quiz/ask REPLACES the chat composer: the @@ -79,19 +80,6 @@ export interface QuestionDockProps { phase?: Phase3; } -/** The six-step confidence ramp (owner, UI round 2026-10-01): color - * follows the number — each stop reads as its own belief level, red - * (believes the pick wrong) through emerald (certain), yellow the coin - * flip. Pairs with CONFIDENCE_STOPS index-for-index. */ -const CONFIDENCE_COLORS: readonly string[] = [ - "#ef4444", // 0 — red - "#f97316", // 30 — orange - "#eab308", // 50 — yellow: a coin flip - "#84cc16", // 70 — lime - "#22c55e", // 90 — green - "#10b981", // 100 — emerald -]; - export function QuestionDock({ toolName, toolCallId, @@ -661,23 +649,7 @@ function QuestionDockForm({ aria-hidden data-testid="confidence-rail" className="pointer-events-none absolute top-1/2 left-2 right-2 h-1.5 -translate-y-1/2 rounded-full border border-border" - style={{ - // Stack: #141414 base (the darker main background), SOLID stop - // ticks as background LAYERS, fill on top — TRANSPARENT past the - // stop, so the ticks read as background and never paint over the - // color. Ticks skip 0 and 100 (the rail's flush ends already say - // those); positions are plain p% — the overlay IS the center range. - backgroundColor: "#141414", - backgroundImage: [ - `linear-gradient(90deg, ${CONFIDENCE_COLORS[confIdx]} 0%, ${CONFIDENCE_COLORS[confIdx]} ${CONFIDENCE_STOPS[confIdx]}%, transparent ${CONFIDENCE_STOPS[confIdx]}%, transparent 100%)`, - ...[30, 50, 70, 90].map( - () => "linear-gradient(90deg, rgba(230,230,230,0.35), rgba(230,230,230,0.35))", - ), - ].join(", "), - backgroundSize: ["100% 100%", "1px 100%", "1px 100%", "1px 100%", "1px 100%"].join(", "), - backgroundPosition: ["0 0", ...[30, 50, 70, 90].map((p) => `${p}% 0`)].join(", "), - backgroundRepeat: "no-repeat", - }} + style={confidenceRailStyle(CONFIDENCE_STOPS[confIdx]!)} /> { assert.ok(html.includes('data-mark="check"') && html.includes('data-mark="x"')); }); + it("AnsweredCard shows the confidence rail on quiz answers (record, no disc)", () => { + // The slider's rail, disc removed: same paint, same ticks, the % read-out. + const answer = buildAnswer(quizQD(), { values: ["Venus"], confidence: 70 }); + const html = renderToStaticMarkup(); + assert.match(html, /data-testid="answered-confidence"/); + assert.match(html, /data-testid="confidence-rail"/); + assert.match(html, /70%/); + // It is a RECORD, not a control — no range input on the card. + assert.ok(!html.includes('type="range"')); + }); + + it("AnsweredCard hides the rail where no confidence exists (IDK, older records)", () => { + const idk = buildAnswer(quizQD(), { values: [], dontKnow: true }); + const idkHtml = renderToStaticMarkup(); + assert.ok(!idkHtml.includes("answered-confidence"), "IDK sent no confidence"); + + const ask = buildAnswer(askQD(), { other: "Shaky" }); + const askHtml = renderToStaticMarkup(); + assert.ok(!askHtml.includes("answered-confidence"), "ask never had one"); + }); + it("AnsweredCard renders the learner's own Other answer AS an option row", () => { const answer = buildAnswer(askQD(), { other: "Fairly confident — but shaky on applications" }); const html = renderToStaticMarkup();