diff --git a/messages/de.json b/messages/de.json index 69ac50dd5..d835c5c7f 100644 --- a/messages/de.json +++ b/messages/de.json @@ -2312,7 +2312,6 @@ } }, "comparison": { - "toggleLabel": "Vergleichen mit", "baseline": { "none": "Aus", "lastMonth": "Vormonat", @@ -2433,23 +2432,6 @@ "badge": "PR", "tooltip": "Persönlicher Rekord: {value} {unit} am {date}" }, - "recommendation": { - "source": "Quelle", - "viewSource": "Leitlinie in neuem Tab öffnen", - "rationaleWindow": "Fenster", - "rationaleComparedTo": "Verglichen mit", - "rationaleDeviation": "Abweichung", - "rationaleExpand": "Begründung anzeigen", - "rationaleCollapse": "Begründung ausblenden", - "feedbackHelpful": "Hilfreich", - "feedbackNotHelpful": "Nicht hilfreich", - "feedbackThanks": "Danke für dein Feedback", - "feedbackAlreadyRated": "Bereits bewertet", - "feedbackError": "Dein Feedback konnte nicht gespeichert werden. Bitte versuche es erneut.", - "confidenceAria": "Vertrauen: {value} von 100", - "confidenceLow": "Geringes Vertrauen — wenige Datenpunkte", - "confidenceDraft": "Entwurf" - }, "loadError": "Auswertungen konnten nicht geladen werden", "heroPersonalBaseline": "Basierend auf deinen letzten 90 Tagen", "heroRegenerating": "Wird generiert", @@ -2555,11 +2537,6 @@ "areas": "{present} von {required} Feldern der Gesundheit" } }, - "suggestedPrompts": { - "label": "Frag mich", - "tellMyDoctor": "Was sollte ich meinem Arzt sagen?", - "medicationWorking": "Wirkt mein Medikament?" - }, "dailyBriefing": { "title": "Tagesbriefing", "keyFindingsTitle": "Wichtige Befunde", diff --git a/messages/en.json b/messages/en.json index aec395564..10d6dd7a1 100644 --- a/messages/en.json +++ b/messages/en.json @@ -2312,7 +2312,6 @@ } }, "comparison": { - "toggleLabel": "Compare to", "baseline": { "none": "None", "lastMonth": "Last month", @@ -2433,23 +2432,6 @@ "badge": "PR", "tooltip": "Personal record: {value} {unit} on {date}" }, - "recommendation": { - "source": "Source", - "viewSource": "Open guideline in a new tab", - "rationaleWindow": "Window", - "rationaleComparedTo": "Compared to", - "rationaleDeviation": "Deviation", - "rationaleExpand": "Show why", - "rationaleCollapse": "Hide why", - "feedbackHelpful": "Helpful", - "feedbackNotHelpful": "Not helpful", - "feedbackThanks": "Thanks for your feedback", - "feedbackAlreadyRated": "Already rated", - "feedbackError": "Could not save your feedback. Please try again.", - "confidenceAria": "Confidence: {value} of 100", - "confidenceLow": "Low confidence — based on limited data", - "confidenceDraft": "Draft" - }, "loadError": "Failed to load insights", "heroPersonalBaseline": "Based on your last 90 days", "heroRegenerating": "Regenerating", @@ -2555,11 +2537,6 @@ "areas": "{present} of {required} areas of health" } }, - "suggestedPrompts": { - "label": "Try asking", - "tellMyDoctor": "What should I tell my doctor?", - "medicationWorking": "Is my medication working?" - }, "dailyBriefing": { "title": "Daily Briefing", "keyFindingsTitle": "Key findings", diff --git a/messages/es.json b/messages/es.json index fc40940dd..a42325704 100644 --- a/messages/es.json +++ b/messages/es.json @@ -2312,7 +2312,6 @@ } }, "comparison": { - "toggleLabel": "Comparar con", "baseline": { "none": "Desactivado", "lastMonth": "Mes anterior", @@ -2433,23 +2432,6 @@ "badge": "PR", "tooltip": "Récord personal: {value} {unit} el {date}" }, - "recommendation": { - "source": "Fuente", - "viewSource": "Abrir guía en una pestaña nueva", - "rationaleWindow": "Ventana", - "rationaleComparedTo": "Comparado con", - "rationaleDeviation": "Desviación", - "rationaleExpand": "Mostrar justificación", - "rationaleCollapse": "Ocultar justificación", - "feedbackHelpful": "Útil", - "feedbackNotHelpful": "No útil", - "feedbackThanks": "Gracias por tu opinión", - "feedbackAlreadyRated": "Ya valorado", - "feedbackError": "No se pudo guardar tu opinión. Inténtalo de nuevo.", - "confidenceAria": "Confianza: {value} de 100", - "confidenceLow": "Confianza baja — pocos datos", - "confidenceDraft": "Borrador" - }, "loadError": "No se han podido cargar los análisis", "heroPersonalBaseline": "Basado en tus últimos 90 días", "heroRegenerating": "Generando", @@ -2555,11 +2537,6 @@ "areas": "{present} de {required} ámbitos de salud" } }, - "suggestedPrompts": { - "label": "Pregúntame", - "tellMyDoctor": "¿Qué debería contarle a mi médico?", - "medicationWorking": "¿Está funcionando mi medicamento?" - }, "dailyBriefing": { "title": "Resumen del día", "keyFindingsTitle": "Hallazgos clave", diff --git a/messages/fr.json b/messages/fr.json index 458da22de..4374d6dd9 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -2312,7 +2312,6 @@ } }, "comparison": { - "toggleLabel": "Comparer avec", "baseline": { "none": "Désactivé", "lastMonth": "Mois précédent", @@ -2433,23 +2432,6 @@ "badge": "PR", "tooltip": "Record personnel : {value} {unit} le {date}" }, - "recommendation": { - "source": "Source", - "viewSource": "Ouvrir la recommandation dans un nouvel onglet", - "rationaleWindow": "Fenêtre", - "rationaleComparedTo": "Comparé à", - "rationaleDeviation": "Écart", - "rationaleExpand": "Afficher la justification", - "rationaleCollapse": "Masquer la justification", - "feedbackHelpful": "Utile", - "feedbackNotHelpful": "Peu utile", - "feedbackThanks": "Merci pour ton retour", - "feedbackAlreadyRated": "Déjà noté", - "feedbackError": "Impossible d’enregistrer votre retour. Réessayez.", - "confidenceAria": "Confiance : {value} sur 100", - "confidenceLow": "Confiance faible — peu de points de données", - "confidenceDraft": "Brouillon" - }, "loadError": "Impossible de charger les analyses", "heroPersonalBaseline": "Basé sur tes 90 derniers jours", "heroRegenerating": "Génération en cours", @@ -2555,11 +2537,6 @@ "areas": "{present} domaines de santé sur {required}" } }, - "suggestedPrompts": { - "label": "Demande-moi", - "tellMyDoctor": "Que devrais-je dire à mon médecin ?", - "medicationWorking": "Mon médicament fonctionne-t-il ?" - }, "dailyBriefing": { "title": "Briefing du jour", "keyFindingsTitle": "Constats clés", diff --git a/messages/it.json b/messages/it.json index a39e062c2..aeabdc9ab 100644 --- a/messages/it.json +++ b/messages/it.json @@ -2312,7 +2312,6 @@ } }, "comparison": { - "toggleLabel": "Confronta con", "baseline": { "none": "Disattivato", "lastMonth": "Mese precedente", @@ -2433,23 +2432,6 @@ "badge": "PR", "tooltip": "Record personale: {value} {unit} il {date}" }, - "recommendation": { - "source": "Fonte", - "viewSource": "Apri la linea guida in una nuova scheda", - "rationaleWindow": "Finestra", - "rationaleComparedTo": "Confrontato con", - "rationaleDeviation": "Scostamento", - "rationaleExpand": "Mostra motivazione", - "rationaleCollapse": "Nascondi motivazione", - "feedbackHelpful": "Utile", - "feedbackNotHelpful": "Poco utile", - "feedbackThanks": "Grazie per il riscontro", - "feedbackAlreadyRated": "Già valutato", - "feedbackError": "Impossibile salvare il tuo riscontro. Riprova.", - "confidenceAria": "Confidenza: {value} su 100", - "confidenceLow": "Confidenza bassa — pochi dati", - "confidenceDraft": "Bozza" - }, "loadError": "Impossibile caricare le analisi", "heroPersonalBaseline": "Basato sugli ultimi 90 giorni", "heroRegenerating": "Generazione in corso", @@ -2555,11 +2537,6 @@ "areas": "{present} ambiti di salute su {required}" } }, - "suggestedPrompts": { - "label": "Chiedimi", - "tellMyDoctor": "Cosa dovrei dire al mio medico?", - "medicationWorking": "Il mio farmaco sta funzionando?" - }, "dailyBriefing": { "title": "Briefing del giorno", "keyFindingsTitle": "Riscontri principali", diff --git a/messages/pl.json b/messages/pl.json index 39a1c7476..3b5eff3c7 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -2312,7 +2312,6 @@ } }, "comparison": { - "toggleLabel": "Porównaj z", "baseline": { "none": "Wyłączone", "lastMonth": "Poprzedni miesiąc", @@ -2433,23 +2432,6 @@ "badge": "PR", "tooltip": "Rekord osobisty: {value} {unit} dnia {date}" }, - "recommendation": { - "source": "Źródło", - "viewSource": "Otwórz wytyczne w nowej karcie", - "rationaleWindow": "Okno", - "rationaleComparedTo": "W porównaniu z", - "rationaleDeviation": "Odchylenie", - "rationaleExpand": "Pokaż uzasadnienie", - "rationaleCollapse": "Ukryj uzasadnienie", - "feedbackHelpful": "Przydatne", - "feedbackNotHelpful": "Mało przydatne", - "feedbackThanks": "Dzięki za opinię", - "feedbackAlreadyRated": "Już ocenione", - "feedbackError": "Nie udało się zapisać opinii. Spróbuj ponownie.", - "confidenceAria": "Pewność: {value} na 100", - "confidenceLow": "Niska pewność — mało danych", - "confidenceDraft": "Wersja robocza" - }, "loadError": "Nie udało się załadować analiz", "heroPersonalBaseline": "Na podstawie ostatnich 90 dni", "heroRegenerating": "Generowanie", @@ -2555,11 +2537,6 @@ "areas": "{present} z {required} obszarów zdrowia" } }, - "suggestedPrompts": { - "label": "Zapytaj mnie", - "tellMyDoctor": "Co powinienem powiedzieć lekarzowi?", - "medicationWorking": "Czy mój lek działa?" - }, "dailyBriefing": { "title": "Briefing dnia", "keyFindingsTitle": "Kluczowe wnioski", diff --git a/src/__tests__/success-affordance-guard.test.ts b/src/__tests__/success-affordance-guard.test.ts index 79b1b0a9e..f41817193 100644 --- a/src/__tests__/success-affordance-guard.test.ts +++ b/src/__tests__/success-affordance-guard.test.ts @@ -196,7 +196,6 @@ const PINNED_AFFORDANCES: Record< "text-success": 1, }, "src/components/insights/personal-record-badge.tsx": { "text-success": 1 }, - "src/components/insights/recommendation-feedback.tsx": { "text-success": 3 }, "src/components/labs/biomarker-form.tsx": { "toast.success": 1 }, "src/components/labs/biomarker-manager.tsx": { "toast.success": 2 }, "src/components/labs/lab-biomarker-detail.tsx": { "toast.success": 1 }, diff --git a/src/components/comparison/__tests__/compare-toggle.test.tsx b/src/components/comparison/__tests__/compare-toggle.test.tsx deleted file mode 100644 index a3f26bf58..000000000 --- a/src/components/comparison/__tests__/compare-toggle.test.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { describe, it, expect, vi } from "vitest"; -import { renderToStaticMarkup } from "react-dom/server"; - -import { I18nProvider } from "@/lib/i18n/context"; - -vi.mock("@tanstack/react-query", () => ({ - useQuery: () => ({ - data: { - version: 1, - widgets: [], - comparisonBaseline: "lastMonth" as const, - }, - isLoading: false, - }), - useMutation: () => ({ - mutate: vi.fn(), - isPending: false, - isSuccess: false, - isError: false, - error: null, - }), - useQueryClient: () => ({ - cancelQueries: vi.fn(), - getQueryData: vi.fn(), - setQueryData: vi.fn(), - invalidateQueries: vi.fn(), - }), -})); - -import { CompareToggle } from "../compare-toggle"; - -function render(node: React.ReactNode, locale: "en" | "de" = "en") { - return renderToStaticMarkup( - {node}, - ); -} - -describe("", () => { - it("renders all three baseline segments", () => { - const html = render(); - expect(html).toContain('data-slot="compare-toggle-option-none"'); - expect(html).toContain('data-slot="compare-toggle-option-lastMonth"'); - expect(html).toContain('data-slot="compare-toggle-option-lastYear"'); - }); - - it("marks the persisted baseline as the active segment", () => { - const html = render(); - // Mocked layout above persists `lastMonth`; that segment must be - // the only one carrying aria-pressed=true / data-active="true". - expect(html).toMatch( - /data-slot="compare-toggle-option-lastMonth"[^>]*data-active="true"/, - ); - expect(html).not.toMatch( - /data-slot="compare-toggle-option-none"[^>]*data-active="true"/, - ); - expect(html).not.toMatch( - /data-slot="compare-toggle-option-lastYear"[^>]*data-active="true"/, - ); - }); - - it("uses 44px (min-h-11) tap targets per WCAG 2.5.5", () => { - const html = render(); - // Every segment carries the min-h-11 utility — the WCAG-compliance - // floor we promise mobile users. - const segmentCount = ( - html.match(/data-slot="compare-toggle-option-/g) ?? [] - ).length; - const minH11Count = (html.match(/min-h-11/g) ?? []).length; - expect(segmentCount).toBe(3); - expect(minH11Count).toBeGreaterThanOrEqual(3); - }); - - it("translates the segment labels (EN)", () => { - const html = render(); - expect(html).toContain("None"); - expect(html).toContain("Last month"); - expect(html).toContain("Last year"); - }); - - it("translates the segment labels (DE)", () => { - const html = render(, "de"); - expect(html).toContain("Aus"); - expect(html).toContain("Vormonat"); - expect(html).toContain("Vorjahr"); - }); - - it("attaches role=group with the toggleLabel for screen readers", () => { - const html = render(); - expect(html).toContain('role="group"'); - expect(html).toMatch(/aria-label="(Compare to|Vergleichen mit)"/); - }); -}); diff --git a/src/components/comparison/compare-toggle.tsx b/src/components/comparison/compare-toggle.tsx deleted file mode 100644 index 474f4105e..000000000 --- a/src/components/comparison/compare-toggle.tsx +++ /dev/null @@ -1,111 +0,0 @@ -"use client"; - -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { useTranslations } from "@/lib/i18n/context"; -import { cn } from "@/lib/utils"; -import { queryKeys } from "@/lib/query-keys"; -import { - COMPARISON_BASELINES, - resolveDashboardLayout, - type ComparisonBaseline, - type DashboardLayout, -} from "@/lib/dashboard-layout"; -import { apiGet, apiPut } from "@/lib/api/api-fetch"; - -/** - * v1.4.16 phase D reconcile (CRITICAL C3) — on-surface comparison - * toggle. Replaces the Settings-only Select buried 3 clicks deep with - * a 3-segment control mounted next to the dashboard greeting and the - * insights page hero. Persists via the same `/api/dashboard/widgets` - * PUT the Settings page uses, so a flip on either surface updates the - * other on next refetch. - * - * Hit area: each segment is `min-h-11` (44 px) per WCAG 2.5.5; pressed - * state is the `default` button variant, idle is `outline`. - */ -export function CompareToggle({ className }: { className?: string }) { - const { t } = useTranslations(); - const queryClient = useQueryClient(); - - const { data: layoutData } = useQuery({ - queryKey: queryKeys.dashboardWidgets(), - queryFn: async () => { - return apiGet("/api/dashboard/widgets"); - }, - }); - - const layout = layoutData ? resolveDashboardLayout(layoutData) : null; - const value: ComparisonBaseline = layout?.comparisonBaseline ?? "none"; - - const mutation = useMutation({ - mutationFn: async (next: ComparisonBaseline) => { - if (!layout) throw new Error("layout-not-loaded"); - const body: DashboardLayout = { ...layout, comparisonBaseline: next }; - return apiPut("/api/dashboard/widgets", body); - }, - onMutate: async (next) => { - await queryClient.cancelQueries({ - queryKey: queryKeys.dashboardWidgets(), - }); - const prev = queryClient.getQueryData( - queryKeys.dashboardWidgets(), - ); - if (prev) { - queryClient.setQueryData( - queryKeys.dashboardWidgets(), - { ...prev, comparisonBaseline: next }, - ); - } - return { prev }; - }, - onError: (_err, _next, ctx) => { - if (ctx?.prev) { - queryClient.setQueryData(queryKeys.dashboardWidgets(), ctx.prev); - } - }, - onSettled: () => { - queryClient.invalidateQueries({ - queryKey: queryKeys.dashboardWidgets(), - }); - }, - }); - - return ( -
- {COMPARISON_BASELINES.map((option) => { - const active = value === option; - return ( - - ); - })} -
- ); -} diff --git a/src/components/insights/__tests__/confidence-meter.test.tsx b/src/components/insights/__tests__/confidence-meter.test.tsx deleted file mode 100644 index 0ed33d0cd..000000000 --- a/src/components/insights/__tests__/confidence-meter.test.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { renderToStaticMarkup } from "react-dom/server"; -import { I18nProvider } from "@/lib/i18n/context"; -import { ConfidenceMeter } from "../confidence-meter"; - -/** - * v1.4.16 phase B5d — ``. - * - * Two variants: - * - `bars` (default): five vertical bars, lit count = ceil(value/20). - * 0..20 = 1, 21..40 = 2, 41..60 = 3, 61..80 = 4, 81..100 = 5. - * - `ring`: SVG ring, fill proportional to value/100. - * - * Color bands match research §2.A "three-band visual" (extended to 4 - * for resolution under 50): - * - >=80 → green - * - 50..79 → yellow - * - 25..49 → orange - * - <25 → red, AND the meter is replaced with a "draft" pill so the - * UI signals "model is unsure, not asserting" - * - * aria-label translates correctly per locale ("Confidence: 67 of 100"). - * Mobile-friendly: meter components fit within 96 px wide. - */ - -function render(node: React.ReactNode, locale: "en" | "de" = "en") { - return renderToStaticMarkup( - {node}, - ); -} - -describe(" bars variant (default)", () => { - it("renders 5 lit bars at value=100", () => { - const html = render(); - const lit = (html.match(/data-bar-state="lit"/g) ?? []).length; - expect(lit).toBe(5); - }); - - it("renders 4 lit bars at value=80", () => { - const html = render(); - const lit = (html.match(/data-bar-state="lit"/g) ?? []).length; - expect(lit).toBe(4); - }); - - it("renders 3 lit bars at value=60", () => { - const html = render(); - const lit = (html.match(/data-bar-state="lit"/g) ?? []).length; - expect(lit).toBe(3); - }); - - it("renders 2 lit bars at value=40", () => { - const html = render(); - const lit = (html.match(/data-bar-state="lit"/g) ?? []).length; - expect(lit).toBe(2); - }); - - it("renders 2 lit bars at value=25 (lowest non-draft band)", () => { - // Below 25 the meter is replaced by a draft pill. The bars-only - // floor is therefore at 25 — and ceil(25/20) = 2. - const html = render(); - const lit = (html.match(/data-bar-state="lit"/g) ?? []).length; - expect(lit).toBe(2); - }); - - it("uses the success tint at value>=80", () => { - const html = render(); - expect(html).toContain('data-confidence-band="high"'); - expect(html).toMatch(/bg-success/); - }); - - it("uses the caution tint at value 50..79", () => { - const html = render(); - expect(html).toContain('data-confidence-band="medium"'); - expect(html).toMatch(/bg-warning/); - }); - - // Medium and low share the caution tint (coverage-meter precedent); - // the band attribute + lit-bar count carry the distinction. - it("uses the caution tint at value 25..49", () => { - const html = render(); - expect(html).toContain('data-confidence-band="low"'); - expect(html).toMatch(/bg-warning/); - }); - - it("renders the 'draft' pill INSTEAD of bars when value<25", () => { - const html = render(); - // No bars drawn at all - expect(html).not.toMatch(/data-bar-state="lit"/); - // Pill rendered - expect(html).toContain('data-confidence-band="draft"'); - expect(html).toMatch(/Draft|Entwurf/); - }); - - it("the draft pill is also rendered at value=0", () => { - const html = render(); - expect(html).toContain('data-confidence-band="draft"'); - }); -}); - -describe(" ring variant", () => { - it("renders an SVG when variant=ring", () => { - const html = render(); - expect(html).toContain(" { - const html = render(); - expect(html).toContain('data-confidence-band="draft"'); - // Same draft pill rule as bars. - expect(html).not.toContain(" aria-label", () => { - it("EN: 'Confidence: 67 of 100'", () => { - const html = render(); - expect(html).toMatch(/aria-label="Confidence: 67 of 100"/); - }); - - it("DE: 'Vertrauen: 67 von 100'", () => { - const html = render(, "de"); - expect(html).toMatch(/aria-label="Vertrauen: 67 von 100"/); - }); - - it("draft state aria-label still announces the score (so screenreaders aren't blind to it)", () => { - const html = render(); - expect(html).toMatch(/aria-label="Confidence: 15 of 100"/); - }); -}); - -describe(" input clamping", () => { - it("clamps negative value to 0 (renders draft pill)", () => { - const html = render(); - expect(html).toContain('data-confidence-band="draft"'); - }); - - it("clamps value>100 to 100 (renders 5 bars, high band)", () => { - const html = render(); - const lit = (html.match(/data-bar-state="lit"/g) ?? []).length; - expect(lit).toBe(5); - expect(html).toContain('data-confidence-band="high"'); - }); -}); diff --git a/src/components/insights/__tests__/recommendation-card-confidence.test.tsx b/src/components/insights/__tests__/recommendation-card-confidence.test.tsx deleted file mode 100644 index 12f3edbaa..000000000 --- a/src/components/insights/__tests__/recommendation-card-confidence.test.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import { describe, it, expect, vi } from "vitest"; -import { createContext } from "react"; -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: () =>
, -})); - -// B5e wired RecommendationFeedback into the rec card; that component -// pulls in useAuth + useMutation from tanstack-query, both of which -// need stubbing for SSR rendering in tests. -vi.mock("@tanstack/react-query", () => ({ - useQuery: () => ({ data: null, isLoading: false }), - useMutation: () => ({ - mutate: vi.fn(), - isPending: false, - isSuccess: false, - isError: false, - error: null, - }), - useQueryClient: () => ({ invalidateQueries: vi.fn() }), - // v1.21.0 — the card footer's `` reaches the - // query-client mount probe; a null-default context reads as "no client - // mounted" so the Coach hooks return their fail-open defaults. - QueryClientContext: createContext(null), -})); - -vi.mock("@/hooks/use-auth", () => ({ - useAuth: () => ({ - user: { id: "test-user", username: "tester", role: "USER" }, - isAuthenticated: true, - isLoading: false, - }), -})); - -/** - * 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("rec-feedback-slot is still rendered (B5e plug-in point intact)", () => { - const html = render( - , - ); - // The feedback slot itself is present so B5e (or future phases) - // can plug in. We don't assert anything about its contents - // because B5e fills it with the thumbs component on its own - // surface. - expect(html).toMatch(/data-slot="rec-feedback-slot"/); - }); -}); diff --git a/src/components/insights/__tests__/recommendation-card.test.tsx b/src/components/insights/__tests__/recommendation-card.test.tsx deleted file mode 100644 index 6e7ab768e..000000000 --- a/src/components/insights/__tests__/recommendation-card.test.tsx +++ /dev/null @@ -1,309 +0,0 @@ -import { describe, it, expect, vi } from "vitest"; -import { createContext } from "react"; -import { renderToStaticMarkup } from "react-dom/server"; -import { I18nProvider } from "@/lib/i18n/context"; -import { RecommendationCard } from "../recommendation-card"; -import { MEDICAL_REFERENCES } from "@/lib/ai/medical-references"; - -vi.mock("@/components/charts/health-chart", () => ({ - HealthChart: ({ - types, - title, - mini, - windowOverride, - }: { - types: string[]; - title: string; - mini?: boolean; - windowOverride?: string; - }) => ( -
- chart:{title} -
- ), -})); - -vi.mock("@/components/charts/mood-chart", () => ({ - MoodChart: ({ mini }: { mini?: boolean }) => ( -
- ), -})); - -// v1.18.11 (W5 perf) — the rationale charts now route through `next/dynamic` -// so recharts is off the insights first-load JS. Under `renderToStaticMarkup` -// a `dynamic(..., { ssr:false })` boundary paints its loading skeleton (it -// never resolves the lazy chunk synchronously). For the chart-SELECTION -// assertions below we stub `next/dynamic` to a passthrough that renders the -// (already-mocked) underlying chart component eagerly, so the test still -// verifies the card picks the right chart kind for a metric. Production keeps -// the real lazy boundary + skeleton. -vi.mock("next/dynamic", async () => { - const health = await import("@/components/charts/health-chart"); - const mood = await import("@/components/charts/mood-chart"); - return { - default: (loader: unknown) => { - // Identify which chart the loader targets by its source text. Every - // loader now resolves the shared chart-runtime boundary and picks a - // named export off it, so the export name is the discriminator. - const src = String(loader); - const Comp = ( - src.includes("MoodChart") ? mood.MoodChart : health.HealthChart - ) as React.ComponentType>; - return (props: Record) => ; - }, - }; -}); - -vi.mock("@tanstack/react-query", () => ({ - useQuery: () => ({ data: null, isLoading: false }), - useMutation: () => ({ - mutate: vi.fn(), - isPending: false, - isSuccess: false, - isError: false, - error: null, - }), - useQueryClient: () => ({ invalidateQueries: vi.fn() }), - // v1.21.0 — the card's footer `` reaches the - // query-client mount probe (`useFeatureFlags` / `useDisableCoach`); the - // probe only needs the context to exist. A null-default context reads - // as "no client mounted" so both hooks return their fail-open defaults. - QueryClientContext: createContext(null), -})); - -vi.mock("@/hooks/use-auth", () => ({ - useAuth: () => ({ - user: { id: "test-user", username: "tester", role: "USER" }, - isAuthenticated: true, - isLoading: false, - }), -})); - -const knownRef = MEDICAL_REFERENCES[0]; - -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}, - ); -} - -/** - * v1.4.16 phase B5c — Oura-style RecommendationCard. - * - * Each rec collapses into a one-row summary (text + severity + chevron - * + named slots for confidence ring (B5d) + feedback thumbs (B5e)). - * Expanding reveals the rationale 3-row card + mini-chart pinned to - * the rec's data window + the citation footnote (B5a). - * - * Default: collapsed. The chevron toggles aria-expanded. - */ - -describe("", () => { - it("renders the rec text and severity badge in collapsed state", () => { - const html = render(); - expect(html).toContain("Discuss home BP log"); - // The expand button has aria-expanded="false" when collapsed. - expect(html).toMatch(/aria-expanded="false"/); - }); - - it("renders rationale rows when initially expanded", () => { - const html = render( - , - ); - // All 3 row labels visible (English locale) - expect(html).toContain("Window"); - expect(html).toContain("Compared to"); - expect(html).toContain("Deviation"); - // Values rendered verbatim - expect(html).toContain("last7days"); - expect(html).toContain("your 90-day median (122/78)"); - expect(html).toContain("+16/+8 mmHg above baseline"); - // aria-expanded flips to true - expect(html).toMatch(/aria-expanded="true"/); - }); - - it("renders the mini-chart pinned to the rec's data window when expanded", () => { - const html = render( - , - ); - expect(html).toContain('data-testid="health-chart-mock"'); - expect(html).toContain('data-mini="true"'); - expect(html).toContain('data-window="last7days"'); - }); - - it("does NOT render the rationale block when collapsed (default)", () => { - const html = render(); - expect(html).not.toContain("Window"); - expect(html).not.toContain("your 90-day median (122/78)"); - expect(html).not.toContain('data-testid="health-chart-mock"'); - }); - - it("renders the citation footnote when referenceId resolves and expanded", () => { - const html = render( - , - ); - expect(html).toContain(knownRef.url); - expect(html).toContain('data-slot="insight-recommendation-source"'); - }); - - it("does not render rationale block when rec lacks rationale (legacy migration grace)", () => { - const legacyRec = { - id: "rec-old", - text: "Walk more", - severity: "suggestion" as const, - }; - const html = render( - , - ); - expect(html).toContain("Walk more"); - // No rationale labels - expect(html).not.toContain("Window"); - expect(html).not.toContain("Compared to"); - // No expand control either since there's nothing to expand to - expect(html).not.toMatch(/aria-expanded=/); - }); - - it("treats a plain-string rec as legacy (no expand chevron)", () => { - const html = render( - , - ); - expect(html).toContain("Continue daily logging."); - expect(html).not.toMatch(/aria-expanded=/); - }); - - it("German locale labels — rationale rows translate", () => { - const html = render( - , - "de", - ); - expect(html).toContain("Fenster"); - expect(html).toContain("Verglichen mit"); - expect(html).toContain("Abweichung"); - }); - - it("renders named slots for confidence (B5d) and feedback (B5e) so future phases can plug in", () => { - const html = render(); - // Slot for confidence ring — B5d will fill this. - expect(html).toMatch(/data-slot="rec-confidence-slot"/); - // Slot for feedback thumbs — B5e will fill this. The slot lives - // inside the expanded card, so wrap with initiallyExpanded. - const expanded = render( - , - ); - expect(expanded).toMatch(/data-slot="rec-feedback-slot"/); - }); - - it("maps metricSource.type to the chart's metric handle (BP_SYS for bloodPressure)", () => { - const html = render( - , - ); - // The chart mock echoes its `types` prop. bloodPressure → both - // sys + dia (we render both lines on the same mini-chart). - expect(html).toMatch(/data-types="BLOOD_PRESSURE_SYS,BLOOD_PRESSURE_DIA"/); - }); - - it("maps metricSource.type=mood to the MoodChart, also mini", () => { - const moodRec = { - ...recBase, - metricSource: { - type: "mood", - timeRange: "last7days", - summary: "5/5 logs", - }, - }; - const html = render( - , - ); - expect(html).toContain('data-testid="mood-chart-mock"'); - expect(html).toContain('data-mini="true"'); - }); - - // ── v1.4.16 phase B5e — feedback wiring ─────────────────────── - // Verifies B5c's named slot is now filled by RecommendationFeedback - // when the rec carries every attribute the feedback API requires. - it("wires the feedback thumbs into the expanded rationale card when fully attributed", () => { - const html = render( - , - ); - expect(html).toContain('data-feedback-thumb="up"'); - expect(html).toContain('data-feedback-thumb="down"'); - }); - - it("does NOT render feedback thumbs when the rec lacks an id (defence-in-depth)", () => { - const recNoId = { ...recBase, id: undefined }; - const html = render( - , - ); - expect(html).not.toContain("data-feedback-thumb="); - }); - - it("does NOT render feedback thumbs when the metricSource.timeRange is out of vocabulary", () => { - const recBadWindow = { - ...recBase, - metricSource: { ...recBase.metricSource, timeRange: "lastFortnight" }, - }; - const html = render( - , - ); - expect(html).not.toContain("data-feedback-thumb="); - }); - - /** - * v1.4.22 A6 — strip chart-tokens from the always-visible - * recommendation text. W1a §3 traced the production "metric:WEIGHT" - * leak the maintainer saw at the bottom of the advisor card to this exact - * render path: every other AI-prose surface ran through - * `stripChartTokens()`, the recommendation text didn't. Pin the - * strip so a future copy-paste regression can't reintroduce the - * leak. - */ - it("strips literal chart tokens (metric:WEIGHT et al) from the rec text", () => { - const recWithToken = { - ...recBase, - text: "Increase fluids. metric:WEIGHT", - }; - const html = render(); - expect(html).not.toContain("metric:WEIGHT"); - expect(html).toContain("Increase fluids."); - }); - - it("strips lowercase / mixed-case model output from the rec text", () => { - const recWithLowercaseToken = { - ...recBase, - text: "Watch the trend. metric:blood_pressure_sys today", - }; - const html = render( - , - ); - expect(html).not.toContain("metric:blood_pressure_sys"); - expect(html).toContain("Watch the trend."); - expect(html).toContain("today"); - }); -}); diff --git a/src/components/insights/__tests__/recommendation-feedback.test.tsx b/src/components/insights/__tests__/recommendation-feedback.test.tsx deleted file mode 100644 index 33d97f1b7..000000000 --- a/src/components/insights/__tests__/recommendation-feedback.test.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { describe, it, expect, vi } from "vitest"; -import { renderToStaticMarkup } from "react-dom/server"; -import { I18nProvider } from "@/lib/i18n/context"; -import { RecommendationFeedback } from "../recommendation-feedback"; - -vi.mock("@tanstack/react-query", () => ({ - useQuery: () => ({ data: null, isLoading: false }), - useMutation: () => ({ - mutate: vi.fn(), - isPending: false, - isSuccess: false, - isError: false, - error: null, - }), - useQueryClient: () => ({ invalidateQueries: vi.fn() }), -})); - -vi.mock("@/hooks/use-auth", () => ({ - useAuth: () => ({ - user: { id: "test-user", username: "tester", role: "USER" }, - isAuthenticated: true, - isLoading: false, - }), -})); - -const baseProps = { - recId: "rec-1", - recText: "Discuss home BP log with your physician.", - recSeverity: "important" as const, - metricSourceType: "bloodPressure", - metricSourceTimeRange: "last7days" as const, -}; - -function render(node: React.ReactNode, locale: "en" | "de" = "en") { - return renderToStaticMarkup( - {node}, - ); -} - -describe("", () => { - it("renders thumbs-up + thumbs-down buttons", () => { - const html = render(); - expect(html).toContain('data-feedback-thumb="up"'); - expect(html).toContain('data-feedback-thumb="down"'); - }); - - it("attaches accessible aria-label keys", () => { - const html = render(); - expect(html).toContain('aria-label="Helpful'); - expect(html).toContain('aria-label="Not helpful'); - }); - - it("translates the labels to German when locale=de", () => { - const html = render(, "de"); - expect(html).toContain("Hilfreich"); - expect(html).toContain("Nicht hilfreich"); - }); - - it("renders the 'thanks for your feedback' confirmation when initialState=submitted-up", () => { - const html = render( - , - ); - // Confirmation text replaces both buttons. - expect(html).toContain("Thanks"); - // Highlighted thumb-up still rendered for context. - expect(html).toContain('data-feedback-confirmed="up"'); - // The opposite button should NOT be re-clickable. - expect(html).not.toContain('data-feedback-thumb="down"'); - }); - - it("renders the 'already rated' state with the prior verdict highlighted", () => { - const html = render( - , - ); - expect(html).toContain('data-feedback-already-rated="down"'); - // Buttons are not interactive in this state. - expect(html).not.toContain('data-feedback-thumb="up"'); - expect(html).not.toContain('data-feedback-thumb="down"'); - }); - - it("buttons carry type=button so they don't accidentally submit a parent form", () => { - const html = render(); - // Both up + down buttons must declare type=button. - const upMatch = html.match(/]*data-feedback-thumb="up"[^>]*>/); - const downMatch = html.match( - /]*data-feedback-thumb="down"[^>]*>/, - ); - expect(upMatch?.[0]).toContain('type="button"'); - expect(downMatch?.[0]).toContain('type="button"'); - }); -}); diff --git a/src/components/insights/__tests__/recommendations-grid.test.tsx b/src/components/insights/__tests__/recommendations-grid.test.tsx deleted file mode 100644 index 28c90c81c..000000000 --- a/src/components/insights/__tests__/recommendations-grid.test.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import { describe, it, expect, vi } from "vitest"; -import { createContext } from "react"; -import { renderToStaticMarkup } from "react-dom/server"; - -import { I18nProvider } from "@/lib/i18n/context"; -import { - RecommendationsGrid, - sortRecommendationsBySeverity, -} from "../recommendations-grid"; -import type { InsightRecommendation } from "@/lib/ai/types"; - -/** - * v1.4.16 phase B1b — recommendations grid + severity-ordering wrapper. - * - * Pure presentational shell around the existing RecommendationCard: - * - 1-col mobile, 2-col desktop layout - * - severity-priority ordering: urgent → important → suggestion → info - * - staggered fade-in (animation-delay derived from index) - * - the cards keep their own per-rec rendering — this wrapper is a - * layout shell only. - */ - -vi.mock("@/components/charts/health-chart", () => ({ - HealthChart: () =>
, -})); -vi.mock("@/components/charts/mood-chart", () => ({ - MoodChart: () =>
, -})); -vi.mock("@tanstack/react-query", () => ({ - useQuery: () => ({ data: null, isLoading: false }), - useMutation: () => ({ - mutate: vi.fn(), - isPending: false, - isSuccess: false, - isError: false, - error: null, - }), - useQueryClient: () => ({ invalidateQueries: vi.fn() }), - // v1.21.0 — the card footer's `` reaches the - // query-client mount probe; a null-default context reads as "no client - // mounted" so the Coach hooks return their fail-open defaults. - QueryClientContext: createContext(null), -})); -vi.mock("@/hooks/use-auth", () => ({ - useAuth: () => ({ - user: { id: "test-user", username: "tester", role: "USER" }, - isAuthenticated: true, - isLoading: false, - }), -})); - -function rec( - id: string, - severity: "info" | "suggestion" | "important" | "urgent", - text: string, -): InsightRecommendation { - return { - id, - text, - severity, - rationale: { - dataWindow: "last7days", - comparedTo: "your 90-day median", - deviation: "moderate deviation", - }, - metricSource: { - type: "weight", - timeRange: "last7days", - summary: "84.2 kg avg over 7 days", - }, - }; -} - -function render(node: React.ReactNode, locale: "en" | "de" = "en") { - return renderToStaticMarkup( - {node}, - ); -} - -describe("sortRecommendationsBySeverity", () => { - it("orders urgent → important → suggestion → info", () => { - const recs: InsightRecommendation[] = [ - rec("a", "info", "info-a"), - rec("b", "urgent", "urgent-b"), - rec("c", "suggestion", "suggestion-c"), - rec("d", "important", "important-d"), - ]; - const sorted = sortRecommendationsBySeverity(recs); - const ids = sorted.map((r) => (typeof r === "string" ? r : (r.id ?? ""))); - expect(ids).toEqual(["b", "d", "c", "a"]); - }); - - it("preserves original order within the same severity bucket (stable)", () => { - const recs: InsightRecommendation[] = [ - rec("a", "important", "first"), - rec("b", "important", "second"), - rec("c", "important", "third"), - ]; - const sorted = sortRecommendationsBySeverity(recs); - expect(sorted.map((r) => (typeof r === "string" ? r : r.id))).toEqual([ - "a", - "b", - "c", - ]); - }); - - it("treats plain-string recs and missing-severity recs as lowest priority", () => { - const recs: InsightRecommendation[] = [ - "plain string rec", - rec("a", "urgent", "urgent-a"), - // partial — no severity - { id: "b", text: "untyped" }, - ]; - const sorted = sortRecommendationsBySeverity(recs); - expect(typeof sorted[0]).toBe("object"); - if (typeof sorted[0] !== "string") { - expect(sorted[0].id).toBe("a"); - } - // plain-string and id=b both fall to the bottom; relative order - // among the bottom bucket is stable (the string came first in the - // input). - expect(typeof sorted[1]).toBe("string"); - }); -}); - -describe("", () => { - it("renders cards in a 2-col responsive grid", () => { - const recs = [rec("a", "info", "alpha"), rec("b", "urgent", "bravo")]; - const html = render(); - expect(html).toMatch(/data-slot="rec-grid"/); - expect(html).toMatch(/grid-cols-1/); - expect(html).toMatch(/lg:grid-cols-2/); - }); - - it("renders cards in severity-ordered sequence (urgent first)", () => { - const recs = [rec("a", "info", "alpha"), rec("b", "urgent", "bravo")]; - const html = render(); - // bravo (urgent) must appear before alpha (info) in the output. - expect(html.indexOf("bravo")).toBeLessThan(html.indexOf("alpha")); - expect(html.indexOf("bravo")).toBeGreaterThan(-1); - }); - - it("paints severity-coloured left border on each card", () => { - const recs = [ - rec("a", "urgent", "urgent-rec"), - rec("b", "important", "important-rec"), - rec("c", "suggestion", "suggestion-rec"), - rec("d", "info", "info-rec"), - ]; - const html = render(); - // Each card gets a semantic-token border-l class. The exact class - // varies by severity; we assert at least one of each is in the - // output. - expect(html).toMatch(/border-l-destructive/); - expect(html).toMatch(/border-l-warning/); - expect(html).toMatch(/border-l-primary/); - expect(html).toMatch(/border-l-info/); - }); - - it("renders nothing when recs is empty", () => { - const html = render(); - expect(html).not.toMatch(/data-slot="rec-grid"/); - }); - - it("applies the staggered animation-delay style per card", () => { - const recs = [ - rec("a", "urgent", "first"), - rec("b", "important", "second"), - rec("c", "suggestion", "third"), - ]; - const html = render(); - // Cards 1+ get a non-zero animationDelay; the first card has 0ms. - expect(html).toMatch(/data-stagger-index="0"/); - expect(html).toMatch(/data-stagger-index="1"/); - expect(html).toMatch(/data-stagger-index="2"/); - }); -}); diff --git a/src/components/insights/__tests__/suggested-prompts.test.tsx b/src/components/insights/__tests__/suggested-prompts.test.tsx deleted file mode 100644 index 21cb02ab3..000000000 --- a/src/components/insights/__tests__/suggested-prompts.test.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { describe, it, expect, vi } from "vitest"; -import { renderToStaticMarkup } from "react-dom/server"; -import { I18nProvider } from "@/lib/i18n/context"; -import { SuggestedPrompts } from "../suggested-prompts"; - -/** - * v1.4.20 phase B1 — "Try asking" prompt-chip strip. - * - * Renders a horizontal row of clickable prompts below the hero action - * band. Tests cover: default two-chip rendering, custom prompt list, - * onPick wiring, locale-aware label. - */ - -function render(node: React.ReactNode, locale: "en" | "de" = "en") { - return renderToStaticMarkup( - {node}, - ); -} - -describe("", () => { - it("renders the two default prompt chips in English", () => { - const html = render( {}} />); - expect(html).toContain("What should I tell my doctor?"); - expect(html).toContain("Is my medication working?"); - // The speculative data-specific openers were dropped in v1.12.4. - expect(html).not.toContain("Why was BP higher on Monday?"); - expect(html).not.toContain("How did weight loss affect my pulse?"); - expect(html).not.toContain("Compare this week to last month"); - }); - - it("renders the two default prompt chips in German", () => { - const html = render( {}} />, "de"); - expect(html).toContain("Wirkt mein Medikament?"); - expect(html).toContain("Was sollte ich meinem Arzt sagen?"); - expect(html).not.toContain("Warum war der Blutdruck am Montag höher?"); - }); - - it("renders the 'Try asking' label in English", () => { - const html = render( {}} />); - expect(html).toMatch(/data-slot="insights-suggested-prompts-label"/); - expect(html).toContain("Try asking"); - }); - - it("renders the 'Frag mich' label in German", () => { - const html = render( {}} />, "de"); - expect(html).toContain("Frag mich"); - }); - - it("renders one chip per prompt with the suggested-prompts-chip slot", () => { - const html = render( {}} />); - const matches = - html.match(/data-slot="insights-suggested-prompts-chip"/g) ?? []; - expect(matches.length).toBe(2); - }); - - it("accepts a custom prompts array and renders only those", () => { - const html = render( - {}} - />, - ); - expect(html).toContain("Custom prompt A"); - expect(html).toContain("Custom prompt B"); - expect(html).not.toContain("Why was BP higher on Monday?"); - const matches = - html.match(/data-slot="insights-suggested-prompts-chip"/g) ?? []; - expect(matches.length).toBe(2); - }); - - it("uses the primary-token chip styling", () => { - const html = render( {}} />); - expect(html).toMatch(/border-primary\/18/); - }); - - it("invokes onPick with the localised prompt string when a chip is clicked", () => { - // We can't fire DOM events on SSR'd markup, so we verify the - // wiring via direct invocation of the component's logic. The - // chip's onClick passes the resolved prompt string verbatim. - const handler = vi.fn(); - // Render to ensure no throw + capture the prompt strings the - // component would forward. - const html = render(); - expect(html).toContain("What should I tell my doctor?"); - // Direct call — SSR can't drive a click; smoke-check the contract. - handler("What should I tell my doctor?"); - expect(handler).toHaveBeenCalledWith("What should I tell my doctor?"); - }); - - it("forwards a custom className for layout overrides", () => { - const html = render( - {}} className="my-custom-spacing" />, - ); - expect(html).toContain("my-custom-spacing"); - }); -}); diff --git a/src/components/insights/confidence-meter.tsx b/src/components/insights/confidence-meter.tsx deleted file mode 100644 index 198222662..000000000 --- a/src/components/insights/confidence-meter.tsx +++ /dev/null @@ -1,202 +0,0 @@ -import { useTranslations } from "@/lib/i18n/context"; - -/** - * Renders a 0..100 confidence score as a 5-bar meter (default) or SVG - * ring. Sub-25 replaces the meter with a "draft" pill ("quiet-when- - * unsure"). aria-label always carries the numeric value. - */ - -export type ConfidenceBand = "high" | "medium" | "low" | "draft"; -export type ConfidenceMeterVariant = "bars" | "ring"; - -export interface ConfidenceMeterProps { - /** Score carried on the insight payload — 0..100, integer expected. */ - value: number; - /** Visual variant. Defaults to `bars`. */ - variant?: ConfidenceMeterVariant; - /** Optional className applied to the outer wrapper. */ - className?: string; -} - -const DRAFT_THRESHOLD = 25; - -function clamp(value: number): number { - if (!Number.isFinite(value)) return 0; - if (value < 0) return 0; - if (value > 100) return 100; - return Math.round(value); -} - -function bandFor(value: number): ConfidenceBand { - if (value < DRAFT_THRESHOLD) return "draft"; - if (value < 50) return "low"; - if (value < 80) return "medium"; - return "high"; -} - -/** - * Lit-bar count = ceil(value / 20). 0..20 is 1, 21..40 is 2, etc. - * Note: when band is "draft" we don't render bars at all, so this - * helper is only consulted for non-draft bands. We still clamp to - * [1, 5] to defend against off-by-one drift. - */ -function litBarsFor(value: number): number { - const lit = Math.ceil(value / 20); - if (lit < 1) return 1; - if (lit > 5) return 5; - return lit; -} - -// Semantic severities only (the coverage-meter precedent): medium and -// low both ride the caution `--warning` — the lit-bar count and the -// numeric aria-label carry the medium↔low distinction, never colour -// alone. -const BAR_LIT_COLOUR_BY_BAND: Record< - Exclude, - string -> = { - high: "bg-success", - medium: "bg-warning", - low: "bg-warning", -}; - -const RING_STROKE_COLOUR_BY_BAND: Record< - Exclude, - string -> = { - high: "stroke-success", - medium: "stroke-warning", - low: "stroke-warning", -}; - -function DraftPill({ ariaLabel }: { ariaLabel: string }) { - const { t } = useTranslations(); - return ( - - {t("insights.recommendation.confidenceDraft")} - - ); -} - -function BarsMeter({ - value, - band, - ariaLabel, -}: { - value: number; - band: Exclude; - ariaLabel: string; -}) { - const lit = litBarsFor(value); - const litColour = BAR_LIT_COLOUR_BY_BAND[band]; - return ( - - {[1, 2, 3, 4, 5].map((i) => { - const isLit = i <= lit; - // Bars rise in height to give a visual "more is more" cue - // independent of colour (accessibility): 4 / 6 / 8 / 10 / 12 px. - const heightClass = ["h-1", "h-1.5", "h-2", "h-2.5", "h-3"][i - 1]; - return ( - - ); - })} - - ); -} - -function RingMeter({ - value, - band, - ariaLabel, -}: { - value: number; - band: Exclude; - ariaLabel: string; -}) { - const stroke = RING_STROKE_COLOUR_BY_BAND[band]; - // Ring geometry: r=10, circumference=2π·10≈62.83. Fill ratio = value/100. - const radius = 10; - const circumference = 2 * Math.PI * radius; - const dashArray = circumference; - const dashOffset = circumference * (1 - value / 100); - return ( - - - - - ); -} - -export function ConfidenceMeter({ - value, - variant = "bars", - className, -}: ConfidenceMeterProps) { - const { t } = useTranslations(); - const clamped = clamp(value); - const band = bandFor(clamped); - const ariaLabel = t("insights.recommendation.confidenceAria", { - value: clamped, - }); - - const wrapperClass = `inline-flex items-center ${className ?? ""}`.trim(); - - if (band === "draft") { - return ( - - - - ); - } - - return ( - - {variant === "ring" ? ( - - ) : ( - - )} - - ); -} diff --git a/src/components/insights/recommendation-card.tsx b/src/components/insights/recommendation-card.tsx deleted file mode 100644 index 0868c84ef..000000000 --- a/src/components/insights/recommendation-card.tsx +++ /dev/null @@ -1,454 +0,0 @@ -"use client"; - -import { useState, type ComponentProps } from "react"; -import { ChevronDown, ChevronUp, ExternalLink } from "lucide-react"; -import { Badge } from "@/components/ui/badge"; -import { useTranslations } from "@/lib/i18n/context"; -import { useAuth } from "@/hooks/use-auth"; -import type { - InsightRecommendation, - InsightRecommendationRationale, -} from "@/lib/ai/types"; -import dynamic from "next/dynamic"; -import { ChartErrorBoundary } from "@/components/charts/chart-error-state"; -import { ChartSkeleton } from "@/components/charts/chart-skeleton"; -import { importWithRetry } from "@/lib/retry-import"; -import { getMedicalReferenceById } from "@/lib/ai/medical-references"; -import { stripChartTokens } from "@/lib/insights/chart-tokens"; -import type { Locale } from "@/lib/i18n/config"; -import { AskCoachAction } from "./ask-coach-action"; -import { scopeSourceFromMetricKey } from "./coach-metric-scope"; -import { - RecommendationFeedback, - type RecommendationFeedbackSeverity, - type RecommendationFeedbackTimeRange, -} from "./recommendation-feedback"; -import { ConfidenceMeter } from "./confidence-meter"; - -/** - * v1.18.11 (W5 perf) — route both rationale mini-charts through - * `next/dynamic` so recharts no longer rides the main insights surface's - * first-load JS. Each chart only paints inside an expanded rationale block, - * so deferring it costs nothing visible and matches the loading skeleton - * the canonical `health-chart-dynamic.tsx` boundary uses (`mini` here, - * since both charts render with the `mini` prop in the rationale card). - */ -const HealthChartLazy = dynamic( - () => - importWithRetry(() => import("@/components/charts/chart-runtime")).then( - (mod) => ({ default: mod.HealthChart }), - ), - { ssr: false, loading: () => }, -); -const HealthChart = (props: ComponentProps) => ( - - - -); - -const MoodChartLazy = dynamic( - () => - importWithRetry(() => import("@/components/charts/chart-runtime")).then( - (mod) => ({ default: mod.MoodChart }), - ), - { ssr: false, loading: () => }, -); -const MoodChart = (props: ComponentProps) => ( - - - -); - -/** - * Collapsible recommendation card with severity badge, confidence - * meter, and an expandable rationale block (window / compared-to / - * deviation) + mini-chart. Plain-string and rationale-less recs - * render as a non-expandable row. - */ - -const SEVERITY_BADGE_STYLES: Record = { - info: "bg-info/10 text-info border border-info/25", - suggestion: "bg-dose-accent/10 text-dose-accent border border-dose-accent/25", - important: "bg-warning/10 text-warning border border-warning/25", - urgent: "bg-destructive/10 text-destructive border border-destructive/25", -}; - -interface RecommendationCardProps { - rec: InsightRecommendation; - index: number; - /** Force the rationale block visible — mostly for tests + storybook. */ - initiallyExpanded?: boolean; -} - -interface NormalisedRec { - text: string; - severity?: "info" | "suggestion" | "important" | "urgent"; - rationale?: InsightRecommendationRationale; - 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 { - if (typeof rec === "string") return { text: rec }; - return { - text: rec.text, - severity: rec.severity, - rationale: rec.rationale, - 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", - * "weight", "pulse", "mood", "medications.compliance30") which - * doesn't 1:1 match the measurement-type enum the chart consumes - * ("BLOOD_PRESSURE_SYS", "BLOOD_PRESSURE_DIA", "WEIGHT", …). This - * map is deliberately tight; unknown types fall through to a single- - * type chart so a future provider that emits a new key still renders - * something rather than nothing. - */ -function metricTypeToChartTypes(metricType: string | undefined): string[] { - if (!metricType) return []; - const lower = metricType.toLowerCase(); - if (lower === "bloodpressure" || lower === "blood_pressure") { - return ["BLOOD_PRESSURE_SYS", "BLOOD_PRESSURE_DIA"]; - } - if (lower === "weight") return ["WEIGHT"]; - if (lower === "pulse") return ["PULSE"]; - if (lower === "bodyfat" || lower === "body_fat") return ["BODY_FAT"]; - if (lower === "sleep" || lower === "sleep_duration") { - return ["SLEEP_DURATION"]; - } - if (lower === "activity" || lower === "steps") { - return ["ACTIVITY_STEPS"]; - } - if (lower === "bloodglucose" || lower === "blood_glucose") { - return ["BLOOD_GLUCOSE"]; - } - // Unknown / synthetic key — pass it through verbatim so the chart - // can render empty rather than nothing. - return [metricType]; -} - -function isMoodMetric(metricType: string | undefined): boolean { - return metricType?.toLowerCase() === "mood"; -} - -function isComplianceMetric(metricType: string | undefined): boolean { - return ( - metricType?.toLowerCase().startsWith("medications.compliance") === true || - metricType?.toLowerCase() === "medication" - ); -} - -function CitationFootnote({ - referenceId, - locale, -}: { - referenceId: string; - locale: Locale; -}) { - const { t } = useTranslations(); - const ref = getMedicalReferenceById(referenceId); - if (!ref) return null; - const label = locale === "de" ? ref.titleDe : ref.title; - return ( - - - {t("insights.recommendation.source")}: - - - {ref.org} {ref.publishedYear} — {label} - - - ); -} - -function RationaleRow({ label, value }: { label: string; value: string }) { - return ( -
- {label} - {value} -
- ); -} - -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; - recText: string; - recSeverity: RecommendationFeedbackSeverity; - metricSourceType: string; - metricSourceTimeRange: RecommendationFeedbackTimeRange; - } | null; -}) { - const { t } = useTranslations(); - const { user } = useAuth(); - const metricType = metricSource?.type; - const chartTypes = metricTypeToChartTypes(metricType); - const showLowConfidenceCaption = - typeof confidence === "number" && - confidence < LOW_CONFIDENCE_CAPTION_THRESHOLD; - - return ( -
-
- - - -
- - {/* Mini-chart pinned to the rec's window. mood + medication- - compliance get dedicated wrappers; everything else routes - through HealthChart with the metric-key vocabulary. */} - {isMoodMetric(metricType) ? ( - - ) : isComplianceMetric(metricType) ? null : chartTypes.length > 0 ? ( - - ) : null} - - {showLowConfidenceCaption && ( -

- {t("insights.recommendation.confidenceLow")} -

- )} - - {referenceId && ( -
- -
- )} - - {/* v1.4.16 phase B5e — fills the rec-feedback-slot reserved by - * B5c. The feedback row only appears when the rec carries the - * full attribute set the API endpoint requires; legacy recs - * without an id stay un-rateable (the empty slot keeps the DOM - * stable for downstream tests). */} -
- {feedbackProps && } -
-
- ); -} - -/** - * Validate the rec's metricSource.timeRange against the four-window - * vocabulary the feedback endpoint accepts. Returns null when the - * value is missing or out-of-vocabulary so the feedback slot stays - * empty rather than rendering a feedback row that would 422 on - * submit. - */ -function asFeedbackTimeRange( - value: string | undefined, -): RecommendationFeedbackTimeRange | null { - switch (value) { - case "last7days": - case "last30days": - case "last90days": - case "allTime": - return value; - default: - return null; - } -} - -export function RecommendationCard({ - rec, - index, - initiallyExpanded = false, -}: RecommendationCardProps) { - const { t, locale } = useTranslations(); - const norm = normalise(rec); - const expandable = norm.rationale !== undefined; - const [expanded, setExpanded] = useState(initiallyExpanded); - - // Feedback row is only renderable when the rec carries every - // attribute the API endpoint validates. Legacy / partial recs - // (no id, no severity, or an unknown timeRange) leave the slot - // empty so a thumbs-click can never produce a 422. - const feedbackTimeRange = asFeedbackTimeRange(norm.metricSource?.timeRange); - const feedbackProps = - norm.id && norm.severity && norm.metricSource?.type && feedbackTimeRange - ? { - recId: norm.id, - recText: norm.text, - recSeverity: norm.severity, - metricSourceType: norm.metricSource.type, - metricSourceTimeRange: feedbackTimeRange, - } - : null; - - // v1.21.0 (C4 H2) — reverse-direction Coach hand-off. The opener - // quotes the rec so the conversation lands on this exact suggestion; - // when the rec carries a metric source, scope the snapshot to it. - const coachScopeSource = scopeSourceFromMetricKey(norm.metricSource?.type); - const coachQuestion = t("insights.coach.seed.recommendation", { - text: stripChartTokens(norm.text), - }); - - return ( -
-
-
- -
-
- {norm.severity && ( - - {norm.severity} - - )} -

- {stripChartTokens(norm.text)} -

-
-
-
-
- - {typeof norm.confidence === "number" && ( - - )} - - {expandable && ( - - )} -
-
- - {expandable && expanded && norm.rationale && ( -
- -
- )} - - {/* Inline-only citation footnote: a legacy rec (no rationale) - * still surfaces its medical reference under the rec text. - * Recs WITH rationale only render the footnote inside the - * expanded RationaleCard so it doesn't double-print. */} - {!expandable && norm.referenceId && ( -
- -
- )} - - {/* v1.21.0 (C4 H2) — discreet "ask the Coach about this rec" link, - aligned under the rec text (the number gutter is `ml-6`). */} -
- -
-
- ); -} diff --git a/src/components/insights/recommendation-feedback.tsx b/src/components/insights/recommendation-feedback.tsx deleted file mode 100644 index df8a73fcb..000000000 --- a/src/components/insights/recommendation-feedback.tsx +++ /dev/null @@ -1,291 +0,0 @@ -"use client"; - -/** - * v1.4.16 phase B5e — RecommendationFeedback. - * - * Two small icon buttons (thumb-up / thumb-down) under each rec card, - * surfaced in the `data-slot="rec-feedback-slot"` slot reserved by - * B5c's ``. - * - * UX states: - * - default → both buttons rendered, neither pressed - * - submitting → buttons disabled, the chosen verdict spinner-tinted - * - submitted-up → confirmation row replaces the buttons + the - * pressed thumb is highlighted; "Thanks for your - * feedback" text - * - submitted-down → mirror of submitted-up for the negative verdict - * - already-rated-up / -down → quiet state when the server returned - * 409 (the user already rated this rec text in a previous session) - * - * Local cache: a `(userId, recId, recText)` tuple is keyed in - * localStorage so a refresh doesn't allow re-rating. Server-side dedup - * via the unique constraint on `recommendation_feedback` is the source - * of truth — the local cache is just a UX nicety. - * - * Keyboard accessible: both buttons are real ` - -
- ); -} diff --git a/src/components/insights/recommendations-grid.tsx b/src/components/insights/recommendations-grid.tsx deleted file mode 100644 index ac107799c..000000000 --- a/src/components/insights/recommendations-grid.tsx +++ /dev/null @@ -1,113 +0,0 @@ -"use client"; - -import type { InsightRecommendation } from "@/lib/ai/types"; -import { RecommendationCard } from "./recommendation-card"; - -/** - * v1.4.16 phase B1b — Apple-Health-style recommendations grid. - * - * Pure layout shell around ``: - * - 1-col mobile, 2-col desktop (no `xl:grid-cols-3` — recs benefit - * from breathing room on a wide viewport, and the card's expanded - * rationale + mini-chart pushes the natural max-content well past - * the 3-col break) - * - severity-priority ordering (urgent → important → suggestion → info) - * - severity-coloured left border per card via a thin shell `
` - * wrapper so the underlying `` (B5c/d/e shape) - * doesn't need its own border refactor - * - staggered fade-in via inline animationDelay so the cards sweep in - * left-to-right, top-to-bottom on first render - * - hover-lift on desktop (`md:hover:-translate-y-0.5`, - * `md:hover:shadow-lg`) - * - * The grid is unmounted entirely when `recs` is empty so the parent - * doesn't have to gate every call site. - */ - -interface RecommendationsGridProps { - recs: InsightRecommendation[]; -} - -const SEVERITY_RANK: Record = { - urgent: 0, - important: 1, - suggestion: 2, - info: 3, -}; -const SEVERITY_RANK_FALLBACK = 4; - -/** - * Stable sort by severity priority. Plain-string recs and recs missing - * a severity field fall to the bottom but keep their relative order - * (the wrapping `` still renders them — just under - * the actionable severities). - */ -export function sortRecommendationsBySeverity( - recs: InsightRecommendation[], -): InsightRecommendation[] { - return recs - .map((rec, index) => { - const severity = typeof rec === "string" ? null : rec.severity; - const rank = - severity && severity in SEVERITY_RANK - ? SEVERITY_RANK[severity] - : SEVERITY_RANK_FALLBACK; - return { rec, index, rank }; - }) - .sort((a, b) => { - if (a.rank !== b.rank) return a.rank - b.rank; - return a.index - b.index; - }) - .map(({ rec }) => rec); -} - -const SEVERITY_BORDER_CLASSES: Record = { - urgent: "border-l-destructive/70", - important: "border-l-warning/70", - suggestion: "border-l-primary/70", - info: "border-l-info/70", -}; - -const SEVERITY_BORDER_FALLBACK = "border-l-border"; - -/** - * Stagger interval, in ms. Brief asks for 100ms — chosen so a 4-card - * grid finishes the cascade in ~400ms (matches the existing - * `animate-insight-in` 400ms keyframe, so by the time the last card - * starts animating, the first has finished). Globally muted by - * `prefers-reduced-motion: reduce` via the existing media query in - * `globals.css`. - */ -const STAGGER_INTERVAL_MS = 100; - -export function RecommendationsGrid({ recs }: RecommendationsGridProps) { - if (recs.length === 0) return null; - const ordered = sortRecommendationsBySeverity(recs); - return ( -
- {ordered.map((rec, index) => { - const severity = typeof rec === "string" ? null : rec.severity; - const borderClass = - (severity && SEVERITY_BORDER_CLASSES[severity]) || - SEVERITY_BORDER_FALLBACK; - return ( -
- -
- ); - })} -
- ); -} diff --git a/src/components/insights/suggested-prompts.tsx b/src/components/insights/suggested-prompts.tsx deleted file mode 100644 index a421e5cfe..000000000 --- a/src/components/insights/suggested-prompts.tsx +++ /dev/null @@ -1,107 +0,0 @@ -"use client"; - -import { Quote } from "lucide-react"; -import { useTranslations } from "@/lib/i18n/context"; -import { useFeatureFlags } from "@/hooks/use-feature-flags"; -import { useDisableCoach } from "@/hooks/use-disable-coach"; -import { cn } from "@/lib/utils"; - -/** - * v1.4.20 phase B1 — "Try asking" prompt-chip strip. - * - * Renders a horizontal row of clickable prompt chips below the hero - * action band. Each chip is a single i18n string; clicking a chip - * invokes `onPick` with the localised string so the parent can route - * to the future Coach drawer (B2) or pre-fill an input. - * - * Mobile-first: chips wrap to multiple rows on narrow viewports. - * - * The default 5-chip ordering mirrors the design handoff - * (`prototype/artboard-fullpage.jsx → BriefingHero` quick-prompts - * row). Each label lives in `messages/{en,de}.json` under - * `insights.suggestedPrompts.` so the parent stays Locale- - * agnostic. - */ -export interface SuggestedPromptsProps { - /** - * Override the default prompt list. When omitted, the component - * resolves the two defaults from `insights.suggestedPrompts.*`. - * Useful for tests + future per-user prompt sets. - */ - prompts?: string[]; - /** Click handler — receives the localised prompt string. */ - onPick: (prompt: string) => void; - /** Optional className passthrough for layout overrides. */ - className?: string; -} - -// v1.12.4 — trimmed from five speculative prompts to the two that read as a -// clear next step: ask what to tell the doctor, and whether the medication is -// working. The data-specific openers ("why was Monday higher", weight × pulse, -// week vs month) were guesses that did not always match the user's own log. -const DEFAULT_PROMPT_KEYS = ["tellMyDoctor", "medicationWorking"] as const; - -export function SuggestedPrompts({ - prompts, - onPick, - className, -}: SuggestedPromptsProps) { - const { t } = useTranslations(); - // v1.4.37 W5 — every chip is a Coach affordance: clicking one seeds - // the Coach drawer's composer and opens it. When the operator turns - // the global Coach flag off the strip must vanish along with the - // hero action button and the drawer mount. The HeroStrip caller - // already guards on `flags.coach`; the in-component gate is - // defence-in-depth so a future caller that mounts - // outside the hero band can never leak a Coach surface. - const flags = useFeatureFlags(); - const disableCoach = useDisableCoach(); - if (!flags.coach) return null; - // v1.4.47 W3 — per-user opt-out hides every Coach affordance, - // including the suggested-prompts chip strip. Same posture as the - // FAB / drawer / inline pill gate above. - if (disableCoach) return null; - const items = - prompts ?? - DEFAULT_PROMPT_KEYS.map((key) => t(`insights.suggestedPrompts.${key}`)); - - return ( -
- - {t("insights.suggestedPrompts.label")} - - {items.map((prompt) => ( - - ))} -
- ); -} diff --git a/src/lib/ai/types.ts b/src/lib/ai/types.ts index 67681677f..898edb62c 100644 --- a/src/lib/ai/types.ts +++ b/src/lib/ai/types.ts @@ -33,10 +33,6 @@ export const insightRecommendationRationaleSchema = z.object({ deviation: z.string(), }); -export type InsightRecommendationRationale = z.infer< - typeof insightRecommendationRationaleSchema ->; - /** * v1.4.16 — recommendations can be either the legacy plain-string * shape OR a structured object carrying an optional medical-reference @@ -77,8 +73,6 @@ export const insightRecommendationSchema = z.union([ }), ]); -export type InsightRecommendation = z.infer; - export const insightResultSchema = z.object({ insightType: z.string().optional(), summary: z.string(), diff --git a/src/lib/feature-flags/__tests__/coach-cascade.test.tsx b/src/lib/feature-flags/__tests__/coach-cascade.test.tsx index 63bc1c149..12bba422f 100644 --- a/src/lib/feature-flags/__tests__/coach-cascade.test.tsx +++ b/src/lib/feature-flags/__tests__/coach-cascade.test.tsx @@ -13,7 +13,6 @@ import { type AssistantFlagSet, } from "@/hooks/use-feature-flags"; -import { SuggestedPrompts } from "@/components/insights/suggested-prompts"; import { CoachLaunchButton } from "@/components/insights/coach-launch-button"; import { LayoutCoachFab } from "@/components/insights/layout-coach-fab"; import { LayoutCoachMount } from "@/components/insights/layout-coach-mount"; @@ -138,15 +137,11 @@ interface CoachSurface { } // v1.18.7 — the HeroStrip "Ask the coach" action button + the in-band -// SuggestedPrompts chip strip were removed from the overview hero. The -// Coach is the bottom-right drawer; its launcher + the standalone -// SuggestedPrompts surface still carry the flag cascade below. +// SuggestedPrompts chip strip were removed from the overview hero, and the +// strip's component has since been deleted along with the rest of the +// unmounted insights cluster. The Coach is the bottom-right drawer; the +// launcher surfaces below carry the flag cascade. const COACH_SURFACES: CoachSurface[] = [ - { - name: "SuggestedPrompts standalone", - mount: () => undefined} />, - proofWhenOn: 'data-slot="insights-suggested-prompts"', - }, { name: "CoachLaunchButton inline pill", mount: () => ( @@ -278,7 +273,6 @@ describe("Coach disable cascade invariant", () => { // action button + suggested-prompt strip were removed from the band. "src/components/insights/layout-coach-fab.tsx", "src/components/insights/layout-coach-mount.tsx", - "src/components/insights/suggested-prompts.tsx", // v1.12.0 — the full-page Coach route gates on `flags.coach` and // redirects to `/insights` when the operator master flag is off. // v1.18.0 — moved to the standalone top-level `/coach` route. @@ -405,6 +399,6 @@ describe("Coach disable cascade invariant", () => { // test above pins every other `flags.coach` call site. // v1.18.7 — dropped from 6 to 4: the two HeroStrip coach surfaces // (action button + suggested-prompt strip) were removed from the band. - expect(COACH_SURFACES.length).toBe(4); + expect(COACH_SURFACES.length).toBe(3); }); }); diff --git a/src/lib/feature-flags/__tests__/coach-user-disable.test.tsx b/src/lib/feature-flags/__tests__/coach-user-disable.test.tsx index 0feaa790b..db6ddb379 100644 --- a/src/lib/feature-flags/__tests__/coach-user-disable.test.tsx +++ b/src/lib/feature-flags/__tests__/coach-user-disable.test.tsx @@ -17,7 +17,6 @@ import type { AuthUser } from "@/hooks/use-auth"; import { CoachLaunchButton } from "@/components/insights/coach-launch-button"; import { LayoutCoachFab } from "@/components/insights/layout-coach-fab"; import { LayoutCoachMount } from "@/components/insights/layout-coach-mount"; -import { SuggestedPrompts } from "@/components/insights/suggested-prompts"; /** * v1.4.47 W3 — per-user "Hide Coach" opt-out invariant. @@ -179,14 +178,11 @@ const DISABLE_COACH_SURFACES: DisableCoachSurface[] = [ ), proofWhenVisible: 'data-slot="coach-launch-inline"', }, - { - name: "SuggestedPrompts chip strip", - mount: () => undefined} />, - proofWhenVisible: 'data-slot="insights-suggested-prompts"', - }, - // v1.18.7 — the HeroStrip "Ask the coach" action button was removed from - // the overview hero. The Coach is the bottom-right drawer; its launcher - // surfaces below carry the per-user disable gate. + // v1.18.7 — the HeroStrip "Ask the coach" action button and the + // SuggestedPrompts chip strip were removed from the overview hero, and the + // strip's component has now been deleted with the rest of the unmounted + // insights cluster. The Coach is the bottom-right drawer; the launcher + // surfaces above carry the per-user disable gate. ]; describe("Coach per-user disableCoach invariant", () => { @@ -294,7 +290,6 @@ describe("Coach per-user disableCoach invariant", () => { // the `useDisableCoach` call entirely. "src/components/insights/layout-coach-fab.tsx", "src/components/insights/layout-coach-mount.tsx", - "src/components/insights/suggested-prompts.tsx", // v1.12.0 — the full-page Coach route mirrors the launch-button / // FAB gate: operator master flag OR per-user opt-out redirects the // page back to `/insights` instead of painting a dead chat shell.