mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 14:40:52 +00:00
feat(coach): add a full-page conversation mode with a maximize control
Extract the Coach chat surface (header actions, message thread, composer, provenance rails, settings, mobile trays) into a shared <CoachConversation> component so the drawer and a new full-page route render the exact same implementation — no forked chat logic. The drawer keeps only its <Sheet> chrome (close button + maximize control); the page supplies a minimize-back-to-drawer control. The maximize control in the drawer header navigates to /insights/coach and closes the drawer (aborting any in-flight stream); the page's minimize control routes back to /insights and reopens the drawer through the shared launch context. Both surfaces preserve incremental streaming, collapsible history + provenance, the single composer disclaimer, the auto-grow composer, refusal / budget handling, and the aiMode gating — the full page redirects to /insights when the operator flag is off or the user has opted out. Lift useResettableValue / nextResettableValue into their own module so the shared surface seeds its composer from the same controlled-prefill contract without importing the drawer shell. Register the new route in the coach feature-flag and per-user-disable gate-site invariants, and add the maximize / minimize labels across all six locales.
This commit is contained in:
1 parent
e0f60d82cc
commit
488cc09ae9
12 files changed
+506
-322
No files matched your search
@@ -0,0 +1,88 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Minimize2 } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CoachConversation } from "@/components/insights/coach-panel/coach-conversation";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { useCoachLaunch } from "@/lib/insights/coach-launch-context";
|
||||
import { useFeatureFlags } from "@/hooks/use-feature-flags";
|
||||
import { useDisableCoach } from "@/hooks/use-disable-coach";
|
||||
|
||||
/**
|
||||
* v1.12.0 (Coach v2 #6) — full-page Coach conversation.
|
||||
*
|
||||
* Renders the exact same `<CoachConversation>` surface the drawer uses
|
||||
* (history + streaming thread + composer + provenance + settings) at
|
||||
* full width and height. There is no forked chat logic — the page is a
|
||||
* thin chrome wrapper that supplies a "minimize back to drawer"
|
||||
* control where the drawer supplies "maximize" + "close".
|
||||
*
|
||||
* The route lives inside `app/insights/layout.tsx` (sticky tab strip +
|
||||
* mobile FAB) and the authenticated `<AuthShell>` (which provides the
|
||||
* `<CoachLaunchProvider>` the minimize control reopens the drawer
|
||||
* through), so the page inherits the same Coach feature gating as every
|
||||
* other Insights surface. When the operator's master flag is off or the
|
||||
* user opted out, the page redirects back to `/insights` rather than
|
||||
* painting a dead chat shell.
|
||||
*/
|
||||
export default function CoachPage() {
|
||||
const { t } = useTranslations();
|
||||
const router = useRouter();
|
||||
const launch = useCoachLaunch();
|
||||
const flags = useFeatureFlags();
|
||||
const disableCoach = useDisableCoach();
|
||||
|
||||
const coachUnavailable = !flags.coach || disableCoach;
|
||||
|
||||
// Coach surface gating mirrors `<CoachLaunchButton>` / `<LayoutCoachFab>`:
|
||||
// operator master flag OR per-user opt-out hides the Coach entirely.
|
||||
// Send a direct navigator back to the Insights mother page so the
|
||||
// route is never a dead-end.
|
||||
useEffect(() => {
|
||||
if (coachUnavailable) {
|
||||
router.replace("/insights");
|
||||
}
|
||||
}, [coachUnavailable, router]);
|
||||
|
||||
if (coachUnavailable) return null;
|
||||
|
||||
const handleMinimize = () => {
|
||||
// Hand the conversation back to the drawer overlay: route to the
|
||||
// Insights mother page and reopen the drawer through the shared
|
||||
// launch context.
|
||||
router.push("/insights");
|
||||
launch?.askCoach(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="coach-page"
|
||||
className="flex h-[calc(100dvh-13rem)] min-h-[28rem] flex-col overflow-hidden rounded-2xl border"
|
||||
>
|
||||
<CoachConversation
|
||||
surface="page"
|
||||
autoFocusComposer
|
||||
renderTitle={(title) => (
|
||||
<h1 className="min-w-0 truncate text-sm font-semibold">{title}</h1>
|
||||
)}
|
||||
leadingHeaderActions={
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleMinimize}
|
||||
data-slot="coach-page-minimize"
|
||||
aria-label={t("insights.coach.minimizeAriaLabel")}
|
||||
title={t("insights.coach.minimizeAriaLabel")}
|
||||
className="text-muted-foreground hover:text-foreground size-11 shrink-0"
|
||||
>
|
||||
<Minimize2 className="size-4" aria-hidden="true" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user