diff --git a/src/components/insights/__tests__/recommendation-card-confidence.test.tsx b/src/components/insights/__tests__/recommendation-card-confidence.test.tsx
new file mode 100644
index 000000000..ad27d0ae0
--- /dev/null
+++ b/src/components/insights/__tests__/recommendation-card-confidence.test.tsx
@@ -0,0 +1,141 @@
+import { describe, it, expect, vi } from "vitest";
+import { renderToStaticMarkup } from "react-dom/server";
+import { I18nProvider } from "@/lib/i18n/context";
+import { RecommendationCard } from "../recommendation-card";
+
+vi.mock("@/components/charts/health-chart", () => ({
+ HealthChart: () =>
,
+}));
+
+vi.mock("@/components/charts/mood-chart", () => ({
+ MoodChart: () => ,
+}));
+
+/**
+ * v1.4.16 phase B5d — confidence meter wiring inside
+ * ``.
+ *
+ * The card reserves a `data-slot="rec-confidence-slot"` placeholder
+ * (B5c). B5d fills that slot with ``. Behaviour:
+ *
+ * - rec.confidence undefined -> slot stays empty (legacy payload).
+ * - >=50 -> meter only.
+ * - 25..49 -> meter + low-confidence caption inside the expanded
+ * rationale card ("Low confidence — based on limited data").
+ * - <25 -> "draft" pill replaces the meter (handled by
+ * itself); the low-confidence caption ALSO
+ * surfaces because draft <= low.
+ *
+ * The meter sits in the collapsed-row slot so users can see the
+ * confidence at a glance without expanding.
+ */
+
+const baseRationale = {
+ dataWindow: "last7days" as const,
+ comparedTo: "your 90-day median (122/78)",
+ deviation: "+16/+8 mmHg above baseline over 9 of 9 readings",
+};
+
+const recBase = {
+ id: "rec-1",
+ text: "Discuss home BP log with your physician.",
+ severity: "important" as const,
+ rationale: baseRationale,
+ metricSource: {
+ type: "bloodPressure",
+ timeRange: "last7days",
+ summary: "avg 138/86 across 9 readings",
+ },
+};
+
+function render(node: React.ReactNode, locale: "en" | "de" = "en") {
+ return renderToStaticMarkup(
+ {node},
+ );
+}
+
+describe(" — confidence slot wiring", () => {
+ it("renders ConfidenceMeter inside rec-confidence-slot when confidence is set", () => {
+ const html = render(
+ ,
+ );
+ // Slot wraps the meter
+ expect(html).toMatch(
+ /data-slot="rec-confidence-slot"[\s\S]*?data-slot="confidence-meter"/,
+ );
+ expect(html).toContain('data-confidence-band="high"');
+ });
+
+ it("leaves rec-confidence-slot empty when rec has no confidence (legacy)", () => {
+ const html = render();
+ // Slot is present but should not contain a confidence-meter inside it.
+ expect(html).toMatch(/data-slot="rec-confidence-slot"/);
+ expect(html).not.toContain('data-slot="confidence-meter"');
+ });
+
+ it("yellow band for medium-confidence (50-79)", () => {
+ const html = render(
+ ,
+ );
+ expect(html).toContain('data-confidence-band="medium"');
+ });
+
+ it("orange low band + low-confidence caption when value 25-49 and expanded", () => {
+ const html = render(
+ ,
+ );
+ expect(html).toContain('data-confidence-band="low"');
+ expect(html).toMatch(/Low confidence — based on limited data/);
+ });
+
+ it("low-confidence caption is suppressed when meter is high or medium", () => {
+ const html = render(
+ ,
+ );
+ expect(html).not.toMatch(/Low confidence/);
+ });
+
+ it("draft pill below 25 (rendered by ConfidenceMeter)", () => {
+ const html = render(
+ ,
+ );
+ expect(html).toContain('data-confidence-band="draft"');
+ expect(html).toMatch(/Draft/);
+ });
+
+ it("German locale renders translated low-confidence caption", () => {
+ const html = render(
+ ,
+ "de",
+ );
+ expect(html).toMatch(/Geringes Vertrauen/);
+ });
+
+ it("does NOT touch rec-feedback-slot (owned by B5e)", () => {
+ const html = render(
+ ,
+ );
+ // The feedback slot is still present, but B5d must not have
+ // injected anything into it.
+ expect(html).toMatch(/data-slot="rec-feedback-slot"[^>]*>\s*<\/div>/);
+ });
+});
diff --git a/src/components/insights/recommendation-card.tsx b/src/components/insights/recommendation-card.tsx
index 054853b12..b513d2076 100644
--- a/src/components/insights/recommendation-card.tsx
+++ b/src/components/insights/recommendation-card.tsx
@@ -17,6 +17,7 @@ import {
type RecommendationFeedbackSeverity,
type RecommendationFeedbackTimeRange,
} from "./recommendation-feedback";
+import { ConfidenceMeter } from "./confidence-meter";
/**
* v1.4.16 phase B5c — Oura-style RecommendationCard.
@@ -68,6 +69,13 @@ interface NormalisedRec {
metricSource?: { type: string; timeRange: string; summary: string };
referenceId?: string;
id?: string;
+ /**
+ * v1.4.16 phase B5d — server-computed confidence (0-100). Optional
+ * because legacy cached payloads predate the field; the rec card's
+ * confidence slot stays empty in that case rather than tagging
+ * legacy recs as "draft".
+ */
+ confidence?: number;
}
function normalise(rec: InsightRecommendation): NormalisedRec {
@@ -79,9 +87,20 @@ function normalise(rec: InsightRecommendation): NormalisedRec {
metricSource: rec.metricSource,
referenceId: rec.referenceId,
id: rec.id,
+ confidence: rec.confidence,
};
}
+/**
+ * v1.4.16 phase B5d — caption threshold. Below 50, the expanded
+ * rationale card surfaces a "Low confidence — based on limited data"
+ * sentence so the rec stays visible but framed as preliminary.
+ * Above 50 the meter alone speaks; below 25 the meter is replaced by
+ * a draft pill (handled inside ``) and the caption
+ * still applies (draft <= low).
+ */
+const LOW_CONFIDENCE_CAPTION_THRESHOLD = 50;
+
/**
* Map a `metricSource.type` value to the chart-types[] the dashboard
* uses. The model speaks snapshot-key vocabulary ("bloodPressure",
@@ -174,12 +193,14 @@ function RationaleCard({
rationale,
metricSource,
referenceId,
+ confidence,
locale,
feedbackProps,
}: {
rationale: InsightRecommendationRationale;
metricSource: { type: string; timeRange: string; summary: string } | undefined;
referenceId: string | undefined;
+ confidence: number | undefined;
locale: Locale;
feedbackProps: {
recId: string;
@@ -192,6 +213,9 @@ function RationaleCard({
const { t } = useTranslations();
const metricType = metricSource?.type;
const chartTypes = metricTypeToChartTypes(metricType);
+ const showLowConfidenceCaption =
+ typeof confidence === "number" &&
+ confidence < LOW_CONFIDENCE_CAPTION_THRESHOLD;
return (
) : null}
+ {showLowConfidenceCaption && (
+
+ {t("insights.recommendation.confidenceLow")}
+
+ )}
+
{referenceId && (
@@ -326,7 +359,11 @@ export function RecommendationCard({
-
+
+ {typeof norm.confidence === "number" && (
+
+ )}
+
{expandable && (