feat(search): add header global search (Phase A) — cases + precedents + docs
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>
This commit is contained in:
2026-05-03 18:05:51 +00:00
parent cbdbc522a0
commit f722fa45bd
4 changed files with 508 additions and 1 deletions

View File

@@ -1387,6 +1387,55 @@ async def api_case_search(case_number: str, query: str, limit: int = 10):
return {"message": result}
@app.get("/api/search/cases")
async def api_search_cases(q: str, limit: int = 10):
"""Lightweight SQL search over cases — by case number, address, parties, title.
Powers the global-search dropdown in the header. Returns small projections,
not full case objects.
"""
q = q.strip()
if len(q) < 2:
return {"items": [], "count": 0}
needle = f"%{q}%"
prefix = f"{q}%"
pool = await db.get_pool()
async with pool.acquire() as conn:
rows = await conn.fetch(
"""
SELECT case_number, title, property_address, status,
practice_area, appeal_subtype, updated_at
FROM cases
WHERE case_number ILIKE $1
OR property_address ILIKE $1
OR title ILIKE $1
OR subject ILIKE $1
OR appellants::text ILIKE $1
OR respondents::text ILIKE $1
ORDER BY
CASE WHEN case_number ILIKE $2 THEN 0 ELSE 1 END,
updated_at DESC NULLS LAST
LIMIT $3
""",
needle, prefix, limit,
)
items = [
{
"case_number": r["case_number"],
"title": r["title"],
"property_address": r["property_address"],
"status": r["status"],
"practice_area": r["practice_area"],
"appeal_subtype": r["appeal_subtype"],
}
for r in rows
]
return {"items": items, "count": len(items)}
@app.get("/api/cases/{case_number}/template")
async def api_case_template(case_number: str):
"""Get outcome-aware decision template for a case."""