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 ? (