+ {body}{" "} + + {t("documents.assist.unavailableAction")} + +
+{label}
+{value}
++ {t("documents.assist.reviewTitle")} +
++ {t("documents.assist.reviewHint")} +
++ {t("documents.assist.empty")} +
+ )} +
+
+ {t(errorKey)} +
+ ) : body !== null ? ( ++ {body} +
+ ) : null} + ++ {t("documents.summary.notSaved")} +
+
+
+ {t(suggestErrorKey)} +
+ ) : null} + + {suggestion ? ( +
{mutationError}
diff --git a/src/components/documents/use-content-index.ts b/src/components/documents/use-content-index.ts
new file mode 100644
index 000000000..bfbe9d8ca
--- /dev/null
+++ b/src/components/documents/use-content-index.ts
@@ -0,0 +1,56 @@
+"use client";
+
+/**
+ * v1.27.22 (Document vault P2) — content-search indexing hooks.
+ *
+ * - `useIndexDocument()` populates / refreshes ONE document's blind content
+ * index (encrypted extracted text + opaque HMAC token array). Vision decrypts
+ * the stored original server-side; text OCR's the image on-device and posts
+ * only the text — the same transport split the assist hooks use.
+ * - `useReindexAll()` fires the per-user corpus backfill (its own rate bucket);
+ * the worker indexes every not-yet-indexed document off-request.
+ *
+ * Both invalidate the `["documents"]` prefix so the timeline's per-row
+ * `hasContentIndex`, the open detail sheet, and the usage coverage gauge all
+ * refresh in lockstep.
+ */
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+
+import { apiPost } from "@/lib/api/api-fetch";
+import { invalidateKeys, queryKeys } from "@/lib/query-keys";
+
+import {
+ runDocumentIndex,
+ type DocumentAiMode,
+ type DocumentAiTarget,
+} from "./document-ai-transport";
+
+/** Index / re-index one document for content search. */
+export function useIndexDocument() {
+ const queryClient = useQueryClient();
+ return useMutation<
+ { indexed: boolean; tokenCount: number },
+ Error,
+ { mode: DocumentAiMode; target: DocumentAiTarget }
+ >({
+ mutationFn: ({ mode, target }) => runDocumentIndex({ mode, target }),
+ onSuccess: () => {
+ void invalidateKeys(queryClient, [queryKeys.documents()]);
+ },
+ });
+}
+
+/** Fire the corpus backfill; resolves with how many docs were enqueued. */
+export function useReindexAll() {
+ const queryClient = useQueryClient();
+ return useMutation<{ enqueued: number }, Error, void>({
+ mutationFn: () =>
+ apiPost<{ enqueued: number }>(
+ "/api/documents/inbound/reindex",
+ undefined,
+ ),
+ onSuccess: () => {
+ void invalidateKeys(queryClient, [queryKeys.documents()]);
+ },
+ });
+}
diff --git a/src/components/documents/use-document-assist.ts b/src/components/documents/use-document-assist.ts
new file mode 100644
index 000000000..d0b0c6b53
--- /dev/null
+++ b/src/components/documents/use-document-assist.ts
@@ -0,0 +1,134 @@
+"use client";
+
+/**
+ * v1.27.22 (Document vault P2) — client hooks for the review-first AI layer on
+ * a stored document: the shared capability probe, filing-metadata suggestions,
+ * and the session-only summary / extracted text.
+ *
+ * Every affordance is gated on `usage.assistAvailable` at the call site; these
+ * hooks only RUN the call once the surface has decided to offer it. The
+ * transport (VISION vs local-OCR TEXT) comes from `document-ai-transport`.
+ *
+ * NOTHING here writes a document row. Suggestions are drafts the detail sheet
+ * prefills; the summary is transient. The only mutation is the caller applying
+ * a reviewed draft through the existing edit-on-commit machinery.
+ */
+import { useMutation, useQuery } from "@tanstack/react-query";
+
+import { apiGet, ApiError } from "@/lib/api/api-fetch";
+import { queryKeys } from "@/lib/query-keys";
+import type { OcrCapabilityDto } from "@/lib/validations/labs-ocr";
+import type {
+ DocumentSuggestionDto,
+ DocumentSummaryMode,
+} from "@/lib/validations/inbound-documents";
+
+import {
+ DocumentAssistClientError,
+ runDocumentAi,
+ type DocumentAiMode,
+ type DocumentAiTarget,
+} from "./document-ai-transport";
+
+/** The session-only describe result — a summary XOR the raw extracted text. */
+export type DocumentDescribeResult = { summary: string } | { text: string };
+
+/**
+ * The shared OCR capability probe (`/api/labs/ocr/capability`) reused verbatim:
+ * the same server resolution both the lab-OCR and the document AI routes gate
+ * on, so the client picks the transport the endpoint accepts. Availability of
+ * the affordance itself is gated on `usage.assistAvailable`; this read only
+ * resolves the transport `mode` + PDF support.
+ */
+export function useDocumentAiCapability(enabled: boolean) {
+ return useQuery