From 979ec17a45346256ee4e336af6ff9f07ecf36126 Mon Sep 17 00:00:00 2001 From: Chaim Date: Sat, 20 Jun 2026 12:53:08 +0000 Subject: [PATCH] feat(missing-precedents): accordion grouping by discovery source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chair-approved (Claude Design card 09): split the flat table into collapsible sections by where the gap surfaced, so the high-value "cited by Dafna" rulings aren't buried among the 330 digest rows. - Three
accordion sections within the active status tab: · "צוטט ע״י דפנה" — rows with a resolved chair from the citation-graph bridge (cited_by_chairs non-empty); open by default. · "יומון" — discovery_source='digest'; collapsed. · "אחר" — party-cited / manual remainder; collapsed. - Row JSX extracted to renderRow() and reused across sections; shared TableHeaderRow(); grouping via useMemo over the current result set. - Empty sections are hidden; section header shows source chip + count. No backend change — groups on existing cited_by_chairs / discovery_source. tsc --noEmit clean. Invariants: INV-IA* (task-oriented surface, single gate) — UI only. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../missing-precedents-table.tsx | 492 +++++++++++------- 1 file changed, 291 insertions(+), 201 deletions(-) diff --git a/web-ui/src/components/missing-precedents/missing-precedents-table.tsx b/web-ui/src/components/missing-precedents/missing-precedents-table.tsx index eefe116..a04d4dc 100644 --- a/web-ui/src/components/missing-precedents/missing-precedents-table.tsx +++ b/web-ui/src/components/missing-precedents/missing-precedents-table.tsx @@ -1,7 +1,7 @@ "use client"; -import { useState } from "react"; -import { Trash2, Upload, Pencil, ExternalLink } from "lucide-react"; +import { useMemo, useState } from "react"; +import { Trash2, Upload, Pencil, ExternalLink, ChevronDown } from "lucide-react"; import { toast } from "sonner"; import Link from "next/link"; import { @@ -63,6 +63,24 @@ function SourceChip({ party }: { party: CitedByParty | null }) { ); } +const COLS = 7; + +function TableHeaderRow() { + return ( + + + פסיקה + נושא + תיק + צוטט ע״י + סטטוס + נוצר + + + + ); +} + function TableSkeleton({ cols }: { cols: number }) { return ( <> @@ -79,6 +97,38 @@ function TableSkeleton({ cols }: { cols: number }) { ); } +/** Accordion section meta — groups rows by discovery source (chair's request). + * "צוטט ע״י דפנה" (corpus-decision reliance) is the high-value group and opens + * by default; the noisy יומון group and the misc "אחר" group start collapsed. */ +type SectionKey = "chair" | "digest" | "other"; +const SECTION_META: Record< + SectionKey, + { label: string; title: string; desc: string; chip: string; defaultOpen: boolean } +> = { + chair: { + label: "צוטט ע״י דפנה", + title: "פסיקה שהוועדה נסמכת עליה", + desc: "מצוטטת בהחלטות-הקורפוס (גרף-הציטוטים)", + chip: "bg-success-bg text-success", + defaultOpen: true, + }, + digest: { + label: "יומון", + title: "זוהתה ביומון יומי", + desc: "מצביע, טרם צוטט בהחלטה", + chip: "bg-teal-bg text-teal", + defaultOpen: false, + }, + other: { + label: "אחר", + title: "כתב-טענות / ידני", + desc: "צוטט בערר חי או נרשם ידנית", + chip: "bg-info-bg text-info", + defaultOpen: false, + }, +}; +const SECTION_ORDER: SectionKey[] = ["chair", "digest", "other"]; + type Props = { status?: MissingPrecedentStatus | ""; q?: string; @@ -108,6 +158,196 @@ export function MissingPrecedentsTable({ status, q, legalTopic }: Props) { } }; + /* Partition the current result set by discovery source so each accordion + * section renders only its own rows. A row "cited by the committee" (has a + * resolved chair from the citation-graph bridge) wins over its raw + * discovery_source — that's the group the chair cares about. */ + const groups = useMemo(() => { + const g: Record = { chair: [], digest: [], other: [] }; + for (const mp of data?.items ?? []) { + if (mp.cited_by_chairs?.length) g.chair.push(mp); + else if (mp.discovery_source === "digest") g.digest.push(mp); + else g.other.push(mp); + } + return g; + }, [data]); + + const renderRow = (mp: MissingPrecedent) => ( + setOpenId(mp.id)} + > + + {/* Single line when there's no distinct case_name — the old two-line + layout repeated the citation (name fell back to a truncation of the + same citation). Show the name row only when it adds information. */} + {mp.case_name && mp.case_name.trim() ? ( + <> +
+ {mp.case_name} +
+
+ {mp.citation} +
+ + ) : ( +
+ {mp.citation} +
+ )} +
+ + {mp.legal_topic || "—"} + + + {mp.cited_by_decisions?.length ? ( + /* committee decision(s) that cite this missing ruling + (corpus citation-graph bridge) */ + + + {mp.cited_by_decisions[0]} + + {mp.cited_by_decisions.length > 1 ? ( + + +{mp.cited_by_decisions.length - 1} + + ) : null} + + ) : mp.cited_in_case_number ? ( + e.stopPropagation()} + className="text-sm text-navy hover:text-gold-deep inline-flex items-center gap-1" + > + {mp.cited_in_case_number} + + + ) : ( + + )} + + + {mp.cited_by_chairs?.length ? ( + /* cited by a committee DECISION in the corpus → show the chair who + relied on it (e.g. דפנה תמיר). Bridge from + precedent_internal_citations; takes priority over the generic + discovery-source chips. */ + <> + + {mp.cited_by_chairs[0]} + + {mp.cited_by_chairs.length > 1 ? ( +
+ +{mp.cited_by_chairs.length - 1} יו״ר +
+ ) : null} + + ) : mp.discovery_source === "cited_only" ? ( + <> + + פסיקה בקורפוס + + {mp.cited_by_precedents?.length ? ( +
+ מצוטט ע״י: {mp.cited_by_precedents.join(", ")} +
+ ) : null} + + ) : mp.discovery_source === "digest" ? ( + <> + + יומון + + {mp.yomon_number ? ( +
+ מס׳ {mp.yomon_number} +
+ ) : null} + + ) : ( + <> + + {mp.cited_by_party_name ? ( +
+ {mp.cited_by_party_name} +
+ ) : null} + + )} +
+ + + {mp.linked_case_law_number ? ( +
+ ↳ {mp.linked_case_law_name || mp.linked_case_law_number} +
+ ) : null} +
+ + {formatDate(mp.created_at)} + + +
+ {mp.status === "open" ? ( + /* gold "העלה והשלם" CTA (mockup 09 `.btn`) */ + + ) : ( + /* passive "done" label for non-open rows */ + + {mp.status === "closed" ? "קושר" : STATUS_LABELS[mp.status]} ✓ + + )} + {mp.status !== "open" ? ( + + ) : null} + +
+
+
+ ); + if (error) { return (
@@ -116,211 +356,61 @@ export function MissingPrecedentsTable({ status, q, legalTopic }: Props) { ); } - return ( - <> + if (isPending) { + return (
- - - פסיקה - נושא - תיק - צוטט ע״י - סטטוס - נוצר - - - + - {isPending ? ( - - ) : !data?.items.length ? ( - - - אין פסיקות חסרות בקריטריונים הנוכחיים. - - - ) : ( - data.items.map((mp) => ( - setOpenId(mp.id)} - > - - {/* Single line when there's no distinct case_name — the old - two-line layout repeated the citation (name fell back to a - truncation of the same citation). Show the name row only - when it adds information. */} - {mp.case_name && mp.case_name.trim() ? ( - <> -
- {mp.case_name} -
-
- {mp.citation} -
- - ) : ( -
- {mp.citation} -
- )} -
- - {mp.legal_topic || "—"} - - - {mp.cited_by_decisions?.length ? ( - /* committee decision(s) that cite this missing ruling - (corpus citation-graph bridge) */ - - - {mp.cited_by_decisions[0]} - - {mp.cited_by_decisions.length > 1 ? ( - - +{mp.cited_by_decisions.length - 1} - - ) : null} - - ) : mp.cited_in_case_number ? ( - e.stopPropagation()} - className="text-sm text-navy hover:text-gold-deep inline-flex items-center gap-1" - > - {mp.cited_in_case_number} - - - ) : ( - - )} - - - {mp.cited_by_chairs?.length ? ( - /* cited by a committee DECISION in the corpus → show the - chair who relied on it (e.g. דפנה תמיר). Bridge from - precedent_internal_citations; takes priority over the - generic discovery-source chips. */ - <> - - {mp.cited_by_chairs[0]} - - {mp.cited_by_chairs.length > 1 ? ( -
- +{mp.cited_by_chairs.length - 1} יו״ר -
- ) : null} - - ) : mp.discovery_source === "cited_only" ? ( - <> - - פסיקה בקורפוס - - {mp.cited_by_precedents?.length ? ( -
- מצוטט ע״י: {mp.cited_by_precedents.join(", ")} -
- ) : null} - - ) : mp.discovery_source === "digest" ? ( - <> - - יומון - - {mp.yomon_number ? ( -
- מס׳ {mp.yomon_number} -
- ) : null} - - ) : ( - <> - - {mp.cited_by_party_name ? ( -
- {mp.cited_by_party_name} -
- ) : null} - - )} -
- - - {mp.linked_case_law_number ? ( -
- ↳ {mp.linked_case_law_name || mp.linked_case_law_number} -
- ) : null} -
- - {formatDate(mp.created_at)} - - -
- {mp.status === "open" ? ( - /* gold "העלה והשלם" CTA (mockup 09 `.btn`) */ - - ) : ( - /* passive "done" label for non-open rows */ - - {mp.status === "closed" ? "קושר" : STATUS_LABELS[mp.status]} ✓ - - )} - {mp.status !== "open" ? ( - - ) : null} - -
-
-
- )) - )} +
+ ); + } + + if (!data?.items.length) { + return ( +
+ אין פסיקות חסרות בקריטריונים הנוכחיים. +
+ ); + } + + return ( + <> +
+ {SECTION_ORDER.map((key) => { + const items = groups[key]; + if (!items.length) return null; + const meta = SECTION_META[key]; + return ( +
+ + + + {meta.label} + + {meta.title} + + — {meta.desc} + + + {items.length} + + + + + {items.map(renderRow)} +
+
+ ); + })} +