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

@@ -124,11 +124,30 @@ describe("<HeroStrip>", () => {
);
});
it("renders the ask-the-coach action button as disabled with a 'Coming soon' title", () => {
it("renders the ask-the-coach action button as disabled with a 'Coming soon' title when no handler is supplied", () => {
const html = render(<HeroStrip briefing={null} now={morningLocal} />);
expect(html).toMatch(
/data-slot="insights-hero-strip-action-coach"[^>]*disabled[^>]*title="Coming soon"/,
const coachTag = html.match(
/<button[^>]*data-slot="insights-hero-strip-action-coach"[^>]*>/,
);
expect(coachTag).not.toBeNull();
expect(coachTag?.[0]).toMatch(/\sdisabled(=""|\s|>)/);
expect(coachTag?.[0]).toContain('title="Coming soon"');
});
it("enables the ask-the-coach button when onAskCoach is supplied (B2b)", () => {
const html = render(
<HeroStrip
briefing={null}
now={morningLocal}
onAskCoach={() => {}}
/>,
);
const coachTag = html.match(
/<button[^>]*data-slot="insights-hero-strip-action-coach"[^>]*>/,
);
expect(coachTag).not.toBeNull();
expect(coachTag?.[0]).not.toMatch(/\sdisabled(=""|\s|>)/);
expect(coachTag?.[0]).not.toContain('title="Coming soon"');
});
it("renders the regenerate button when onRegenerate is supplied", () => {
+23 -8
View File
@@ -53,10 +53,18 @@ interface HeroStripProps {
/** Disables the regenerate button + flips its icon to a spinner. */
regenerating?: boolean;
/**
* Click handler for the suggested prompts. The Coach drawer (B2)
* will own the routing; for B1 the parent passes a no-op.
* Click handler for the suggested prompts. The Coach drawer (B2b)
* routes a chip click into the drawer's input; the parent owns the
* drawer state. When omitted the chip click is a no-op.
*/
onPickPrompt?: (prompt: string) => void;
/**
* Click handler for the "Ask the coach" action button. When supplied,
* the button is enabled and the coming-soon tooltip drops; clicking
* opens the drawer (B2b). When omitted the button stays disabled —
* see the v1.4.20 phase B1 dispatch where the drawer was deferred.
*/
onAskCoach?: () => void;
/**
* Now() override for tests so the greeting bucket is deterministic.
* Defaults to `new Date()`. Production callers omit this.
@@ -108,6 +116,7 @@ export function HeroStrip({
onRegenerate,
regenerating = false,
onPickPrompt,
onAskCoach,
now,
}: HeroStripProps) {
const { t } = useTranslations();
@@ -185,13 +194,18 @@ export function HeroStrip({
<FileText className="h-3.5 w-3.5" aria-hidden="true" />
<span>{t("insights.heroActionWeeklyReport")}</span>
</Button>
{/* B2 wires this into the Coach drawer. */}
{/* B2b wires this into the Coach drawer. The button is
enabled whenever the parent supplies an `onAskCoach`
handler; older parents that haven't adopted B2b yet
still get the disabled "Coming soon" affordance so the
hero doesn't break. */}
<Button
type="button"
variant="outline"
size="sm"
disabled
title={comingSoon}
onClick={onAskCoach}
disabled={!onAskCoach}
title={onAskCoach ? undefined : comingSoon}
data-slot="insights-hero-strip-action-coach"
className="gap-1.5"
>
@@ -227,9 +241,10 @@ export function HeroStrip({
className="border-border/50 border-t pt-4"
>
{/*
* TODO(B2): wire onPickPrompt into the Coach drawer so a
* chip click pre-fills the input + opens the drawer. For
* B1 the parent passes a no-op.
* v1.4.20 phase B2b — chip clicks open the Coach drawer
* with the localised prompt pre-filled in the composer.
* The parent owns drawer state so the chip strip stays
* presentational.
*/}
<SuggestedPrompts
onPick={onPickPrompt ?? (() => undefined)}