@@ -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<string, string> = {
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 < Record < string , string > > ( { } ) ;
const [ openNotes , setOpenNotes ] = useState < Set < string > > ( new Set ( ) ) ;
const [ expandedQuotes , setExpandedQuotes ] = useState < Set < string > > ( new Set ( ) ) ;
const [ filter , setFilter ] = useState < FilterKey > ( "all" ) ;
const [ onlyUnverified , setOnlyUnverified ] = useState ( false ) ;
function toggleSet (
setter : React.Dispatch < React.SetStateAction < Set < string > > > ,
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,76 +145,178 @@ 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 (
< div className = "space-y-4" >
{ /* summary */ }
< div className = "flex flex-wrap items-center gap-x-5 gap-y-1 text-sm text-ink-soft bg-parchment border border-rule rounded-lg px-4 py-2.5 " >
< span > ס ו ג י ו ת ע ם פ ס י ק ה : < b className = "text-navy tabular-nums" > { sum . arguments_with_support } / { sum . arguments_total } < / b > < / span >
< span > א ו מ ת ו : < b className = "text-success tabular-nums" > { sum . verified } < / b > < / span >
< span > ל י ד ֵ י ר ד א ר : < b className = "text-warn tabular-nums" > { sum . radar_leads } < / b > < / span >
< span className = "text-ink-muted text-[0.78rem] ms-auto" > ה - writer מ צ ט ט ר ק מ א ו מ ת י ם ( INV - AH ) · ה י ו מ ו ן א י נ ו מ צ ו ט ט ( INV - DIG1 ) < / span >
{ /* summary + progress */ }
< div className = "rounded-xl border border-rule bg-surface shadow-sm px-5 py-4 " >
< div className = "flex items-center gap-6 flex-wrap" >
< Stat k = "סוגיות עם פסיקה" v = { ` ${ sum . arguments_with_support } / ${ sum . arguments_total } ` } / >
< Stat k = "אומתו" v = { String ( sum . verified ) } tone = "text-success" / >
< Stat k = "לידֵי רדאר" v = { String ( sum . radar_leads ) } tone = "text-warn" / >
< span className = "ms-auto text-[0.72rem] text-ink-muted max-w-[280px] leading-snug" >
ה - writer מ צ ט ט ר ק מ א ו מ ת י ם ( INV - AH ) · ה י ו מ ו ן א י נ ו מ צ ו ט ט ( INV - DIG1 )
< / span >
< / div >
< div className = "mt-3.5" >
< div className = "h-2 rounded-full bg-rule-soft overflow-hidden flex" >
< span className = "bg-success h-full" style = { { width : ` ${ okPct } % ` } } / >
< span className = "bg-gold-soft h-full" style = { { width : ` ${ supPct } % ` } } / >
< / div >
< div className = "flex gap-4 mt-1.5 text-[0.7rem] text-ink-muted flex-wrap" >
< span > < Dot className = "bg-success" / > < b className = "text-ink-soft" > { verifiedSup } מ א ו מ ת י ם < / b > < / span >
< span > < Dot className = "bg-gold-soft" / > < b className = "text-ink-soft" > { pendingSup } מ מ ת י נ י ם ל א י מ ו ת < / b > < / span >
< span > < Dot className = "bg-rule-soft" / > { noSupportCount } ס ו ג י ו ת ל ל א פ ס י ק ה ת ו מ כ ת < / span >
< / div >
< / div >
< / div >
{ data . arguments . map ( ( a ) = > (
< Card key = { a . argument_id } className = "bg-surface border-rule shadow-sm overflow-hidden ">
< CardContent className = "p-0" >
{ /* issue header */ }
< div className = "flex items-start gap-3 px-5 py-3.5 border-b border-rule-soft bg-parchment/60" >
< div className = "min-w-0 flex-1" >
< div className = "text-navy font-bold text-[0.98rem]" > { a . title } < / div >
< div className = "text-ink-muted text-xs mt-0.5" >
{ /* filters */ }
< div className = "flex items-center gap-2 flex-wrap ">
{ FILTERS . map ( ( f ) = > (
< button
key = { f . key }
type = "button"
onClick = { ( ) = > 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 }
< / button >
) ) }
< label className = "ms-auto flex items-center gap-2 text-[0.78rem] text-ink-soft font-medium cursor-pointer select-none" >
ר ק ל א - מ א ו מ ת י ם
< input
type = "checkbox"
checked = { onlyUnverified }
onChange = { ( e ) = > setOnlyUnverified ( e . target . checked ) }
className = "accent-gold w-4 h-4"
/ >
< / label >
< / div >
{ /* arguments */ }
< Accordion type = "multiple" defaultValue = { defaultOpen } className = "space-y-2.5" >
{ 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 (
< AccordionItem
key = { a . argument_id }
value = { a . argument_id }
className = "border border-rule rounded-xl bg-surface shadow-sm overflow-hidden data-[state=open]:border-gold-soft"
>
< AccordionTrigger className = "px-4 py-3 hover:no-underline gap-3" >
< span className = "flex flex-1 items-center gap-3 min-w-0" >
< span className = "flex flex-col items-start min-w-0 text-start" >
< span className = "text-navy font-bold text-[0.92rem] truncate max-w-full" >
{ a . title }
< / span >
< span className = "text-ink-muted text-[0.72rem]" >
ס ו ג י ה { a . legal_topic ? ` · ${ a . legal_topic } ` : "" }
< / div >
< / div >
< / span >
< / span >
{ a . priority && (
< span className = "shrink-0 rounded-full bg-gold-wash text-gold-deep text-[0.68 rem] font-semibold px-2.5 py-0.5" >
< span className = "shrink-0 rounded-full bg-gold-wash text-gold-deep text-[0.66 rem] font-semibold px-2.5 py-0.5" >
{ PRIORITY_LABEL [ a . priority ] ? ? a . priority }
< / span >
) }
< span className = { ` shrink-0 rounded-full text-[0.7rem] font-bold px-2.5 py-0.5 tabular-nums ${ tone } ` } >
{ v } / { t } ✓
< / span >
< / span >
< / AccordionTrigger >
< AccordionContent className = "px-4 pb-3 pt-0" >
< div className = "text-[0.72rem] font-bold text-ink-muted mb-1" >
פ ס י ק ה ת ו מ כ ת ב ק ו ר פ ו ס
< / div >
{ /* supporting precedents */ }
< div className = "px-5 py-3" >
< div className = "text-[0.72rem] font-bold text-ink-muted mb-1" > פ ס י ק ה ת ו מ כ ת ב ק ו ר פ ו ס < / div >
{ a . supporting . length === 0 ? (
< p className = "text-ink-muted text-sm py-2" > ל א נ מ צ א ה פ ס י ק ה ת ו מ כ ת ב ק ו ר פ ו ס ל ס ו ג י ה ז ו . < / p >
< p className = "text-ink-muted text-sm py-2" >
ל א נ מ צ א ה פ ס י ק ה ת ו מ כ ת ב ק ו ר פ ו ס ל ס ו ג י ה ז ו .
< / p >
) : supShown . length === 0 ? (
< p className = "text-ink-muted text-sm py-2" >
כ ל ה פ ס י ק ה ל ס ו ג י ה ז ו א ו מ ת ה .
< / p >
) : (
< ul className = "list-none p-0 m-0 divide-y divide-rule-soft " >
{ a . supporting . map ( ( s ) = > (
< li key = { s . case_law_id } className = "py-3 first:pt-1" >
< ul className = "list-none p-0 m-0" >
{ supShown . map ( ( s ) = > {
const noteOpen =
openNotes . has ( s . case_law_id ) || ! ! s . chair_note ;
const quoteExpanded = expandedQuotes . has ( s . case_law_id ) ;
return (
< li
key = { s . case_law_id }
className = "py-3 border-t border-rule-soft first:border-t-0"
>
< div className = "flex items-center gap-2 flex-wrap" >
< a
href = { ` /precedents/ ${ s . case_law_id } ` }
className = "font-bold text-gold-deep text-[0.86 rem] hover:text-navy"
className = "font-bold text-gold-deep text-[0.85 rem] hover:text-navy"
>
{ s . case_number || s . case_name }
< / a >
{ s . cited_by . positive > 0 && (
< span className = "rounded-full bg-success-bg text-success text-[0.68 rem] font-semibold px-2 py-0.5" >
< span className = "rounded-full bg-success-bg text-success text-[0.66 rem] font-semibold px-2 py-0.5" >
א ו מ ץ × { s . cited_by . positive }
< / span >
) }
{ s . cited_by . negative > 0 && (
< span className = "rounded-full bg-danger-bg text-danger text-[0.68 rem] font-semibold px-2 py-0.5" >
< span className = "rounded-full bg-danger-bg text-danger text-[0.66 rem] font-semibold px-2 py-0.5" >
א ו ב ח ן × { s . cited_by . negative }
< / span >
) }
< / div >
{ s . quote && (
< blockquote className = "border-s-[3px] border-gold bg-gold-wash text-ink-soft text-sm leading-7 rounded-e-md px-3.5 py-2 my-2 mx-0" >
{ s . quote }
< / blockquote >
) }
< div className = "flex items-center gap-2 flex-wrap" >
< span className = "ms-auto flex items-center gap-1.5" >
{ s . verified ? (
< >
< span className = "text-success text-xs font-semibold" > ✓ א ו מ ת ע ״ י ה י ו ״ ר < / span >
< span className = "text-success text-[0.72rem] font-semibold" >
✓ א ו מ ת ע ״ י ה י ו ״ ר
< / span >
< button
type = "button"
disabled = { verify . isPending }
onClick = { ( ) = > 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"
className = "text-[0.72rem] text-ink-muted hover:text-danger border border-rule rounded-md px-2.5 py-1 disabled:opacity-40"
>
ב ט ל א י מ ו ת
< / button >
@@ -191,7 +327,7 @@ export function CitationVerificationPanel({ caseNumber }: { caseNumber: string }
type = "button"
disabled = { verify . isPending }
onClick = { ( ) = > 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"
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"
>
✓ מ א מ ת
< / button >
@@ -199,49 +335,84 @@ export function CitationVerificationPanel({ caseNumber }: { caseNumber: string }
type = "button"
disabled = { verify . isPending }
onClick = { ( ) = > 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"
className = "text-[0.72rem] font-semibold text-danger border border-rule rounded-md px-3 py-1 hover:bg-danger-bg disabled:opacity-40"
>
✗ ל א ר ל ו ו נ ט י
< / button >
< / >
) }
{ s . cited_by . negative > 0 && ! s . verified && (
< span className = "text-warn text-[0.72rem] ms-1" > ⚠ א ו ב ח ן — ב ד ו ק ה ק ש ר < / span >
) }
< / span >
< / div >
{ /* chair note */ }
< div className = "mt-2 flex items-start gap-2" >
< label className = "text-[0.72rem] font-bold text-gold-deep whitespace-nowrap pt-1.5" >
{ s . quote && (
< blockquote
onClick = { ( ) = > 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 }
< / blockquote >
) }
{ s . cited_by . negative > 0 && ! s . verified && (
< div className = "text-warn text-[0.72rem] mt-1.5" > ⚠ א ו ב ח ן — ב ד ו ק ה ק ש ר < / div >
) }
{ noteOpen ? (
< div className = "mt-2 flex items-center gap-2" >
< label className = "text-[0.72rem] font-bold text-gold-deep whitespace-nowrap" >
ה ע ר ת י ו ״ ר :
< / label >
< input
type = "text"
defaultValue = { s . chair_note }
placeholder = "הוסף הערה לציטוט — מדוע תומך / הסתייגות / איך לשלב…"
onChange = { ( e ) = > setNotes ( ( n ) = > ( { . . . n , [ s . case_law_id ] : e . target . value } ) ) }
onChange = { ( e ) = >
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"
/ >
< / div >
) : (
< button
type = "button"
onClick = { ( ) = > toggleSet ( setOpenNotes , s . case_law_id ) }
className = "mt-2 text-[0.72rem] text-gold-deep font-semibold hover:underline"
>
+ ה ו ס ף ה ע ר ת י ו ״ ר
< / button >
) }
< / li >
) ) }
) ;
} ) }
< / ul >
) }
< / div >
{ /* radar — unlinked digests for this issue (INV-DIG1: pointer, not citation) */ }
< RadarStrip radar = { a . radar } / >
< / Card Content>
< / Card >
) ) }
< / Accordion Content>
< / AccordionItem >
) ;
} ) }
< / Accordion >
< / div >
) ;
}
function Stat ( { k , v , tone } : { k : string ; v : string ; tone? : string } ) {
return (
< span className = "flex flex-col" >
< span className = "text-[0.72rem] text-ink-muted" > { k } < / span >
< span className = { ` text-lg font-bold tabular-nums mt-0.5 ${ tone ? ? "text-navy" } ` } > { v } < / span >
< / span >
) ;
}
function Dot ( { className } : { className : string } ) {
return < span className = { ` inline-block w-2 h-2 rounded-sm me-1 align-middle ${ className } ` } / > ;
}
function RadarStrip ( { radar } : { radar : RadarLead [ ] } ) {
if ( ! radar . length ) return null ;
return (
< div className = "mx-5 mb-4 rounded-lg border border-dashed border-gold bg-parchment px-4 py-2.5" >
< div className = "mt-3 rounded-lg border border-dashed border-gold bg-parchment px-4 py-2.5" >
< div className = "text-[0.72rem] font-bold text-gold-deep mb-1" >
📡 ר ד א ר — פ ס י ק ה ש א י ן ל נ ו ב ק ו ר פ ו ס ( מ צ ב י ע , ל א מ צ ו ט ט )
< / div >