fix(documents): keep the read action visible for an unread document under auto-read

The AI section of the document detail sheet collapsed whenever auto-read
was on and nothing was pending review, without checking whether the
document actually has a content index. A document the automatic pass
never reached (uploaded before the preference, or the pass did not
complete) then had NO visible way to be read; turning auto-read off was
the only workaround.

Collapse only once the document is read (or when indexing is unavailable
on the transport, where no action could be offered anyway). An unread
document keeps the Read with AI action, introduced by a short honest
note in all six locales; the suggest and summarise extras stay hidden
because the auto-read promise still covers them. The three states stay
distinct: unread shows the note plus the action, an in-flight read shows
the reading label, a read document collapses the block.

Refs #777
This commit is contained in:
Marc-André Bombeck committed 2026-08-12 22:52:03 +02:00
1 parent 19c54082bf
commit 3254e44281
8 files changed
+96 -18

No files matched your search

+2 -1
View File
@@ -9579,7 +9579,8 @@
"readDone": "Die KI hat dein Dokument gelesen.",
"read": "Mit KI auslesen",
"reread": "Erneut lesen",
"reading": "Liest…"
"reading": "Liest…",
"notReadYet": "Dieses Dokument wurde noch nicht gelesen."
},
"contentIndex": {
"indexPrompt": "Dokumentinhalte durchsuchbar machen.",
+2 -1
View File
@@ -9579,7 +9579,8 @@
"readDone": "The AI read your document.",
"read": "Read with AI",
"reread": "Read again",
"reading": "Reading…"
"reading": "Reading…",
"notReadYet": "This document has not been read yet."
},
"contentIndex": {
"indexPrompt": "Make document contents searchable.",
+2 -1
View File
@@ -9579,7 +9579,8 @@
"readDone": "La IA leyó tu documento.",
"read": "Leer con IA",
"reread": "Leer de nuevo",
"reading": "Leyendo…"
"reading": "Leyendo…",
"notReadYet": "Este documento aún no se ha leído."
},
"contentIndex": {
"indexPrompt": "Haz que el contenido de los documentos sea buscable.",
+2 -1
View File
@@ -9579,7 +9579,8 @@
"readDone": "L’IA a lu votre document.",
"read": "Lire avec l’IA",
"reread": "Relire",
"reading": "Lecture…"
"reading": "Lecture…",
"notReadYet": "Ce document n’a pas encore été lu."
},
"contentIndex": {
"indexPrompt": "Rendre le contenu des documents consultable.",
+2 -1
View File
@@ -9579,7 +9579,8 @@
"readDone": "L’IA ha letto il tuo documento.",
"read": "Leggi con l’IA",
"reread": "Leggi di nuovo",
"reading": "Lettura…"
"reading": "Lettura…",
"notReadYet": "Questo documento non è ancora stato letto."
},
"contentIndex": {
"indexPrompt": "Rendi ricercabile il contenuto dei documenti.",
+2 -1
View File
@@ -9579,7 +9579,8 @@
"readDone": "AI odczytało Twój dokument.",
"read": "Odczytaj z AI",
"reread": "Odczytaj ponownie",
"reading": "Odczytywanie…"
"reading": "Odczytywanie…",
"notReadYet": "Ten dokument nie został jeszcze odczytany."
},
"contentIndex": {
"indexPrompt": "Udostępnij treść dokumentów do wyszukiwania.",
@@ -159,16 +159,50 @@ 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", () => {
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"');
it("collapses entirely when auto-read is on and the document is read", () => {
const html = render(
base({
aiEnabled: true,
autoReadEnabled: true,
hasContentIndex: true,
contentIndexSource: "vision",
}),
);
// v1.28.22 — with auto-read ON, the document read, and nothing pending
// the WHOLE block collapses: no chrome, no status pill; the sheet content
// starts straight with the document fields under the preview.
expect(html).toBe("");
});
it("keeps the read action for an unread document even with auto-read on", () => {
// Auto-read promises reading on upload — but a document with no content
// index never got that read (it may predate the preference, or the pass
// did not complete). Collapsing here would leave the document with no
// path to a read at all; the recovery action must stay visible.
const html = render(
base({ aiEnabled: true, autoReadEnabled: true, hasContentIndex: false }),
);
expect(html).toContain('data-slot="document-read-ai"');
expect(html).toContain("Read with AI");
expect(html).toContain('data-slot="document-unread-note"');
expect(html).toContain("This document has not been read yet.");
// The auto-read promise still covers the extras: no suggest / summarise.
expect(html).not.toContain('data-slot="assist-suggest"');
expect(html).not.toContain("Summarise");
expect(html).not.toContain("Show extracted text");
// v1.28.22 — with auto-read ON and nothing pending the WHOLE block
// collapses: no chrome, no status pill; the sheet content starts straight
// with the document fields under the preview.
});
it("still collapses an unread document when indexing is unavailable", () => {
// No transport can index here, so there is no action to recover with —
// an empty chrome box would be the only thing left to render.
const html = render(
base({
aiEnabled: true,
autoReadEnabled: true,
hasContentIndex: false,
indexEnabled: false,
}),
);
expect(html).toBe("");
});
@@ -17,8 +17,13 @@
* - "Suggest details" drafts a title / type / date to review before saving.
* - "Summarise" / "Show text" surface a transient, session-only read-out.
*
* With auto-read ON and nothing pending to review the block collapses entirely —
* the sheet content starts straight with the document fields under the preview.
* With auto-read ON, the document already read, and nothing pending to review
* the block collapses entirely — the sheet content starts straight with the
* document fields under the preview. An UNREAD document keeps the read action
* visible whatever the preference says: auto-read is a promise that reading
* happens on upload, and a document the automatic pass never reached (it may
* predate the preference, or the pass did not complete) would otherwise have
* no path to a read at all.
*
* Presentational: the sheet owns the mutations + capability probe and passes
* their state + handlers down. When `aiEnabled` is false the actions collapse to
@@ -117,12 +122,16 @@ export function DocumentAiSection({
// v1.28.22 — the block carries NO header/status chrome any more (the
// provenance pill moved to the vault card's meta row; auto-read makes an
// in-sheet "read by AI" banner redundant). With auto-read ON and nothing
// pending to review, nothing below would render — collapse the whole box so
// the content under the preview starts straight with the fields.
// in-sheet "read by AI" banner redundant). With auto-read ON, the document
// read, and nothing pending to review, nothing below would render —
// collapse the whole box so the content under the preview starts straight
// with the fields. An unindexed document does NOT collapse (unless indexing
// is unavailable on this transport): it still needs its read, and this
// section is the only place that action lives.
if (
aiEnabled &&
autoReadEnabled &&
(hasContentIndex || !indexEnabled) &&
!suggestErrorKey &&
!suggestion &&
!summaryOutput
@@ -191,6 +200,35 @@ export function DocumentAiSection({
{t("documents.summary.showText")}
</Button>
</div>
) : !hasContentIndex && indexEnabled ? (
// Auto-read is ON but this document has no content index: the
// automatic pass never reached it or did not complete. The
// suggest / summarise extras stay hidden (auto-read's promise
// covers them), but the read itself must stay reachable.
<div className="space-y-2">
<p
data-slot="document-unread-note"
className="text-muted-foreground text-xs"
>
{t("documents.ai.notReadYet")}
</p>
<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>
</div>
) : null}
{suggestErrorKey ? (