diff --git a/messages/de.json b/messages/de.json index 47fca5e71..93f63d8d9 100644 --- a/messages/de.json +++ b/messages/de.json @@ -8321,7 +8321,10 @@ "statusSearchable": "Durchsuchbar", "statusIndexing": "Wird durchsuchbar gemacht…", "statusNotYet": "Noch nicht durchsuchbar", - "readDone": "Die KI hat dein Dokument gelesen." + "readDone": "Die KI hat dein Dokument gelesen.", + "read": "Mit KI auslesen", + "reread": "Erneut lesen", + "reading": "Liest…" }, "contentIndex": { "indexPrompt": "Dokumentinhalte durchsuchbar machen.", diff --git a/messages/en.json b/messages/en.json index e0cdbf0c5..c99619f58 100644 --- a/messages/en.json +++ b/messages/en.json @@ -8321,7 +8321,10 @@ "statusSearchable": "Searchable", "statusIndexing": "Making searchable…", "statusNotYet": "Not searchable yet", - "readDone": "The AI read your document." + "readDone": "The AI read your document.", + "read": "Read with AI", + "reread": "Read again", + "reading": "Reading…" }, "contentIndex": { "indexPrompt": "Make document contents searchable.", diff --git a/messages/es.json b/messages/es.json index 34690e903..ccf25e291 100644 --- a/messages/es.json +++ b/messages/es.json @@ -8321,7 +8321,10 @@ "statusSearchable": "Buscable", "statusIndexing": "Haciéndolo buscable…", "statusNotYet": "Aún no buscable", - "readDone": "La IA leyó tu documento." + "readDone": "La IA leyó tu documento.", + "read": "Leer con IA", + "reread": "Leer de nuevo", + "reading": "Leyendo…" }, "contentIndex": { "indexPrompt": "Haz que el contenido de los documentos sea buscable.", diff --git a/messages/fr.json b/messages/fr.json index 32d8429cc..e3a275ee0 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -8321,7 +8321,10 @@ "statusSearchable": "Consultable", "statusIndexing": "Rendu consultable…", "statusNotYet": "Pas encore consultable", - "readDone": "L’IA a lu votre document." + "readDone": "L’IA a lu votre document.", + "read": "Lire avec l’IA", + "reread": "Relire", + "reading": "Lecture…" }, "contentIndex": { "indexPrompt": "Rendre le contenu des documents consultable.", diff --git a/messages/it.json b/messages/it.json index c97e7c53b..3c7c91572 100644 --- a/messages/it.json +++ b/messages/it.json @@ -8321,7 +8321,10 @@ "statusSearchable": "Ricercabile", "statusIndexing": "Rendo ricercabile…", "statusNotYet": "Non ancora ricercabile", - "readDone": "L’IA ha letto il tuo documento." + "readDone": "L’IA ha letto il tuo documento.", + "read": "Leggi con l’IA", + "reread": "Leggi di nuovo", + "reading": "Lettura…" }, "contentIndex": { "indexPrompt": "Rendi ricercabile il contenuto dei documenti.", diff --git a/messages/pl.json b/messages/pl.json index da436481a..aa22b3209 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -8321,7 +8321,10 @@ "statusSearchable": "Wyszukiwalne", "statusIndexing": "Udostępnianie do wyszukiwania…", "statusNotYet": "Jeszcze nie do wyszukania", - "readDone": "AI odczytało Twój dokument." + "readDone": "AI odczytało Twój dokument.", + "read": "Odczytaj z AI", + "reread": "Odczytaj ponownie", + "reading": "Odczytywanie…" }, "contentIndex": { "indexPrompt": "Udostępnij treść dokumentów do wyszukiwania.", diff --git a/src/components/documents/__tests__/document-ai-section.test.tsx b/src/components/documents/__tests__/document-ai-section.test.tsx index 649b25916..87955d85c 100644 --- a/src/components/documents/__tests__/document-ai-section.test.tsx +++ b/src/components/documents/__tests__/document-ai-section.test.tsx @@ -6,13 +6,11 @@ import type { DocumentSuggestionDto } from "@/lib/validations/inbound-documents" import { DocumentAiSection } from "../document-ai-section"; /** - * The detail sheet's AI-assist block is availability-gated: with a provider it - * offers the review-first Suggest / Summarise / Show-text actions; without one - * the actions collapse to the calm settings pointer (never an action the - * endpoint would 422). The redundant "read with AI" button + the searchable - * status pill were removed — reading + indexing happen automatically on upload, - * so with auto-read ON and no active suggestion/summary the whole block - * collapses. The summary panel, when open, keeps the session-only note visible. + * The detail sheet's "Read with AI" block is availability-gated: with a provider + * it offers the prominent "Read with AI" action alongside suggest / summary; + * without one the actions collapse to the calm settings pointer (never an action + * the endpoint would 422). The searchable status pill reflects the auto-index in + * both states. The summary panel, when open, keeps the session-only note visible. */ function render(node: React.ReactNode) { @@ -64,20 +62,29 @@ function base( } describe("", () => { - it("offers the review-first assist actions when a provider is available", () => { + it("leads with the prominent Read-with-AI action when a provider is available", () => { const html = render(base({ aiEnabled: true })); - // The standalone "read with AI" button is gone — reading is automatic. - expect(html).not.toContain('data-slot="document-read-ai"'); + expect(html).toContain('data-slot="document-read-ai"'); + expect(html).toContain("Read with AI"); expect(html).toContain('data-slot="assist-suggest"'); expect(html).toContain("Suggest details"); expect(html).toContain("Summarise"); expect(html).toContain("Show extracted text"); - // The searchable status pill was removed from this block. - expect(html).not.toContain('data-slot="content-search-status"'); // No unavailable pointer when the actions are offered. expect(html).not.toContain('data-slot="assist-unavailable"'); }); + it("labels the action as a re-read once a provider has already read it", () => { + const html = render( + base({ hasContentIndex: true, contentIndexSource: "vision" }), + ); + expect(html).toContain('data-slot="document-read-ai"'); + expect(html).toContain("Read again"); + // The status pill reflects the AI-read provenance. + expect(html).toContain('data-state="ai-read"'); + expect(html).toContain("Read by AI"); + }); + it("shows the calm settings pointer (no 422 actions) when no provider is available", () => { const html = render( base({ @@ -91,6 +98,23 @@ describe("", () => { // Every AI action must be absent — never a 422-guaranteed tap. expect(html).not.toContain('data-slot="assist-suggest"'); expect(html).not.toContain('data-slot="document-read-ai"'); + // But the searchable status pill still renders — auto-index is honest here. + expect(html).toContain('data-slot="content-search-status"'); + }); + + it("keeps a locally-indexed document honestly searchable without a provider", () => { + const html = render( + base({ + aiEnabled: false, + indexEnabled: false, + unavailableReason: "no-provider", + hasContentIndex: true, + contentIndexSource: "local-pdf", + }), + ); + expect(html).toContain('data-state="searchable"'); + expect(html).toContain("Searchable"); + expect(html).not.toContain("Read by AI"); }); it("renders the reviewed suggestion drafts when present", () => { @@ -123,36 +147,32 @@ describe("", () => { expect(html).not.toContain('data-slot="document-ai-egress-notice"'); // The actions are still offered. expect(html).toContain('data-slot="assist-suggest"'); + expect(html).toContain('data-slot="document-read-ai"'); }); - it("collapses the whole block when auto-read is on with nothing to review", () => { + it("hides the Read-with-AI action when indexing is unavailable but keeps suggest", () => { + const html = render(base({ aiEnabled: true, indexEnabled: false })); + expect(html).toContain('data-slot="assist-suggest"'); + expect(html).not.toContain('data-slot="document-read-ai"'); + }); + + 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, - // and with no active suggestion/summary the block renders nothing at all - // rather than an empty bordered box. - expect(html).toBe(""); - }); - - it("keeps the review panels when auto-read is on and a suggestion is present", () => { - const html = render( - base({ - aiEnabled: true, - autoReadEnabled: true, - suggestion, - suggestionKindLabel: "Imaging", - suggestionDateLabel: "14 Feb 2026", - }), - ); - // The manual action row stays hidden, but the block itself renders to host - // the review panel. - expect(html).toContain('data-slot="document-ai-section"'); + // 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).toContain('data-slot="assist-suggestion-review"'); + expect(html).not.toContain("Summarise"); + expect(html).not.toContain("Show extracted text"); + // 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"'); }); it("shows the manual AI action row when auto-read is off", () => { const html = render(base({ aiEnabled: true, autoReadEnabled: false })); - expect(html).not.toContain('data-slot="document-read-ai"'); + expect(html).toContain('data-slot="document-read-ai"'); expect(html).toContain('data-slot="assist-suggest"'); expect(html).toContain("Summarise"); }); diff --git a/src/components/documents/document-ai-section.tsx b/src/components/documents/document-ai-section.tsx index 6a52656f2..1810b2f74 100644 --- a/src/components/documents/document-ai-section.tsx +++ b/src/components/documents/document-ai-section.tsx @@ -22,11 +22,13 @@ * status pill stays, honestly reflecting any local auto-index, and the manual * form below stays fully usable. */ -import { FileText, WandSparkles } from "lucide-react"; +import { FileText, ScanText, WandSparkles } from "lucide-react"; import { Button } from "@/components/ui/button"; import { useTranslations } from "@/lib/i18n/context"; +import { cn } from "@/lib/utils"; import { + isAiReadSource, type DocumentContentIndexSourceValue, type DocumentSuggestionDto, type DocumentSummaryMode, @@ -35,6 +37,7 @@ import { import { AiUnavailableHint, AssistSuggestionReview, + ContentSearchStatus, DocumentSummaryPanel, } from "./document-ai-panels"; import type { DocumentDescribeResult } from "./use-document-assist"; @@ -42,6 +45,7 @@ import type { DocumentDescribeResult } from "./use-document-assist"; export function DocumentAiSection({ aiEnabled, autoReadEnabled = false, + indexEnabled, unavailableReason, actionsDisabled, onSuggest, @@ -61,6 +65,10 @@ export function DocumentAiSection({ summaryResult, summaryErrorKey, onCloseSummary, + hasContentIndex, + contentIndexSource, + indexPending, + onIndex, }: { aiEnabled: boolean; /** @@ -98,30 +106,53 @@ export function DocumentAiSection({ onIndex: () => void; }) { const { t } = useTranslations(); - - // The redundant "read with AI / read again" button + the searchable-status - // pill header have been removed — reading + indexing happen automatically on - // upload, so this block now only carries the review-first assist actions - // (Suggest / Summarise / Show text) and their transient panels. With - // auto-read ON and no active suggestion/summary there is nothing to show, so - // the whole block collapses rather than render an empty bordered box. - const hasContent = - !aiEnabled || - !autoReadEnabled || - Boolean(suggestErrorKey) || - Boolean(suggestion) || - Boolean(summaryOutput); - if (!hasContent) return null; + const aiRead = hasContentIndex && isAiReadSource(contentIndexSource); + const readLabel = indexPending + ? t("documents.ai.reading") + : aiRead + ? t("documents.ai.reread") + : t("documents.ai.read"); return (
+ {/* v1.28.19 — the "read with AI" block title + wand were redundant with + the read-provenance pill (auto-read already surfaces "read by AI"), so + only the provenance pill leads the block now. The pill itself stays — + it is the read-state contract the recipient/owner relies on. */} +
+ +
+ {aiEnabled ? ( <> {!autoReadEnabled ? (
+ {indexEnabled ? ( + + ) : null}