/** * Global header search — fans out to three independent sources in parallel. * * Each source is its own `useQuery`, so the fastest one (cases, plain SQL) * shows up immediately while the slower vector searches stream in. A failure * in one source does not block the others — the result panel renders per- * source skeletons and per-source error states. * * Sources: * - cases → GET /api/search/cases (SQL ILIKE on case_number/address/parties) * - precedent → GET /api/precedent-library/search (semantic, halachot+chunks) * - documents → GET /api/search (semantic, all case docs + past decisions) */ import { useQuery } from "@tanstack/react-query"; import { apiRequest } from "./client"; import type { SearchHit as PrecedentHit } from "./precedent-library"; export type CaseHit = { case_number: string; title: string; property_address: string | null; status: string | null; practice_area: string | null; appeal_subtype: string | null; }; export type DocumentHit = { score: number; case_number: string; document: string; section: string; page: number | null; content: string; }; const MIN_QUERY_LEN = 2; const STALE_MS = 10_000; const PER_SOURCE_LIMIT = 5; const enabled = (q: string) => q.trim().length >= MIN_QUERY_LEN; export function useCasesSearch(query: string) { return useQuery({ queryKey: ["global-search", "cases", query], queryFn: ({ signal }) => apiRequest<{ items: CaseHit[]; count: number }>( `/api/search/cases?q=${encodeURIComponent(query)}&limit=${PER_SOURCE_LIMIT}`, { signal }, ), enabled: enabled(query), staleTime: STALE_MS, placeholderData: (prev) => prev, }); } export function usePrecedentSearch(query: string) { return useQuery({ queryKey: ["global-search", "precedent", query], queryFn: ({ signal }) => { const p = new URLSearchParams({ q: query, limit: String(PER_SOURCE_LIMIT), include_halachot: "true", }); return apiRequest<{ items: PrecedentHit[]; count: number }>( `/api/precedent-library/search?${p.toString()}`, { signal }, ); }, enabled: enabled(query), staleTime: STALE_MS, placeholderData: (prev) => prev, }); } /** * The /api/search endpoint returns either an array of DocumentHit, or * `{message: "לא נמצאו תוצאות."}` when empty. Normalize to a plain array. */ export function useDocumentsSearch(query: string) { return useQuery({ queryKey: ["global-search", "documents", query], queryFn: async ({ signal }) => { const raw = await apiRequest( `/api/search?query=${encodeURIComponent(query)}&limit=${PER_SOURCE_LIMIT}`, { signal }, ); return Array.isArray(raw) ? raw : []; }, enabled: enabled(query), staleTime: STALE_MS, placeholderData: (prev) => prev, }); } export function useGlobalSearch(query: string) { const cases = useCasesSearch(query); const precedent = usePrecedentSearch(query); const documents = useDocumentsSearch(query); const isQueryReady = enabled(query); const anyLoading = isQueryReady && (cases.isLoading || precedent.isLoading || documents.isLoading); return { cases, precedent, documents, isQueryReady, anyLoading, }; }