diff --git a/web-ui/src/components/compose/citation-verification-panel.tsx b/web-ui/src/components/compose/citation-verification-panel.tsx index 8da64f4..969e79b 100644 --- a/web-ui/src/components/compose/citation-verification-panel.tsx +++ b/web-ui/src/components/compose/citation-verification-panel.tsx @@ -5,12 +5,22 @@ * 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, @@ -33,10 +43,34 @@ const RADAR_LABEL: Record = { 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 ( @@ -111,137 +145,274 @@ export function CitationVerificationPanel({ caseNumber }: { caseNumber: string } } 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 */} -
- סוגיות עם פסיקה: {sum.arguments_with_support}/{sum.arguments_total} - אומתו: {sum.verified} - לידֵי רדאר: {sum.radar_leads} - ה-writer מצטט רק מאומתים (INV-AH) · היומון אינו מצוטט (INV-DIG1) + {/* summary + progress */} +
+
+ + + + + ה-writer מצטט רק מאומתים (INV-AH) · היומון אינו מצוטט (INV-DIG1) + +
+
+
+ + +
+
+ {verifiedSup} מאומתים + {pendingSup} ממתינים לאימות + {noSupportCount} סוגיות ללא פסיקה תומכת +
+
- {data.arguments.map((a) => ( - - - {/* issue header */} -
-
-
{a.title}
-
- סוגיה{a.legal_topic ? ` · ${a.legal_topic}` : ""} -
-
- {a.priority && ( - - {PRIORITY_LABEL[a.priority] ?? a.priority} + {/* 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} ✓ + - )} -
- - {/* supporting precedents */} -
-
פסיקה תומכת בקורפוס
- {a.supporting.length === 0 ? ( -

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

- ) : ( -
    - {a.supporting.map((s) => ( -
  • - - {s.quote && ( -
    - {s.quote} -
    - )} -
    - {s.verified ? ( - <> - ✓ אומת ע״י היו״ר +
    + + {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" + /> +
    + ) : ( - - ) : ( - <> - - - - )} - {s.cited_by.negative > 0 && !s.verified && ( - ⚠ אובחן — בדוק הקשר - )} -
    - {/* chair note */} -
    - - 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" - /> -
    -
  • - ))} -
- )} -
+ )} + + ); + })} + + )} - {/* radar — unlinked digests for this issue (INV-DIG1: pointer, not citation) */} - -
-
- ))} + + + + ); + })} +
); } +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 ( -
+
📡 רדאר — פסיקה שאין לנו בקורפוס (מצביע, לא מצוטט)