fix(documents): point the on-device scan hint at the Labs switch

The hint shown when a text-only provider needs on-device text recognition
sent people to the AI settings, where no such switch exists. It now names
the switch by its label under Settings → Modules → Labs in every locale and
links straight to that card. Inside somebody else's record the link is
withheld, since Settings there is the caller's own and closed; the sentence
stays.
This commit is contained in:
Marc-André Bombeck committed 2026-10-25 12:12:14 +01:00
1 parent 28340e7c66
commit 856eb036f8
12 files changed
+90 -38

No files matched your search

+3 -2
View File
@@ -11769,8 +11769,9 @@
"dismiss": "Verwerfen",
"empty": "Aus diesem Dokument konnte nichts gelesen werden.",
"unavailableBody": "Richte einen KI-Anbieter ein, um Titel, Typ und Datum vorzuschlagen.",
"unavailableLocalOcr": "Aktiviere lokale Texterkennung in den KI-Einstellungen, um Bilder auf diesem Gerät zu lesen.",
"unavailableLocalOcr": "Schalte „Scans auf diesem Gerät lesen“ unter Einstellungen → Module → Labor ein, um Bilder auf diesem Gerät zu lesen.",
"unavailableAction": "KI-Einstellungen öffnen",
"unavailableLocalOcrAction": "Labor-Einstellungen öffnen",
"errorGeneric": "Dokument konnte nicht gelesen werden. Bitte erneut versuchen.",
"errorOcr": "Aus diesem Bild konnte kein Text gelesen werden.",
"errorRateLimited": "Das stündliche KI-Leselimit ist erreicht. Bitte später erneut versuchen.",
@@ -11782,7 +11783,7 @@
"errorFileType": "Dieser Dateityp kann nicht gelesen werden.",
"errorNotIndexed": "Lies das Dokument zuerst aus, dann extrahiere.",
"errorOutputLimit": "Der KI-Anbieter hat sein Ausgabelimit erreicht, bevor er einen einzigen Wert geliefert hat. Versuche ein Modell, das längere Antworten erlaubt.",
"errorLocalOcrDisabled": "Aktiviere zuerst die lokale Texterkennung in den KI-Einstellungen.",
"errorLocalOcrDisabled": "Schalte zuerst „Scans auf diesem Gerät lesen“ unter Einstellungen → Module → Labor ein.",
"errorProvider": "Dafür ist kein KI-Anbieter eingerichtet.",
"errorTextImageOnly": "Lokale Texterkennung liest nur Bilder, nicht diese Datei.",
"errorConsent": "Für das Lesen von Dokumenten mit KI brauchst du zuerst deine Einwilligung.",
+3 -2
View File
@@ -11769,8 +11769,9 @@
"dismiss": "Dismiss",
"empty": "Nothing could be read from this document.",
"unavailableBody": "Set up an AI provider to suggest a title, type, and date.",
"unavailableLocalOcr": "Turn on local OCR in the AI settings to read images on this device.",
"unavailableLocalOcr": "Turn on “Read scans on this device” under Settings → Modules → Labs to read images on this device.",
"unavailableAction": "Open AI settings",
"unavailableLocalOcrAction": "Open lab settings",
"errorGeneric": "Couldn't read the document. Try again.",
"errorOcr": "Couldn't read text from this image.",
"errorRateLimited": "The hourly AI reading limit is reached. Try again later.",
@@ -11782,7 +11783,7 @@
"errorFileType": "This file type can't be read.",
"errorNotIndexed": "Read the document first, then extract.",
"errorOutputLimit": "The AI provider reached its output limit before it returned a single value. Try a model that allows longer answers.",
"errorLocalOcrDisabled": "Turn on local OCR in the AI settings first.",
"errorLocalOcrDisabled": "Turn on “Read scans on this device” under Settings → Modules → Labs first.",
"errorProvider": "No AI provider is set up for this.",
"errorTextImageOnly": "Local OCR reads images only, not this file.",
"errorConsent": "Reading documents with AI needs your consent first.",
+3 -2
View File
@@ -11769,8 +11769,9 @@
"dismiss": "Descartar",
"empty": "No se pudo leer nada de este documento.",
"unavailableBody": "Configura un proveedor de IA para sugerir título, tipo y fecha.",
"unavailableLocalOcr": "Activa el OCR local en los ajustes de IA para leer imágenes en este dispositivo.",
"unavailableLocalOcr": "Activa «Leer escaneos en este dispositivo» en Ajustes → Módulos → Laboratorio para leer imágenes en este dispositivo.",
"unavailableAction": "Abrir ajustes de IA",
"unavailableLocalOcrAction": "Abrir ajustes de laboratorio",
"errorGeneric": "No se pudo leer el documento. Inténtalo de nuevo.",
"errorOcr": "No se pudo leer texto de esta imagen.",
"errorRateLimited": "Se alcanzó el límite de lecturas de IA por hora. Inténtalo más tarde.",
@@ -11782,7 +11783,7 @@
"errorFileType": "Este tipo de archivo no se puede leer.",
"errorNotIndexed": "Primero lee el documento y luego extrae.",
"errorOutputLimit": "El proveedor de IA alcanzó su límite de salida antes de devolver un solo valor. Prueba con un modelo que permita respuestas más largas.",
"errorLocalOcrDisabled": "Activa primero el OCR local en los ajustes de IA.",
"errorLocalOcrDisabled": "Activa primero «Leer escaneos en este dispositivo» en Ajustes → Módulos → Laboratorio.",
"errorProvider": "No hay ningún proveedor de IA configurado para esto.",
"errorTextImageOnly": "El OCR local solo lee imágenes, no este archivo.",
"errorConsent": "Leer documentos con IA necesita primero tu consentimiento.",
+3 -2
View File
@@ -11769,8 +11769,9 @@
"dismiss": "Ignorer",
"empty": "Impossible de lire ce document.",
"unavailableBody": "Configurez un fournisseur d'IA pour suggérer un titre, un type et une date.",
"unavailableLocalOcr": "Activez l'OCR local dans les paramètres d'IA pour lire les images sur cet appareil.",
"unavailableLocalOcr": "Activez « Lire les scans sur cet appareil » dans Paramètres → Modules → Analyses pour lire les images sur cet appareil.",
"unavailableAction": "Ouvrir les paramètres d'IA",
"unavailableLocalOcrAction": "Ouvrir les paramètres des analyses",
"errorGeneric": "Impossible de lire le document. Réessayez.",
"errorOcr": "Impossible de lire le texte de cette image.",
"errorRateLimited": "La limite horaire de lectures IA est atteinte. Réessayez plus tard.",
@@ -11782,7 +11783,7 @@
"errorFileType": "Ce type de fichier ne peut pas être lu.",
"errorNotIndexed": "Lisez d’abord le document, puis extrayez.",
"errorOutputLimit": "Le fournisseur d’IA a atteint sa limite de sortie avant de renvoyer la moindre valeur. Essayez un modèle qui accepte des réponses plus longues.",
"errorLocalOcrDisabled": "Activez d'abord l'OCR local dans les paramètres d'IA.",
"errorLocalOcrDisabled": "Activez d'abord « Lire les scans sur cet appareil » dans Paramètres → Modules → Analyses.",
"errorProvider": "Aucun fournisseur d'IA n'est configuré pour cela.",
"errorTextImageOnly": "L'OCR local ne lit que les images, pas ce fichier.",
"errorConsent": "La lecture de documents par IA nécessite d'abord votre consentement.",
+3 -2
View File
@@ -11769,8 +11769,9 @@
"dismiss": "Ignora",
"empty": "Impossibile leggere questo documento.",
"unavailableBody": "Configura un provider IA per suggerire titolo, tipo e data.",
"unavailableLocalOcr": "Attiva l'OCR locale nelle impostazioni IA per leggere le immagini su questo dispositivo.",
"unavailableLocalOcr": "Attiva «Leggi le scansioni su questo dispositivo» in Impostazioni → Moduli → Esami per leggere le immagini su questo dispositivo.",
"unavailableAction": "Apri impostazioni IA",
"unavailableLocalOcrAction": "Apri le impostazioni degli esami",
"errorGeneric": "Impossibile leggere il documento. Riprova.",
"errorOcr": "Impossibile leggere il testo da questa immagine.",
"errorRateLimited": "Il limite orario di letture IA è stato raggiunto. Riprova più tardi.",
@@ -11782,7 +11783,7 @@
"errorFileType": "Questo tipo di file non può essere letto.",
"errorNotIndexed": "Leggi prima il documento, poi estrai.",
"errorOutputLimit": "Il fornitore di IA ha raggiunto il suo limite di output prima di restituire un solo valore. Prova con un modello che consente risposte più lunghe.",
"errorLocalOcrDisabled": "Attiva prima l'OCR locale nelle impostazioni IA.",
"errorLocalOcrDisabled": "Attiva prima «Leggi le scansioni su questo dispositivo» in Impostazioni → Moduli → Esami.",
"errorProvider": "Nessun provider IA è configurato per questo.",
"errorTextImageOnly": "L'OCR locale legge solo immagini, non questo file.",
"errorConsent": "La lettura dei documenti con IA richiede prima il tuo consenso.",
+3 -2
View File
@@ -11769,8 +11769,9 @@
"dismiss": "닫기",
"empty": "이 문서에서 읽어낼 내용이 없었어요.",
"unavailableBody": "AI 제공자를 설정하면 제목·종류·날짜를 추천해요.",
"unavailableLocalOcr": "AI 설정에서 로컬 OCR을 켜면 이 기기에서 이미지를 읽어요.",
"unavailableLocalOcr": "설정 → 모듈 → 검사에서 “이 기기에서 스캔 읽기”를 켜면 이 기기에서 이미지를 읽어요.",
"unavailableAction": "AI 설정 열기",
"unavailableLocalOcrAction": "검사 설정 열기",
"errorGeneric": "문서를 읽지 못했어요. 다시 시도해 주세요.",
"errorOcr": "이 이미지에서 텍스트를 읽지 못했어요.",
"errorRateLimited": "시간당 AI 읽기 한도에 도달했어요. 나중에 다시 시도해 주세요.",
@@ -11782,7 +11783,7 @@
"errorFileType": "이 파일 형식은 읽을 수 없어요.",
"errorNotIndexed": "문서를 먼저 읽은 뒤에 추출해 주세요.",
"errorOutputLimit": "AI 제공자가 값을 하나도 돌려주기 전에 출력 한도에 도달했어요. 더 긴 답변을 허용하는 모델로 다시 시도해 주세요.",
"errorLocalOcrDisabled": "AI 설정에서 로컬 OCR을 먼저 켜 주세요.",
"errorLocalOcrDisabled": "설정 → 모듈 → 검사에서 “이 기기에서 스캔 읽기”를 먼저 켜 주세요.",
"errorProvider": "이 작업에 쓸 AI 제공자가 설정돼 있지 않아요.",
"errorTextImageOnly": "로컬 OCR은 이미지만 읽어요. 이 파일은 읽을 수 없어요.",
"errorConsent": "AI로 문서를 읽으려면 먼저 동의가 필요합니다.",
+3 -2
View File
@@ -11769,8 +11769,9 @@
"dismiss": "Negeren",
"empty": "Er kon niets uit dit document worden gelezen.",
"unavailableBody": "Stel een AI-provider in om een titel, soort en datum voor te stellen.",
"unavailableLocalOcr": "Zet lokale OCR aan in de AI-instellingen om afbeeldingen op dit apparaat te lezen.",
"unavailableLocalOcr": "Zet “Scans op dit apparaat lezen” aan onder Instellingen → Modules → Labwaarden om afbeeldingen op dit apparaat te lezen.",
"unavailableAction": "AI-instellingen openen",
"unavailableLocalOcrAction": "Instellingen voor labwaarden openen",
"errorGeneric": "Het document kon niet worden gelezen. Probeer het opnieuw.",
"errorOcr": "Er kon geen tekst uit deze afbeelding worden gelezen.",
"errorRateLimited": "De limiet voor lezen met AI per uur is bereikt. Probeer het later opnieuw.",
@@ -11782,7 +11783,7 @@
"errorFileType": "Dit bestandstype kan niet worden gelezen.",
"errorNotIndexed": "Laat het document eerst lezen en extraheer daarna.",
"errorOutputLimit": "De AI-provider bereikte zijn uitvoerlimiet voordat hij één waarde teruggaf. Probeer een model dat langere antwoorden toestaat.",
"errorLocalOcrDisabled": "Zet eerst lokale OCR aan in de AI-instellingen.",
"errorLocalOcrDisabled": "Zet eerst “Scans op dit apparaat lezen” aan onder Instellingen → Modules → Labwaarden.",
"errorProvider": "Hiervoor is geen AI-provider ingesteld.",
"errorTextImageOnly": "Lokale OCR leest alleen afbeeldingen, niet dit bestand.",
"errorConsent": "Voor het lezen van documenten met AI is eerst je toestemming nodig.",
+3 -2
View File
@@ -11769,8 +11769,9 @@
"dismiss": "Odrzuć",
"empty": "Nie udało się nic odczytać z tego dokumentu.",
"unavailableBody": "Skonfiguruj dostawcę AI, aby zaproponować tytuł, typ i datę.",
"unavailableLocalOcr": "Włącz lokalne OCR w ustawieniach AI, aby odczytywać obrazy na tym urządzeniu.",
"unavailableLocalOcr": "Włącz „Odczytuj skany na tym urządzeniu” w Ustawienia → Moduły → Badania, aby odczytywać obrazy na tym urządzeniu.",
"unavailableAction": "Otwórz ustawienia AI",
"unavailableLocalOcrAction": "Otwórz ustawienia badań",
"errorGeneric": "Nie udało się odczytać dokumentu. Spróbuj ponownie.",
"errorOcr": "Nie udało się odczytać tekstu z tego obrazu.",
"errorRateLimited": "Godzinowy limit odczytów AI został osiągnięty. Spróbuj ponownie później.",
@@ -11782,7 +11783,7 @@
"errorFileType": "Tego typu pliku nie można odczytać.",
"errorNotIndexed": "Najpierw odczytaj dokument, potem wyodrębnij.",
"errorOutputLimit": "Dostawca AI osiągnął limit odpowiedzi, zanim zwrócił choć jedną wartość. Spróbuj modelu, który pozwala na dłuższe odpowiedzi.",
"errorLocalOcrDisabled": "Najpierw włącz lokalne OCR w ustawieniach AI.",
"errorLocalOcrDisabled": "Najpierw włącz „Odczytuj skany na tym urządzeniu” w Ustawienia → Moduły → Badania.",
"errorProvider": "Nie skonfigurowano dostawcy AI do tego celu.",
"errorTextImageOnly": "Lokalne OCR odczytuje tylko obrazy, nie ten plik.",
"errorConsent": "Odczyt dokumentów przez AI wymaga najpierw Twojej zgody.",
+3 -2
View File
@@ -11769,8 +11769,9 @@
"dismiss": "Dispensar",
"empty": "Nada pôde ser lido deste documento.",
"unavailableBody": "Configure um provedor de IA para sugerir um título, tipo e data.",
"unavailableLocalOcr": "Ative o OCR local nas configurações de IA para ler imagens neste dispositivo.",
"unavailableLocalOcr": "Ative “Ler digitalizações neste dispositivo” em Configurações → Módulos → Exames para ler imagens neste dispositivo.",
"unavailableAction": "Abrir configurações de IA",
"unavailableLocalOcrAction": "Abrir configurações de exames",
"errorGeneric": "Não foi possível ler o documento. Tente novamente.",
"errorOcr": "Não foi possível ler texto desta imagem.",
"errorRateLimited": "O limite horário de leitura por IA foi atingido. Tente novamente mais tarde.",
@@ -11782,7 +11783,7 @@
"errorFileType": "Este tipo de arquivo não pode ser lido.",
"errorNotIndexed": "Leia o documento primeiro, depois extraia.",
"errorOutputLimit": "O provedor de IA atingiu o limite de saída antes de devolver um único valor. Tente um modelo que permita respostas mais longas.",
"errorLocalOcrDisabled": "Ative o OCR local nas configurações de IA primeiro.",
"errorLocalOcrDisabled": "Ative primeiro “Ler digitalizações neste dispositivo” em Configurações → Módulos → Exames.",
"errorProvider": "Nenhum provedor de IA está configurado para isso.",
"errorTextImageOnly": "O OCR local lê apenas imagens, não este arquivo.",
"errorConsent": "Ler documentos com IA precisa do seu consentimento primeiro.",
@@ -1,7 +1,12 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
import { I18nProvider } from "@/lib/i18n/context";
const record = vi.hoisted(() => ({ inSharedRecord: false }));
vi.mock("@/hooks/use-record-capabilities", () => ({
useRecordCapabilities: () => ({ inSharedRecord: record.inSharedRecord }),
}));
import type { DocumentSuggestionDto } from "@/lib/validations/inbound-documents";
import {
AiUnavailableHint,
@@ -57,10 +62,26 @@ describe("<AiUnavailableHint>", () => {
expect(html).not.toContain('role="alert"');
});
it("nudges toward local OCR when a text-only provider needs it", () => {
it("points at the on-device scan switch when a text-only provider needs it", () => {
const html = render(<AiUnavailableHint reason="enable-local-ocr" />);
expect(html).toContain("local OCR");
expect(html).toContain('href="/settings/ai"');
// The switch lives on the Labs module page, under its own label.
expect(html).toContain("Read scans on this device");
expect(html).toContain("Settings → Modules → Labs");
expect(html).toContain('href="/settings/modules/labs#labs-local-ocr"');
expect(html).toContain("Open lab settings");
expect(html).not.toContain('href="/settings/ai"');
});
it("keeps the sentence but withholds the Settings link inside somebody else's record", () => {
record.inSharedRecord = true;
try {
const html = render(<AiUnavailableHint reason="enable-local-ocr" />);
expect(html).toContain("Read scans on this device");
expect(html).not.toContain('data-slot="assist-settings-link"');
expect(html).not.toContain('href="/settings');
} finally {
record.inSharedRecord = false;
}
});
});
@@ -1,7 +1,12 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
import { I18nProvider } from "@/lib/i18n/context";
const record = vi.hoisted(() => ({ inSharedRecord: false }));
vi.mock("@/hooks/use-record-capabilities", () => ({
useRecordCapabilities: () => ({ inSharedRecord: record.inSharedRecord }),
}));
import type { DocumentSuggestionDto } from "@/lib/validations/inbound-documents";
import { DocumentAiSection } from "../document-ai-section";
+32 -15
View File
@@ -22,6 +22,7 @@ import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import type { AiRunPhase } from "@/hooks/use-ai-run";
import { useRecordCapabilities } from "@/hooks/use-record-capabilities";
import { useTranslations } from "@/lib/i18n/context";
import { cn } from "@/lib/utils";
import {
@@ -64,9 +65,13 @@ export function AiRunPhaseNote({
}
/**
* Calm pointer to the AI settings, shown when assist is unavailable. When the
* document is already searchable (auto-indexed locally) the copy stays honest —
* it is searchable, an AI provider only adds a richer read.
* Calm pointer to the setting that would make assist available: the AI
* settings when no provider is configured, the on-device scan switch on the
* Labs module page when the provider cannot read images. Both live under
* `/settings`, which is the caller's own and closed inside somebody else's
* record, so the link is withheld there and the sentence stands alone. When
* the document is already searchable (auto-indexed locally) the copy stays
* honest — it is searchable, an AI provider only adds a richer read.
*/
export function AiUnavailableHint({
reason,
@@ -74,10 +79,11 @@ export function AiUnavailableHint({
reason: "no-provider" | "enable-local-ocr" | null;
}) {
const { t } = useTranslations();
const body =
reason === "enable-local-ocr"
? t("documents.assist.unavailableLocalOcr")
: t("documents.assist.unavailableBody");
const { inSharedRecord } = useRecordCapabilities();
const localOcr = reason === "enable-local-ocr";
const body = localOcr
? t("documents.assist.unavailableLocalOcr")
: t("documents.assist.unavailableBody");
return (
<div
data-slot="assist-unavailable"
@@ -85,14 +91,25 @@ export function AiUnavailableHint({
>
<Sparkles className="mt-0.5 size-3.5 shrink-0" aria-hidden />
<p className="min-w-0">
{body}{" "}
<Link
href="/settings/ai"
data-slot="assist-settings-link"
className="text-primary font-medium underline-offset-4 hover:underline"
>
{t("documents.assist.unavailableAction")}
</Link>
{body}
{!inSharedRecord && (
<>
{" "}
<Link
href={
localOcr
? "/settings/modules/labs#labs-local-ocr"
: "/settings/ai"
}
data-slot="assist-settings-link"
className="text-primary font-medium underline-offset-4 hover:underline"
>
{localOcr
? t("documents.assist.unavailableLocalOcrAction")
: t("documents.assist.unavailableAction")}
</Link>
</>
)}
</p>
</div>
);