Files
legal-ai/web-ui/src/components/precedents/library-search-panel.tsx
Chaim 36bae6c592
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 13s
fix(ia): IA גל-1 — סנכרון-cache + נתונים-שגויים + מחיקת-מתים (#130, X17)
גל-1 מבקלוג #127 (docs/ia-audit-redesign.md §4) — תיקון מקומי, ללא הגירת-IA.
מקיים G2 בשכבת-ה-UI דרך INV-IA1/IA2/IA5/IA6 (docs/spec/X17).

א) פערי-סנכרון (INV-IA2 — mutation מבטל כל קורא):
- CAS-1/2: העלאת-DOCX/export מבטלים ['decision-blocks'] (מחוון source_of_truth)
- APR-1/4: פתרון/יצירת-הערה מבטלים ['chair','pending'] (תיבה+תג-סרגל)
- APR-5/ADM-2: אישור/batch הלכות מבטלים ['chair','pending']+['operations']
- APR-6/ADM-3: create/update/delete/upload פסיקה-חסרה מבטלים שניהם
- LRN-6: ComparePanel גוזר בחירה מהקורפוס המרוענן (אין POST ל-id מחוק → 404)
- LRN-8: מחיקת-קורפוס מבטלת רשימת-צ'אטים (chat שהתייתם לא נשאר עם קישור-קורפוס תקוע)
- LRN-10/MET-1/MET-8: promote מבטל גם lessons וגם methodology (LessonsTab+/methodology)

ב) נתונים-שגויים (INV-IA5 — סטטוס מגובה-צרכן):
- LRN-4: KPI "דפוסי סגנון" — הוסר היחס-השקרי "מתוך total_patterns" (שאילתות עצמאיות)
- LRN-5: findings_applied (דגל אינפורמטיבי-בלבד) → findings_approved (שער INV-LRN1 האמיתי)
- ADM-1: halacha_backlog שהוחזר ונזרק → מרונדר ב-/diagnostics, מצביע ל-/approvals (INV-IA1)
- ADM-6: מוני-סוכנים מסמנים "חלקי+" כשחברת-Paperclip לא נטענה
- APR-3: מכוסה ע"י APR-1 (count+sample מאותה שאילתה; הבעיה היתה staleness-cache)
- MET-6: עורך-צ'קליסטים מציג איזה case בוחר כל צ'קליסט (explainer-תחולה)
- ADM-5: ערך-Container מסומן "ממתין ל-redeploy" כש-Coolify≠Container

ג) מתים/jargon:
- PRE-2: הוסר GET /api/precedent-library/queue/pending (אפס צרכני-frontend)
- PRE-3/5: AuthorityBadge (binding/persuasive) מרונדר גם בחיפוש, לא רק בתור-הביקורת
- MET-5: הוסר ז'רגון T7/T15 מטקסט-העזר ב-/methodology (INV-IA6)

Invariants: מקיים INV-IA1/IA2/IA5/IA6 (X17), G2 (מקור-אמת יחיד בשכבת-UI), G10
(לא הוסר שום שער-אנושי — רק סנכרון/נתון/קוד-מת). שומר INV-LRN1.

בדיקות: py_compile web/app.py ✓ · tsc --noEmit ✓ · eslint ✓ (לבד מ-learning-panel:109
unescaped-quote — קיים-מראש ב-main, מחוץ לסט-הממצאים). next build נכשל רק בגלל
symlink node_modules ב-worktree (Turbopack) — ה-build ב-Docker/CI תקין.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 20:47:32 +00:00

179 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState } from "react";
import { Search } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from "@/components/ui/select";
import {
useLibrarySearch, type PracticeArea, type SearchHit,
} from "@/lib/api/precedent-library";
import { PRACTICE_AREAS, PRECEDENT_LEVELS } from "./practice-area";
import { AuthorityBadge } from "./halacha-meta";
function formatDate(iso: string | null) {
if (!iso) return "—";
try {
return new Date(iso).toLocaleDateString("he-IL");
} catch {
return iso;
}
}
function HalachaCard({ hit }: { hit: Extract<SearchHit, { type: "halacha" }> }) {
return (
<div className="rounded-lg border border-gold/40 bg-gold-wash/40 p-4 space-y-2">
<div className="flex items-center gap-2 text-[0.78rem] text-ink-muted flex-wrap">
<Badge className="bg-gold text-navy border-0">הלכה</Badge>
<span className="font-mono" dir="ltr">{hit.case_number}</span>
{hit.court && <span>· {hit.court}</span>}
{hit.decision_date && <span>· {formatDate(hit.decision_date)}</span>}
{hit.precedent_level && <span>· {hit.precedent_level}</span>}
{/* PRE-3/PRE-5 (INV-IA5): the derived authority (binding/persuasive)
rides on the wire but was dropped here — render it as in the review
tab so search shows the same provenance everywhere. */}
<AuthorityBadge authority={hit.authority} />
<span className="ms-auto tabular-nums">דירוג {hit.score.toFixed(2)}</span>
</div>
<p className="text-navy font-medium text-[0.95rem]" dir="rtl">
{hit.rule_statement}
</p>
<blockquote className="text-ink-soft text-sm border-r-2 border-gold pr-3" dir="rtl">
&ldquo;{hit.supporting_quote}&rdquo;
{hit.page_reference && <span className="text-ink-muted text-[0.72rem] ms-2">({hit.page_reference})</span>}
</blockquote>
{hit.subject_tags?.length > 0 && (
<div className="flex flex-wrap gap-1">
{hit.subject_tags.map((t) => (
<Badge key={t} variant="outline" className="text-[0.65rem] bg-surface">
{t}
</Badge>
))}
</div>
)}
</div>
);
}
function PassageCard({ hit }: { hit: Extract<SearchHit, { type: "passage" }> }) {
return (
<div className="rounded-lg border border-rule bg-surface p-4 space-y-2">
<div className="flex items-center gap-2 text-[0.78rem] text-ink-muted flex-wrap">
<Badge variant="outline" className="bg-rule-soft text-ink-muted">קטע</Badge>
<span className="font-mono" dir="ltr">{hit.case_number}</span>
{hit.court && <span>· {hit.court}</span>}
{hit.decision_date && <span>· {formatDate(hit.decision_date)}</span>}
<span className="text-[0.7rem]">· {hit.section_type}</span>
<span className="ms-auto tabular-nums">דירוג {hit.score.toFixed(2)}</span>
</div>
<p className="text-ink text-sm leading-relaxed" dir="rtl">
{hit.content.slice(0, 600)}
{hit.content.length > 600 && <span></span>}
</p>
</div>
);
}
export function LibrarySearchPanel() {
const [draft, setDraft] = useState("");
const [query, setQuery] = useState("");
const [practiceArea, setPracticeArea] = useState<PracticeArea>("");
const [precedentLevel, setPrecedentLevel] = useState("");
const [includeHalachot, setIncludeHalachot] = useState(true);
const { data, isFetching, error } = useLibrarySearch(query, {
practiceArea: practiceArea || undefined,
precedentLevel: precedentLevel || undefined,
includeHalachot,
limit: 20,
});
const onSubmit = (e: React.FormEvent) => {
e.preventDefault();
setQuery(draft.trim());
};
return (
<div className="space-y-4">
<form onSubmit={onSubmit} className="flex items-end gap-3 flex-wrap">
<div className="flex-1 min-w-[300px]">
<label className="text-[0.78rem] text-ink-muted">שאילתת חיפוש</label>
<Input value={draft} onChange={(e) => setDraft(e.target.value)}
placeholder="השבחה אובייקטיבית" dir="rtl" />
</div>
<div className="min-w-[180px]">
<label className="text-[0.78rem] text-ink-muted">תחום</label>
<Select value={practiceArea || "_all"}
onValueChange={(v) => setPracticeArea(v === "_all" ? "" : v as PracticeArea)}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="_all">הכל</SelectItem>
{PRACTICE_AREAS.map((a) => (
<SelectItem key={a.value} value={a.value}>{a.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="min-w-[170px]">
<label className="text-[0.78rem] text-ink-muted">רמת תקדים</label>
<Select value={precedentLevel || "_all"}
onValueChange={(v) => setPrecedentLevel(v === "_all" ? "" : v)}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="_all">הכל</SelectItem>
{PRECEDENT_LEVELS.map((l) => (
<SelectItem key={l.value} value={l.value}>{l.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Button type="submit" className="bg-navy text-parchment hover:bg-navy-soft">
<Search className="w-4 h-4 me-1" />
חפש
</Button>
</form>
<label className="flex items-center gap-2 cursor-pointer text-sm text-ink-muted">
<input type="checkbox" checked={includeHalachot}
onChange={(e) => setIncludeHalachot(e.target.checked)} />
כלול הלכות (rule-level matches)
</label>
{!query.trim() ? (
<div className="text-center text-ink-muted py-12">
הקלד שאילתא כדי לחפש בקורפוס. החיפוש סמנטי לא טקסטואלי.
</div>
) : error ? (
<div className="rounded bg-danger-bg border border-danger/40 px-6 py-5 text-danger text-center">
{error.message}
</div>
) : isFetching ? (
<div className="space-y-3">
{[...Array(3)].map((_, i) => <Skeleton key={i} className="h-24 w-full" />)}
</div>
) : !data?.items.length ? (
<div className="text-center text-ink-muted py-12">
לא נמצאו תוצאות. נסה ניסוח אחר או הסר פילטרים.
</div>
) : (
<div className="space-y-3">
<p className="text-[0.78rem] text-ink-muted">
{data.count} תוצאות (הלכות מאושרות בלבד)
</p>
{data.items.map((hit, i) =>
hit.type === "halacha" ? (
<HalachaCard key={`h-${hit.halacha_id ?? i}`} hit={hit} />
) : (
<PassageCard key={`p-${hit.chunk_id ?? i}`} hit={hit} />
),
)}
</div>
)}
</div>
);
}