feat(precedents): surface auto-detected incoming citations in the "ציטוטים מקושרים" panel #317
@@ -404,7 +404,10 @@ async def list_citations_to_case_law(case_law_id: UUID) -> list[dict]:
|
||||
cl.case_number AS source_case_number,
|
||||
cl.case_name AS source_case_name,
|
||||
cl.chair_name AS source_chair_name,
|
||||
cl.district AS source_district
|
||||
cl.district AS source_district,
|
||||
cl.precedent_level AS source_precedent_level,
|
||||
cl.court AS source_court,
|
||||
cl.date AS source_date
|
||||
FROM precedent_internal_citations pic
|
||||
JOIN case_law cl ON cl.id = pic.source_case_law_id
|
||||
WHERE pic.cited_case_law_id = $1
|
||||
|
||||
@@ -411,6 +411,28 @@ async def get_precedent(case_law_id: UUID | str) -> dict | None:
|
||||
return None
|
||||
record["halachot"] = await db.list_halachot(case_law_id=case_law_id, limit=500)
|
||||
record["related_cases"] = await db.get_case_law_relations(case_law_id)
|
||||
# Auto-detected citation graph: which decisions cite THIS ruling (incoming
|
||||
# edges in precedent_internal_citations). Reuses the canonical query (G2) —
|
||||
# no parallel resolution path. Shaped to match the front-end RelatedCase row
|
||||
# so the existing "ציטוטים מקושרים" panel renders them with no visual change.
|
||||
from legal_mcp.services import citation_extractor as _ce
|
||||
raw = await _ce.list_citations_to_case_law(case_law_id)
|
||||
record["incoming_citations"] = [
|
||||
{
|
||||
"id": r["source_case_law_id"],
|
||||
"case_number": r.get("source_case_number") or r.get("cited_case_number") or "",
|
||||
"case_name": r.get("source_case_name") or "",
|
||||
"court": r.get("source_court") or "",
|
||||
"precedent_level": r.get("source_precedent_level") or "",
|
||||
"chair_name": r.get("source_chair_name") or "",
|
||||
"date": (
|
||||
r["source_date"].isoformat()
|
||||
if r.get("source_date") is not None else None
|
||||
),
|
||||
"confidence": r.get("confidence"),
|
||||
}
|
||||
for r in raw
|
||||
]
|
||||
return record
|
||||
|
||||
|
||||
|
||||
@@ -236,7 +236,11 @@ export default function PrecedentDetailPage({
|
||||
|
||||
{/* side rail — citations + corroboration */}
|
||||
<div className="space-y-4">
|
||||
<RelatedCasesSection caseId={id} related={data.related_cases ?? []} />
|
||||
<RelatedCasesSection
|
||||
caseId={id}
|
||||
related={data.related_cases ?? []}
|
||||
incoming={data.incoming_citations ?? []}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
useLinkRelatedCase,
|
||||
useUnlinkRelatedCase,
|
||||
RelatedCase,
|
||||
IncomingCitation,
|
||||
} from "@/lib/api/precedent-library";
|
||||
|
||||
const LEVEL_LABELS: Record<string, string> = {
|
||||
@@ -133,19 +134,28 @@ function LinkDialog({ caseId, currentRelated, open, onOpenChange }: DialogProps)
|
||||
type SectionProps = {
|
||||
caseId: string;
|
||||
related: RelatedCase[];
|
||||
/** Decisions that cite THIS ruling — auto-detected from the citation graph
|
||||
* (read-only). Optional so existing call-sites stay valid. */
|
||||
incoming?: IncomingCitation[];
|
||||
};
|
||||
|
||||
/* Rail-styled citations card (mockup 08 side rail). Renders linked related
|
||||
* decisions as a navy-headed card with arrow-prefixed rows; keeps the full
|
||||
* link/unlink logic. Used in the precedent-detail side rail. */
|
||||
export function RelatedCasesSection({ caseId, related }: SectionProps) {
|
||||
* link/unlink logic. Auto-detected incoming citations (decisions that cite this
|
||||
* ruling) render in the SAME row style, read-only (no unlink). Used in the
|
||||
* precedent-detail side rail. */
|
||||
export function RelatedCasesSection({ caseId, related, incoming = [] }: SectionProps) {
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
// De-dupe: a decision already linked manually shouldn't appear twice.
|
||||
const manualIds = new Set(related.map((r) => r.id));
|
||||
const autoCites = incoming.filter((c) => !manualIds.has(c.id));
|
||||
const total = related.length + autoCites.length;
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-rule bg-surface shadow-sm px-4 py-3.5 space-y-2.5">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h3 className="text-navy text-[0.92rem] font-semibold m-0">
|
||||
ציטוטים מקושרים{related.length > 0 ? ` (${related.length})` : ""}
|
||||
ציטוטים מקושרים{total > 0 ? ` (${total})` : ""}
|
||||
</h3>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -157,10 +167,46 @@ export function RelatedCasesSection({ caseId, related }: SectionProps) {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{related.length === 0 ? (
|
||||
{total === 0 ? (
|
||||
<p className="text-ink-muted text-[0.82rem] m-0">אין החלטות קשורות עדיין</p>
|
||||
) : (
|
||||
<ul className="list-none p-0 m-0">
|
||||
{autoCites.map((c) => (
|
||||
<li
|
||||
key={c.id}
|
||||
className="flex items-start gap-2 py-2 border-b border-rule-soft last:border-b-0"
|
||||
>
|
||||
<span className="text-gold font-bold leading-6 shrink-0" aria-hidden>←</span>
|
||||
<a
|
||||
href={`/precedents/${c.id}`}
|
||||
className="min-w-0 flex-1 group hover:opacity-90 transition-opacity"
|
||||
>
|
||||
<div className="text-[0.82rem] text-ink-soft leading-5 group-hover:text-navy">
|
||||
{c.case_name || c.case_number}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-0.5 flex-wrap">
|
||||
{c.precedent_level && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`text-[0.6rem] ${LEVEL_COLORS[c.precedent_level] ?? ""}`}
|
||||
>
|
||||
{LEVEL_LABELS[c.precedent_level] ?? c.precedent_level}
|
||||
</Badge>
|
||||
)}
|
||||
{(c.chair_name || c.court) && (
|
||||
<span className="text-[0.68rem] text-ink-muted truncate">
|
||||
{c.chair_name || c.court}
|
||||
</span>
|
||||
)}
|
||||
{c.date && (
|
||||
<span className="text-[0.68rem] text-ink-muted tabular-nums" dir="ltr">
|
||||
{c.date.slice(0, 10)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
{related.map((r) => (
|
||||
<li
|
||||
key={r.id}
|
||||
|
||||
@@ -143,10 +143,24 @@ export type RelatedCase = {
|
||||
relation_type: string;
|
||||
};
|
||||
|
||||
/** A decision that cites THIS ruling — auto-detected from the citation graph
|
||||
* (precedent_internal_citations incoming edges). Read-only (no unlink). */
|
||||
export type IncomingCitation = {
|
||||
id: string;
|
||||
case_number: string;
|
||||
case_name: string;
|
||||
court: string;
|
||||
precedent_level: string;
|
||||
chair_name: string;
|
||||
date: string | null;
|
||||
confidence: number | null;
|
||||
};
|
||||
|
||||
export type PrecedentDetail = Precedent & {
|
||||
full_text: string;
|
||||
halachot: Halacha[];
|
||||
related_cases: RelatedCase[];
|
||||
incoming_citations: IncomingCitation[];
|
||||
};
|
||||
|
||||
export type SearchHit =
|
||||
|
||||
Reference in New Issue
Block a user