mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
Merge branch 'fix/docai-governance-rebased' into release/v1.27.31
This commit is contained in:
commit
7b1f077888
31 files changed
+786
-100
No files matched your search
@@ -34,6 +34,7 @@ function base(
|
||||
aiEnabled: true,
|
||||
indexEnabled: true,
|
||||
unavailableReason: null,
|
||||
egressExternal: false,
|
||||
actionsDisabled: false,
|
||||
onSuggest: noop,
|
||||
suggestPending: false,
|
||||
@@ -142,6 +143,22 @@ describe("<DocumentAiSection>", () => {
|
||||
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 }));
|
||||
expect(html).not.toContain('data-slot="document-ai-egress-notice"');
|
||||
});
|
||||
|
||||
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"');
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
* 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, ScanText, ShieldAlert, WandSparkles } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
@@ -46,6 +46,7 @@ export function DocumentAiSection({
|
||||
aiEnabled,
|
||||
indexEnabled,
|
||||
unavailableReason,
|
||||
egressExternal,
|
||||
actionsDisabled,
|
||||
onSuggest,
|
||||
suggestPending,
|
||||
@@ -72,6 +73,12 @@ 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;
|
||||
@@ -126,6 +133,17 @@ export function DocumentAiSection({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{egressExternal ? (
|
||||
<div
|
||||
data-slot="document-ai-egress-notice"
|
||||
role="note"
|
||||
className="border-border text-muted-foreground flex items-start gap-2 rounded-lg border border-dashed px-3 py-2.5 text-xs"
|
||||
>
|
||||
<ShieldAlert className="mt-0.5 size-3.5 shrink-0" aria-hidden />
|
||||
<p className="min-w-0">{t("documents.ai.egressNotice")}</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{aiEnabled ? (
|
||||
<>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
|
||||
@@ -505,6 +505,7 @@ export function DocumentDetailSheet({
|
||||
aiEnabled={aiEnabled}
|
||||
indexEnabled={indexEnabled}
|
||||
unavailableReason={capability.data?.reason ?? null}
|
||||
egressExternal={capability.data?.egress === "external"}
|
||||
actionsDisabled={capability.isPending}
|
||||
onSuggest={runSuggest}
|
||||
suggestPending={suggest.isPending}
|
||||
|
||||
@@ -17,8 +17,8 @@ import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { apiGet, ApiError } from "@/lib/api/api-fetch";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import type { OcrCapabilityDto } from "@/lib/validations/labs-ocr";
|
||||
import type {
|
||||
DocumentAiCapabilityDto,
|
||||
DocumentSuggestionDto,
|
||||
DocumentSummaryMode,
|
||||
} from "@/lib/validations/inbound-documents";
|
||||
@@ -34,16 +34,18 @@ import {
|
||||
export type DocumentDescribeResult = { summary: string } | { text: string };
|
||||
|
||||
/**
|
||||
* The shared OCR capability probe (`/api/labs/ocr/capability`) reused verbatim:
|
||||
* the same server resolution both the lab-OCR and the document AI routes gate
|
||||
* on, so the client picks the transport the endpoint accepts. Availability of
|
||||
* the affordance itself is gated on `usage.assistAvailable`; this read only
|
||||
* resolves the transport `mode` + PDF support.
|
||||
* The document-scoped AI capability probe (`/api/documents/inbound/capability`).
|
||||
* Resolved over the DOCUMENT provider order (local-first, codex last), so the
|
||||
* transport `mode` + PDF support match what the document routes do, and it
|
||||
* carries the vendor-blind `egress` class the detail sheet uses to warn before a
|
||||
* read leaves the machine. Availability of the affordance itself is still gated
|
||||
* on `usage.assistAvailable`.
|
||||
*/
|
||||
export function useDocumentAiCapability(enabled: boolean) {
|
||||
return useQuery<OcrCapabilityDto>({
|
||||
queryKey: queryKeys.ocrCapability(),
|
||||
queryFn: () => apiGet<OcrCapabilityDto>("/api/labs/ocr/capability"),
|
||||
return useQuery<DocumentAiCapabilityDto>({
|
||||
queryKey: queryKeys.inboundDocumentAiCapability(),
|
||||
queryFn: () =>
|
||||
apiGet<DocumentAiCapabilityDto>("/api/documents/inbound/capability"),
|
||||
enabled,
|
||||
staleTime: 60_000,
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user