From 31b7fa6fc642272c334216fd250f30b381fb0cf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 27 Jun 2026 11:21:32 +0200 Subject: [PATCH 1/2] feat(coach): move conversation history to a dedicated page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the left slide-in conversations drawer with a standalone /coach/conversations page: a search field above a recency-grouped list (Today / Yesterday / This week / Earlier) built on the existing conversations query. Selecting a row routes to /coach?c=, reusing the deep-link open mechanism — no new API, no new state. The drawer's Conversations affordance now navigates to the new route, and the Coach page drops its ?view=conversations in-page drawer handling. --- e2e/v1427-coach-launch.spec.ts | 22 +- src/app/coach/conversations/page.tsx | 277 ++++++++++++++++++ src/app/coach/page.tsx | 13 +- .../insights/coach-panel/coach-drawer.tsx | 20 +- 4 files changed, 305 insertions(+), 27 deletions(-) create mode 100644 src/app/coach/conversations/page.tsx diff --git a/e2e/v1427-coach-launch.spec.ts b/e2e/v1427-coach-launch.spec.ts index ffa64c88e..90e0a3765 100644 --- a/e2e/v1427-coach-launch.spec.ts +++ b/e2e/v1427-coach-launch.spec.ts @@ -280,8 +280,8 @@ test.describe("Coach launch surfaces on insights sub-pages", () => { page, }, testInfo) => { // The in-panel history tray is gone from the drawer; the button - // navigates to `/coach?view=conversations`, where the full page opens - // the conversation list on arrival instead of a blank pane. + // navigates to the dedicated `/coach/conversations` page, which renders + // the search + recency-grouped conversation list. test.skip( testInfo.project.name !== "chromium-desktop", "exercises the desktop drawer layout", @@ -312,12 +312,18 @@ test.describe("Coach launch surfaces on insights sub-pages", () => { await expect(historyButton).toBeVisible({ timeout: 10_000 }); await historyButton.click(); - // No in-panel tray opens — the click navigates to the full view, which - // carries the `view=conversations` handoff so the list opens on arrival. - await page.waitForURL(/\/coach(\?|$)/, { timeout: 10_000 }); - await expect(page.locator('[data-slot="coach-page"]')).toBeVisible({ - timeout: 10_000, - }); + // No in-panel tray opens — the click navigates to the dedicated + // conversation-history page, which renders the search field + list. + await page.waitForURL(/\/coach\/conversations(\?|$)/, { timeout: 10_000 }); + await expect( + page.locator('[data-slot="coach-conversations-page"]'), + ).toBeVisible({ timeout: 10_000 }); + await expect( + page.locator('[data-slot="coach-conversations-search"]'), + ).toBeVisible({ timeout: 10_000 }); + await expect( + page.locator('[data-slot="coach-conversations-list"]'), + ).toBeVisible({ timeout: 10_000 }); await expect( page.locator('[data-slot="coach-drawer-history-tray"]'), ).toHaveCount(0); diff --git a/src/app/coach/conversations/page.tsx b/src/app/coach/conversations/page.tsx new file mode 100644 index 000000000..0855a364b --- /dev/null +++ b/src/app/coach/conversations/page.tsx @@ -0,0 +1,277 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { useRouter } from "next/navigation"; +import { MessagesSquare, Search, Trash2 } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { COACH_SCROLLBAR } from "@/components/insights/coach-panel/message-thread"; +import { + useCoachConversations, + useDeleteCoachConversation, +} from "@/components/insights/coach-panel/use-coach"; +import type { CoachConversationDTO } from "@/lib/ai/coach/types"; +import { useCoachLaunch } from "@/lib/insights/coach-launch-context"; +import { useFeatureFlags } from "@/hooks/use-feature-flags"; +import { useDisableCoach } from "@/hooks/use-disable-coach"; +import { useTranslations } from "@/lib/i18n/context"; +import { formatRelativeTime } from "@/lib/i18n/relative-time"; +import { cn } from "@/lib/utils"; + +/** + * v1.21.4 (Coach-UI B) — dedicated conversation-history page. + * + * Replaces the left slide-in `` drawer that the `+` menu's + * "Conversations" affordance used to open. The list now reads as a + * sibling of the Coach page itself: a search field at the top, then the + * recent conversations grouped by recency (Today / Yesterday / This week + * / Earlier). Selecting a row routes to `/coach?c=` — the existing + * deep-link open mechanism — so there is NO new API and NO new state; + * the page is a thin read over `useCoachConversations()`. + * + * Gating mirrors `/coach`: the operator master flag OR a per-user opt-out + * hides the Coach entirely and redirects back to `/insights` rather than + * painting a dead shell. The full-bleed sizing wrapper matches the Coach + * page so the two surfaces share one chrome. + */ + +type RecencyGroupId = "today" | "yesterday" | "thisWeek" | "earlier"; + +interface RecencyGroup { + id: RecencyGroupId; + labelKey: string; + conversations: CoachConversationDTO[]; +} + +/** + * Bucket conversations by LOCAL calendar recency. `updatedAt` is an ISO + * string; comparisons run against local day boundaries so "Today" tracks + * the user's own midnight, not UTC. + */ +function groupByRecency(conversations: CoachConversationDTO[]): RecencyGroup[] { + const now = new Date(); + const startOfToday = new Date( + now.getFullYear(), + now.getMonth(), + now.getDate(), + ).getTime(); + const startOfYesterday = startOfToday - 24 * 60 * 60 * 1000; + // "This week" = the six days before yesterday (a rolling 7-day window + // that already excludes today + yesterday, which have their own groups). + const startOfWeek = startOfToday - 6 * 24 * 60 * 60 * 1000; + + const groups: Record = { + today: [], + yesterday: [], + thisWeek: [], + earlier: [], + }; + + for (const conversation of conversations) { + const updated = new Date(conversation.updatedAt).getTime(); + if (updated >= startOfToday) groups.today.push(conversation); + else if (updated >= startOfYesterday) groups.yesterday.push(conversation); + else if (updated >= startOfWeek) groups.thisWeek.push(conversation); + else groups.earlier.push(conversation); + } + + return ( + [ + { id: "today", labelKey: "insights.coach.history.groupToday" }, + { id: "yesterday", labelKey: "insights.coach.history.groupYesterday" }, + { id: "thisWeek", labelKey: "insights.coach.history.groupThisWeek" }, + { id: "earlier", labelKey: "insights.coach.history.groupEarlier" }, + ] as const + ) + .map((g) => ({ ...g, conversations: groups[g.id] })) + .filter((g) => g.conversations.length > 0); +} + +function CoachConversationsBody() { + const { t } = useTranslations(); + const router = useRouter(); + const { conversations, isLoading } = useCoachConversations(); + const deleteMutation = useDeleteCoachConversation(); + const [filter, setFilter] = useState(""); + const [confirmId, setConfirmId] = useState(null); + + // Mirror the rail's title substring filter. + const filtered = useMemo(() => { + const q = filter.trim().toLowerCase(); + if (!q) return conversations; + return conversations.filter((c) => c.title.toLowerCase().includes(q)); + }, [conversations, filter]); + + const groups = useMemo(() => groupByRecency(filtered), [filtered]); + + function handleSelect(id: string) { + // Reuse the existing `?c=` open mechanism on the Coach page. + router.push(`/coach?c=${id}`); + } + + function handleDeleteRequest(id: string) { + if (confirmId === id) { + deleteMutation.mutate(id); + setConfirmId(null); + } else { + setConfirmId(id); + } + } + + return ( +
+
+

+

+
+
+
+ +
+ {isLoading && conversations.length === 0 ? ( +

+ {t("common.loading")} +

+ ) : groups.length === 0 ? ( +
+ + +

+ {t("insights.coach.historyEmpty")} +

+
+ ) : ( + groups.map((group) => ( +
+

+ {t(group.labelKey)} +

+
    + {group.conversations.map((c) => { + const isConfirming = confirmId === c.id; + return ( +
  • + + +
  • + ); + })} +
+
+ )) + )} +
+
+ ); +} + +export default function CoachConversationsPage() { + const router = useRouter(); + const launch = useCoachLaunch(); + const flags = useFeatureFlags(); + const disableCoach = useDisableCoach(); + + const coachUnavailable = !flags.coach || disableCoach; + + // Same gating as `/coach`: operator master flag OR per-user opt-out + // redirects back to the Insights mother page so the route is never a + // dead-end. + useEffect(() => { + if (coachUnavailable) { + router.replace("/insights"); + } + }, [coachUnavailable, router]); + + // Keep the launch context referenced so the shared FAB drawer the page + // hands back to stays mounted (mirrors `/coach`). + void launch; + + if (coachUnavailable) return null; + + return ( +
+ +
+ ); +} diff --git a/src/app/coach/page.tsx b/src/app/coach/page.tsx index 3e82f4059..85870cf99 100644 --- a/src/app/coach/page.tsx +++ b/src/app/coach/page.tsx @@ -49,10 +49,9 @@ import { apiGet } from "@/lib/api/api-fetch"; * the v1.19.0 (W7) "always resume most-recent" default the maintainer * disliked. Resolution order on mount: * - `?c=` → open that specific thread (explicit deep-link, unchanged). + * The dedicated conversation-history page (`/coach/conversations`, added + * in v1.21.4) routes back here with `?c=` when a row is selected. * - `?c=new` or no param → the new-chat hero. - * - `?view=conversations` → land on the new-chat hero AND open the - * conversation-history drawer, so the drawer's "Conversations" handoff - * (C5) never drops the user on a blank pane. * - EXCEPTION: when the Coach has proactively written an UNREAD message * (`/api/insights/coach/nudge-status` → `unread`), auto-open the * most-recent thread so the user lands on what the Coach said. @@ -66,19 +65,16 @@ function CoachPageBody() { // defaults to the new-chat hero. const rawC = searchParams.get("c"); const deepLinkedId = rawC && rawC !== "new" ? rawC : null; - // C5 — the drawer's "Conversations" handoff routes here with this flag so - // the page opens the history drawer on arrival instead of a blank hero. - const openConversations = searchParams.get("view") === "conversations"; // C1 exception — an unread coach-initiated message opens the most-recent // conversation (which holds that proactive turn). Only consulted when the - // entry did not pin a specific thread or ask for a fresh chat / list. + // entry did not pin a specific thread or ask for a fresh chat. const exceptionEligible = deepLinkedId === null && rawC !== "new"; const { data: nudge } = useQuery({ queryKey: queryKeys.coachNudgeStatus(), queryFn: async (): Promise => apiGet("/api/insights/coach/nudge-status"), - enabled: exceptionEligible && !openConversations, + enabled: exceptionEligible, staleTime: 5 * 60 * 1000, }); const hasUnreadCoachMessage = exceptionEligible && nudge?.unread === true; @@ -91,7 +87,6 @@ function CoachPageBody() { // Default is the new-chat hero; only resume most-recent for the // unread coach-initiated exception. autoOpenMostRecent={hasUnreadCoachMessage} - openHistoryOnMount={openConversations} /> ); } diff --git a/src/components/insights/coach-panel/coach-drawer.tsx b/src/components/insights/coach-panel/coach-drawer.tsx index 06f2cf7bf..f0bf22f75 100644 --- a/src/components/insights/coach-panel/coach-drawer.tsx +++ b/src/components/insights/coach-panel/coach-drawer.tsx @@ -100,15 +100,15 @@ export function CoachDrawer({ router.push("/coach"); }, [onOpenChange, router]); - // v1.19.1 (C5) — the drawer's "Conversations" affordance hands off to the - // full page WITH the `?view=conversations` flag so the page opens the - // conversation-history drawer on arrival. Previously it shared - // `handleMaximize` and, after the v1.19.1 new-chat default (C1), landed on - // a blank hero with no list — the dead-end the maintainer reported. + // v1.21.4 (Coach-UI B) — the drawer's "Conversations" affordance now hands + // off to the dedicated conversation-history page (`/coach/conversations`), + // which renders the search + recency-grouped list as a sibling of the + // Coach page. The former `?view=conversations` in-page slide-in drawer is + // gone; selecting a row there routes back to `/coach?c=`. const handleOpenConversations = useCallback(() => { onOpenChange(false); resetRef.current?.(); - router.push("/coach?view=conversations"); + router.push("/coach/conversations"); }, [onOpenChange, router]); return ( @@ -141,10 +141,10 @@ export function CoachDrawer({ launchScope={scope} autoFocusComposer registerReset={registerReset} - // v1.16.1 — the "Conversations" affordance hands off to the - // full-page route instead of opening the broken in-panel left - // tray. v1.19.1 (C5) — it now carries `?view=conversations` so the - // page opens the history list on arrival, never a blank hero. + // v1.16.1 — the "Conversations" affordance hands off to a + // dedicated route instead of opening the broken in-panel left + // tray. v1.21.4 (Coach-UI B) — it now navigates to + // `/coach/conversations`, the standalone search + grouped-list page. onRequestFullView={handleOpenConversations} // Radix requires an accessible name + description on the // dialog content. Mount them through the title/description From 5117bc603c024bdc3bceecdebac39046a146c79c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 27 Jun 2026 11:23:47 +0200 Subject: [PATCH 2/2] feat(coach): settings in composer menu, dismissable seeded opener Add a Settings entry to the composer's "+" actions menu and remove the page-toolbar gear; the drawer header keeps its own gear. Let the seeded "worth a look" opener fill the composer column and give it a per-day dismiss control. Route the Conversations action to /coach/conversations and drop the in-page history drawer. --- .../coach-conversation-deeplink.test.tsx | 44 +++--- .../__tests__/scope-hint-badge.test.tsx | 54 +++++++ .../coach-panel/coach-conversation.tsx | 135 ++++++------------ .../insights/coach-panel/coach-input.tsx | 17 ++- .../insights/coach-panel/scope-hint-badge.tsx | 70 ++++++--- 5 files changed, 184 insertions(+), 136 deletions(-) diff --git a/src/components/insights/coach-panel/__tests__/coach-conversation-deeplink.test.tsx b/src/components/insights/coach-panel/__tests__/coach-conversation-deeplink.test.tsx index e695b6148..a12f2b8e8 100644 --- a/src/components/insights/coach-panel/__tests__/coach-conversation-deeplink.test.tsx +++ b/src/components/insights/coach-panel/__tests__/coach-conversation-deeplink.test.tsx @@ -1,7 +1,15 @@ -import { describe, it, expect } from "vitest"; +import { describe, it, expect, vi } from "vitest"; import { renderToStaticMarkup } from "react-dom/server"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +// v1.21.4 (B) — the page surface routes "Conversations" to the dedicated +// `/coach/conversations` page via the app router, so the shared surface now +// reads `useRouter`. Stub it for the static-markup render. +const pushMock = vi.fn(); +vi.mock("next/navigation", () => ({ + useRouter: () => ({ push: pushMock }), +})); + import { I18nProvider } from "@/lib/i18n/context"; import { CoachConversation } from "../coach-conversation"; import type { @@ -136,30 +144,16 @@ describe(" page deep-link (#67)", () => { expect(html).toContain('data-slot="coach-hero"'); }); - // v1.21.0 — the page toolbar is a single trailing affordance: the settings - // gear in the top-right corner. The "Conversations" + "New chat" buttons - // were removed from the toolbar (both still live in the composer `+` menu), - // and the settings gear moved here from the composer. - it("renders the settings gear in the top-right page toolbar", () => { + // v1.21.4 (A) — the page-toolbar gear was removed; Settings now lives in the + // composer's `+` actions menu. The page chrome is the composer alone, so the + // surface exposes the `+` actions trigger and no standalone toolbar gear. + it("drops the page-toolbar gear and exposes the composer + actions menu", () => { const html = render(, makeClient()); - expect(html).toContain('data-slot="coach-page-settings"'); - const gear = html.match(/]*data-slot="coach-page-settings"[^>]*>/); - expect(gear?.[0]).toContain('href="/settings/ai"'); - // The old toolbar Conversations + New chat buttons are gone. - expect(html).not.toContain('data-slot="coach-page-conversations"'); - expect(html).not.toContain('data-slot="coach-page-new-chat"'); - }); - - // v1.21.0 — entering via the drawer handoff (`?view=conversations`) keeps - // the new-chat hero (no thread auto-resumed) while opening the history - // drawer; the toolbar gear stays present so the pane is never a blank - // dead-end. - it("keeps the hero and toolbar gear when opened with openHistoryOnMount", () => { - const html = render( - , - makeClient(), - ); - expect(html).toContain('data-slot="coach-hero"'); - expect(html).toContain('data-slot="coach-page-settings"'); + // The old page toolbar + its gear are gone. + expect(html).not.toContain('data-slot="coach-page-settings"'); + expect(html).not.toContain('data-slot="coach-page-toolbar"'); + // Settings now lives behind the composer's `+` actions menu (the menu + // content is portalled open-on-demand, so SSR shows only the trigger). + expect(html).toContain('data-slot="coach-input-actions"'); }); }); diff --git a/src/components/insights/coach-panel/__tests__/scope-hint-badge.test.tsx b/src/components/insights/coach-panel/__tests__/scope-hint-badge.test.tsx index e16775507..268bce7a0 100644 --- a/src/components/insights/coach-panel/__tests__/scope-hint-badge.test.tsx +++ b/src/components/insights/coach-panel/__tests__/scope-hint-badge.test.tsx @@ -71,6 +71,60 @@ describe("", () => { onSeed(question); expect(onSeed).toHaveBeenCalledWith(question); }); + + // v1.21.4 (C2) — the seeded opener carries a dismiss control so the user can + // wave off today's suggestion. The control is gated to the seeded variant. + it("renders the dismiss control for the seeded variant when onDismiss is given", () => { + const html = render( + {}} + onDismiss={() => {}} + />, + ); + expect(html).toContain('data-slot="coach-scope-hint-dismiss"'); + }); + + it("omits the dismiss control for the seeded variant without onDismiss", () => { + const html = render( + {}} + />, + ); + expect(html).not.toContain('data-slot="coach-scope-hint-dismiss"'); + }); + + it("never renders the dismiss control for the non-dismissable scope pill", () => { + const html = render( + {}} + onDismiss={() => {}} + />, + ); + expect(html).not.toContain('data-slot="coach-scope-hint-dismiss"'); + }); + + it("lets the seeded opener fill its column (no max-w-md cap)", () => { + const html = render( + {}} + onDismiss={() => {}} + />, + ); + // The opener grows to the composer column width; the old narrow cap is gone. + expect(html).not.toContain("max-w-md"); + }); }); describe(" scope hint slot", () => { diff --git a/src/components/insights/coach-panel/coach-conversation.tsx b/src/components/insights/coach-panel/coach-conversation.tsx index c50af1cfb..3dc1dd5b2 100644 --- a/src/components/insights/coach-panel/coach-conversation.tsx +++ b/src/components/insights/coach-panel/coach-conversation.tsx @@ -2,17 +2,12 @@ import { useEffect, useReducer, useState } from "react"; import Link from "next/link"; +import { useRouter } from "next/navigation"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Plus, Settings, Sparkles } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; -import { - Sheet, - SheetContent, - SheetHeader, - SheetTitle, -} from "@/components/ui/sheet"; import { cn } from "@/lib/utils"; import { useTranslations } from "@/lib/i18n/context"; import { queryKeys } from "@/lib/query-keys"; @@ -93,6 +88,19 @@ export function launchScopeToCoachScope( }; } +/** + * v1.21.4 (C2) — the localStorage key that records the seeded "worth a look" + * opener as dismissed for a given LOCAL calendar day. Date-stamped so the + * dismissal resets at midnight: a new day mints a new key the flag has not + * been written under yet, and the opener returns. + */ +function seededDismissStorageKey(now: Date = new Date()): string { + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, "0"); + const day = String(now.getDate()).padStart(2, "0"); + return `coach-seeded-dismissed:${year}-${month}-${day}`; +} + export interface CoachConversationProps { /** * Pre-fill for the composer (suggested-prompt chip click). Resets the @@ -175,13 +183,6 @@ export interface CoachConversationProps { * "new chat" or thread switch is never overridden. */ autoOpenMostRecent?: boolean; - /** - * v1.19.1 (C5) — page surface only: open the conversation-history drawer - * once on mount. The drawer's "Conversations" handoff routes to - * `/coach?view=conversations`, which sets this so the user always sees the - * conversation list on arrival instead of a blank new-chat pane. - */ - openHistoryOnMount?: boolean; } export function CoachConversation({ @@ -198,26 +199,15 @@ export function CoachConversation({ surface, initialConversationId, autoOpenMostRecent = false, - openHistoryOnMount = false, }: CoachConversationProps) { const { t } = useTranslations(); + const router = useRouter(); const [currentConversationId, setCurrentConversationId] = useState< string | null >(initialConversationId ?? null); const [historyTrayOpen, setHistoryTrayOpen] = useState(false); const [sourcesTrayOpen, setSourcesTrayOpen] = useState(false); - // v1.18.11 (W11) — page surface only: conversation history is a LEFT - // slide-in drawer (overlay on every viewport, ChatGPT-style), opened from - // the composer's `+` actions menu. Replaces the old inline collapsible - // rail + top rail-tray strip; the top header bar is gone entirely on the - // page so the composer is the single control hub. - // v1.19.1 (C5) — seed the history drawer open when the page is entered via - // the drawer's "Conversations" handoff (`?view=conversations`), so the - // list is on screen immediately rather than a blank hero. - const [historyDrawerOpen, setHistoryDrawerOpen] = useState( - () => openHistoryOnMount, - ); const [inputValue, setInputValue] = useResettableValue(prefill ?? ""); // v1.16.4 — self-context backflow: `pendingAdopt` raises a quiet // offer to fold a clarifying-question answer back into the @@ -425,7 +415,7 @@ export function CoachConversation({ } showHub={surface === "page"} onNewChat={handleNewChat} - onOpenHistory={() => setHistoryDrawerOpen(true)} + onOpenHistory={() => router.push("/coach/conversations")} /> ); @@ -461,7 +451,19 @@ export function CoachConversation({ // it. When the server returns no signal the hint is null and the neutral // greeting stands — never a fabricated opener. const a2Metric = launchScope?.metric ?? null; - const seededEnabled = heroActive && a2Metric === null; + // v1.21.4 (C2) — once the user dismisses today's seeded opener it stays gone + // for the rest of the local calendar day; the fetch is skipped too, so a + // dismissed day costs nothing. SSR-safe lazy init guards `window`. + const [seededDismissedToday, setSeededDismissedToday] = useState(() => { + if (typeof window === "undefined") return false; + try { + return window.localStorage.getItem(seededDismissStorageKey()) === "1"; + } catch { + return false; + } + }); + const seededEnabled = + heroActive && a2Metric === null && !seededDismissedToday; const { data: seeded } = useQuery({ queryKey: queryKeys.coachSeededQuestion(), queryFn: async () => @@ -474,6 +476,17 @@ export function CoachConversation({ setInputValue(question); } + function dismissSeeded() { + setSeededDismissedToday(true); + if (typeof window === "undefined") return; + try { + window.localStorage.setItem(seededDismissStorageKey(), "1"); + } catch { + // Storage can throw (private mode / quota); the in-memory flag still + // hides the opener for this session. + } + } + // `t()` returns the key string itself when a key is missing, so the only // reliable "is this key defined" signal is `t(key) !== key`. function tOrNull(key: string): string | null { @@ -507,7 +520,7 @@ export function CoachConversation({ onSeed={seedComposer} /> ); - } else if (seeded?.signal) { + } else if (seeded?.signal && !seededDismissedToday) { // A3 — the notable derived signal. The label + opener are keyed on the // signal's sourceMetric (`readiness` / `recovery`); an unknown sentinel // (future detector additions) skips the opener rather than guessing. @@ -523,6 +536,7 @@ export function CoachConversation({ label={signalLabel} question={signalQuestion} onSeed={seedComposer} + onDismiss={dismissSeeded} /> ); } @@ -577,41 +591,6 @@ export function CoachConversation({ ); - // v1.18.11 (W11) — page surface: the conversation history is a LEFT - // slide-in drawer (overlay on every viewport), opened from the composer's - // `+` actions menu. Reuses the shared `` + conversation-list - // query; respects `prefers-reduced-motion` (the Sheet primitive disables - // its slide there). Selecting a conversation closes the drawer. - const historyDrawer = ( - - - - - {t("insights.coach.historyTitle")} - - -
- { - // v1.16.5 — switching conversations drops the guided session; - // unanswered questions stay pending. - setCurrentConversationId(id); - setHistoryDrawerOpen(false); - setPendingAdopt(null); - dispatchGuided({ type: "RESET" }); - }} - /> -
-
-
- ); - // v1.18.11 (W11) — the PAGE surface drops the top header bar and the // rail-tray strip entirely. The composer is the single control hub; the // thread (`[&>*]:max-w-2xl` inner gutter) and the docked composer @@ -626,32 +605,9 @@ export function CoachConversation({ data-variant={surface} className={cn("flex min-h-0 flex-1 flex-col", className)} > - {/* v1.21.0 — the page toolbar is now a single trailing affordance: - the settings gear in the top-right corner. The "Conversations" - and "New chat" controls were removed here — both still live in the - composer's `+` actions menu, keeping the new-chat surface calm and - uncluttered. The gear deep-links to Settings → AI (one place for - model + behaviour), matching the drawer header's gear. */} -
- -
+ {/* v1.21.4 (A) — the page-toolbar gear was removed; Settings now lives + in the composer's `+` actions menu alongside New chat and + Conversations, keeping the page chrome to the composer alone. */} {heroActive ? ( ) : ( @@ -675,7 +631,6 @@ export function CoachConversation({ )} - {historyDrawer} ); } diff --git a/src/components/insights/coach-panel/coach-input.tsx b/src/components/insights/coach-panel/coach-input.tsx index 372682588..dfa9540a0 100644 --- a/src/components/insights/coach-panel/coach-input.tsx +++ b/src/components/insights/coach-panel/coach-input.tsx @@ -9,7 +9,16 @@ import { useState, useSyncExternalStore, } from "react"; -import { Loader2, MessagesSquare, Mic, Plus, Send, Square } from "lucide-react"; +import Link from "next/link"; +import { + Loader2, + MessagesSquare, + Mic, + Plus, + Send, + Settings, + Square, +} from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; @@ -479,6 +488,12 @@ export function CoachInput({