mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 15:02:04 +00:00
feat(documents): open the doc-scoped Coach in the side drawer, not a full page
"Ask the Coach" on a document used to navigate to /coach?doc=<id> — a full-page jump away from the vault. It now opens the real, doc-scoped conversation in the shared side drawer, and the drawer's maximize control expands it to the full /coach page while preserving scope. - Thread an optional documentId through the launch context: askCoach gains a fourth argument, the provider tracks it alongside prefill/scope and clears it on close, and LayoutCoachMount forwards it to the drawer as initialDocumentId. The open-state precedence moves into a pure resolveLaunchState helper. - The vault detail sheet closes itself and calls askCoach(..., doc.id) instead of routing away, so the drawer owns the surface. - Maximize preserves continuity: the drawer reads the live conversation id via a new imperative getter out of the conversation surface (parallel to the reset registration) and hands off to /coach?c=<id> once a thread exists, falling back to /coach?doc=<id> for a chat maximized before its first turn. The send path is untouched: every document turn still routes through the hardened fenced endpoint keyed on documentId — only where the conversation renders (drawer vs page) changed. The documents-chat e2e now asserts the drawer opens in place, doc-scoped, rather than a URL change.
This commit is contained in:
1 parent
7c96abc8ec
commit
d72f078764
9 files changed
+390
-51
No files matched your search
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { coachMaximizeHref } from "../coach-drawer";
|
||||
|
||||
/**
|
||||
* v1.28.52 (Documents R3) — the drawer's maximize control must preserve scope
|
||||
* when it hands off to the full page. `coachMaximizeHref` pins the continuity
|
||||
* rules so a doc-scoped drawer never expands into a blank `/coach`:
|
||||
*
|
||||
* - a live conversation id (a turn created the thread) wins → `/coach?c=<id>`
|
||||
* so the exact thread re-opens, scope and all;
|
||||
* - else a document scope (maximized before the first turn) → `/coach?doc=<id>`;
|
||||
* - else the plain new-chat surface.
|
||||
*/
|
||||
describe("coachMaximizeHref (Coach drawer maximize continuity)", () => {
|
||||
it("re-opens an existing thread by conversation id", () => {
|
||||
expect(coachMaximizeHref("conv-1", "doc-1")).toBe("/coach?c=conv-1");
|
||||
});
|
||||
|
||||
it("prefers the conversation id even when no document scope exists", () => {
|
||||
expect(coachMaximizeHref("conv-1", null)).toBe("/coach?c=conv-1");
|
||||
expect(coachMaximizeHref("conv-1", undefined)).toBe("/coach?c=conv-1");
|
||||
});
|
||||
|
||||
it("falls back to the document scope for a pre-first-turn doc chat", () => {
|
||||
expect(coachMaximizeHref(null, "doc-1")).toBe("/coach?doc=doc-1");
|
||||
});
|
||||
|
||||
it("lands on the plain new-chat surface with neither thread nor document", () => {
|
||||
expect(coachMaximizeHref(null, null)).toBe("/coach");
|
||||
expect(coachMaximizeHref(null, undefined)).toBe("/coach");
|
||||
});
|
||||
});
|
||||
@@ -201,6 +201,14 @@ export interface CoachConversationProps {
|
||||
* authoritatively from the loaded conversation's `documentId` instead.
|
||||
*/
|
||||
initialDocumentId?: string | null;
|
||||
/**
|
||||
* v1.28.52 (Documents R3) — hand the chrome an imperative getter for the
|
||||
* live conversation id (parallel to `registerReset`). The drawer reads it at
|
||||
* maximize time so it can hand off to `/coach?c=<id>` when a thread already
|
||||
* exists, falling back to `/coach?doc=<id>` for a pre-first-turn doc chat —
|
||||
* the surface-toggle preserves scope. The page omits it.
|
||||
*/
|
||||
registerConversationIdGetter?: (getter: () => string | null) => void;
|
||||
}
|
||||
|
||||
export function CoachConversation({
|
||||
@@ -219,6 +227,7 @@ export function CoachConversation({
|
||||
initialConversationId,
|
||||
autoOpenMostRecent = false,
|
||||
initialDocumentId,
|
||||
registerConversationIdGetter,
|
||||
}: CoachConversationProps) {
|
||||
const { t } = useTranslations();
|
||||
const router = useRouter();
|
||||
@@ -347,6 +356,20 @@ export function CoachConversation({
|
||||
});
|
||||
}, [registerReset, send, setInputValue]);
|
||||
|
||||
// v1.28.52 (Documents R3) — expose the live conversation id imperatively so
|
||||
// the drawer chrome can read it at maximize time (it lives inside this
|
||||
// surface, not the chrome). A latest-value ref keeps the registered getter
|
||||
// stable while always returning the current thread id, so maximize hands off
|
||||
// to `/coach?c=<id>` once a turn has created the thread.
|
||||
const conversationIdRef = useRef<string | null>(currentConversationId);
|
||||
useEffect(() => {
|
||||
conversationIdRef.current = currentConversationId;
|
||||
});
|
||||
useEffect(() => {
|
||||
if (!registerConversationIdGetter) return;
|
||||
registerConversationIdGetter(() => conversationIdRef.current);
|
||||
}, [registerConversationIdGetter]);
|
||||
|
||||
async function handleSubmit(value: string) {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed || send.isStreaming) return;
|
||||
|
||||
@@ -28,6 +28,26 @@ export {
|
||||
nextResettableValue,
|
||||
} from "./use-resettable-value";
|
||||
|
||||
/**
|
||||
* v1.28.52 (Documents R3) — the full-page target the maximize control hands
|
||||
* off to, kept pure so the continuity rules are unit-testable.
|
||||
*
|
||||
* Precedence:
|
||||
* - a live conversation id (a turn has created the thread) → `/coach?c=<id>`
|
||||
* so the exact thread re-opens on the page, scope and all.
|
||||
* - else a document scope (a doc-scoped drawer maximized before its first
|
||||
* turn) → `/coach?doc=<id>` so the fresh page chat stays scoped.
|
||||
* - else the plain `/coach` new-chat surface.
|
||||
*/
|
||||
export function coachMaximizeHref(
|
||||
conversationId: string | null,
|
||||
documentId?: string | null,
|
||||
): string {
|
||||
if (conversationId) return `/coach?c=${conversationId}`;
|
||||
if (documentId) return `/coach?doc=${documentId}`;
|
||||
return "/coach";
|
||||
}
|
||||
|
||||
/**
|
||||
* v1.4.20 phase B2b — AI Coach drawer (right-side `<Sheet>` overlay).
|
||||
*
|
||||
@@ -59,6 +79,14 @@ export interface CoachDrawerProps {
|
||||
* exactly once on open. Used by the assessment hand-off.
|
||||
*/
|
||||
autoSend?: boolean;
|
||||
/**
|
||||
* v1.28.52 (Documents R3) — the stored document this drawer chat is scoped
|
||||
* to (the vault "Ask the Coach" action). Forwarded to `<CoachConversation>`
|
||||
* as `initialDocumentId` so the first turn routes through the hardened
|
||||
* fenced endpoint; the maximize control preserves the scope when no thread
|
||||
* exists yet (`/coach?doc=<id>`).
|
||||
*/
|
||||
documentId?: string | null;
|
||||
}
|
||||
|
||||
export function CoachDrawer({
|
||||
@@ -67,6 +95,7 @@ export function CoachDrawer({
|
||||
prefill,
|
||||
scope,
|
||||
autoSend,
|
||||
documentId,
|
||||
}: CoachDrawerProps) {
|
||||
const { t } = useTranslations();
|
||||
const router = useRouter();
|
||||
@@ -85,6 +114,18 @@ export function CoachDrawer({
|
||||
resetRef.current = reset;
|
||||
}, []);
|
||||
|
||||
// v1.28.52 (Documents R3) — the live conversation id lives inside the shared
|
||||
// surface; this imperative getter (registered parallel to `registerReset`)
|
||||
// lets the maximize control read it so it can preserve the thread / doc scope
|
||||
// when handing off to the full page.
|
||||
const conversationIdGetterRef = useRef<(() => string | null) | null>(null);
|
||||
const registerConversationIdGetter = useCallback(
|
||||
(getter: () => string | null) => {
|
||||
conversationIdGetterRef.current = getter;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleOpenChange = useCallback(
|
||||
(next: boolean) => {
|
||||
if (!next) {
|
||||
@@ -98,13 +139,20 @@ export function CoachDrawer({
|
||||
);
|
||||
|
||||
const handleMaximize = useCallback(() => {
|
||||
// Hand the conversation off to the full-page surface. Close the
|
||||
// drawer first (aborts any in-flight stream via `handleOpenChange`)
|
||||
// Hand the conversation off to the full-page surface, PRESERVING scope:
|
||||
// read the live thread id (or fall back to the document scope) BEFORE the
|
||||
// reset clears it, so a doc-scoped drawer maximizes into the same doc /
|
||||
// thread instead of a blank `/coach`.
|
||||
const href = coachMaximizeHref(
|
||||
conversationIdGetterRef.current?.() ?? null,
|
||||
documentId,
|
||||
);
|
||||
// Close the drawer (aborts any in-flight stream via `handleOpenChange`)
|
||||
// then route to the dedicated page.
|
||||
onOpenChange(false);
|
||||
resetRef.current?.();
|
||||
router.push("/coach");
|
||||
}, [onOpenChange, router]);
|
||||
router.push(href);
|
||||
}, [onOpenChange, router, documentId]);
|
||||
|
||||
// v1.21.4 (Coach-UI B) — the drawer's "Conversations" affordance now hands
|
||||
// off to the dedicated conversation-history page (`/coach/conversations`),
|
||||
@@ -146,8 +194,14 @@ export function CoachDrawer({
|
||||
prefill={prefill}
|
||||
launchScope={scope}
|
||||
autoSend={autoSend}
|
||||
// v1.28.52 (Documents R3) — scope a fresh drawer chat to a stored
|
||||
// document (vault "Ask the Coach"); the send path is unchanged (the
|
||||
// fenced document endpoint), only WHERE it renders (drawer vs page).
|
||||
initialDocumentId={documentId}
|
||||
autoFocusComposer
|
||||
registerReset={registerReset}
|
||||
// Lets the maximize control read the live thread id at hand-off time.
|
||||
registerConversationIdGetter={registerConversationIdGetter}
|
||||
// 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
|
||||
|
||||
@@ -59,6 +59,10 @@ export function LayoutCoachMount() {
|
||||
// Auto-send the prefill as the first turn when the launch requested it
|
||||
// (assessment hand-off), so the answer lands without a manual send.
|
||||
autoSend={launch.autoSend}
|
||||
// v1.28.52 (Documents R3) — carry the stored-document scope so the vault
|
||||
// "Ask the Coach" action opens the REAL fenced conversation in the drawer
|
||||
// scoped to that document (maximizing then preserves the scope).
|
||||
documentId={launch.documentId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user