mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 14:32:03 +00:00
feat(documents): hide manual AI actions under auto-read, trim suggestion chrome
When "read documents automatically with AI" is on, the detail sheet no longer shows the manual per-document action row (Read / Suggest / Summarise) — reading already happens on upload, so the buttons only nagged. The section header and the searchable status pill stay, and content search keeps working. With the setting off the manual controls render exactly as before. Drop the "review before saving" heading and its grey sub-caption from the suggestion panel; the reviewable fields and the apply / dismiss controls stay. Remove the now-unused documents.assist.reviewTitle / reviewHint keys from every locale.
This commit is contained in:
1 parent
6f439b1572
commit
e5de72cae2
15 files changed
+168
-86
No files matched your search
@@ -14,8 +14,8 @@ import {
|
||||
* The AI panels' render contract:
|
||||
* - the unavailable state is a CALM pointer to the AI settings, never an
|
||||
* error, and adapts its copy to the actionable reason;
|
||||
* - the suggestion card carries the review-first affordance ("review before
|
||||
* saving") and applies nothing on its own — every value has its own tap;
|
||||
* - the suggestion card is review-first: it applies nothing on its own —
|
||||
* every value has its own explicit apply tap, and a dismiss control;
|
||||
* - the summary panel ALWAYS carries the "not saved · not a diagnosis" note,
|
||||
* even while loading, so the session-only contract is never off-screen.
|
||||
*/
|
||||
@@ -55,7 +55,7 @@ describe("<AiUnavailableHint>", () => {
|
||||
});
|
||||
|
||||
describe("<AssistSuggestionReview>", () => {
|
||||
it("presents drafts behind an explicit review-before-saving affordance", () => {
|
||||
it("presents drafts as reviewable fields with explicit apply controls", () => {
|
||||
const html = render(
|
||||
<AssistSuggestionReview
|
||||
suggestion={suggestion()}
|
||||
@@ -69,8 +69,10 @@ describe("<AssistSuggestionReview>", () => {
|
||||
/>,
|
||||
);
|
||||
expect(html).toContain('data-slot="assist-suggestion-review"');
|
||||
expect(html).toContain("AI suggestion — review before saving");
|
||||
expect(html).toContain("Nothing is saved until you apply it");
|
||||
// The heading + grey sub-caption were retired — the panel leads straight
|
||||
// with the reviewable draft fields.
|
||||
expect(html).not.toContain("review before saving");
|
||||
expect(html).not.toContain("Nothing is saved until you apply it");
|
||||
// The suggested values are shown as reviewable drafts.
|
||||
expect(html).toContain("Blood panel — March 2026");
|
||||
expect(html).toContain("Lab result");
|
||||
|
||||
@@ -127,7 +127,6 @@ describe("<DocumentAiSection>", () => {
|
||||
);
|
||||
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", () => {
|
||||
@@ -157,6 +156,29 @@ describe("<DocumentAiSection>", () => {
|
||||
expect(html).not.toContain('data-slot="document-read-ai"');
|
||||
});
|
||||
|
||||
it("hides the manual AI action row when auto-read is on, keeping status + chat", () => {
|
||||
const chat = <div data-slot="chat-probe">chat here</div>;
|
||||
const html = render(
|
||||
base({ aiEnabled: true, autoReadEnabled: true, chatSlot: chat }),
|
||||
);
|
||||
// Auto-read reads on upload — the manual per-document actions are gone.
|
||||
expect(html).not.toContain('data-slot="document-read-ai"');
|
||||
expect(html).not.toContain('data-slot="assist-suggest"');
|
||||
expect(html).not.toContain("Summarise");
|
||||
expect(html).not.toContain("Show extracted text");
|
||||
// The header, the searchable status pill, and content-search chat stay.
|
||||
expect(html).toContain('data-slot="document-ai-section"');
|
||||
expect(html).toContain('data-slot="content-search-status"');
|
||||
expect(html).toContain('data-slot="chat-probe"');
|
||||
});
|
||||
|
||||
it("shows the manual AI action row when auto-read is off", () => {
|
||||
const html = render(base({ aiEnabled: true, autoReadEnabled: false }));
|
||||
expect(html).toContain('data-slot="document-read-ai"');
|
||||
expect(html).toContain('data-slot="assist-suggest"');
|
||||
expect(html).toContain("Summarise");
|
||||
});
|
||||
|
||||
it("renders the chat slot inside the AI area only when a provider is available", () => {
|
||||
const chat = <div data-slot="chat-probe">chat here</div>;
|
||||
const enabled = render(base({ aiEnabled: true, chatSlot: chat }));
|
||||
|
||||
@@ -148,21 +148,7 @@ export function AssistSuggestionReview({
|
||||
data-slot="assist-suggestion-review"
|
||||
className="border-primary/30 bg-primary/5 space-y-3 rounded-lg border p-3"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex items-start gap-2">
|
||||
<Sparkles
|
||||
className="text-primary mt-0.5 size-4 shrink-0"
|
||||
aria-hidden
|
||||
/>
|
||||
<div>
|
||||
<p className="text-sm font-medium">
|
||||
{t("documents.assist.reviewTitle")}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("documents.assist.reviewHint")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
|
||||
@@ -45,6 +45,7 @@ import type { DocumentDescribeResult } from "./use-document-assist";
|
||||
|
||||
export function DocumentAiSection({
|
||||
aiEnabled,
|
||||
autoReadEnabled = false,
|
||||
indexEnabled,
|
||||
unavailableReason,
|
||||
actionsDisabled,
|
||||
@@ -72,6 +73,14 @@ export function DocumentAiSection({
|
||||
chatSlot,
|
||||
}: {
|
||||
aiEnabled: boolean;
|
||||
/**
|
||||
* Whether "read documents automatically with AI" is ON. When true, reading +
|
||||
* indexing happen on upload, so the manual per-document action row (Read /
|
||||
* Suggest / Summarise) is hidden — only the header + searchable status +
|
||||
* content-search chat remain. Defaults to false so an omitting caller keeps
|
||||
* the manual controls.
|
||||
*/
|
||||
autoReadEnabled?: boolean;
|
||||
indexEnabled: boolean;
|
||||
unavailableReason: "no-provider" | "enable-local-ocr" | null;
|
||||
/** Capability still resolving — the transport mode isn't known yet. */
|
||||
@@ -138,58 +147,61 @@ export function DocumentAiSection({
|
||||
|
||||
{aiEnabled ? (
|
||||
<>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{indexEnabled ? (
|
||||
{!autoReadEnabled ? (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{indexEnabled ? (
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
data-slot="document-read-ai"
|
||||
onClick={onIndex}
|
||||
disabled={indexPending || actionsDisabled}
|
||||
>
|
||||
<ScanText
|
||||
className={cn(
|
||||
"size-4",
|
||||
indexPending &&
|
||||
"animate-pulse motion-reduce:animate-none",
|
||||
)}
|
||||
aria-hidden
|
||||
/>
|
||||
{readLabel}
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
data-slot="document-read-ai"
|
||||
onClick={onIndex}
|
||||
disabled={indexPending || actionsDisabled}
|
||||
data-slot="assist-suggest"
|
||||
onClick={onSuggest}
|
||||
disabled={suggestPending || actionsDisabled}
|
||||
>
|
||||
<ScanText
|
||||
className={cn(
|
||||
"size-4",
|
||||
indexPending && "animate-pulse motion-reduce:animate-none",
|
||||
)}
|
||||
aria-hidden
|
||||
/>
|
||||
{readLabel}
|
||||
<WandSparkles className="size-4" aria-hidden />
|
||||
{suggestPending
|
||||
? t("documents.assist.suggesting")
|
||||
: t("documents.assist.suggest")}
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
data-slot="assist-suggest"
|
||||
onClick={onSuggest}
|
||||
disabled={suggestPending || actionsDisabled}
|
||||
>
|
||||
<WandSparkles className="size-4" aria-hidden />
|
||||
{suggestPending
|
||||
? t("documents.assist.suggesting")
|
||||
: t("documents.assist.suggest")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onSummarise("summary")}
|
||||
disabled={summaryPending || actionsDisabled}
|
||||
>
|
||||
<FileText className="size-4" aria-hidden />
|
||||
{t("documents.summary.summarise")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onSummarise("text")}
|
||||
disabled={summaryPending || actionsDisabled}
|
||||
>
|
||||
{t("documents.summary.showText")}
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onSummarise("summary")}
|
||||
disabled={summaryPending || actionsDisabled}
|
||||
>
|
||||
<FileText className="size-4" aria-hidden />
|
||||
{t("documents.summary.summarise")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onSummarise("text")}
|
||||
disabled={summaryPending || actionsDisabled}
|
||||
>
|
||||
{t("documents.summary.showText")}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{suggestErrorKey ? (
|
||||
<p role="alert" className="text-destructive text-sm">
|
||||
|
||||
@@ -67,6 +67,7 @@ import { useIndexDocument } from "./use-content-index";
|
||||
import {
|
||||
documentAiErrorKey,
|
||||
useDocumentAiCapability,
|
||||
useDocumentsAutoAiRead,
|
||||
useDocumentSummary,
|
||||
useSuggestDetails,
|
||||
} from "./use-document-assist";
|
||||
@@ -221,6 +222,9 @@ export function DocumentDetailSheet({
|
||||
// The affordance itself is gated on `assistAvailable` (from usage); the
|
||||
// probe only resolves the transport mode + the actionable unavailable reason.
|
||||
const capability = useDocumentAiCapability(open && documentId !== null);
|
||||
// When auto-read is ON, reading happens automatically on upload — the manual
|
||||
// per-document AI action row is redundant and collapses away.
|
||||
const autoRead = useDocumentsAutoAiRead(open && documentId !== null);
|
||||
const suggest = useSuggestDetails();
|
||||
const summary = useDocumentSummary();
|
||||
const indexDoc = useIndexDocument();
|
||||
@@ -335,6 +339,7 @@ export function DocumentDetailSheet({
|
||||
const aiEnabled = assistAvailable ?? capability.data?.available ?? false;
|
||||
const indexEnabled =
|
||||
contentIndexEnabled ?? capability.data?.available ?? false;
|
||||
const autoReadEnabled = autoRead.data?.documentsAutoAiRead ?? false;
|
||||
const aiMode = capability.data?.mode === "text" ? "text" : "vision";
|
||||
const aiTarget: DocumentAiTarget | null = doc
|
||||
? {
|
||||
@@ -504,6 +509,7 @@ export function DocumentDetailSheet({
|
||||
|
||||
<DocumentAiSection
|
||||
aiEnabled={aiEnabled}
|
||||
autoReadEnabled={autoReadEnabled}
|
||||
indexEnabled={indexEnabled}
|
||||
unavailableReason={capability.data?.reason ?? null}
|
||||
actionsDisabled={capability.isPending}
|
||||
|
||||
@@ -51,6 +51,27 @@ export function useDocumentAiCapability(enabled: boolean) {
|
||||
});
|
||||
}
|
||||
|
||||
/** The per-user "read documents automatically with AI" opt-in. */
|
||||
export interface DocumentsAutoAiReadPref {
|
||||
documentsAutoAiRead: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the per-user auto-AI-read flag (`GET /api/auth/me/documents-auto-ai-read`).
|
||||
* Shares the cache key with the AI-settings toggle, so a flip there and this
|
||||
* read stay in lockstep. When ON, reading happens automatically on upload and
|
||||
* the detail sheet drops the manual per-document AI action row.
|
||||
*/
|
||||
export function useDocumentsAutoAiRead(enabled: boolean) {
|
||||
return useQuery<DocumentsAutoAiReadPref>({
|
||||
queryKey: queryKeys.documentsAutoAiRead(),
|
||||
queryFn: () =>
|
||||
apiGet<DocumentsAutoAiReadPref>("/api/auth/me/documents-auto-ai-read"),
|
||||
enabled,
|
||||
staleTime: 60_000,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Suggest filing metadata for a stored document. Returns DRAFTS only — the
|
||||
* detail sheet prefills its edit fields and the user saves; this call writes
|
||||
|
||||
Reference in new issue
Block a user