"use client"; /** * "אימות פסיקה" tab (X11 Phase 2 / #154) — per legal argument, the in-corpus * supporting precedents with the cumulative authority signal (cited_by), a verify * gate + chair note, and per-issue radar (unlinked digests). The writer cites only * verified quotes (INV-AH); the digest is never cited (INV-DIG1, radar only). * * Redesign (mockup 18g): summary + verification progress bar, status filters, * collapsible per-argument accordion with a verify-progress chip, and compact * precedent rows (clamped quote, collapsible chair note) — easier to triage. */ import { useState } from "react"; import { toast } from "sonner"; import { Card, CardContent } from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@/components/ui/accordion"; import { useCitationVerification, useVerifyCitation, type ArgumentBlock, type SupportingPrecedent, type RadarLead, } from "@/lib/api/citation-verification"; const PRIORITY_LABEL: Record = { threshold: "סף", substantive: "מהותית", procedural: "פרוצדורלית", relief: "סעד", }; const RADAR_LABEL: Record = { new_lead: "ליד חדש", gap_open: "בתור-חסרים", fetched: "נמשך", available_link: "בקורפוס — לקשר", }; type FilterKey = "all" | "pending" | "verified" | "radar"; const FILTERS: { key: FilterKey; label: string }[] = [ { key: "all", label: "הכל" }, { key: "pending", label: "ממתין לאימות" }, { key: "verified", label: "מאומת" }, { key: "radar", label: "עם רדאר" }, ]; export function CitationVerificationPanel({ caseNumber }: { caseNumber: string }) { const { data, isPending, isError } = useCitationVerification(caseNumber); const verify = useVerifyCitation(caseNumber); const [notes, setNotes] = useState>({}); const [openNotes, setOpenNotes] = useState>(new Set()); const [expandedQuotes, setExpandedQuotes] = useState>(new Set()); const [filter, setFilter] = useState("all"); const [onlyUnverified, setOnlyUnverified] = useState(false); function toggleSet( setter: React.Dispatch>>, id: string, ) { setter((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); } if (isPending) { return ( ); } if (isError || !data || data.status !== "ok") { return ( לא ניתן לטעון את אימות-הפסיקה כעת. ); } if (!data.arguments.length) { return (

אין עדיין סוגיות מזוקקות לתיק

הרץ את ניתוח-הטענות (aggregate) כדי שהמערכת תזהה סוגיות ותתאים להן פסיקה.

); } function runVerify(a: ArgumentBlock, s: SupportingPrecedent, verified: boolean) { verify.mutate( { argument_id: a.argument_id, case_law_id: s.case_law_id, quote: s.quote, citation: s.case_number || s.case_name, chair_note: notes[s.case_law_id] ?? s.chair_note, verified, attached_id: s.attached_id ?? "", }, { onSuccess: () => toast.success(verified ? "הציטוט אומת" : "סומן כלא-רלוונטי"), onError: (e: Error) => toast.error(`שגיאה: ${e.message}`), }, ); } function saveNote(a: ArgumentBlock, s: SupportingPrecedent) { const note = notes[s.case_law_id]; if (note === undefined || note === s.chair_note) return; verify.mutate( { argument_id: a.argument_id, case_law_id: s.case_law_id, quote: s.quote, citation: s.case_number || s.case_name, chair_note: note, verified: s.verified, attached_id: s.attached_id ?? "", }, { onSuccess: () => toast.success("הערת-יו״ר נשמרה"), onError: (e: Error) => toast.error(`שגיאה: ${e.message}`), }, ); } const sum = data.summary; const args = data.arguments; // verification progress across all supporting precedents let totalSup = 0; let verifiedSup = 0; for (const a of args) { totalSup += a.supporting.length; verifiedSup += a.supporting.filter((s) => s.verified).length; } const pendingSup = totalSup - verifiedSup; const okPct = totalSup ? Math.round((verifiedSup / totalSup) * 100) : 0; const supPct = totalSup ? Math.round((pendingSup / totalSup) * 100) : 0; const noSupportCount = sum.arguments_total - sum.arguments_with_support; const argVerified = (a: ArgumentBlock) => a.supporting.filter((s) => s.verified).length; const argHasPending = (a: ArgumentBlock) => a.supporting.some((s) => !s.verified); const shownArgs = args.filter((a) => { if (filter === "pending") return argHasPending(a); if (filter === "verified") return argVerified(a) > 0; if (filter === "radar") return a.radar.length > 0; return true; }); // default-open the actionable arguments (those with unverified support) const defaultOpen = args.filter(argHasPending).map((a) => a.argument_id); return (
{/* summary + progress */}
ה-writer מצטט רק מאומתים (INV-AH) · היומון אינו מצוטט (INV-DIG1)
{verifiedSup} מאומתים {pendingSup} ממתינים לאימות {noSupportCount} סוגיות ללא פסיקה תומכת
{/* filters */}
{FILTERS.map((f) => ( ))}
{/* arguments */} {shownArgs.map((a) => { const v = argVerified(a); const t = a.supporting.length; const tone = t === 0 || v === 0 ? "bg-rule-soft text-ink-muted" : v === t ? "bg-success-bg text-success" : "bg-warn-bg text-warn"; const supShown = onlyUnverified ? a.supporting.filter((s) => !s.verified) : a.supporting; return ( {a.title} סוגיה{a.legal_topic ? ` · ${a.legal_topic}` : ""} {a.priority && ( {PRIORITY_LABEL[a.priority] ?? a.priority} )} {v}/{t} ✓
פסיקה תומכת בקורפוס
{a.supporting.length === 0 ? (

לא נמצאה פסיקה תומכת בקורפוס לסוגיה זו.

) : supShown.length === 0 ? (

כל הפסיקה לסוגיה זו אומתה.

) : (
    {supShown.map((s) => { const noteOpen = openNotes.has(s.case_law_id) || !!s.chair_note; const quoteExpanded = expandedQuotes.has(s.case_law_id); return (
  • {s.case_number || s.case_name} {s.cited_by.positive > 0 && ( אומץ ×{s.cited_by.positive} )} {s.cited_by.negative > 0 && ( אובחן ×{s.cited_by.negative} )} {s.verified ? ( <> ✓ אומת ע״י היו״ר ) : ( <> )}
    {s.quote && (
    toggleSet(setExpandedQuotes, s.case_law_id)} title="לחצי להרחבה/כיווץ" className={`mt-2 cursor-pointer border-s-[3px] border-gold bg-gold-wash text-ink-soft text-[0.82rem] leading-6 rounded-e-md px-3 py-1.5 ${quoteExpanded ? "" : "line-clamp-2"}`} > {s.quote}
    )} {s.cited_by.negative > 0 && !s.verified && (
    ⚠ אובחן — בדוק הקשר
    )} {noteOpen ? (
    setNotes((n) => ({ ...n, [s.case_law_id]: e.target.value })) } onBlur={() => saveNote(a, s)} className="flex-1 text-[0.8rem] text-ink-soft bg-parchment border border-dashed border-rule rounded-md px-2.5 py-1.5 focus:outline-none focus:border-gold" />
    ) : ( )}
  • ); })}
)}
); })}
); } function Stat({ k, v, tone }: { k: string; v: string; tone?: string }) { return ( {k} {v} ); } function Dot({ className }: { className: string }) { return ; } function RadarStrip({ radar }: { radar: RadarLead[] }) { if (!radar.length) return null; return (
📡 רדאר — פסיקה שאין לנו בקורפוס (מצביע, לא מצוטט)
    {radar.map((r) => (
  • {r.underlying_citation || "(אין מראה-מקום)"} {RADAR_LABEL[r.action] ?? r.action} {r.headline}
  • ))}
); }