answered quiz cards show the confidence rail — the slider's record form
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).
This commit is contained in:
@@ -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 && <div className="lh-prose mt-2 whitespace-pre-wrap">“{answer.text}”</div>}
|
||||
|
||||
{/* 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 && (
|
||||
<div className="mt-3 flex items-center gap-3" data-testid="answered-confidence">
|
||||
<ConfidenceRail value={answer.confidence} className="min-w-0 flex-1" />
|
||||
<span className="w-9 shrink-0 text-right font-mono text-sm tabular-nums text-muted-foreground">
|
||||
{answer.confidence}%
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{answer.dontKnow && isQuiz && (
|
||||
<div className="mt-2 text-sm text-muted-foreground">Answered “I don't know”.</div>
|
||||
)}
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
aria-hidden
|
||||
data-testid="confidence-rail"
|
||||
className={cn("h-1.5 w-full rounded-full border border-border", className)}
|
||||
style={confidenceRailStyle(value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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]!)}
|
||||
/>
|
||||
<input
|
||||
type="range"
|
||||
|
||||
Reference in New Issue
Block a user