feat(insights): wire Coach drawer into /insights via hero strip

Closes the loop between the hero strip's "Ask the coach" /
suggested-prompt chips and the Coach drawer:

- `<HeroStrip>` gains an `onAskCoach` prop. When supplied, the
  "Ask the coach" button drops its disabled state + Coming-soon
  tooltip and clicking calls the handler. Older parents that
  haven't adopted the drawer still get the disabled affordance —
  no breaking change.
- `<SuggestedPrompts>`'s `onPick` already routed up via
  `onPickPrompt`; the page wires it to a setter that pre-fills the
  drawer's input AND opens it in the same tick.
- `/insights` owns `coachOpen` + `coachPrefill` state. The drawer
  mounts with `key={prefill ?? "blank"}` so the lazy `useState`
  initialiser fires fresh on every chip transition, sidestepping
  the `react-hooks/set-state-in-effect` rule.

Tests:
- Updates the hero-strip ratchet to allow either disabled (no
  handler) or enabled (handler supplied) states.
- Adds three guards under `insights-polish.test.ts` that pin the
  page-side wiring (CoachDrawer import, hero handler hook-up,
  state ownership).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-10 16:32:14 +02:00
1 parent 8dac3891b8
commit e54179997b
4 files changed
+96 -11

No files matched your search

+24
View File
@@ -64,6 +64,30 @@ describe("v1.4.19 A3 — dashboard polish", () => {
});
});
describe("v1.4.20 B2b — /insights wires the Coach drawer", () => {
it("imports <CoachDrawer> and mounts it at the bottom of the page", () => {
const src = load(INSIGHTS_PATH);
expect(src).toContain(
'from "@/components/insights/coach-panel/coach-drawer"',
);
expect(src).toMatch(/<CoachDrawer\b[\s\S]*?\/>/);
});
it("hero strip's onAskCoach handler opens the drawer with no prefill", () => {
const src = load(INSIGHTS_PATH);
// The hero supplies onAskCoach which sets coachPrefill to null and
// flips coachOpen.
expect(src).toMatch(/<HeroStrip[\s\S]*?onAskCoach=\{/);
expect(src).toMatch(/<HeroStrip[\s\S]*?onPickPrompt=\{/);
});
it("page owns coachOpen + coachPrefill state", () => {
const src = load(INSIGHTS_PATH);
expect(src).toContain("setCoachOpen");
expect(src).toContain("setCoachPrefill");
});
});
describe("v1.4.19 A3 — /insights polish", () => {
it("does NOT mount the legacy advisor-card regenerate handler", () => {
const src = load(INSIGHTS_PATH);
+27
View File
@@ -43,6 +43,7 @@ import { InsightStatusCard } from "@/components/insights/insight-status-card";
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 { useInsightsAdvisorQuery } from "@/components/insights/use-insights-advisor";
import { CompareToggle } from "@/components/comparison/compare-toggle";
// Recharts is ~108 KiB Brotli — defer-load it via a self-contained scatter
@@ -489,6 +490,13 @@ export default function InsightsPage() {
const { isAuthenticated, user } = useAuth();
const { t, locale } = useTranslations();
// v1.4.20 phase B2b — Coach drawer 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<boolean>(false);
const [coachPrefill, setCoachPrefill] = useState<string | null>(null);
const STRENGTH_LABELS: Record<string, string> = {
stark: t("insights.strengthStrong"),
moderat: t("insights.strengthModerate"),
@@ -861,6 +869,14 @@ export default function InsightsPage() {
userName={heroGreetingName}
onRegenerate={advisor.regenerate}
regenerating={advisor.isRegenerating}
onAskCoach={() => {
setCoachPrefill(null);
setCoachOpen(true);
}}
onPickPrompt={(prompt) => {
setCoachPrefill(prompt);
setCoachOpen(true);
}}
/>
<DailyBriefing
@@ -1490,6 +1506,17 @@ export default function InsightsPage() {
loading={isBmiStatusLoading}
/>
</section>
{/* v1.4.20 phase B2b — AI Coach drawer. The drawer reads its
initial input value from `prefill`; we change the React `key`
on every prefill transition so the lazy `useState` initialiser
fires fresh and the composer surfaces the latest chip. */}
<CoachDrawer
key={coachPrefill ?? "blank"}
open={coachOpen}
onOpenChange={setCoachOpen}
prefill={coachPrefill}
/>
</div>
);
}