mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
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.
243 lines
7.9 KiB
TypeScript
243 lines
7.9 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* The "Read with AI" block of the document detail sheet — one coherent, inviting
|
|
* home for every AI reading action, lifted out of the sheet so the availability
|
|
* gate and the review-first wiring render without the sheet's dialog portal and
|
|
* are pinned by static-render tests.
|
|
*
|
|
* Indexing is AUTOMATIC on upload, so this block never nags a "please index"
|
|
* chore. It shows the document's searchable status (auto-indexed) as a pill, and
|
|
* offers AI reading as a capability the user reaches for when they want more:
|
|
*
|
|
* - "Read with AI" (prominent) runs a provider read of the document — the
|
|
* richer pass that also refreshes the searchable index. Labelled "Read
|
|
* again" once a provider has already read it.
|
|
* - "Suggest details" drafts a title / type / date to review before saving.
|
|
* - "Summarise" / "Show text" surface a transient, session-only read-out.
|
|
*
|
|
* Presentational: the sheet owns the mutations + capability probe and passes
|
|
* their state + handlers down. When `aiEnabled` is false the actions collapse to
|
|
* the calm "set up an AI provider" pointer (never an error) — the searchable
|
|
* 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 type { ReactNode } from "react";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
import { useTranslations } from "@/lib/i18n/context";
|
|
import { cn } from "@/lib/utils";
|
|
import {
|
|
isAiReadSource,
|
|
type DocumentContentIndexSourceValue,
|
|
type DocumentSuggestionDto,
|
|
type DocumentSummaryMode,
|
|
} from "@/lib/validations/inbound-documents";
|
|
|
|
import {
|
|
AiUnavailableHint,
|
|
AssistSuggestionReview,
|
|
ContentSearchStatus,
|
|
DocumentSummaryPanel,
|
|
} from "./document-ai-panels";
|
|
import type { DocumentDescribeResult } from "./use-document-assist";
|
|
|
|
export function DocumentAiSection({
|
|
aiEnabled,
|
|
autoReadEnabled = false,
|
|
indexEnabled,
|
|
unavailableReason,
|
|
actionsDisabled,
|
|
onSuggest,
|
|
suggestPending,
|
|
suggestErrorKey,
|
|
onSummarise,
|
|
summaryPending,
|
|
suggestion,
|
|
suggestionKindLabel,
|
|
suggestionDateLabel,
|
|
appliedFields,
|
|
onUseTitle,
|
|
onUseKind,
|
|
onUseDate,
|
|
onDismissSuggestion,
|
|
summaryOutput,
|
|
summaryResult,
|
|
summaryErrorKey,
|
|
onCloseSummary,
|
|
hasContentIndex,
|
|
contentIndexSource,
|
|
indexPending,
|
|
onIndex,
|
|
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. */
|
|
actionsDisabled: boolean;
|
|
onSuggest: () => void;
|
|
suggestPending: boolean;
|
|
suggestErrorKey: string | null;
|
|
onSummarise: (output: DocumentSummaryMode) => void;
|
|
summaryPending: boolean;
|
|
suggestion: DocumentSuggestionDto | null;
|
|
suggestionKindLabel: string | null;
|
|
suggestionDateLabel: string | null;
|
|
appliedFields: { title: boolean; kind: boolean; date: boolean };
|
|
onUseTitle: () => void;
|
|
onUseKind: () => void;
|
|
onUseDate: () => void;
|
|
onDismissSuggestion: () => void;
|
|
summaryOutput: DocumentSummaryMode | null;
|
|
summaryResult: DocumentDescribeResult | null;
|
|
summaryErrorKey: string | null;
|
|
onCloseSummary: () => void;
|
|
hasContentIndex: boolean;
|
|
contentIndexSource: DocumentContentIndexSourceValue | null;
|
|
indexPending: boolean;
|
|
onIndex: () => void;
|
|
/**
|
|
* The scoped "chat about this document" panel, rendered inside the AI area
|
|
* beneath the reading actions. Only supplied (and only meaningful) when a
|
|
* provider is available — the panel itself gates on whether the document is
|
|
* indexed. A pure slot so this section stays presentational + statically
|
|
* renderable (the stateful, query-driven panel lives in the sheet).
|
|
*/
|
|
chatSlot?: ReactNode;
|
|
}) {
|
|
const { t } = useTranslations();
|
|
const aiRead = hasContentIndex && isAiReadSource(contentIndexSource);
|
|
const readLabel = indexPending
|
|
? t("documents.ai.reading")
|
|
: aiRead
|
|
? t("documents.ai.reread")
|
|
: t("documents.ai.read");
|
|
|
|
return (
|
|
<div
|
|
data-slot="document-ai-section"
|
|
className="border-border bg-muted/30 space-y-3 rounded-lg border p-3 md:p-4"
|
|
>
|
|
<div className="flex items-start justify-between gap-2">
|
|
<div className="flex min-w-0 items-center gap-2">
|
|
<WandSparkles
|
|
className="text-foreground size-5 shrink-0"
|
|
aria-hidden
|
|
/>
|
|
<p className="text-sm font-semibold">
|
|
{t("documents.ai.blockTitle")}
|
|
</p>
|
|
</div>
|
|
<ContentSearchStatus
|
|
hasContentIndex={hasContentIndex}
|
|
source={contentIndexSource}
|
|
isPending={indexPending}
|
|
/>
|
|
</div>
|
|
|
|
{aiEnabled ? (
|
|
<>
|
|
{!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="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>
|
|
) : null}
|
|
|
|
{suggestErrorKey ? (
|
|
<p role="alert" className="text-destructive text-sm">
|
|
{t(suggestErrorKey)}
|
|
</p>
|
|
) : null}
|
|
|
|
{suggestion ? (
|
|
<AssistSuggestionReview
|
|
suggestion={suggestion}
|
|
kindLabel={suggestionKindLabel}
|
|
dateLabel={suggestionDateLabel}
|
|
applied={appliedFields}
|
|
onUseTitle={onUseTitle}
|
|
onUseKind={onUseKind}
|
|
onUseDate={onUseDate}
|
|
onDismiss={onDismissSuggestion}
|
|
/>
|
|
) : null}
|
|
|
|
{summaryOutput ? (
|
|
<DocumentSummaryPanel
|
|
output={summaryOutput}
|
|
result={summaryResult}
|
|
isPending={summaryPending}
|
|
errorKey={summaryErrorKey}
|
|
onClose={onCloseSummary}
|
|
/>
|
|
) : null}
|
|
|
|
{chatSlot}
|
|
</>
|
|
) : (
|
|
<AiUnavailableHint reason={unavailableReason} />
|
|
)}
|
|
</div>
|
|
);
|
|
}
|