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

@@ -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>
);