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, - }; -}