fix(documents): keep the AI read action + provenance pill, trim only the block title

The detail sheet's AI block over-removed: the read-with-AI action and the
read-provenance pill are both contract surfaces (the action is the explicit read
path when auto-read is off / for an external provider; the pill is the read-state
indicator). Only the redundant block title + wand were the wasted row. Restore
the action and pill; drop just the title. The action row still collapses when
auto-read is on, as before.
This commit is contained in:
Marc-André Bombeck committed 2026-07-10 14:06:12 +02:00
1 parent c2c4d56b44
commit 993e693ad6
8 files changed
+124 -55

No files matched your search

+4 -1
View File
@@ -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.",
+4 -1
View File
@@ -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.",
+4 -1
View File
@@ -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.",
+4 -1
View File
@@ -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.",
+4 -1
View File
@@ -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.",
+4 -1
View File
@@ -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.",
@@ -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("<DocumentAiSection>", () => {
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("<DocumentAiSection>", () => {
// 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("<DocumentAiSection>", () => {
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");
});
@@ -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 (
<div
data-slot="document-ai-section"
className="border-border bg-muted/30 space-y-3 rounded-lg border p-3 md:p-4"
>
{/* 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. */}
<div className="flex items-center justify-end">
<ContentSearchStatus
hasContentIndex={hasContentIndex}
source={contentIndexSource}
isPending={indexPending}
/>
</div>
{aiEnabled ? (
<>
{!autoReadEnabled ? (
<div className="flex flex-wrap items-center gap-2">
{indexEnabled ? (
<Button
type="button"
size="sm"
data-slot="document-read-ai"
onClick={onIndex}
disabled={indexPending || actionsDisabled}
>
<ScanText
className={cn(
"size-4",
indexPending &&
"animate-pulse motion-reduce:animate-none",
)}
aria-hidden
/>
{readLabel}
</Button>
) : null}
<Button
type="button"
variant="outline"