From c2c4d56b44716b80e8bab242469b8d4d34fc3077 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 10 Jul 2026 13:43:24 +0200 Subject: [PATCH 1/2] =?UTF-8?q?chore(release):=20v1.28.19=20=E2=80=94=20do?= =?UTF-8?q?cument=20and=20card=20polish?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Documents vault card: date + size and an AI-read marker now sit directly under the title instead of the meta line running off under the thumbnail. Document detail view: a larger close control replaces the small X and the redundant top-right shortcut; the inline read-with-AI row under the preview is gone (the suggest / summarise / show-text actions stay); a button to ask about the document sits next to share and download. Mental-wellbeing cards no longer wash the whole tile on hover — only the title is the open target, matching the medication cards. Medication API settings render inline instead of behind an expander that never had anything beside it. --- CHANGELOG.md | 18 +++- docs/api/openapi.yaml | 2 +- messages/de.json | 5 +- messages/en.json | 5 +- messages/es.json | 5 +- messages/fr.json | 5 +- messages/it.json | 5 +- messages/pl.json | 5 +- package.json | 2 +- public/sw.js | 2 +- .../__tests__/document-ai-section.test.tsx | 88 +++++++------------ .../documents/document-ai-section.tsx | 66 ++++---------- src/components/documents/document-card.tsx | 33 +++++-- .../documents/document-detail-sheet.tsx | 62 ++++++------- .../detail/medication-detail-tabs.tsx | 2 - .../mental-health/instrument-card.tsx | 79 +++++++++-------- .../__tests__/sync-sleep-replace.test.ts | 4 +- 17 files changed, 174 insertions(+), 214 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed8190455..2fded7059 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,23 @@ ## [Unreleased] -## [1.28.18] — 2026-07-10 — Google Health sleep reads its true total +## [1.28.19] — 2026-07-10 — Document and card polish + +Small refinements across a few surfaces. + +In the documents vault, each card now carries the date, size and an "read by AI" +marker directly under the document's title, instead of the meta line sitting off +under the thumbnail. + +The document view gets a larger, easier close button, drops a redundant top-right +shortcut and the inline "read with AI" row under the preview, and gains a button +to ask about the document right next to share and download. + +The mental-wellbeing cards no longer wash the whole tile on hover — only the +title is the tap target now, matching how the medication cards behave. + +The medication API settings render inline instead of behind an expander that +never had anything else beside it. Sleep imported from Google Health could read far too long — a night of about seven and a half hours showing as ten. Google re-scores a night after the fact, diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 7ec6fcb1e..78916a428 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: HealthLog API - version: 1.28.18 + version: 1.28.19 description: >- Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest. diff --git a/messages/de.json b/messages/de.json index b3a4cd525..47fca5e71 100644 --- a/messages/de.json +++ b/messages/de.json @@ -8317,10 +8317,6 @@ "close": "Ausblenden" }, "ai": { - "blockTitle": "Mit KI auslesen", - "read": "Mit KI auslesen", - "reread": "Erneut lesen", - "reading": "Liest…", "statusAiRead": "Von KI gelesen", "statusSearchable": "Durchsuchbar", "statusIndexing": "Wird durchsuchbar gemacht…", @@ -8357,6 +8353,7 @@ "errorConsent": "Aktiviere die KI-Zustimmung in den Einstellungen, um über dieses Dokument zu chatten.", "errorNetwork": "Du scheinst offline zu sein. Prüfe deine Verbindung und versuche es erneut.", "openAria": "Über dieses Dokument chatten", + "openButton": "Zu diesem Dokument fragen", "scopeBadge": "Chat über: {title}" } }, diff --git a/messages/en.json b/messages/en.json index ad815d6df..e0cdbf0c5 100644 --- a/messages/en.json +++ b/messages/en.json @@ -8317,10 +8317,6 @@ "close": "Hide" }, "ai": { - "blockTitle": "Read with AI", - "read": "Read with AI", - "reread": "Read again", - "reading": "Reading…", "statusAiRead": "Read by AI", "statusSearchable": "Searchable", "statusIndexing": "Making searchable…", @@ -8357,6 +8353,7 @@ "errorConsent": "Turn on AI consent in settings to chat about this document.", "errorNetwork": "You appear to be offline. Check your connection and try again.", "openAria": "Chat about this document", + "openButton": "Ask about this document", "scopeBadge": "Chatting about: {title}" } }, diff --git a/messages/es.json b/messages/es.json index 71ef8f355..34690e903 100644 --- a/messages/es.json +++ b/messages/es.json @@ -8317,10 +8317,6 @@ "close": "Ocultar" }, "ai": { - "blockTitle": "Leer con IA", - "read": "Leer con IA", - "reread": "Leer de nuevo", - "reading": "Leyendo…", "statusAiRead": "Leído por IA", "statusSearchable": "Buscable", "statusIndexing": "Haciéndolo buscable…", @@ -8357,6 +8353,7 @@ "errorConsent": "Activa el consentimiento de IA en los ajustes para chatear sobre este documento.", "errorNetwork": "Parece que no tienes conexión. Comprueba tu red e inténtalo de nuevo.", "openAria": "Chatea sobre este documento", + "openButton": "Pregunta sobre este documento", "scopeBadge": "Chateando sobre: {title}" } }, diff --git a/messages/fr.json b/messages/fr.json index e58d77646..32d8429cc 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -8317,10 +8317,6 @@ "close": "Masquer" }, "ai": { - "blockTitle": "Lire avec l’IA", - "read": "Lire avec l’IA", - "reread": "Relire", - "reading": "Lecture…", "statusAiRead": "Lu par l’IA", "statusSearchable": "Consultable", "statusIndexing": "Rendu consultable…", @@ -8357,6 +8353,7 @@ "errorConsent": "Activez le consentement à l'IA dans les paramètres pour discuter de ce document.", "errorNetwork": "Vous semblez hors ligne. Vérifiez votre connexion et réessayez.", "openAria": "Discuter de ce document", + "openButton": "Poser une question sur ce document", "scopeBadge": "Discussion sur : {title}" } }, diff --git a/messages/it.json b/messages/it.json index 0b4806912..c97e7c53b 100644 --- a/messages/it.json +++ b/messages/it.json @@ -8317,10 +8317,6 @@ "close": "Nascondi" }, "ai": { - "blockTitle": "Leggi con l’IA", - "read": "Leggi con l’IA", - "reread": "Leggi di nuovo", - "reading": "Lettura…", "statusAiRead": "Letto dall’IA", "statusSearchable": "Ricercabile", "statusIndexing": "Rendo ricercabile…", @@ -8357,6 +8353,7 @@ "errorConsent": "Attiva il consenso all'IA nelle impostazioni per chattare su questo documento.", "errorNetwork": "Sembri offline. Controlla la connessione e riprova.", "openAria": "Chatta su questo documento", + "openButton": "Fai una domanda su questo documento", "scopeBadge": "Stai chattando su: {title}" } }, diff --git a/messages/pl.json b/messages/pl.json index d16923fb0..da436481a 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -8317,10 +8317,6 @@ "close": "Ukryj" }, "ai": { - "blockTitle": "Odczytaj z AI", - "read": "Odczytaj z AI", - "reread": "Odczytaj ponownie", - "reading": "Odczytywanie…", "statusAiRead": "Odczytane przez AI", "statusSearchable": "Wyszukiwalne", "statusIndexing": "Udostępnianie do wyszukiwania…", @@ -8357,6 +8353,7 @@ "errorConsent": "Włącz zgodę na AI w ustawieniach, aby porozmawiać o tym dokumencie.", "errorNetwork": "Wygląda na to, że jesteś offline. Sprawdź połączenie i spróbuj ponownie.", "openAria": "Porozmawiaj o tym dokumencie", + "openButton": "Zapytaj o ten dokument", "scopeBadge": "Rozmowa o: {title}" } }, diff --git a/package.json b/package.json index a5025a826..f14c617f3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "healthlog", - "version": "1.28.18", + "version": "1.28.19", "description": "Self-hosted personal-health-tracking PWA with Withings integration, AI insights, and doctor-report PDF export.", "license": "PolyForm-Noncommercial-1.0.0", "homepage": "https://healthlog.dev", diff --git a/public/sw.js b/public/sw.js index 9c4ffa9d6..4eb8b44e0 100644 --- a/public/sw.js +++ b/public/sw.js @@ -36,7 +36,7 @@ try { // v1.4.38.4 → v1.4.42. Do not hand-edit; bump `package.json` and rebuild. const CACHE_VERSION = (typeof self !== "undefined" && self.__APP_VERSION__) || - /* @sw-version-fallback */ "v1.28.18"; + /* @sw-version-fallback */ "v1.28.19"; const STATIC_CACHE = `healthlog-static-${CACHE_VERSION}`; const PAGE_CACHE = `healthlog-pages-${CACHE_VERSION}`; // v1.18.6 — read-only data cache for a curated allowlist of safe GET `/api/*` diff --git a/src/components/documents/__tests__/document-ai-section.test.tsx b/src/components/documents/__tests__/document-ai-section.test.tsx index 87955d85c..649b25916 100644 --- a/src/components/documents/__tests__/document-ai-section.test.tsx +++ b/src/components/documents/__tests__/document-ai-section.test.tsx @@ -6,11 +6,13 @@ import type { DocumentSuggestionDto } from "@/lib/validations/inbound-documents" import { DocumentAiSection } from "../document-ai-section"; /** - * 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. + * 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. */ function render(node: React.ReactNode) { @@ -62,29 +64,20 @@ function base( } describe("", () => { - it("leads with the prominent Read-with-AI action when a provider is available", () => { + it("offers the review-first assist actions when a provider is available", () => { const html = render(base({ aiEnabled: true })); - expect(html).toContain('data-slot="document-read-ai"'); - expect(html).toContain("Read with AI"); + // 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="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({ @@ -98,23 +91,6 @@ 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", () => { @@ -147,32 +123,36 @@ 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("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", () => { + it("collapses the whole block when auto-read is on with nothing to review", () => { 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 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. + // 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"'); - expect(html).toContain('data-slot="content-search-status"'); + expect(html).not.toContain('data-slot="assist-suggest"'); + expect(html).toContain('data-slot="assist-suggestion-review"'); }); 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).not.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 da39c1341..6a52656f2 100644 --- a/src/components/documents/document-ai-section.tsx +++ b/src/components/documents/document-ai-section.tsx @@ -22,13 +22,11 @@ * status pill stays, honestly reflecting any local auto-index, and the manual * form below stays fully usable. */ -import { FileText, ScanText, WandSparkles } from "lucide-react"; +import { FileText, 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, @@ -37,7 +35,6 @@ import { import { AiUnavailableHint, AssistSuggestionReview, - ContentSearchStatus, DocumentSummaryPanel, } from "./document-ai-panels"; import type { DocumentDescribeResult } from "./use-document-assist"; @@ -45,7 +42,6 @@ import type { DocumentDescribeResult } from "./use-document-assist"; export function DocumentAiSection({ aiEnabled, autoReadEnabled = false, - indexEnabled, unavailableReason, actionsDisabled, onSuggest, @@ -65,10 +61,6 @@ export function DocumentAiSection({ summaryResult, summaryErrorKey, onCloseSummary, - hasContentIndex, - contentIndexSource, - indexPending, - onIndex, }: { aiEnabled: boolean; /** @@ -106,58 +98,30 @@ export function DocumentAiSection({ onIndex: () => void; }) { const { t } = useTranslations(); - const aiRead = hasContentIndex && isAiReadSource(contentIndexSource); - const readLabel = indexPending - ? t("documents.ai.reading") - : aiRead - ? t("documents.ai.reread") - : t("documents.ai.read"); + + // 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; return (
-
-
- -

- {t("documents.ai.blockTitle")} -

-
- -
- {aiEnabled ? ( <> {!autoReadEnabled ? (
- {indexEnabled ? ( - - ) : null}
-

- {date} · {size} - {showFilename ? ` · ${document.filename}` : ""} -

{document.conditionLinks.length > 0 || document.servingClass === "attachment" ? (
diff --git a/src/components/documents/document-detail-sheet.tsx b/src/components/documents/document-detail-sheet.tsx index 8959688b0..924440243 100644 --- a/src/components/documents/document-detail-sheet.tsx +++ b/src/components/documents/document-detail-sheet.tsx @@ -25,6 +25,7 @@ import { Share2, Sparkles, Trash2, + X, } from "lucide-react"; import Link from "next/link"; import { useEffect, useRef, useState } from "react"; @@ -49,12 +50,6 @@ import { SelectValue, } from "@/components/ui/select"; import { Skeleton } from "@/components/ui/skeleton"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; import { apiDelete, ApiError, @@ -434,32 +429,24 @@ export function DocumentDetailSheet({ title={title} description={t("documents.detail.title")} contentWidth="3xl" + showCloseButton={false} headerAction={ - doc && aiEnabled ? ( - // Neutral, ghost Coach entry pinned top-right of the document — - // mirrors the Insights `AskCoachIconButton`. Deliberately NOT the - // purple FAB styling: `text-muted-foreground` only. Opens the - // doc-scoped chat drawer; the drawer body shows the calm - // read-it-first hint when the document is not yet indexed. - - - - - - {t("documents.chat.openAria")} - - - ) : undefined + // Enlarged close control rendered in the header's trailing slot — + // the sheet's own X glyph is `size-4` via the primitive; we hide it + // (`showCloseButton={false}`) and render this larger `size-5` X with + // a comfortable min tap target instead. Scoped to this sheet only, + // so the global primitive default is unchanged for other callers. + } footer={ doc ? ( @@ -480,6 +467,19 @@ export function DocumentDetailSheet({ {t("documents.detail.delete")}
+ {aiEnabled ? ( + + ) : null} + {/* Label/value block in the Vorsorge card's next-last grammar. With + no history yet, one calm line replaces the pair — no dashes + pretending a value exists. */} + {last ? ( +
+
+ + {t("mentalHealth.lastResult")} + + + {relativeCalendarDate(last.takenAt, t, formatDate)} + +
+
+ + {t("mentalHealth.lastScore")} + + + {last.totalScore} + {" · "} + {t(`mentalHealth.band.${last.instrument}.${last.severityBand}`)} + +
+
+ ) : ( +
+ {t("mentalHealth.noResultYet")} +
+ )} + {/* Bottom-pinned single primary action — the med-/Vorsorge card slot. Nothing sits below it now: the attribution moved to the header's diff --git a/src/lib/google-health/__tests__/sync-sleep-replace.test.ts b/src/lib/google-health/__tests__/sync-sleep-replace.test.ts index 33af0ffd4..ee2debec9 100644 --- a/src/lib/google-health/__tests__/sync-sleep-replace.test.ts +++ b/src/lib/google-health/__tests__/sync-sleep-replace.test.ts @@ -9,7 +9,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; const { updateManyMock } = vi.hoisted(() => ({ - updateManyMock: vi.fn(async (_args: unknown) => ({ count: 0 })), + updateManyMock: vi.fn(async () => ({ count: 0 })), })); vi.mock("@/lib/db", () => ({ @@ -53,7 +53,7 @@ describe("replaceStaleGoogleHealthSleep — bounded replace-by-window", () => { expect(removed).toBe(1); expect(updateManyMock).toHaveBeenCalledTimes(1); - const arg = updateManyMock.mock.calls[0]![0] as { + const arg = (updateManyMock.mock.calls[0]! as unknown[])[0] as { where: Record; data: Record; }; From 993e693ad6d06b7ca13418743c8cf0ad8ec958a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 10 Jul 2026 14:06:12 +0200 Subject: [PATCH 2/2] 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. --- messages/de.json | 5 +- messages/en.json | 5 +- messages/es.json | 5 +- messages/fr.json | 5 +- messages/it.json | 5 +- messages/pl.json | 5 +- .../__tests__/document-ai-section.test.tsx | 88 ++++++++++++------- .../documents/document-ai-section.tsx | 61 +++++++++---- 8 files changed, 124 insertions(+), 55 deletions(-) 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}