From 9cf2b3086a4f3487b5a25bad9951afe486c7b237 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 5 Jun 2026 10:40:44 +0200 Subject: [PATCH] feat(insights): reorder the overview, fix briefing legibility and steps link, trim the coach prompts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reorder the Insights overview top → bottom to Hero + Coach, Wellnesswerte, Vitalwerte, Trends, period retrospective, then today's signals — the wellness-score strip and vitals grid already render in that sequence inside the vitals dashboard, so the change is the surrounding section order plus moving the signal cards below the retrospective. Make the steps reference value in the daily briefing tappable: it was the one key finding with no destination, so it now routes to the generic readings view for its measurement type like every other metric drills in. Paint the health-score headline number through the semantic success / warning / destructive tokens instead of the raw dracula primitives, which carry no light-mode override and read illegibly on the Alucard light theme. Widen the wellness-score strip so the tiles always fill the row: the fixed five-column desktop track left an empty cell on the common four-score case and read as a missing tile. Drop the duplicate footer "prepare assessments" control — the tab-strip regenerate button is the single affordance — and remove the now-dead warm hook and its strings. Trim the coach suggested prompts to the two that read as a clear next step (ask the doctor, is the medication working) and remove the three speculative data-specific openers, across all six locales. (cherry picked from commit e819c9c12777f76b66b5b466b8d159b26f169868) --- messages/de.json | 5 -- messages/en.json | 5 -- messages/es.json | 5 -- messages/fr.json | 5 -- messages/it.json | 5 -- messages/pl.json | 5 -- src/app/insights/page.tsx | 61 ++++++----------- .../__tests__/health-score-card.test.tsx | 9 +-- .../insights/__tests__/hero-strip.test.tsx | 2 +- .../__tests__/suggested-prompts.test.tsx | 23 ++++--- src/components/insights/daily-briefing.tsx | 7 +- .../insights/derived/wellness-scores.tsx | 16 ++++- src/components/insights/health-score-card.tsx | 12 +++- src/components/insights/suggested-prompts.tsx | 16 ++--- src/components/insights/use-insights-warm.ts | 67 ------------------- 15 files changed, 75 insertions(+), 168 deletions(-) delete mode 100644 src/components/insights/use-insights-warm.ts diff --git a/messages/de.json b/messages/de.json index f63ed1c59..73279398e 100644 --- a/messages/de.json +++ b/messages/de.json @@ -1592,8 +1592,6 @@ "heroRegenerating": "Wird generiert", "regenerateAnalysis": "Einschätzung neu erstellen", "regenerateSuccess": "Einschätzung wurde neu erstellt", - "warmAssessments": "Einschätzungen vorbereiten", - "warmAssessmentsHint": "Einschätzungen werden über Nacht automatisch aktualisiert. Bereite sie jetzt vor, um den neuesten Stand zu sehen.", "warmStarted": "Einschätzungen werden im Hintergrund erstellt", "narrativeTitle": "Dein Zeitraum im Rückblick", "narrativeWeek": "Diese Woche", @@ -1647,9 +1645,6 @@ }, "suggestedPrompts": { "label": "Frag mich", - "whyMonday": "Warum war der Blutdruck am Montag höher?", - "weightVsPulse": "Wie hat sich die Gewichtsabnahme auf meinen Puls ausgewirkt?", - "weekVsMonth": "Vergleich diese Woche mit letztem Monat", "tellMyDoctor": "Was sollte ich meinem Arzt sagen?", "medicationWorking": "Wirkt mein Medikament?" }, diff --git a/messages/en.json b/messages/en.json index 9b74a7ae0..ff734fe34 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1592,8 +1592,6 @@ "heroRegenerating": "Regenerating", "regenerateAnalysis": "Re-run assessment", "regenerateSuccess": "Assessment refreshed", - "warmAssessments": "Prepare assessments", - "warmAssessmentsHint": "Assessments refresh overnight automatically. Prepare them now to read the latest.", "warmStarted": "Assessments are being prepared in the background", "narrativeTitle": "Your period in review", "narrativeWeek": "This week", @@ -1647,9 +1645,6 @@ }, "suggestedPrompts": { "label": "Try asking", - "whyMonday": "Why was BP higher on Monday?", - "weightVsPulse": "How did weight loss affect my pulse?", - "weekVsMonth": "Compare this week to last month", "tellMyDoctor": "What should I tell my doctor?", "medicationWorking": "Is my medication working?" }, diff --git a/messages/es.json b/messages/es.json index b71f2d98d..4d6f06a78 100644 --- a/messages/es.json +++ b/messages/es.json @@ -1592,8 +1592,6 @@ "heroRegenerating": "Generando", "regenerateAnalysis": "Volver a generar la valoración", "regenerateSuccess": "Valoración regenerada", - "warmAssessments": "Preparar valoraciones", - "warmAssessmentsHint": "Las valoraciones se actualizan automáticamente durante la noche. Prepáralas ahora para ver lo más reciente.", "warmStarted": "Las valoraciones se están preparando en segundo plano", "narrativeTitle": "Tu período en resumen", "narrativeWeek": "Esta semana", @@ -1647,9 +1645,6 @@ }, "suggestedPrompts": { "label": "Pregúntame", - "whyMonday": "¿Por qué fue más alta mi tensión el lunes?", - "weightVsPulse": "¿Cómo ha afectado la pérdida de peso a mi pulso?", - "weekVsMonth": "Compara esta semana con el mes pasado", "tellMyDoctor": "¿Qué debería contarle a mi médico?", "medicationWorking": "¿Está funcionando mi medicamento?" }, diff --git a/messages/fr.json b/messages/fr.json index ae2827a11..13c542728 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -1592,8 +1592,6 @@ "heroRegenerating": "Génération en cours", "regenerateAnalysis": "Relancer l’évaluation", "regenerateSuccess": "Évaluation regénérée", - "warmAssessments": "Préparer les évaluations", - "warmAssessmentsHint": "Les évaluations sont actualisées automatiquement pendant la nuit. Préparez-les maintenant pour voir les dernières données.", "warmStarted": "Les évaluations sont en cours de préparation en arrière-plan", "narrativeTitle": "Votre période en revue", "narrativeWeek": "Cette semaine", @@ -1647,9 +1645,6 @@ }, "suggestedPrompts": { "label": "Demande-moi", - "whyMonday": "Pourquoi ma tension était-elle plus haute lundi ?", - "weightVsPulse": "Comment la perte de poids a-t-elle influé sur mon pouls ?", - "weekVsMonth": "Compare cette semaine avec le mois dernier", "tellMyDoctor": "Que devrais-je dire à mon médecin ?", "medicationWorking": "Mon médicament fonctionne-t-il ?" }, diff --git a/messages/it.json b/messages/it.json index 6ede77c17..5245295ef 100644 --- a/messages/it.json +++ b/messages/it.json @@ -1592,8 +1592,6 @@ "heroRegenerating": "Generazione in corso", "regenerateAnalysis": "Rigenera la valutazione", "regenerateSuccess": "Valutazione rigenerata", - "warmAssessments": "Prepara le valutazioni", - "warmAssessmentsHint": "Le valutazioni si aggiornano automaticamente durante la notte. Preparale ora per vedere i dati più recenti.", "warmStarted": "Le valutazioni vengono preparate in background", "narrativeTitle": "Il tuo periodo in sintesi", "narrativeWeek": "Questa settimana", @@ -1647,9 +1645,6 @@ }, "suggestedPrompts": { "label": "Chiedimi", - "whyMonday": "Perché la mia pressione era più alta lunedì?", - "weightVsPulse": "Come ha inciso il calo di peso sulle mie pulsazioni?", - "weekVsMonth": "Confronta questa settimana con il mese scorso", "tellMyDoctor": "Cosa dovrei dire al mio medico?", "medicationWorking": "Il mio farmaco sta funzionando?" }, diff --git a/messages/pl.json b/messages/pl.json index 5153f4a78..d4b5efcff 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -1592,8 +1592,6 @@ "heroRegenerating": "Generowanie", "regenerateAnalysis": "Uruchom ocenę ponownie", "regenerateSuccess": "Ocena wygenerowana ponownie", - "warmAssessments": "Przygotuj oceny", - "warmAssessmentsHint": "Oceny odświeżają się automatycznie w nocy. Przygotuj je teraz, aby zobaczyć najnowsze dane.", "warmStarted": "Oceny są przygotowywane w tle", "narrativeTitle": "Twój okres w skrócie", "narrativeWeek": "Ten tydzień", @@ -1647,9 +1645,6 @@ }, "suggestedPrompts": { "label": "Zapytaj mnie", - "whyMonday": "Dlaczego moje ciśnienie było wyższe w poniedziałek?", - "weightVsPulse": "Jak spadek wagi wpłynął na moje tętno?", - "weekVsMonth": "Porównaj ten tydzień z poprzednim miesiącem", "tellMyDoctor": "Co powinienem powiedzieć lekarzowi?", "medicationWorking": "Czy mój lek działa?" }, diff --git a/src/app/insights/page.tsx b/src/app/insights/page.tsx index 435c053a2..a98cfe275 100644 --- a/src/app/insights/page.tsx +++ b/src/app/insights/page.tsx @@ -3,7 +3,7 @@ import { useQuery } from "@tanstack/react-query"; import dynamic from "next/dynamic"; import Link from "next/link"; -import { TrendingUp, Sparkles } from "lucide-react"; +import { TrendingUp } from "lucide-react"; import { useAuth } from "@/hooks/use-auth"; import { queryKeys } from "@/lib/query-keys"; @@ -16,7 +16,6 @@ import { Skeleton } from "@/components/ui/skeleton"; import { cn } from "@/lib/utils"; import { HeroStrip } from "@/components/insights/hero-strip"; import { useInsightsAdvisorQuery } from "@/components/insights/use-insights-advisor"; -import { useInsightsWarm } from "@/components/insights/use-insights-warm"; import { useCoachLaunch } from "@/lib/insights/coach-launch-context"; import { useAnalyticsQuery } from "@/lib/queries/use-analytics-query"; // v1.4.41 W-ORG — shared shape lives in `src/types/analytics.ts` as @@ -225,13 +224,6 @@ export default function InsightsPage() { const analyticsQuery = useAnalyticsQuery(); const analytics = analyticsQuery.data as AnalyticsData | undefined; - // The "prepare assessments" button below re-warms every assessment on - // demand. There is no warm-on-mount: the nightly cron (04:30) keeps the - // caches warm and the per-metric status GETs revalidate gently on their - // own (stale-while-revalidate), so opening the overview only reads cached - // text — it never fans out a full provider warm on a page visit. - const { warm, isWarming } = useInsightsWarm(); - // Empty-state shortcut — only paint once the comprehensive query has // resolved AND reported zero measurements. While it's in-flight we // fall through to the streamed shell so the user gets the hero + @@ -273,15 +265,22 @@ export default function InsightsPage() { // server children would require a Server-Component refactor; that's // a v1.5.x track. - // v1.12.0 — section order mirrors the iOS Insights overview - // (`v0.14` app-structure handover §2c): Coach hero + briefing + chips - // (the HeroStrip bundles all three) → detailed "Heute auf einen Blick" - // briefing → dynamics / alerts zone (today's signal + rhythm events, - // out of the AI gate so a health alert is never hidden) → vitals - // dashboard → trends row → "Zeitraum im Rückblick" retrospective → - // footer (the warm-assessments utility control, no iOS equivalent — - // iOS auto-warms). The per-metric correlation cards moved onto the - // metric pages, so the overview renders no correlation row. + // v1.12.4 — overview section order, top → bottom: + // 1. "Guten Morgen" Hero + Coach questions (HeroStrip) + the detailed + // "Heute auf einen Blick" briefing right below it (same briefing + // cluster, stays at the top). + // 2. Wellnesswerte ┐ both live inside , which renders + // 3. Vitalwerte ┘ the wellness-score strip first, then the vitals + // grid — so a single mount already gives the 2 → 3 sequence. + // 4. Trends (TrendsRow). + // 5. "Dein Zeitraum im Rückblick" retrospective (PeriodNarrativeCard). + // 6. "Signale des Tages" — today's signal (CoincidentDeviationCard) plus + // the rhythm-events alert timeline, kept out of the AI gate so a + // health alert is never hidden. + // The per-metric correlation cards moved onto the metric pages, so the + // overview renders no correlation row. The duplicate footer "prepare + // assessments" control was removed in v1.12.4 — the tab-strip regenerate + // button is the single affordance. return (
)} - - - - } - {/* v1.11.3 — anchor the warm-assessments control. It used to float - right-aligned with no label, reading as an orphan affordance. The - left-aligned caption explains the nightly-refresh model and gives the - button context. */} -
-

- {t("insights.warmAssessmentsHint")} -

- -
+ + +
); } diff --git a/src/components/insights/__tests__/health-score-card.test.tsx b/src/components/insights/__tests__/health-score-card.test.tsx index 25209ebe4..c6f3b7158 100644 --- a/src/components/insights/__tests__/health-score-card.test.tsx +++ b/src/components/insights/__tests__/health-score-card.test.tsx @@ -55,8 +55,9 @@ describe("", () => { expect(html).toMatch(/data-slot="health-score-card-number"/); expect(html).toContain(">86<"); expect(html).toContain("/ 100"); - // Green band uses the dracula-green token. - expect(html).toContain("text-dracula-green"); + // v1.12.4 — the headline number paints through the semantic `--success` + // token (AA-safe on both themes) rather than the raw dracula primitive. + expect(html).toContain("text-success"); }); it("paints the yellow band correctly", () => { @@ -69,7 +70,7 @@ describe("", () => { />, ); expect(html).toMatch(/data-band="yellow"/); - expect(html).toContain("text-dracula-orange"); + expect(html).toContain("text-warning"); }); it("paints the red band correctly", () => { @@ -82,7 +83,7 @@ describe("", () => { />, ); expect(html).toMatch(/data-band="red"/); - expect(html).toContain("text-dracula-red"); + expect(html).toContain("text-destructive"); }); it("renders the unavailable-delta caption when delta is null", () => { diff --git a/src/components/insights/__tests__/hero-strip.test.tsx b/src/components/insights/__tests__/hero-strip.test.tsx index 5e4261850..b0d4009d1 100644 --- a/src/components/insights/__tests__/hero-strip.test.tsx +++ b/src/components/insights/__tests__/hero-strip.test.tsx @@ -157,7 +157,7 @@ describe("", () => { expect(html).toMatch(/data-slot="insights-hero-strip-prompts"/); expect(html).toMatch(/data-slot="insights-suggested-prompts"/); expect(html).toContain("Try asking"); - expect(html).toContain("Why was BP higher on Monday?"); + expect(html).toContain("What should I tell my doctor?"); }); it("forwards onPickPrompt down to the prompt strip without throwing", () => { diff --git a/src/components/insights/__tests__/suggested-prompts.test.tsx b/src/components/insights/__tests__/suggested-prompts.test.tsx index 47951a3e8..6ada6debc 100644 --- a/src/components/insights/__tests__/suggested-prompts.test.tsx +++ b/src/components/insights/__tests__/suggested-prompts.test.tsx @@ -7,7 +7,7 @@ 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 5-chip rendering, custom prompt list, + * band. Tests cover: default two-chip rendering, custom prompt list, * onPick wiring, locale-aware label. */ @@ -18,20 +18,21 @@ function render(node: React.ReactNode, locale: "en" | "de" = "en") { } describe("", () => { - it("renders the 5 default prompt chips in English", () => { + it("renders the two default prompt chips in English", () => { const html = render( {}} />); - expect(html).toContain("Why was BP higher on Monday?"); - expect(html).toContain("How did weight loss affect my pulse?"); - expect(html).toContain("Compare this week to last month"); 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 5 default prompt chips in German", () => { + it("renders the two default prompt chips in German", () => { const html = render( {}} />, "de"); - expect(html).toContain("Warum war der Blutdruck am Montag höher?"); 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", () => { @@ -49,7 +50,7 @@ describe("", () => { const html = render( {}} />); const matches = html.match(/data-slot="insights-suggested-prompts-chip"/g) ?? []; - expect(matches.length).toBe(5); + expect(matches.length).toBe(2); }); it("accepts a custom prompts array and renders only those", () => { @@ -80,10 +81,10 @@ describe("", () => { // Render to ensure no throw + capture the prompt strings the // component would forward. const html = render(); - expect(html).toContain("Why was BP higher on Monday?"); + expect(html).toContain("What should I tell my doctor?"); // Direct call — SSR can't drive a click; smoke-check the contract. - handler("Why was BP higher on Monday?"); - expect(handler).toHaveBeenCalledWith("Why was BP higher on Monday?"); + handler("What should I tell my doctor?"); + expect(handler).toHaveBeenCalledWith("What should I tell my doctor?"); }); it("forwards a custom className for layout overrides", () => { diff --git a/src/components/insights/daily-briefing.tsx b/src/components/insights/daily-briefing.tsx index a8d391d0d..67eef59ea 100644 --- a/src/components/insights/daily-briefing.tsx +++ b/src/components/insights/daily-briefing.tsx @@ -49,7 +49,12 @@ const METRIC_HREF: Record = { + 1: "lg:grid-cols-1", + 2: "lg:grid-cols-2", + 3: "lg:grid-cols-3", + 4: "lg:grid-cols-4", + 5: "lg:grid-cols-5", + }; + const lgCols = LG_COLS[Math.min(tiles.length, 5)] ?? "lg:grid-cols-5"; + return (
{tiles}
diff --git a/src/components/insights/health-score-card.tsx b/src/components/insights/health-score-card.tsx index 54a2e410c..87f5f0985 100644 --- a/src/components/insights/health-score-card.tsx +++ b/src/components/insights/health-score-card.tsx @@ -106,10 +106,16 @@ const SOURCE_PILL_CLASS: Record = { none: "border-muted-foreground/30 text-muted-foreground", }; +// v1.12.4 — the headline score number used the raw `--dracula-*` primitives, +// which carry no light-mode override and render illegibly on the Alucard +// light theme (the band-tokens helper documents the same trap). Route through +// the semantic `--success` / `--warning` / `--destructive` tokens, which both +// themes override to an AA-safe tone — same mapping the wellness-score rings +// already use. const BAND_NUMBER_CLASS: Record = { - green: "text-dracula-green", - yellow: "text-dracula-orange", - red: "text-dracula-red", + green: "text-success", + yellow: "text-warning", + red: "text-destructive", }; const BAND_BORDER_CLASS: Record = { diff --git a/src/components/insights/suggested-prompts.tsx b/src/components/insights/suggested-prompts.tsx index dce24bdb2..5fe2abe2f 100644 --- a/src/components/insights/suggested-prompts.tsx +++ b/src/components/insights/suggested-prompts.tsx @@ -25,7 +25,7 @@ import { cn } from "@/lib/utils"; export interface SuggestedPromptsProps { /** * Override the default prompt list. When omitted, the component - * resolves the 5 defaults from `insights.suggestedPrompts.*`. + * resolves the two defaults from `insights.suggestedPrompts.*`. * Useful for tests + future per-user prompt sets. */ prompts?: string[]; @@ -35,13 +35,11 @@ export interface SuggestedPromptsProps { className?: string; } -const DEFAULT_PROMPT_KEYS = [ - "whyMonday", - "weightVsPulse", - "weekVsMonth", - "tellMyDoctor", - "medicationWorking", -] as const; +// 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, @@ -89,7 +87,7 @@ export function SuggestedPrompts({ data-slot="insights-suggested-prompts-chip" // v1.8.5 W4a — denser chips. The tap target stays comfortable // (`min-h-9` ≈ 36 px, the WCAG/iOS floor) but the horizontal - // padding + type size drop a notch so five prompts pack into + // padding + type size drop a notch so the two prompts pack into // fewer rows and the left column stops towering over the // Health-Score card. className={cn( diff --git a/src/components/insights/use-insights-warm.ts b/src/components/insights/use-insights-warm.ts deleted file mode 100644 index 5120d5052..000000000 --- a/src/components/insights/use-insights-warm.ts +++ /dev/null @@ -1,67 +0,0 @@ -"use client"; - -import { useCallback } from "react"; -import { useMutation } from "@tanstack/react-query"; -import { toast } from "sonner"; - -import { queryKeys } from "@/lib/query-keys"; -import { useTranslations } from "@/lib/i18n/context"; - -/** - * On-demand full assessment warm. - * - * POSTs `/api/insights/pregenerate`, which enqueues a forced warm of every - * AI assessment (comprehensive insight + the seven specialised status cards - * + every data-bearing generic metric assessment) for the calling user on - * the worker and returns immediately. The cards then fill via their existing - * stale-while-revalidate GETs — there is nothing to invalidate here. - * - * This is the explicit "prepare assessments" button only. There is no - * warm-on-mount: the nightly cron (04:30 Europe/Berlin) keeps every user's - * caches warm, and the per-metric status GETs revalidate gently on their - * own — so a page visit reads cached text instead of fanning out a full - * provider warm that would contend with foreground requests. - */ - -async function postWarm(): Promise { - // Same-origin relative-path fetch — exempt from the safe-fetch rule by - // construction. A 429 (warm already in progress) is an expected, - // harmless outcome, not an error. - const res = await fetch("/api/insights/pregenerate", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: "{}", - }); - if (!res.ok && res.status !== 429) { - throw new Error(`HTTP ${res.status}`); - } -} - -export interface UseInsightsWarmResult { - /** Manual trigger — POSTs the warm and shows a toast. */ - warm: () => void; - /** True while the warm POST is in flight. */ - isWarming: boolean; -} - -export function useInsightsWarm(): UseInsightsWarmResult { - const { t } = useTranslations(); - - const mutation = useMutation({ - mutationKey: queryKeys.insightsPregenerate(), - mutationFn: postWarm, - }); - - const { mutate } = mutation; - - const warm = useCallback(() => { - mutate(undefined, { - onSuccess: () => toast.success(t("insights.warmStarted")), - }); - }, [mutate, t]); - - return { - warm, - isWarming: mutation.isPending, - }; -}