diff --git a/src/app/insights/blutdruck/page.tsx b/src/app/insights/blutdruck/page.tsx index 84e74486f..046bf3171 100644 --- a/src/app/insights/blutdruck/page.tsx +++ b/src/app/insights/blutdruck/page.tsx @@ -11,6 +11,7 @@ import { useTranslations } from "@/lib/i18n/context"; import { useInsightsLayoutPrefs } from "@/hooks/use-insights-layout-prefs"; import { Button } from "@/components/ui/button"; import { EmptyState } from "@/components/ui/empty-state"; +import { CoachLaunchButton } from "@/components/insights/coach-launch-button"; import { InsightStatusCard } from "@/components/insights/insight-status-card"; import { SubPageShell } from "@/components/insights/sub-page-shell"; import { getBpTargets } from "@/lib/analytics/bp-targets"; @@ -139,6 +140,8 @@ export default function InsightsBlutdruckPage() { updatedAt={status?.updatedAt ?? null} loading={isStatusLoading} /> + + ); } diff --git a/src/app/insights/bmi/page.tsx b/src/app/insights/bmi/page.tsx index e1ef69dab..314a5ae1c 100644 --- a/src/app/insights/bmi/page.tsx +++ b/src/app/insights/bmi/page.tsx @@ -11,6 +11,7 @@ import { useTranslations } from "@/lib/i18n/context"; import { useInsightsLayoutPrefs } from "@/hooks/use-insights-layout-prefs"; import { Button } from "@/components/ui/button"; import { EmptyState } from "@/components/ui/empty-state"; +import { CoachLaunchButton } from "@/components/insights/coach-launch-button"; import { InsightStatusCard } from "@/components/insights/insight-status-card"; import { SubPageShell } from "@/components/insights/sub-page-shell"; import type { DataSummary } from "@/lib/analytics/trends"; @@ -140,6 +141,8 @@ export default function InsightsBmiPage() { updatedAt={status?.updatedAt ?? null} loading={isStatusLoading} /> + + ); } diff --git a/src/app/insights/gewicht/page.tsx b/src/app/insights/gewicht/page.tsx index 6d9003f5a..726c7ebbc 100644 --- a/src/app/insights/gewicht/page.tsx +++ b/src/app/insights/gewicht/page.tsx @@ -11,6 +11,7 @@ import { useTranslations } from "@/lib/i18n/context"; import { useInsightsLayoutPrefs } from "@/hooks/use-insights-layout-prefs"; import { Button } from "@/components/ui/button"; import { EmptyState } from "@/components/ui/empty-state"; +import { CoachLaunchButton } from "@/components/insights/coach-launch-button"; import { InsightStatusCard } from "@/components/insights/insight-status-card"; import { SubPageShell } from "@/components/insights/sub-page-shell"; import { buildWeightBandsFromHeight } from "@/lib/analytics/value-bands"; @@ -117,6 +118,8 @@ export default function InsightsGewichtPage() { updatedAt={status?.updatedAt ?? null} loading={isStatusLoading} /> + + ); } diff --git a/src/app/insights/layout.tsx b/src/app/insights/layout.tsx index 6db91e360..25b0b4cb7 100644 --- a/src/app/insights/layout.tsx +++ b/src/app/insights/layout.tsx @@ -1,6 +1,8 @@ import type { ReactNode } from "react"; import { InsightsLayoutShell } from "@/components/insights/insights-layout-shell"; +import { LayoutCoachMount } from "@/components/insights/layout-coach-mount"; +import { CoachLaunchProvider } from "@/lib/insights/coach-launch-context"; /** * v1.4.25 W4 — shared layout for the routed `/insights` sub-pages. @@ -10,12 +12,20 @@ import { InsightsLayoutShell } from "@/components/insights/insights-layout-shell * into a sub-page. The actual page body still owns its own data fetches * — the layout is a thin presentational frame. * - * Critical design rule (Marc directive 2026-05-11): - * The `` is NOT mounted in this layout. It stays inside - * `src/app/insights/page.tsx` body so navigating into a sub-page - * unmounts the drawer (matches the Apple Health convention that AI/ - * coach surfaces only live on the overview, never on a metric page). + * v1.4.27 R3d MB4 — the Coach drawer now mounts here (above the routed + * children) inside a `` so navigating into a sub- + * page no longer unmounts the drawer. Every sub-page mounts a + * `` that calls `askCoach()` on the same context; + * the mother `/insights/page.tsx` consumes the same hook for its hero + * strip + suggested-prompt chips. Decision F (audit MA3) drove the + * promotion: a mobile user who navigates from `/insights` to + * `/insights/blutdruck` keeps a one-tap path back into the Coach. */ export default function InsightsLayout({ children }: { children: ReactNode }) { - return {children}; + return ( + + {children} + + + ); } diff --git a/src/app/insights/medikamente/page.tsx b/src/app/insights/medikamente/page.tsx index ff982be9e..022a3fbaf 100644 --- a/src/app/insights/medikamente/page.tsx +++ b/src/app/insights/medikamente/page.tsx @@ -12,6 +12,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { EmptyState } from "@/components/ui/empty-state"; import { Progress } from "@/components/ui/progress"; import { ComplianceHeatmap } from "@/components/charts/compliance-heatmap"; +import { CoachLaunchButton } from "@/components/insights/coach-launch-button"; import { InsightStatusCard } from "@/components/insights/insight-status-card"; import { SubPageShell } from "@/components/insights/sub-page-shell"; import { TherapyTimeline } from "@/components/insights/therapy-timeline"; @@ -225,6 +226,8 @@ export default function InsightsMedikamentePage() { without an active GLP-1 medication, so the page collapses back to the compliance grid for everyone else. */} + + ); } diff --git a/src/app/insights/page.tsx b/src/app/insights/page.tsx index 62b698b49..e563ddbe6 100644 --- a/src/app/insights/page.tsx +++ b/src/app/insights/page.tsx @@ -1,6 +1,5 @@ "use client"; -import { useState } from "react"; import { useQuery } from "@tanstack/react-query"; import Link from "next/link"; import { Loader2, TrendingUp } from "lucide-react"; @@ -12,10 +11,10 @@ import { EmptyState } from "@/components/ui/empty-state"; import { InsightAdvisorCard } from "@/components/insights/insight-advisor-card"; import { HeroStrip } from "@/components/insights/hero-strip"; import { DailyBriefing } from "@/components/insights/daily-briefing"; -import { CoachDrawer } from "@/components/insights/coach-panel/coach-drawer"; import { TrendsRow } from "@/components/insights/trends-row"; import { CorrelationRow } from "@/components/insights/correlation-row"; import { useInsightsAdvisorQuery } from "@/components/insights/use-insights-advisor"; +import { useCoachLaunch } from "@/lib/insights/coach-launch-context"; import type { CorrelationResult } from "@/lib/insights/correlations"; import { toWeekISO } from "@/lib/insights/week-iso"; import type { DataSummary } from "@/lib/analytics/trends"; @@ -96,12 +95,12 @@ export default function InsightsPage() { const { isAuthenticated, user } = useAuth(); const { t } = useTranslations(); - // CoachDrawer state. The hero strip's "Ask the coach" button + - // suggested-prompt chips toggle the drawer here; the drawer ingests - // `coachPrefill` once on open and resets on close so the next open - // starts blank. - const [coachOpen, setCoachOpen] = useState(false); - const [coachPrefill, setCoachPrefill] = useState(null); + // v1.4.27 R3d MB4 — Coach drawer state lives in the layout-level + // `` so every routed sub-page can reach it. + // The hero strip + suggested-prompt chips call `askCoach(prefill)` + // on the same context, and the drawer itself is mounted next to + // the provider in `src/app/insights/layout.tsx`. + const coachLaunch = useCoachLaunch(); const { data, isLoading } = useQuery({ queryKey: ["insights", "comprehensive"], @@ -183,14 +182,16 @@ export default function InsightsPage() { briefing={briefingPayload} updatedAt={heroStripUpdatedAt} userName={heroGreetingName} - onAskCoach={(prefill?: string) => { - setCoachPrefill(prefill ?? null); - setCoachOpen(true); - }} - onPickPrompt={(prompt) => { - setCoachPrefill(prompt); - setCoachOpen(true); - }} + onAskCoach={ + coachLaunch + ? (prefill?: string) => coachLaunch.askCoach(prefill ?? null) + : undefined + } + onPickPrompt={ + coachLaunch + ? (prompt) => coachLaunch.askCoach(prompt) + : undefined + } weeklyReportReady={weeklyReportReady} weeklyReportHref={currentWeekHref} healthScore={analytics?.healthScore ?? undefined} @@ -217,12 +218,6 @@ export default function InsightsPage() { cachedAt={advisor.payload?.cachedAt ?? null} legacyPayload={advisor.payload?.legacyPayload ?? false} /> - - ); } diff --git a/src/app/insights/puls/page.tsx b/src/app/insights/puls/page.tsx index 2a031d43f..d8e5f76b4 100644 --- a/src/app/insights/puls/page.tsx +++ b/src/app/insights/puls/page.tsx @@ -11,6 +11,7 @@ import { useTranslations } from "@/lib/i18n/context"; import { useInsightsLayoutPrefs } from "@/hooks/use-insights-layout-prefs"; import { Button } from "@/components/ui/button"; import { EmptyState } from "@/components/ui/empty-state"; +import { CoachLaunchButton } from "@/components/insights/coach-launch-button"; import { InsightStatusCard } from "@/components/insights/insight-status-card"; import { SubPageShell } from "@/components/insights/sub-page-shell"; import { Vo2MaxChartRow } from "@/components/insights/vo2-max-chart-row"; @@ -164,6 +165,8 @@ export default function InsightsPulsPage() { compareBaseline={compareBaseline} userTimezone={user?.timezone} /> + + ); } diff --git a/src/app/insights/schlaf/page.tsx b/src/app/insights/schlaf/page.tsx index 596c0941c..be0bcc06d 100644 --- a/src/app/insights/schlaf/page.tsx +++ b/src/app/insights/schlaf/page.tsx @@ -8,6 +8,7 @@ import { useAuth } from "@/hooks/use-auth"; import { useTranslations } from "@/lib/i18n/context"; import { Button } from "@/components/ui/button"; import { EmptyState } from "@/components/ui/empty-state"; +import { CoachLaunchButton } from "@/components/insights/coach-launch-button"; import { SleepOverview } from "@/components/insights/sleep-overview"; import { SubPageShell } from "@/components/insights/sub-page-shell"; import type { DataSummary } from "@/lib/analytics/trends"; @@ -79,6 +80,8 @@ export default function InsightsSchlafPage() { description={t("insights.sleep.description")} > + + ); } diff --git a/src/app/insights/stimmung/page.tsx b/src/app/insights/stimmung/page.tsx index 21df0ed7b..d4889c2dc 100644 --- a/src/app/insights/stimmung/page.tsx +++ b/src/app/insights/stimmung/page.tsx @@ -11,6 +11,7 @@ import { useTranslations } from "@/lib/i18n/context"; import { useInsightsLayoutPrefs } from "@/hooks/use-insights-layout-prefs"; import { Button } from "@/components/ui/button"; import { EmptyState } from "@/components/ui/empty-state"; +import { CoachLaunchButton } from "@/components/insights/coach-launch-button"; import { InsightStatusCard } from "@/components/insights/insight-status-card"; import { SubPageShell } from "@/components/insights/sub-page-shell"; @@ -99,6 +100,8 @@ export default function InsightsStimmungPage() { updatedAt={status?.updatedAt ?? null} loading={isStatusLoading} /> + + ); } diff --git a/src/components/insights/coach-launch-button.tsx b/src/components/insights/coach-launch-button.tsx new file mode 100644 index 000000000..69454539c --- /dev/null +++ b/src/components/insights/coach-launch-button.tsx @@ -0,0 +1,93 @@ +"use client"; + +import { Sparkles } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import { useTranslations } from "@/lib/i18n/context"; +import { useCoachLaunch } from "@/lib/insights/coach-launch-context"; + +/** + * v1.4.27 R3d MB4 — Coach launch button. + * + * Decision F: every routed Insights sub-page mounts this button so the + * Coach drawer (mounted at the layout root) is always reachable. The + * button renders as a sticky-bottom FAB on ` + {/* Sticky FAB on `