From dc75ae69aef792656d04f2064cd7846cd445da74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 9 Jul 2026 18:00:06 +0200 Subject: [PATCH] feat(documents): move document chat into the Coach drawer, neutral header entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the collapsed bottom-of-card "Chat about this document" outline button with a neutral, ghost Coach icon pinned to the top-right of the document detail-sheet header, mirroring the Insights ask-coach affordance. The icon uses the muted-foreground token only — never the primary/FAB gradient — and opens a document-scoped chat drawer. Present the chat in the Coach chat drawer chrome (right-side sheet on desktop, bottom sheet on phones; same width caps; close-only, no maximize or conversations rail) with a quiet "Chatting about: " scope badge. The drawer reuses the existing pure conversation body and the existing client hooks, so it still posts /api/documents/inbound/[id]/chat verbatim: no tools, no health snapshot, fenced document, inbound/replay refusal, outbound screen, numeric grounding, consent + budget + per-user rate bucket, indexed-only, plain-text render — the route is untouched. Thread isolation is unchanged (the general Coach filters documentId: null; the document route always scopes { documentId: id }). Add a small reusable headerAction slot to ResponsiveSheet for the header icon. Retire the DocumentChatPanel container and the AI-section chatSlot; keep the pure DocumentChatConversation body and documentChatErrorKey. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01KPyLteNeTWwEC154X7vLWc --- e2e/documents-chat.spec.ts | 68 ++++-- messages/de.json | 5 +- messages/en.json | 5 +- messages/es.json | 5 +- messages/fr.json | 5 +- messages/it.json | 5 +- messages/pl.json | 5 +- .../__tests__/document-ai-section.test.tsx | 31 +-- .../__tests__/document-chat-panel.test.tsx | 44 +--- .../documents/document-ai-section.tsx | 12 - .../documents/document-chat-drawer.tsx | 211 ++++++++++++++++++ .../documents/document-chat-panel.tsx | 132 +---------- .../documents/document-detail-sheet.tsx | 65 +++++- src/components/ui/responsive-sheet.tsx | 76 +++++-- 14 files changed, 414 insertions(+), 255 deletions(-) create mode 100644 src/components/documents/document-chat-drawer.tsx diff --git a/e2e/documents-chat.spec.ts b/e2e/documents-chat.spec.ts index 75987d1a6..ce76987aa 100644 --- a/e2e/documents-chat.spec.ts +++ b/e2e/documents-chat.spec.ts @@ -123,25 +123,29 @@ test.describe("document vault — chat about a document", () => { ); await page.goto(`/documents?doc=${CONTENT_DOC_ID}`); - const sheet = page.getByRole("dialog"); + const sheet = page.locator('[data-slot="responsive-sheet-content"]'); await expect(sheet).toBeVisible(); + // The neutral Coach icon lives in the detail-sheet header; tapping it + // opens the doc-scoped chat drawer (a sibling overlay). const open = sheet.locator('[data-slot="document-chat-open"]'); await expect(open).toBeVisible(); await open.click(); + const drawer = page.locator('[data-slot="document-chat-drawer"]'); + await expect(drawer).toBeVisible(); await expect( - sheet.locator('[data-slot="document-chat-log"]'), + drawer.locator('[data-slot="document-chat-log"]'), ).toBeVisible(); await expect( - sheet.locator('[data-slot="document-chat-empty"]'), + drawer.locator('[data-slot="document-chat-empty"]'), ).toBeVisible(); await expect( - sheet.locator('[data-slot="document-chat-input"]'), + drawer.locator('[data-slot="document-chat-input"]'), ).toBeVisible(); // The always-on safety note is present. await expect( - sheet.locator('[data-slot="document-chat-safety"]'), + drawer.locator('[data-slot="document-chat-safety"]'), ).toContainText("not medical advice"); }); @@ -238,16 +242,18 @@ test.describe("document vault — chat about a document", () => { ); await page.goto(`/documents?doc=${CONTENT_DOC_ID}`); - const sheet = page.getByRole("dialog"); + const sheet = page.locator('[data-slot="responsive-sheet-content"]'); await expect(sheet).toBeVisible(); await sheet.locator('[data-slot="document-chat-open"]').click(); - const input = sheet.locator('[data-slot="document-chat-input"]'); + const drawer = page.locator('[data-slot="document-chat-drawer"]'); + await expect(drawer).toBeVisible(); + const input = drawer.locator('[data-slot="document-chat-input"]'); await input.fill(QUESTION); - await sheet.locator('[data-slot="document-chat-send"]').click(); + await drawer.locator('[data-slot="document-chat-send"]').click(); // The user's question and the streamed/settled reply both land in the log. - const log = sheet.locator('[data-slot="document-chat-log"]'); + const log = drawer.locator('[data-slot="document-chat-log"]'); await expect(log.getByText(QUESTION)).toBeVisible(); await expect(log.getByText(/findings are unremarkable/)).toBeVisible(); // The reply settled to a single assistant bubble (no duplicate). @@ -354,15 +360,17 @@ test.describe("document vault — chat about a document", () => { ); await page.goto(`/documents?doc=${CONTENT_DOC_ID}`); - const sheet = page.getByRole("dialog"); + const sheet = page.locator('[data-slot="responsive-sheet-content"]'); await expect(sheet).toBeVisible(); await sheet.locator('[data-slot="document-chat-open"]').click(); - const input = sheet.locator('[data-slot="document-chat-input"]'); + const drawer = page.locator('[data-slot="document-chat-drawer"]'); + await expect(drawer).toBeVisible(); + const input = drawer.locator('[data-slot="document-chat-input"]'); await input.fill(QUESTION); - await sheet.locator('[data-slot="document-chat-send"]').click(); + await drawer.locator('[data-slot="document-chat-send"]').click(); - const log = sheet.locator('[data-slot="document-chat-log"]'); + const log = drawer.locator('[data-slot="document-chat-log"]'); // The describe-only answer lands; the panel renders the model's