mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 06:32:03 +00:00
feat(documents): move document chat into the Coach drawer, neutral header entry
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: <document>" 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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPyLteNeTWwEC154X7vLWc
This commit is contained in:
1 parent
c07ffe78c0
commit
dc75ae69ae
14 files changed
+414
-255
No files matched your search
@@ -156,20 +156,18 @@ describe("<DocumentAiSection>", () => {
|
||||
expect(html).not.toContain('data-slot="document-read-ai"');
|
||||
});
|
||||
|
||||
it("hides the manual AI action row when auto-read is on, keeping status + chat", () => {
|
||||
const chat = <div data-slot="chat-probe">chat here</div>;
|
||||
const html = render(
|
||||
base({ aiEnabled: true, autoReadEnabled: true, chatSlot: chat }),
|
||||
);
|
||||
it("hides the manual AI action row when auto-read is on, keeping status", () => {
|
||||
const html = render(base({ aiEnabled: true, autoReadEnabled: true }));
|
||||
// Auto-read reads on upload — the manual per-document actions are gone.
|
||||
expect(html).not.toContain('data-slot="document-read-ai"');
|
||||
expect(html).not.toContain('data-slot="assist-suggest"');
|
||||
expect(html).not.toContain("Summarise");
|
||||
expect(html).not.toContain("Show extracted text");
|
||||
// The header, the searchable status pill, and content-search chat stay.
|
||||
// The header and the searchable status pill stay. The scoped chat entry
|
||||
// now lives in the detail-sheet header (a neutral Coach icon), not in this
|
||||
// section, so this block no longer owns a chat slot.
|
||||
expect(html).toContain('data-slot="document-ai-section"');
|
||||
expect(html).toContain('data-slot="content-search-status"');
|
||||
expect(html).toContain('data-slot="chat-probe"');
|
||||
});
|
||||
|
||||
it("shows the manual AI action row when auto-read is off", () => {
|
||||
@@ -178,23 +176,4 @@ describe("<DocumentAiSection>", () => {
|
||||
expect(html).toContain('data-slot="assist-suggest"');
|
||||
expect(html).toContain("Summarise");
|
||||
});
|
||||
|
||||
it("renders the chat slot inside the AI area only when a provider is available", () => {
|
||||
const chat = <div data-slot="chat-probe">chat here</div>;
|
||||
const enabled = render(base({ aiEnabled: true, chatSlot: chat }));
|
||||
expect(enabled).toContain('data-slot="chat-probe"');
|
||||
|
||||
// No provider → the whole block collapses to the calm pointer; the chat
|
||||
// slot (provider-only affordance) must not render.
|
||||
const disabled = render(
|
||||
base({
|
||||
aiEnabled: false,
|
||||
indexEnabled: false,
|
||||
unavailableReason: "no-provider",
|
||||
chatSlot: chat,
|
||||
}),
|
||||
);
|
||||
expect(disabled).not.toContain('data-slot="chat-probe"');
|
||||
expect(disabled).toContain('data-slot="assist-unavailable"');
|
||||
});
|
||||
});
|
||||
@@ -1,24 +1,24 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import {
|
||||
DocumentChatConversation,
|
||||
DocumentChatPanel,
|
||||
documentChatErrorKey,
|
||||
} from "../document-chat-panel";
|
||||
import type { DocumentChatMessage } from "../use-document-chat";
|
||||
|
||||
/**
|
||||
* The scoped "chat about this document" panel:
|
||||
* The scoped "chat about this document" surface:
|
||||
* - the OPEN conversation body renders a plain-text message log + composer +
|
||||
* an always-on safety note; assistant/user prose is XSS-safe React text
|
||||
* (raw HTML in a turn is escaped, never injected);
|
||||
* - the container gates on indexing: a content-indexed document offers the
|
||||
* chat entry, a non-indexed one shows a calm read-it-first hint (never an
|
||||
* error);
|
||||
* - error codes map to calm translation keys, never a raw provider string.
|
||||
*
|
||||
* The stateful container + indexing gate live in `document-chat-drawer.tsx`
|
||||
* (Coach-drawer chrome scoped to one document); its open/not-indexed branches
|
||||
* ride the Radix Sheet portal, so they are pinned by the e2e suite rather than
|
||||
* a static render here.
|
||||
*/
|
||||
|
||||
function renderPure(node: React.ReactNode) {
|
||||
@@ -27,17 +27,6 @@ function renderPure(node: React.ReactNode) {
|
||||
);
|
||||
}
|
||||
|
||||
function renderContainer(node: React.ReactNode) {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
return renderToStaticMarkup(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<I18nProvider initialLocale="en">{node}</I18nProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
function conversation(
|
||||
@@ -154,27 +143,6 @@ describe("<DocumentChatConversation> (open body)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("<DocumentChatPanel> (gating)", () => {
|
||||
it("offers the chat entry when the document is content-indexed", () => {
|
||||
const html = renderContainer(
|
||||
<DocumentChatPanel documentId="doc1" indexed />,
|
||||
);
|
||||
expect(html).toContain('data-slot="document-chat-open"');
|
||||
expect(html).toContain("Chat about this document");
|
||||
// Not the not-indexed hint.
|
||||
expect(html).not.toContain('data-slot="document-chat-not-indexed"');
|
||||
});
|
||||
|
||||
it("shows a calm read-it-first hint (never an error) when not indexed", () => {
|
||||
const html = renderContainer(
|
||||
<DocumentChatPanel documentId="doc1" indexed={false} />,
|
||||
);
|
||||
expect(html).toContain('data-slot="document-chat-not-indexed"');
|
||||
expect(html).not.toContain('data-slot="document-chat-open"');
|
||||
expect(html).not.toContain('role="alert"');
|
||||
});
|
||||
});
|
||||
|
||||
describe("documentChatErrorKey", () => {
|
||||
it("maps provider + client codes to calm keys, defaulting to generic", () => {
|
||||
expect(documentChatErrorKey("documents.chat.budget.exceeded")).toBe(
|
||||
|
||||
Reference in new issue
Block a user