// PURPOSE: Corpus search (spec §5) — the input + results panel, lifted out of // the app so every consumer (embedded + standalone) renders search the // same way instead of hand-rolling hit rendering. Owns query state, // paginated results, loading/error/clear; shows the caller's `browse` // content when there is no active search. Pure: the actual search runs // through an injected `onSearch` callback. // =========================================================================== import { useState, type FC, type FormEvent, type ReactNode } from "react"; import { Loader2, Search, X } from "lucide-react"; import type { ObjectSummary, SearchHit, SearchPage } from "./types"; import { ObjectList } from "./components-object"; function hitToSummary(h: SearchHit): ObjectSummary { return { object_id: h.object_id, kind: h.kind, title: h.title, status: "active", updated_at: h.updated_at ?? "", effective_at: h.effective_at ?? null, sensitivity: h.sensitivity, }; } export interface KnowledgeSearchProps { /** Run a search and return a page; `cursor` drives "Load more". */ onSearch: (query: string, cursor?: string) => Promise; onOpen?: (objectId: string) => void; placeholder?: string; /** Shown below the input while there is no active search (the browse list). */ browse?: ReactNode; className?: string; } export const KnowledgeSearch: FC = ({ onSearch, onOpen, placeholder, browse, className, }) => { const [query, setQuery] = useState(""); const [hits, setHits] = useState(null); const [cursor, setCursor] = useState(null); const [loading, setLoading] = useState(false); const [loadingMore, setLoadingMore] = useState(false); const [error, setError] = useState(null); function clear() { setHits(null); setCursor(null); setQuery(""); setError(null); } async function run(e?: FormEvent) { e?.preventDefault(); const q = query.trim(); if (!q) { clear(); return; } setLoading(true); setError(null); try { const page = await onSearch(q); setHits(page.hits); setCursor(page.next_cursor ?? null); } catch { setError("Search failed. Try again."); } finally { setLoading(false); } } async function loadMore() { if (!cursor || loadingMore) return; setLoadingMore(true); try { const page = await onSearch(query.trim(), cursor); setHits((prev) => [...(prev ?? []), ...page.hits]); setCursor(page.next_cursor ?? null); } catch { setError("Couldn't load more results. Try again."); } finally { setLoadingMore(false); } } return (
setQuery(e.target.value)} /> {query && ( )}
{loading ? (

Searching…

) : error ? (

{error}

) : hits ? (
{hits.length} result{hits.length === 1 ? "" : "s"}
onOpen?.(o.object_id)} onLoadMore={loadMore} hasMore={!!cursor} loadingMore={loadingMore} emptyState={
No matches.
} />
) : ( (browse ?? null) )}
); };