diff --git a/CHANGELOG.md b/CHANGELOG.md index 955e7cb9a..0e7f0bd0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ ## [Unreleased] +## [1.28.5] — 2026-07-09 — Read documents with a subscription AI, cleaner surfaces + +Documents can now be read by a signed-in (subscription) AI provider — the +capability check no longer wrongly treats those models as unable to see images, +so "read with AI" works on that path instead of erroring. Scanned PDFs and +photos are read the same way. Two now-redundant blurbs are gone from the document +panel, and on the recovery insights page each metric shows its explanation inside +its own card instead of repeating the heading above it. + ## [1.28.4] — 2026-07-09 — Medication efficacy A per-medication "effect" view: see whether a medication is moving the outcome diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 8d60cb066..d45566003 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.4 + version: 1.28.5 description: >- Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest. diff --git a/e2e/documents-auto-read.spec.ts b/e2e/documents-auto-read.spec.ts index bce3a9870..db4da6e05 100644 --- a/e2e/documents-auto-read.spec.ts +++ b/e2e/documents-auto-read.spec.ts @@ -171,20 +171,19 @@ test.describe("automatic AI reading — vault per-document contract", () => { test.beforeEach(() => test.slow()); - test("the vault shows the per-egress notice once and the explicit read action", async ({ + test("the vault shows the explicit read action for an external provider", async ({ page, }) => { - // External egress (the codex/OAuth or any non-local provider case). + // External egress (the codex/OAuth or any non-local provider case). The + // per-document egress notice was retired — the settings-toggle honesty + // confirm already covers the egress trade — so no per-document notice + // renders here regardless of egress mode. await mockDocumentCapability(page, { egress: "external" }); await page.goto(`/documents?doc=${AI_PROBE_DOC_ID}`); const sheet = page.getByRole("dialog"); await expect(sheet).toBeVisible(); - // The vendor-blind "leaves your machine" notice is shown exactly once. - await expect( - sheet.locator('[data-slot="document-ai-egress-notice"]'), - ).toHaveCount(1); // 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). await expect(sheet.locator('[data-slot="document-read-ai"]')).toBeVisible(); @@ -218,8 +217,12 @@ test.describe("automatic AI reading — vault per-document contract", () => { expect(indexCalls).toBe(1); }); - test("a local-egress read shows no external notice", async ({ page }) => { - // A self-hosted local model never leaves the machine — no egress notice. + test("a local-egress provider shows the explicit read action", async ({ + page, + }) => { + // A self-hosted local model never leaves the machine; the read action is + // offered the same way as for an external provider (no per-document notice + // in either mode after the notice was retired). await mockDocumentCapability(page, { egress: "local", pdfSupported: false, @@ -229,8 +232,6 @@ test.describe("automatic AI reading — vault per-document contract", () => { const sheet = page.getByRole("dialog"); await expect(sheet).toBeVisible(); - await expect( - sheet.locator('[data-slot="document-ai-egress-notice"]'), - ).toHaveCount(0); + await expect(sheet.locator('[data-slot="document-read-ai"]')).toBeVisible(); }); }); diff --git a/messages/de.json b/messages/de.json index 9d6e5c66c..77e50b4eb 100644 --- a/messages/de.json +++ b/messages/de.json @@ -8261,9 +8261,7 @@ "close": "Ausblenden" }, "ai": { - "egressNotice": "Dies sendet den Inhalt des Dokuments an deinen konfigurierten KI-Anbieter — es verlässt dieses Gerät zu einem KI-Dienst eines Drittanbieters. Ist der Anbieter ein persönliches Abonnement, gelten dessen eigene Datenschutz-Einstellungen, einschließlich Modelltraining, sofern du es dort nicht deaktiviert hast.", "blockTitle": "Mit KI auslesen", - "available": "Lass die KI dieses Dokument lesen — Titel, Art und Datum vorschlagen, zusammenfassen oder den ganzen Text durchsuchbar machen.", "read": "Mit KI auslesen", "reread": "Erneut lesen", "reading": "Liest…", diff --git a/messages/en.json b/messages/en.json index 15bd8dfc7..ab2ead205 100644 --- a/messages/en.json +++ b/messages/en.json @@ -8261,9 +8261,7 @@ "close": "Hide" }, "ai": { - "egressNotice": "This sends the document's contents to your configured AI provider — it leaves this machine to a third-party AI service. If that provider is a personal subscription plan, its own data-use settings apply, including model training unless you have turned it off there.", "blockTitle": "Read with AI", - "available": "Let the AI read this document — suggest a title, type and date, summarise it, or make its full text searchable.", "read": "Read with AI", "reread": "Read again", "reading": "Reading…", diff --git a/messages/es.json b/messages/es.json index 053ecf2bb..349d0c9ed 100644 --- a/messages/es.json +++ b/messages/es.json @@ -8261,9 +8261,7 @@ "close": "Ocultar" }, "ai": { - "egressNotice": "Esto envía el contenido del documento a tu proveedor de IA configurado — sale de esta máquina hacia un servicio de IA de terceros. Si ese proveedor es una suscripción personal, se aplican sus propios ajustes de uso de datos, incluido el entrenamiento del modelo salvo que lo hayas desactivado allí.", "blockTitle": "Leer con IA", - "available": "Deja que la IA lea este documento: sugerir un título, tipo y fecha, resumirlo o hacer que todo su texto sea buscable.", "read": "Leer con IA", "reread": "Leer de nuevo", "reading": "Leyendo…", diff --git a/messages/fr.json b/messages/fr.json index 16d1ff066..b6e907177 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -8261,9 +8261,7 @@ "close": "Masquer" }, "ai": { - "egressNotice": "Ceci envoie le contenu du document à votre fournisseur d'IA configuré — il quitte cette machine vers un service d'IA tiers. Si ce fournisseur est un abonnement personnel, ses propres paramètres d'utilisation des données s'appliquent, y compris l'entraînement du modèle sauf si vous l'avez désactivé là-bas.", "blockTitle": "Lire avec l’IA", - "available": "Laissez l’IA lire ce document — suggérer un titre, un type et une date, le résumer ou rendre tout son texte consultable.", "read": "Lire avec l’IA", "reread": "Relire", "reading": "Lecture…", diff --git a/messages/it.json b/messages/it.json index b2a5954fa..b3e75070d 100644 --- a/messages/it.json +++ b/messages/it.json @@ -8261,9 +8261,7 @@ "close": "Nascondi" }, "ai": { - "egressNotice": "Questo invia il contenuto del documento al provider IA configurato — lascia questa macchina verso un servizio IA di terze parti. Se quel provider è un abbonamento personale, si applicano le sue impostazioni sull'uso dei dati, incluso l'addestramento del modello a meno che tu non l'abbia disattivato lì.", "blockTitle": "Leggi con l’IA", - "available": "Lascia che l’IA legga questo documento: suggerire un titolo, un tipo e una data, riassumerlo o rendere ricercabile tutto il testo.", "read": "Leggi con l’IA", "reread": "Leggi di nuovo", "reading": "Lettura…", diff --git a/messages/pl.json b/messages/pl.json index 5d26255a0..d08590f4e 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -8261,9 +8261,7 @@ "close": "Ukryj" }, "ai": { - "egressNotice": "To wysyła treść dokumentu do skonfigurowanego dostawcy AI — opuszcza ona to urządzenie i trafia do zewnętrznej usługi AI. Jeśli ten dostawca to subskrypcja osobista, obowiązują jego własne ustawienia wykorzystania danych, w tym trenowanie modelu, o ile nie wyłączysz go tam.", "blockTitle": "Odczytaj z AI", - "available": "Pozwól AI odczytać ten dokument — zaproponować tytuł, typ i datę, streścić go lub udostępnić cały tekst do wyszukiwania.", "read": "Odczytaj z AI", "reread": "Odczytaj ponownie", "reading": "Odczytywanie…", diff --git a/package.json b/package.json index af0eebd99..a02003788 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "healthlog", - "version": "1.28.4", + "version": "1.28.5", "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 ba3e14d3e..e6b3b6c29 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.4"; + /* @sw-version-fallback */ "v1.28.5"; 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 6a718d754..0a9a307ce 100644 --- a/src/components/documents/__tests__/document-ai-section.test.tsx +++ b/src/components/documents/__tests__/document-ai-section.test.tsx @@ -34,7 +34,6 @@ function base( aiEnabled: true, indexEnabled: true, unavailableReason: null, - egressExternal: false, actionsDisabled: false, onSuggest: noop, suggestPending: false, @@ -143,20 +142,14 @@ describe("", () => { expect(html).toContain("Not saved"); }); - it("shows the vendor-blind egress notice before an external document read", () => { - const html = render(base({ aiEnabled: true, egressExternal: true })); - expect(html).toContain('data-slot="document-ai-egress-notice"'); - expect(html).toContain("third-party AI service"); - // Vendor-blind: never names a specific AI vendor in the document surface. - expect(html).not.toContain("OpenAI"); - expect(html).not.toContain("ChatGPT"); - // The actions are still offered — the notice informs, it does not block. - expect(html).toContain('data-slot="assist-suggest"'); - }); - - it("omits the egress notice when the read stays on the local machine", () => { - const html = render(base({ aiEnabled: true, egressExternal: false })); + it("renders no per-document egress notice — the settings confirm covers it", () => { + // The per-document egress notice was retired; the settings-toggle honesty + // confirm is now the single place the egress trade is stated. + const html = render(base({ aiEnabled: true })); 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", () => { diff --git a/src/components/documents/document-ai-section.tsx b/src/components/documents/document-ai-section.tsx index a6aad46f4..c3d8368bd 100644 --- a/src/components/documents/document-ai-section.tsx +++ b/src/components/documents/document-ai-section.tsx @@ -22,7 +22,7 @@ * status pill stays, honestly reflecting any local auto-index, and the manual * form below stays fully usable. */ -import { FileText, ScanText, ShieldAlert, WandSparkles } from "lucide-react"; +import { FileText, ScanText, WandSparkles } from "lucide-react"; import type { ReactNode } from "react"; import { Button } from "@/components/ui/button"; @@ -47,7 +47,6 @@ export function DocumentAiSection({ aiEnabled, indexEnabled, unavailableReason, - egressExternal, actionsDisabled, onSuggest, suggestPending, @@ -75,12 +74,6 @@ export function DocumentAiSection({ aiEnabled: boolean; indexEnabled: boolean; unavailableReason: "no-provider" | "enable-local-ocr" | null; - /** - * True when a document read will egress to a third-party AI service (any - * provider other than a self-hosted local model). Drives the vendor-blind - * "this leaves your machine" notice shown before the AI actions. - */ - egressExternal: boolean; /** Capability still resolving — the transport mode isn't known yet. */ actionsDisabled: boolean; onSuggest: () => void; @@ -143,23 +136,8 @@ export function DocumentAiSection({ /> - {egressExternal ? ( -
- -

{t("documents.ai.egressNotice")}

-
- ) : null} - {aiEnabled ? ( <> -

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

-
{indexEnabled ? (