"use client"; /** * v1.27.22 (Document vault P2) — the presentational AI panels for the document * detail sheet. Pure (props in, markup out) so the review-first contract and * the session-only note are pinned by static-render tests. * * - `AiUnavailableHint` — the calm "set up an AI provider" pointer shown in * place of the AI actions when no provider is configured (never an error). * - `AssistSuggestionReview` — the reviewed DRAFT card: suggested title / type * / date, each applied only by an explicit tap. Nothing is written until the * user applies it (and the title lands in the editable field, not on disk). * - `DocumentSummaryPanel` — the transient summary / extracted-text panel with * the persistent "not saved · not a diagnosis" note. * - `AiRunPhaseNote` — the calm line under an action while its read runs in * the background (v1.40), shared by every document AI action. */ import { Sparkles } from "lucide-react"; import { Check, FileText, X } from "lucide-react"; import Link from "next/link"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; import type { AiRunPhase } from "@/hooks/use-ai-run"; import { useTranslations } from "@/lib/i18n/context"; import { cn } from "@/lib/utils"; import { type DocumentSuggestionDto, type DocumentSummaryMode, } from "@/lib/validations/inbound-documents"; import type { DocumentDescribeResult } from "./use-document-assist"; /** * The line under a document AI action while its read runs in the background, * or nothing when no read is running. * * `outcome` keeps the sentence true: a read whose result lands on the document * (the index, a stored summary, staged facts) tells the person they may leave; * a read whose answer lives only on this screen (suggestions, a transient * summary or transcription) does not promise that, because leaving drops it. */ export function AiRunPhaseNote({ phase, outcome, slot, }: { phase: AiRunPhase; outcome: "savedWithDocument" | "shownHere"; slot: string; }) { const { t } = useTranslations(); if (phase === "idle") return null; return (

{phase === "waitingForWorker" ? t("aiRuns.waitingForWorker") : outcome === "savedWithDocument" ? t("aiRuns.backgroundDocument") : t("aiRuns.backgroundScan")}

); } /** * Calm pointer to the AI settings, shown when assist is unavailable. When the * document is already searchable (auto-indexed locally) the copy stays honest — * it is searchable, an AI provider only adds a richer read. */ export function AiUnavailableHint({ reason, }: { reason: "no-provider" | "enable-local-ocr" | null; }) { const { t } = useTranslations(); const body = reason === "enable-local-ocr" ? t("documents.assist.unavailableLocalOcr") : t("documents.assist.unavailableBody"); return (

{body}{" "} {t("documents.assist.unavailableAction")}

); } /** One reviewed draft row: a suggested value + an explicit apply control. */ function ReviewRow({ label, value, applied, onApply, applyLabel, }: { label: string; value: string; applied: boolean; onApply: () => void; applyLabel: string; }) { const { t } = useTranslations(); return (

{label}

{value}

{applied ? ( {t("documents.assist.applied")} ) : ( )}
); } /** * The review-first suggestion card. Suggestions are DRAFTS: applying the title * seeds the editable title field (the user still saves it); applying the type * or date commits that single field through the existing edit-on-commit path — * always an explicit tap, never automatic. */ export function AssistSuggestionReview({ suggestion, kindLabel, dateLabel, applied, onUseTitle, onUseKind, onUseDate, onDismiss, }: { suggestion: DocumentSuggestionDto; /** Translated label for the suggested kind, or null when none was read. */ kindLabel: string | null; /** Formatted suggested date, or null when none was read. */ dateLabel: string | null; applied: { title: boolean; kind: boolean; date: boolean }; onUseTitle: () => void; onUseKind: () => void; onUseDate: () => void; onDismiss: () => void; }) { const { t } = useTranslations(); const hasAny = suggestion.title !== null || suggestion.kind !== null || suggestion.documentDate !== null; return (
{hasAny ? (
{suggestion.title !== null ? ( ) : null} {suggestion.kind !== null && kindLabel !== null ? ( ) : null} {suggestion.documentDate !== null && dateLabel !== null ? ( ) : null}
) : (

{t("documents.assist.empty")}

)}
); } /** * The transient summary / extracted-text panel. The result is shown once and * never persisted; the "not saved · not a diagnosis" note is always present. */ export function DocumentSummaryPanel({ output, result, isPending, errorText, onClose, }: { output: DocumentSummaryMode; result: DocumentDescribeResult | null; isPending: boolean; /** Resolved user-facing error sentence (already translated), or null. */ errorText: string | null; onClose: () => void; }) { const { t } = useTranslations(); const heading = output === "text" ? t("documents.summary.textTitle") : t("documents.summary.summaryTitle"); const body = result === null ? null : "summary" in result ? result.summary : result.text; return (

{heading}

{isPending ? (
) : errorText ? (

{errorText}

) : body !== null ? (

{body}

) : null}
); }