+ {/* documents (wider) + agent-activity preview (mockup 18j) */}
+
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) => (
+ setFilter(f.key)}
+ className={`text-[0.78rem] font-semibold rounded-full px-3.5 py-1.5 border transition-colors ${
+ filter === f.key
+ ? "bg-navy text-parchment border-transparent"
+ : "bg-surface text-ink-soft border-rule hover:bg-parchment"
+ }`}
+ >
+ {f.label}
+
+ ))}
+
+ רק לא-מאומתים
+ setOnlyUnverified(e.target.checked)}
+ className="accent-gold w-4 h-4"
+ />
+
+
+
+ {/* 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 ? (
+ <>
+
+ ✓ אומת ע״י היו״ר
+
+ runVerify(a, s, false)}
+ className="text-[0.72rem] text-ink-muted hover:text-danger border border-rule rounded-md px-2.5 py-1 disabled:opacity-40"
+ >
+ בטל אימות
+
+ >
+ ) : (
+ <>
+ runVerify(a, s, true)}
+ className="text-[0.72rem] font-semibold text-success bg-success-bg border border-success/30 rounded-md px-3 py-1 hover:brightness-95 disabled:opacity-40"
+ >
+ ✓ מאמת
+
+ runVerify(a, s, false)}
+ className="text-[0.72rem] font-semibold text-danger border border-rule rounded-md px-3 py-1 hover:bg-danger-bg disabled:opacity-40"
+ >
+ ✗ לא רלוונטי
+
+ >
+ )}
+
+
+ {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"
+ />
+
+ ) : (
runVerify(a, s, false)}
- className="text-xs text-ink-muted hover:text-danger border border-rule rounded-md px-2.5 py-1 disabled:opacity-40"
+ onClick={() => toggleSet(setOpenNotes, s.case_law_id)}
+ className="mt-2 text-[0.72rem] text-gold-deep font-semibold hover:underline"
>
- בטל אימות
+ + הוסף הערת יו״ר
- >
- ) : (
- <>
-
runVerify(a, s, true)}
- className="text-xs font-semibold text-success bg-success-bg border border-success/30 rounded-md px-3 py-1 hover:brightness-95 disabled:opacity-40"
- >
- ✓ מאמת
-
-
runVerify(a, s, false)}
- className="text-xs font-semibold text-danger border border-rule rounded-md px-3 py-1 hover:bg-danger-bg disabled:opacity-40"
- >
- ✗ לא רלוונטי
-
- >
- )}
- {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 (
-
+
📡 רדאר — פסיקה שאין לנו בקורפוס (מצביע, לא מצוטט)