From 2319988a962f1f382cb6d7d5f5c2878fc15efa90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Tue, 7 Jul 2026 19:14:59 +0200 Subject: [PATCH] feat(documents): surface content search in the vault list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The list search already unions matches from the encrypted content index server-side; surface it so it is legible and reachable: - a subtle "search now looks inside your documents too (whole words)" hint under the search box once any document is indexed; - a per-row "searchable" marker on indexed documents; - an "index all for search" affordance that fires the corpus backfill when some documents are not yet indexed, driven off the usage coverage gauge. All of it is gated on the same provider/consent precondition as the assist layer — with no provider, search degrades to title and filename exactly as before. --- .../__tests__/document-card.test.tsx | 27 ++++++++++++ src/components/documents/document-card.tsx | 19 ++++++++- .../documents/document-filter-bar.tsx | 41 ++++++++++++++++++- src/components/documents/documents-view.tsx | 35 ++++++++++++++++ 4 files changed, 119 insertions(+), 3 deletions(-) diff --git a/src/components/documents/__tests__/document-card.test.tsx b/src/components/documents/__tests__/document-card.test.tsx index defae0372..9e58adefa 100644 --- a/src/components/documents/__tests__/document-card.test.tsx +++ b/src/components/documents/__tests__/document-card.test.tsx @@ -84,6 +84,33 @@ describe("", () => { expect(html).toContain("befund.docx"); }); + it("marks a content-indexed document as searchable", () => { + const html = render( + , + ); + expect(html).toContain('data-slot="document-searchable"'); + expect(html).toContain("Contents searchable"); + }); + + it("shows no searchable marker when the document is not indexed", () => { + const html = render( + , + ); + expect(html).not.toContain('data-slot="document-searchable"'); + }); + it("falls back to the untitled label and rings when highlighted", () => { const html = render( {document.conditionLinks.length > 0 || - document.servingClass === "attachment" ? ( + document.servingClass === "attachment" || + document.hasContentIndex ? (
{document.conditionLinks.map((link) => ( ) : null} + {document.hasContentIndex ? ( + // Subtle "contents searchable" marker — icon-only to keep the + // row calm; the label rides an accessible name. + + + + {t("documents.card.searchableBadge")} + + + ) : null}
) : null} diff --git a/src/components/documents/document-filter-bar.tsx b/src/components/documents/document-filter-bar.tsx index b17dfe4a0..c71ef91df 100644 --- a/src/components/documents/document-filter-bar.tsx +++ b/src/components/documents/document-filter-bar.tsx @@ -11,7 +11,7 @@ * sticks below the top edge inside the shell's scroll container (sticky, * translucent background, border-b — no viewport-height tricks). */ -import { Search, X } from "lucide-react"; +import { ScanSearch, Search, X } from "lucide-react"; import type { RefObject } from "react"; import { Button } from "@/components/ui/button"; @@ -45,6 +45,10 @@ export function DocumentFilterBar({ onToggleYear, activeCount, onClearAll, + contentSearchActive = false, + showIndexAll = false, + indexAllPending = false, + onIndexAll, }: { searchValue: string; onSearchChange: (value: string) => void; @@ -60,6 +64,12 @@ export function DocumentFilterBar({ onToggleYear: (year: number) => void; activeCount: number; onClearAll: () => void; + /** Search already matches indexed document CONTENT (whole words). */ + contentSearchActive?: boolean; + /** Some documents are not yet indexed — offer the corpus backfill. */ + showIndexAll?: boolean; + indexAllPending?: boolean; + onIndexAll?: () => void; }) { const { t } = useTranslations(); @@ -205,6 +215,35 @@ export function DocumentFilterBar({ ) : null} + + {contentSearchActive || showIndexAll ? ( +
+ + + {contentSearchActive + ? t("documents.contentIndex.searchHint") + : t("documents.contentIndex.indexPrompt")} + + {showIndexAll ? ( + + ) : null} +
+ ) : null} ); } diff --git a/src/components/documents/documents-view.tsx b/src/components/documents/documents-view.tsx index ea817d8b9..11cdf20a9 100644 --- a/src/components/documents/documents-view.tsx +++ b/src/components/documents/documents-view.tsx @@ -56,6 +56,7 @@ import { DocumentDetailSheet } from "./document-detail-sheet"; import { DocumentFilterBar, type ConditionChip } from "./document-filter-bar"; import { DocumentTimeline } from "./document-timeline"; import { UploadZone } from "./upload-zone"; +import { useReindexAll } from "./use-content-index"; import { useDocumentUpload } from "./use-document-upload"; import { usePageFileDrop } from "./use-page-file-drop"; import { @@ -244,6 +245,34 @@ export function DocumentsView() { const episodes = useIllnessEpisodes(true); + // Content-search coverage: fire the corpus backfill when indexing is + // available and some documents are not yet searchable. The list search + // already unions content matches server-side — this only surfaces the hint + // and the "index all" affordance from the usage gauge. + const reindexAll = useReindexAll(); + const contentIndex = usage.data?.contentIndex; + const canIndexContent = contentIndex?.enabled ?? false; + const showIndexAll = + canIndexContent && + contentIndex !== undefined && + contentIndex.totalCount > 0 && + contentIndex.indexedCount < contentIndex.totalCount; + const contentSearchActive = + canIndexContent && (contentIndex?.indexedCount ?? 0) > 0; + const handleIndexAll = useCallback(() => { + reindexAll.mutate(undefined, { + onSuccess: (result) => + toast.success( + result.enqueued > 0 + ? t("documents.contentIndex.indexAllQueued", { + count: result.enqueued, + }) + : t("documents.contentIndex.indexAllNothing"), + ), + onError: () => toast.error(t("documents.contentIndex.indexAllError")), + }); + }, [reindexAll, t]); + // Condition chips: every episode carrying at least one live document // link, served by the usage endpoint (NOT derived from the loaded corpus // — an old linked document pages deep must still surface its chip), plus @@ -557,6 +586,10 @@ export function DocumentsView() { onToggleYear={toggleYear} activeCount={activeCount} onClearAll={clearFilters} + contentSearchActive={contentSearchActive} + showIndexAll={showIndexAll} + indexAllPending={reindexAll.isPending} + onIndexAll={handleIndexAll} /> {list.isPending ? ( @@ -618,6 +651,8 @@ export function DocumentsView() { documentId={detailId} open={detailOpen} onOpenChange={handleDetailOpenChange} + assistAvailable={usage.data?.assistAvailable} + contentIndexEnabled={usage.data?.contentIndex.enabled} /> {selectedIds.size > 0 ? (