mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 13:52:20 +00:00
Replace the collapsed bottom-of-card "Chat about this document" outline
button with a neutral, ghost Coach icon pinned to the top-right of the
document detail-sheet header, mirroring the Insights ask-coach affordance.
The icon uses the muted-foreground token only — never the primary/FAB
gradient — and opens a document-scoped chat drawer.
Present the chat in the Coach chat drawer chrome (right-side sheet on
desktop, bottom sheet on phones; same width caps; close-only, no maximize
or conversations rail) with a quiet "Chatting about: <document>" scope
badge. The drawer reuses the existing pure conversation body and the
existing client hooks, so it still posts /api/documents/inbound/[id]/chat
verbatim: no tools, no health snapshot, fenced document, inbound/replay
refusal, outbound screen, numeric grounding, consent + budget + per-user
rate bucket, indexed-only, plain-text render — the route is untouched.
Thread isolation is unchanged (the general Coach filters documentId: null;
the document route always scopes { documentId: id }).
Add a small reusable headerAction slot to ResponsiveSheet for the header
icon. Retire the DocumentChatPanel container and the AI-section chatSlot;
keep the pure DocumentChatConversation body and documentChatErrorKey.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPyLteNeTWwEC154X7vLWc
231 lines
7.4 KiB
TypeScript
231 lines
7.4 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 { 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,
|
|
}: {
|
|
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;
|
|
}) {
|
|
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}
|
|
</>
|
|
) : (
|
|
<AiUnavailableHint reason={unavailableReason} />
|
|
)}
|
|
</div>
|
|
);
|
|
}
|