"use client"; import { useState } from "react"; import Link from "next/link"; import { Trash2, Plus, Pencil, Wand2, Loader2 } from "lucide-react"; import { toast } from "sonner"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; import { Progress } from "@/components/ui/progress"; import { Skeleton } from "@/components/ui/skeleton"; import { cn } from "@/lib/utils"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { usePrecedents, useDeletePrecedent, useRequestHalachotExtraction, isPrecedentActive, type Precedent, type PracticeArea, } from "@/lib/api/precedent-library"; import { PRACTICE_AREAS, practiceAreaShort } from "./practice-area"; import { PrecedentUploadSheet } from "./precedent-upload-sheet"; import { PrecedentEditSheet } from "./precedent-edit-sheet"; import { FormattedCitation, CitationCopyButton, } from "./formatted-citation"; function formatDate(iso: string | null) { if (!iso) return "—"; try { return new Date(iso).toLocaleDateString("he-IL"); } catch { return iso; } } function cleanCitation(s: string | null | undefined): string { if (!s) return "—"; return s.replace(/[‎‏‪-‮⁦-⁩]/g, "").trim(); } // Show the "extract halachot" button only when the precedent hasn't had a // successful (or even attempted) extraction yet. Hide while processing or // after completion to avoid duplicate requests. function needsHalachaExtraction(p: Precedent): boolean { if (p.extraction_status !== "completed") return false; // text not ready if (p.halacha_extraction_status === "processing") return false; if (p.halacha_extraction_status === "completed") return false; if (p.halacha_extraction_status === "pending" && p.halacha_extraction_requested_at) { return false; // already queued } // Remaining cases: pending+no-requested_at (never tried) or failed (retry). return true; } function ActivePill({ label }: { label: string }) { return ( {label} ); } function StatusPill({ p }: { p: Precedent }) { if (p.extraction_status === "failed") { return ( נכשל ); } if (p.extraction_status === "processing") { return ; } if (p.extraction_status !== "completed") { return ( בתור ); } // Metadata extraction (orthogonal one-off re-extract). Surface it while // active so the chair sees the queued button actually ran on something. if (p.metadata_extraction_status === "processing") { return ; } if (p.metadata_extraction_status === "failed") { return ( מטא-דאטה נכשל ); } if ( p.metadata_extraction_requested_at && p.metadata_extraction_status !== "completed" ) { return ( ממתין למטא-דאטה ); } if (p.halacha_extraction_status === "processing") { return ; } if (p.halacha_extraction_status === "failed") { return ( חילוץ נכשל ); } if (p.halacha_extraction_status === "pending") { if (p.halacha_extraction_requested_at) { return ( ממתין לחילוץ ); } return ( לא חולץ ); } if (p.halachot_count === 0) { return ללא הלכות; } return ( {p.approved_count}/{p.halachot_count} מאושרות ); } function CourtRow({ p, onEdit }: { p: Precedent; onEdit: (id: string) => void }) { const del = useDeletePrecedent(); const reqHalachot = useRequestHalachotExtraction(); const active = isPrecedentActive(p); const showExtractHalachot = needsHalachaExtraction(p); const onDelete = async () => { if (active) { toast.error("מתבצע עיבוד — לא ניתן למחוק עכשיו."); return; } if (!window.confirm(`למחוק את ${p.case_number}?`)) return; try { await del.mutateAsync(p.id); toast.success("נמחק"); } catch (e) { toast.error(e instanceof Error ? e.message : "שגיאה"); } }; const onExtractHalachot = async () => { try { await reqHalachot.mutateAsync(p.id); toast.success("סומן לחילוץ הלכות. הריצי מ-Claude Code: precedent_process_pending_halachot"); } catch (e) { toast.error(e instanceof Error ? e.message : "שגיאה"); } }; return (
{p.citation_formatted ? ( ) : ( cleanCitation(p.case_number) )} {p.citation_formatted ? ( ) : null}
{/* Column "שם / ערכאה" hidden by request (case_name often equals case_number prefix). Keep field in DB; restore by un-hiding. */}
{cleanCitation(p.case_name)}
{p.court ?
{p.court}
: null}
{p.date ? formatDate(p.date) : } {p.practice_area ? ( {practiceAreaShort(p.practice_area)} ) : } {p.precedent_level || }
{showExtractHalachot && ( )}
); } function CommitteeRow({ p, onEdit }: { p: Precedent; onEdit: (id: string) => void }) { const del = useDeletePrecedent(); const reqHalachot = useRequestHalachotExtraction(); const active = isPrecedentActive(p); const showExtractHalachot = needsHalachaExtraction(p); const onDelete = async () => { if (active) { toast.error("מתבצע עיבוד — לא ניתן למחוק עכשיו."); return; } if (!window.confirm(`למחוק את ${p.case_number}?`)) return; try { await del.mutateAsync(p.id); toast.success("נמחק"); } catch (e) { toast.error(e instanceof Error ? e.message : "שגיאה"); } }; const onExtractHalachot = async () => { try { await reqHalachot.mutateAsync(p.id); toast.success("סומן לחילוץ הלכות. הריצי מ-Claude Code: precedent_process_pending_halachot"); } catch (e) { toast.error(e instanceof Error ? e.message : "שגיאה"); } }; return (
{p.citation_formatted ? ( ) : ( cleanCitation(p.case_number) )} {p.citation_formatted ? ( ) : null}
{/* Column "שם" hidden by request (case_name often equals case_number prefix). Keep field in DB; restore by un-hiding. */}
{cleanCitation(p.case_name)}
{p.district || } {p.chair_name || } {p.date ? formatDate(p.date) : } {p.practice_area ? ( {practiceAreaShort(p.practice_area)} ) : }
{showExtractHalachot && ( )}
); } /** * Live banner for the metadata-extraction queue. The local-MCP worker * (`precedent_process_pending kind=metadata`) drains stamped rows serially; * while it runs, rows carry metadata_extraction_status='processing' (current * item) or a request timestamp (still queued). We surface a real, advancing * percentage by tracking the session high-water-mark of the queue depth: * percent = (peak - remaining) / peak. Hidden when the queue is empty. */ function MetadataQueueBanner({ items }: { items: Precedent[] }) { const processing = items.filter( (p) => p.metadata_extraction_status === "processing", ).length; const pending = items.filter( (p) => p.metadata_extraction_status !== "processing" && p.metadata_extraction_requested_at !== null && p.metadata_extraction_status !== "completed" && p.metadata_extraction_status !== "failed", ).length; const remaining = processing + pending; // Session high-water-mark of the queue depth, using React's documented // "adjust state during render" pattern (no effect, no ref-in-render). React // re-renders immediately without committing the intermediate UI. Resets to 0 // when the queue drains, so the next batch starts a fresh 0→100% sweep. const [peak, setPeak] = useState(0); if (remaining === 0) { if (peak !== 0) setPeak(0); } else if (remaining > peak) { setPeak(remaining); } if (remaining === 0) return null; const done = Math.max(0, peak - remaining); const percent = peak > 0 ? Math.round((done / peak) * 100) : 0; return (
חילוץ מטא-דאטה פעיל — מעבד {processing}, נותרו {pending} בתור {peak > 0 ? ` · ${done}/${peak}` : ""}
); } function SegButton({ active, onClick, label, count, }: { active: boolean; onClick: () => void; label: string; count?: number; }) { return ( ); } function TableSkeleton({ cols }: { cols: number }) { return ( <> {[...Array(4)].map((_, i) => ( {[...Array(cols)].map((_, j) => ( ))} ))} ); } export function LibraryListPanel() { const [practiceArea, setPracticeArea] = useState(""); const [search, setSearch] = useState(""); const [uploadOpen, setUploadOpen] = useState(false); const [editingId, setEditingId] = useState(null); // Which of the two corpora to show. Both queries stay mounted (counts + // cross-table polling), but only the active table renders — no long scroll. // Default to committee decisions (the chair's primary corpus). const [view, setView] = useState<"court" | "committee">("committee"); const sharedFilters = { practiceArea: practiceArea || undefined, search: search.trim() || undefined, limit: 200, }; const courts = usePrecedents({ ...sharedFilters, sourceKind: "external_upload", sourceType: "court_ruling" }); const committee = usePrecedents({ ...sharedFilters, sourceKind: "all_committees" }); // Metadata-queue banner draws on both corpora (a stamped row can be either). const allItems = [ ...(courts.data?.items ?? []), ...(committee.data?.items ?? []), ]; return (
{/* Shared filters */}
setSearch(e.target.value)} placeholder="עע״מ 3975/22 / 1200-25" dir="rtl" />
{/* Metadata-extraction queue progress (hidden when idle) */} {/* Segmented control — show one corpus at a time */}
setView("court")} label="פסיקת בתי משפט" count={courts.data?.count} /> setView("committee")} label="החלטות ועדות ערר" count={committee.data?.count} />
{/* Table 1 — Court rulings */} {view === "court" && (
{courts.error ? (
{courts.error.message}
) : (
מס׳ / מראה מקום {/* "שם / ערכאה" hidden by request — see CourtRow */} שם / ערכאה תאריך תחום רמה הלכות {courts.isPending ? ( ) : !courts.data?.items.length ? ( אין פסיקת בתי משפט בקורפוס. ) : ( courts.data.items.map((p) => ( )) )}
)}
)} {/* Table 2 — Appeals committee decisions */} {view === "committee" && (
{committee.error ? (
{committee.error.message}
) : (
מספר ערר {/* "שם" hidden by request — see CommitteeRow */} שם מחוז יו״ר תאריך תחום הלכות {committee.isPending ? ( ) : !committee.data?.items.length ? ( אין החלטות ועדת ערר בקורפוס. ) : ( committee.data.items.map((p) => ( )) )}
)}
)} { if (!open) setEditingId(null); }} />
); }