mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
fix(coach): default to new chat and never dead-end on conversations
Reverse the resume-most-recent entry default: opening the Coach now lands on the new-chat hero. The only exception is an unread coach-initiated message (nudge-status `unread`), which auto-opens the most-recent thread so the user sees what the Coach wrote. The `?c=<id>` deep-link is unchanged. Add a dedicated, always-visible Conversations button (plus a New chat button) to the page surface so reaching past conversations no longer hides in the composer menu. The drawer's Conversations handoff now routes to `/coach?view=conversations`, which opens the history drawer on arrival, so the side-view to full-view path always shows the list instead of a blank pane.
This commit is contained in:
1 parent
a86abd3662
commit
c6f4772606
4 files changed
+120
-18
No files matched your search
+38
-13
@@ -2,11 +2,15 @@
|
||||
|
||||
import { Suspense, useEffect } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { CoachConversation } from "@/components/insights/coach-panel/coach-conversation";
|
||||
import type { CoachNudgeStatus } from "@/components/insights/layout-coach-fab";
|
||||
import { useCoachLaunch } from "@/lib/insights/coach-launch-context";
|
||||
import { useFeatureFlags } from "@/hooks/use-feature-flags";
|
||||
import { useDisableCoach } from "@/hooks/use-disable-coach";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { apiGet } from "@/lib/api/api-fetch";
|
||||
|
||||
/**
|
||||
* v1.12.0 (Coach v2 #6) — full-page Coach conversation.
|
||||
@@ -41,32 +45,53 @@ import { useDisableCoach } from "@/hooks/use-disable-coach";
|
||||
* the active conversation. All of that lives in `<CoachConversation>`'s
|
||||
* page branch, so the page stays a thin gating + sizing wrapper.
|
||||
*
|
||||
* v1.18.11 (W11, #67) — the page is now a deep-link target INTO a
|
||||
* conversation. A `?c=<id>` query param opens that specific thread on
|
||||
* mount; absent it, the page auto-opens the user's most-recent thread
|
||||
* (server-authoritative `updatedAt desc` head of the rail, so it lands the
|
||||
* same thread on web + mobile). Both feed `<CoachConversation>`'s existing
|
||||
* selection state — the dashboard Coach entry navigates straight into the
|
||||
* live conversation instead of a blank landing. `?c=new` (or no thread at
|
||||
* all) keeps the new-chat hero. The search-param read sits in a Suspense
|
||||
* child so the client-bailout never de-opts the route.
|
||||
* v1.19.1 (C1/C5) — the Coach now DEFAULTS to the new-chat hero, reversing
|
||||
* the v1.19.0 (W7) "always resume most-recent" default the maintainer
|
||||
* disliked. Resolution order on mount:
|
||||
* - `?c=<id>` → open that specific thread (explicit deep-link, unchanged).
|
||||
* - `?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.
|
||||
* The search-param read sits in a Suspense child so the client-bailout
|
||||
* never de-opts the route.
|
||||
*/
|
||||
function CoachPageBody() {
|
||||
const searchParams = useSearchParams();
|
||||
// `?c=new` is an explicit "start a fresh chat" escape hatch; any other
|
||||
// value is treated as a conversation id to open. A blank/absent param
|
||||
// falls through to most-recent auto-open.
|
||||
// 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.
|
||||
const exceptionEligible = deepLinkedId === null && rawC !== "new";
|
||||
const { data: nudge } = useQuery({
|
||||
queryKey: queryKeys.coachNudgeStatus(),
|
||||
queryFn: async (): Promise<CoachNudgeStatus> =>
|
||||
apiGet<CoachNudgeStatus>("/api/insights/coach/nudge-status"),
|
||||
enabled: exceptionEligible && !openConversations,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
const hasUnreadCoachMessage = exceptionEligible && nudge?.unread === true;
|
||||
|
||||
return (
|
||||
<CoachConversation
|
||||
surface="page"
|
||||
autoFocusComposer
|
||||
initialConversationId={deepLinkedId}
|
||||
// Only auto-resolve most-recent when the entry did not pin a thread
|
||||
// and did not ask for a fresh chat.
|
||||
autoOpenMostRecent={deepLinkedId === null && rawC !== "new"}
|
||||
// Default is the new-chat hero; only resume most-recent for the
|
||||
// unread coach-initiated exception.
|
||||
autoOpenMostRecent={hasUnreadCoachMessage}
|
||||
openHistoryOnMount={openConversations}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -135,4 +135,25 @@ describe("<CoachConversation> page deep-link (#67)", () => {
|
||||
// though recent conversations exist in the cache.
|
||||
expect(html).toContain('data-slot="coach-hero"');
|
||||
});
|
||||
|
||||
// v1.19.1 (C2) — a dedicated, always-visible Conversations button on the
|
||||
// page surface (not buried in the composer `+` menu).
|
||||
it("renders the dedicated Conversations button on the page surface", () => {
|
||||
const html = render(<CoachConversation surface="page" />, makeClient());
|
||||
expect(html).toContain('data-slot="coach-page-conversations"');
|
||||
expect(html).toContain('data-slot="coach-page-new-chat"');
|
||||
});
|
||||
|
||||
// v1.19.1 (C5) — entering via the drawer handoff (`?view=conversations`)
|
||||
// keeps the new-chat hero (no thread auto-resumed) while opening the
|
||||
// history drawer; the toolbar + button stay present so the pane is never
|
||||
// a blank dead-end.
|
||||
it("keeps the hero and toolbar when opened with openHistoryOnMount", () => {
|
||||
const html = render(
|
||||
<CoachConversation surface="page" openHistoryOnMount />,
|
||||
makeClient(),
|
||||
);
|
||||
expect(html).toContain('data-slot="coach-hero"');
|
||||
expect(html).toContain('data-slot="coach-page-conversations"');
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useEffect, useReducer, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Plus, Settings, Sparkles } from "lucide-react";
|
||||
import { MessagesSquare, Plus, Settings, Sparkles } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -137,6 +137,13 @@ 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({
|
||||
@@ -152,6 +159,7 @@ export function CoachConversation({
|
||||
surface,
|
||||
initialConversationId,
|
||||
autoOpenMostRecent = false,
|
||||
openHistoryOnMount = false,
|
||||
}: CoachConversationProps) {
|
||||
const { t } = useTranslations();
|
||||
|
||||
@@ -165,7 +173,12 @@ export function CoachConversation({
|
||||
// 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.
|
||||
const [historyDrawerOpen, setHistoryDrawerOpen] = useState(false);
|
||||
// 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
|
||||
@@ -484,6 +497,37 @@ export function CoachConversation({
|
||||
data-variant={surface}
|
||||
className={cn("flex min-h-0 flex-1 flex-col", className)}
|
||||
>
|
||||
{/* v1.19.1 (C2) — a clear, always-visible "Conversations" button on
|
||||
the page surface. The composer's `+` menu still carries the same
|
||||
action, but the maintainer wanted an obvious, dedicated affordance
|
||||
to reach past conversations rather than one buried in a menu. */}
|
||||
<div
|
||||
data-slot="coach-page-toolbar"
|
||||
className="flex shrink-0 items-center justify-between gap-2 px-4 pt-2 sm:px-6"
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setHistoryDrawerOpen(true)}
|
||||
data-slot="coach-page-conversations"
|
||||
className="text-muted-foreground hover:text-foreground -ml-1 gap-1.5"
|
||||
>
|
||||
<MessagesSquare className="size-4" aria-hidden="true" />
|
||||
{t("insights.coach.historyTitle")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleNewChat}
|
||||
data-slot="coach-page-new-chat"
|
||||
className="text-muted-foreground hover:text-foreground -mr-1 gap-1.5"
|
||||
>
|
||||
<Plus className="size-4" aria-hidden="true" />
|
||||
{t("insights.coach.newChat")}
|
||||
</Button>
|
||||
</div>
|
||||
{heroActive ? (
|
||||
<CoachHero composer={composerNode} />
|
||||
) : (
|
||||
|
||||
@@ -88,6 +88,17 @@ export function CoachDrawer({ open, onOpenChange, prefill }: CoachDrawerProps) {
|
||||
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.
|
||||
const handleOpenConversations = useCallback(() => {
|
||||
onOpenChange(false);
|
||||
resetRef.current?.();
|
||||
router.push("/coach?view=conversations");
|
||||
}, [onOpenChange, router]);
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={handleOpenChange}>
|
||||
<SheetContent
|
||||
@@ -118,9 +129,10 @@ export function CoachDrawer({ open, onOpenChange, prefill }: CoachDrawerProps) {
|
||||
autoFocusComposer
|
||||
registerReset={registerReset}
|
||||
// v1.16.1 — the "Conversations" affordance hands off to the
|
||||
// full-page route (same path as maximize) instead of opening
|
||||
// the broken in-panel left tray.
|
||||
onRequestFullView={handleMaximize}
|
||||
// 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.
|
||||
onRequestFullView={handleOpenConversations}
|
||||
// Radix requires an accessible name + description on the
|
||||
// dialog content. Mount them through the title/description
|
||||
// render-props so the page can swap in a plain heading.
|
||||
|
||||
Reference in new issue
Block a user