feat(search): add header global search (Phase A) — cases + precedents + docs
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 41s
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:
49
web/app.py
49
web/app.py
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user