diff --git a/e2e/documents-ai.spec.ts b/e2e/documents-ai.spec.ts index 44170a542..096aaa98e 100644 --- a/e2e/documents-ai.spec.ts +++ b/e2e/documents-ai.spec.ts @@ -110,6 +110,13 @@ async function mockAiEnabled( error: null, }), ); + // The detail sheet reads the auto-AI-read opt-in to decide whether to show + // the manual per-document action row. Default it OFF here so the manual + // Read / Suggest / Summarise actions render for these assertions; an unmocked + // fetch would leave the sheet reading a live value. + await page.route("**/api/auth/me/documents-auto-ai-read", (route) => + fulfilJson(route, { data: { documentsAutoAiRead: false }, error: null }), + ); } test.describe("document vault — AI assist + content search", () => { @@ -172,9 +179,6 @@ test.describe("document vault — AI assist + content search", () => { const review = sheet.locator('[data-slot="assist-suggestion-review"]'); await expect(review).toBeVisible(); - await expect( - review.getByText("AI suggestion — review before saving"), - ).toBeVisible(); // Applying the title seeds the EDITABLE field only — no write yet. await review.getByRole("button", { name: "Use title" }).click(); diff --git a/e2e/documents-auto-read.spec.ts b/e2e/documents-auto-read.spec.ts index db4da6e05..702258ada 100644 --- a/e2e/documents-auto-read.spec.ts +++ b/e2e/documents-auto-read.spec.ts @@ -44,9 +44,21 @@ async function fulfilJson( */ async function mockDocumentCapability( page: Page, - opts: { egress: "local" | "external"; pdfSupported?: boolean }, + opts: { + egress: "local" | "external"; + pdfSupported?: boolean; + /** The per-user auto-AI-read opt-in. OFF keeps the manual action row. */ + autoRead?: boolean; + }, ): Promise { const pdfSupported = opts.pdfSupported ?? true; + const autoRead = opts.autoRead ?? false; + await page.route("**/api/auth/me/documents-auto-ai-read", (route) => + fulfilJson(route, { + data: { documentsAutoAiRead: autoRead }, + error: null, + }), + ); await page.route("**/api/documents/inbound/usage", (route) => fulfilJson(route, { data: { @@ -184,8 +196,9 @@ test.describe("automatic AI reading — vault per-document contract", () => { const sheet = page.getByRole("dialog"); await expect(sheet).toBeVisible(); - // The per-document read is the explicit path — the action is present and the - // user must tap it (auto-read never removes the affordance, only the tap). + // With auto-read OFF the per-document read is the explicit path — the action + // is present and the user taps it. (When auto-read is ON the row collapses; + // that is covered by its own test below.) await expect(sheet.locator('[data-slot="document-read-ai"]')).toBeVisible(); }); @@ -234,4 +247,27 @@ test.describe("automatic AI reading — vault per-document contract", () => { await expect(sheet.locator('[data-slot="document-read-ai"]')).toBeVisible(); }); + + test("with auto-read ON the sheet drops the manual AI action row", async ({ + page, + }) => { + // Auto-read reads every upload with no per-document tap, so the manual + // Read / Suggest actions are redundant and collapse away. The section + // header and the searchable status pill stay. + await mockDocumentCapability(page, { egress: "external", autoRead: true }); + + await page.goto(`/documents?doc=${AI_PROBE_DOC_ID}`); + const sheet = page.getByRole("dialog"); + await expect(sheet).toBeVisible(); + + const section = sheet.locator('[data-slot="document-ai-section"]'); + await expect(section).toBeVisible(); + await expect( + section.locator('[data-slot="content-search-status"]'), + ).toBeVisible(); + await expect(sheet.locator('[data-slot="document-read-ai"]')).toHaveCount( + 0, + ); + await expect(sheet.locator('[data-slot="assist-suggest"]')).toHaveCount(0); + }); }); diff --git a/e2e/documents-chat.spec.ts b/e2e/documents-chat.spec.ts index 9d693a5ae..75987d1a6 100644 --- a/e2e/documents-chat.spec.ts +++ b/e2e/documents-chat.spec.ts @@ -83,6 +83,11 @@ async function mockAiEnabled( error: null, }), ); + // The detail sheet reads the auto-AI-read opt-in; default it OFF so the AI + // section renders its full set of controls alongside the chat entry. + await page.route("**/api/auth/me/documents-auto-ai-read", (route) => + fulfilJson(route, { data: { documentsAutoAiRead: false }, error: null }), + ); } test.describe("document vault — chat about a document", () => { diff --git a/messages/de.json b/messages/de.json index 9702f9964..cc0acbaaf 100644 --- a/messages/de.json +++ b/messages/de.json @@ -8229,8 +8229,6 @@ "assist": { "suggest": "Details vorschlagen", "suggesting": "Wird gelesen…", - "reviewTitle": "KI-Vorschlag — vor dem Speichern prüfen", - "reviewHint": "Es wird nichts gespeichert, bis du es übernimmst. Vorschläge können falsch sein — prüfe sie.", "suggestedTitle": "Vorgeschlagener Titel", "suggestedKind": "Vorgeschlagener Typ", "suggestedDate": "Vorgeschlagenes Datum", diff --git a/messages/en.json b/messages/en.json index 4744fc5c8..7f1d98165 100644 --- a/messages/en.json +++ b/messages/en.json @@ -8229,8 +8229,6 @@ "assist": { "suggest": "Suggest details", "suggesting": "Reading…", - "reviewTitle": "AI suggestion — review before saving", - "reviewHint": "Nothing is saved until you apply it. Suggestions can be wrong — check them.", "suggestedTitle": "Suggested title", "suggestedKind": "Suggested type", "suggestedDate": "Suggested date", diff --git a/messages/es.json b/messages/es.json index e2056fac8..b57d835bb 100644 --- a/messages/es.json +++ b/messages/es.json @@ -8229,8 +8229,6 @@ "assist": { "suggest": "Sugerir detalles", "suggesting": "Leyendo…", - "reviewTitle": "Sugerencia de IA: revísala antes de guardar", - "reviewHint": "No se guarda nada hasta que la apliques. Las sugerencias pueden fallar: compruébalas.", "suggestedTitle": "Título sugerido", "suggestedKind": "Tipo sugerido", "suggestedDate": "Fecha sugerida", diff --git a/messages/fr.json b/messages/fr.json index 6501d839a..019f2e55b 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -8229,8 +8229,6 @@ "assist": { "suggest": "Suggérer des détails", "suggesting": "Lecture…", - "reviewTitle": "Suggestion IA — à vérifier avant d'enregistrer", - "reviewHint": "Rien n'est enregistré tant que vous ne l'appliquez pas. Les suggestions peuvent être erronées — vérifiez-les.", "suggestedTitle": "Titre suggéré", "suggestedKind": "Type suggéré", "suggestedDate": "Date suggérée", diff --git a/messages/it.json b/messages/it.json index 821af3453..63168d001 100644 --- a/messages/it.json +++ b/messages/it.json @@ -8229,8 +8229,6 @@ "assist": { "suggest": "Suggerisci dettagli", "suggesting": "Lettura…", - "reviewTitle": "Suggerimento IA — verifica prima di salvare", - "reviewHint": "Nulla viene salvato finché non lo applichi. I suggerimenti possono essere errati: controllali.", "suggestedTitle": "Titolo suggerito", "suggestedKind": "Tipo suggerito", "suggestedDate": "Data suggerita", diff --git a/messages/pl.json b/messages/pl.json index ebdeee94a..72e409730 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -8229,8 +8229,6 @@ "assist": { "suggest": "Zaproponuj szczegóły", "suggesting": "Odczytywanie…", - "reviewTitle": "Sugestia AI — sprawdź przed zapisaniem", - "reviewHint": "Nic nie zostanie zapisane, dopóki tego nie zastosujesz. Sugestie mogą być błędne — sprawdź je.", "suggestedTitle": "Sugerowany tytuł", "suggestedKind": "Sugerowany typ", "suggestedDate": "Sugerowana data", diff --git a/src/components/documents/__tests__/document-ai-panels.test.tsx b/src/components/documents/__tests__/document-ai-panels.test.tsx index 6e6874adf..fcdd6757e 100644 --- a/src/components/documents/__tests__/document-ai-panels.test.tsx +++ b/src/components/documents/__tests__/document-ai-panels.test.tsx @@ -14,8 +14,8 @@ import { * The AI panels' render contract: * - the unavailable state is a CALM pointer to the AI settings, never an * error, and adapts its copy to the actionable reason; - * - the suggestion card carries the review-first affordance ("review before - * saving") and applies nothing on its own — every value has its own tap; + * - the suggestion card is review-first: it applies nothing on its own — + * every value has its own explicit apply tap, and a dismiss control; * - the summary panel ALWAYS carries the "not saved · not a diagnosis" note, * even while loading, so the session-only contract is never off-screen. */ @@ -55,7 +55,7 @@ describe("", () => { }); describe("", () => { - it("presents drafts behind an explicit review-before-saving affordance", () => { + it("presents drafts as reviewable fields with explicit apply controls", () => { const html = render( ", () => { />, ); expect(html).toContain('data-slot="assist-suggestion-review"'); - expect(html).toContain("AI suggestion — review before saving"); - expect(html).toContain("Nothing is saved until you apply it"); + // The heading + grey sub-caption were retired — the panel leads straight + // with the reviewable draft fields. + expect(html).not.toContain("review before saving"); + expect(html).not.toContain("Nothing is saved until you apply it"); // The suggested values are shown as reviewable drafts. expect(html).toContain("Blood panel — March 2026"); expect(html).toContain("Lab result"); diff --git a/src/components/documents/__tests__/document-ai-section.test.tsx b/src/components/documents/__tests__/document-ai-section.test.tsx index 78e963ba9..aedba8dc2 100644 --- a/src/components/documents/__tests__/document-ai-section.test.tsx +++ b/src/components/documents/__tests__/document-ai-section.test.tsx @@ -127,7 +127,6 @@ describe("", () => { ); expect(html).toContain('data-slot="assist-suggestion-review"'); expect(html).toContain("MRI knee report"); - expect(html).toContain("review before saving"); }); it("renders the transient summary panel", () => { @@ -157,6 +156,29 @@ describe("", () => { 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 =
chat here
; + const html = render( + base({ aiEnabled: true, autoReadEnabled: true, chatSlot: chat }), + ); + // 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. + 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", () => { + const html = render(base({ aiEnabled: true, autoReadEnabled: false })); + expect(html).toContain('data-slot="document-read-ai"'); + 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 =
chat here
; const enabled = render(base({ aiEnabled: true, chatSlot: chat })); diff --git a/src/components/documents/document-ai-panels.tsx b/src/components/documents/document-ai-panels.tsx index a0080ffac..28e764770 100644 --- a/src/components/documents/document-ai-panels.tsx +++ b/src/components/documents/document-ai-panels.tsx @@ -148,21 +148,7 @@ export function AssistSuggestionReview({ data-slot="assist-suggestion-review" className="border-primary/30 bg-primary/5 space-y-3 rounded-lg border p-3" > -
-
- -
-

- {t("documents.assist.reviewTitle")} -

-

- {t("documents.assist.reviewHint")} -

-
-
+
+ ) : null} - ) : null} - - - -
+ + +
+ ) : null} {suggestErrorKey ? (

diff --git a/src/components/documents/document-detail-sheet.tsx b/src/components/documents/document-detail-sheet.tsx index 0bd2c131b..559954de2 100644 --- a/src/components/documents/document-detail-sheet.tsx +++ b/src/components/documents/document-detail-sheet.tsx @@ -67,6 +67,7 @@ import { useIndexDocument } from "./use-content-index"; import { documentAiErrorKey, useDocumentAiCapability, + useDocumentsAutoAiRead, useDocumentSummary, useSuggestDetails, } from "./use-document-assist"; @@ -221,6 +222,9 @@ export function DocumentDetailSheet({ // The affordance itself is gated on `assistAvailable` (from usage); the // probe only resolves the transport mode + the actionable unavailable reason. const capability = useDocumentAiCapability(open && documentId !== null); + // When auto-read is ON, reading happens automatically on upload — the manual + // per-document AI action row is redundant and collapses away. + const autoRead = useDocumentsAutoAiRead(open && documentId !== null); const suggest = useSuggestDetails(); const summary = useDocumentSummary(); const indexDoc = useIndexDocument(); @@ -335,6 +339,7 @@ export function DocumentDetailSheet({ const aiEnabled = assistAvailable ?? capability.data?.available ?? false; const indexEnabled = contentIndexEnabled ?? capability.data?.available ?? false; + const autoReadEnabled = autoRead.data?.documentsAutoAiRead ?? false; const aiMode = capability.data?.mode === "text" ? "text" : "vision"; const aiTarget: DocumentAiTarget | null = doc ? { @@ -504,6 +509,7 @@ export function DocumentDetailSheet({ ({ + queryKey: queryKeys.documentsAutoAiRead(), + queryFn: () => + apiGet("/api/auth/me/documents-auto-ai-read"), + enabled, + staleTime: 60_000, + }); +} + /** * Suggest filing metadata for a stored document. Returns DRAFTS only — the * detail sheet prefills its edit fields and the user saves; this call writes