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();