mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 13:52:20 +00:00
Add an optional AI layer to a stored document, gated on the existing AI provider and consent — no new toggle. All of it rides the shared OCR capability probe so the client only posts a transport the endpoint accepts (vision empty-body POST, or local-OCR text posted from the browser). - "Suggest details" prefills the title / type / date as reviewable DRAFTS. Nothing is written on its own: the title lands in the editable field and is saved through the existing commit path; type and date each apply on an explicit tap. The card carries a visible "review before saving" affordance. - A summary / extracted-text button opens a transient panel that renders the result once and always shows a "not saved / not a diagnosis" note; closing it discards the text. Nothing persists beyond the view. - When no provider is configured the AI area is a calm pointer to the AI settings, never an error; the manual metadata form stays fully usable. The detail-sheet AI area is lifted into a presentational section component so the availability gate and the review-first wiring render without the sheet's dialog portal and are pinned by static-render tests. Strings land in all six locales.
115 lines
3.8 KiB
TypeScript
115 lines
3.8 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
|
|
import { I18nProvider } from "@/lib/i18n/context";
|
|
import type { DocumentSuggestionDto } from "@/lib/validations/inbound-documents";
|
|
import { DocumentAiSection } from "../document-ai-section";
|
|
|
|
/**
|
|
* The detail sheet's AI area is availability-gated: with a provider it shows the
|
|
* assist / summary toolbar; without one it shows ONLY the calm settings pointer
|
|
* (never an action the endpoint would 422). The summary panel, when open, keeps
|
|
* the session-only note visible.
|
|
*/
|
|
|
|
function render(node: React.ReactNode) {
|
|
return renderToStaticMarkup(
|
|
<I18nProvider initialLocale="en">{node}</I18nProvider>,
|
|
);
|
|
}
|
|
|
|
const noop = () => {};
|
|
|
|
const suggestion: DocumentSuggestionDto = {
|
|
title: "MRI knee report",
|
|
kind: "IMAGING",
|
|
documentDate: "2026-02-14",
|
|
};
|
|
|
|
function base(
|
|
overrides: Partial<React.ComponentProps<typeof DocumentAiSection>> = {},
|
|
) {
|
|
const props: React.ComponentProps<typeof DocumentAiSection> = {
|
|
aiEnabled: true,
|
|
indexEnabled: true,
|
|
unavailableReason: null,
|
|
actionsDisabled: false,
|
|
onSuggest: noop,
|
|
suggestPending: false,
|
|
suggestErrorKey: null,
|
|
onSummarise: noop,
|
|
summaryPending: false,
|
|
suggestion: null,
|
|
suggestionKindLabel: null,
|
|
suggestionDateLabel: null,
|
|
appliedFields: { title: false, kind: false, date: false },
|
|
onUseTitle: noop,
|
|
onUseKind: noop,
|
|
onUseDate: noop,
|
|
onDismissSuggestion: noop,
|
|
summaryOutput: null,
|
|
summaryResult: null,
|
|
summaryErrorKey: null,
|
|
onCloseSummary: noop,
|
|
hasContentIndex: false,
|
|
indexPending: false,
|
|
onIndex: noop,
|
|
...overrides,
|
|
};
|
|
return <DocumentAiSection {...props} />;
|
|
}
|
|
|
|
describe("<DocumentAiSection>", () => {
|
|
it("offers the assist + summary toolbar when a provider is available", () => {
|
|
const html = render(base({ aiEnabled: true }));
|
|
expect(html).toContain('data-slot="assist-suggest"');
|
|
expect(html).toContain("Suggest details");
|
|
expect(html).toContain("Summarise");
|
|
expect(html).toContain("Show extracted text");
|
|
// No unavailable pointer when the affordance is offered.
|
|
expect(html).not.toContain('data-slot="assist-unavailable"');
|
|
});
|
|
|
|
it("shows ONLY the calm settings pointer when no provider is available", () => {
|
|
const html = render(
|
|
base({ aiEnabled: false, unavailableReason: "no-provider" }),
|
|
);
|
|
expect(html).toContain('data-slot="assist-unavailable"');
|
|
expect(html).toContain('href="/settings/ai"');
|
|
// The assist / index actions must be absent — never a 422-guaranteed tap.
|
|
expect(html).not.toContain('data-slot="assist-suggest"');
|
|
expect(html).not.toContain('data-slot="content-index-status"');
|
|
});
|
|
|
|
it("renders the reviewed suggestion drafts when present", () => {
|
|
const html = render(
|
|
base({
|
|
suggestion,
|
|
suggestionKindLabel: "Imaging",
|
|
suggestionDateLabel: "14 Feb 2026",
|
|
}),
|
|
);
|
|
expect(html).toContain('data-slot="assist-suggestion-review"');
|
|
expect(html).toContain("MRI knee report");
|
|
expect(html).toContain("review before saving");
|
|
});
|
|
|
|
it("renders the transient summary panel with the session-only note", () => {
|
|
const html = render(
|
|
base({
|
|
summaryOutput: "summary",
|
|
summaryResult: { summary: "An MRI report of the left knee." },
|
|
}),
|
|
);
|
|
expect(html).toContain('data-slot="document-summary-panel"');
|
|
expect(html).toContain("An MRI report of the left knee.");
|
|
expect(html).toContain("Not saved");
|
|
});
|
|
|
|
it("hides the content-index row when indexing is unavailable", () => {
|
|
const html = render(base({ aiEnabled: true, indexEnabled: false }));
|
|
expect(html).toContain('data-slot="assist-suggest"');
|
|
expect(html).not.toContain('data-slot="content-index-status"');
|
|
});
|
|
});
|