All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 41s
Adds an always-visible debounced search input in the AppShell header that fans out to three independent sources in parallel and renders per-source result groups with their own loading/empty/error states: - /api/search/cases (NEW): SQL ILIKE on case_number, address, parties, title, subject. Returns small projections, no embeddings needed. - /api/precedent-library/search (existing): semantic over case-law halachot + passages. - /api/search (existing): semantic over case documents + past decisions. Cmd/Ctrl+K focuses the input; Esc and click-outside close the panel. This is Phase A of the header redesign — the bar layout itself is unchanged; row grouping + dynamic context follow in Phase B. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
114 lines
3.3 KiB
TypeScript
114 lines
3.3 KiB
TypeScript
/**
|
|
* 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<DocumentHit[] | { message: string }>(
|
|
`/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,
|
|
};
|
|
}
|