feat(precedents): formal citation per Israeli citation rules + copy/edit UI
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 3m25s

Until now, "case_number" was the only stored identifier for a precedent.
But a *citation per the Israeli unified citation rules* is a different
beast — it has bold parties, an unbold prefix (court abbrev + panel/
district parenthetical + case number), and an unbold trailing reporter
(נבו / פ"ד...).  Without storing it as a first-class field we couldn't
hand the chair a one-click "copy as citation" experience for pasting
into decisions.

Changes:
- Schema V19: case_law.citation_formatted TEXT (Markdown — parties
  wrapped in **…** so the copy helper can render <strong> for Word/Docs
  paste and keep plain-text fallback meaningful).
- Metadata extractor: composes citation_formatted from the document
  text per the unified citation rules, with worked examples for ע"א /
  עת"מ / ערר / בל"מ in the prompt. Refuses to store half-formed strings.
- PATCH /api/precedent-library/{id} accepts citation_formatted so the
  chair can correct LLM mistakes.
- /precedents/[id]: dedicated "מראה מקום" block with bold rendering,
  a copy-to-clipboard button (text/html + text/plain so Word keeps
  the bolds), and an inline edit textarea.
- /precedents list rows: link displays the formatted citation when
  available, with a small inline copy button — falls back to the bare
  case_number for older rows.

Backfill of existing rows happens by re-stamping the extraction queue
once V19 has rolled out and the new field is reachable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-27 07:14:34 +00:00
parent a02a4e3a64
commit cbc7a1e336
7 changed files with 369 additions and 13 deletions

View File

@@ -2,14 +2,24 @@
import { use, useState } from "react";
import Link from "next/link";
import { Pencil } from "lucide-react";
import { Pencil, Check, X } from "lucide-react";
import { toast } from "sonner";
import { AppShell } from "@/components/app-shell";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import { usePrecedent } from "@/lib/api/precedent-library";
import { Textarea } from "@/components/ui/textarea";
import {
usePrecedent,
useUpdatePrecedent,
type Precedent,
} from "@/lib/api/precedent-library";
import { PrecedentEditSheet } from "@/components/precedents/precedent-edit-sheet";
import {
FormattedCitation,
CitationCopyButton,
} from "@/components/precedents/formatted-citation";
import { ExtractedHalachotSection } from "@/components/precedents/extracted-halachot";
import { RelatedCasesSection } from "@/components/precedents/link-related-dialog";
@@ -34,6 +44,9 @@ export default function PrecedentDetailPage({
const { id } = use(params);
const [editing, setEditing] = useState(false);
const { data, isPending, error } = usePrecedent(id);
const update = useUpdatePrecedent();
const [editingCitation, setEditingCitation] = useState(false);
const [citationDraft, setCitationDraft] = useState("");
return (
<AppShell>
@@ -80,6 +93,36 @@ export default function PrecedentDetailPage({
</Button>
</div>
{/* Citation per Israeli unified citation rules. The LLM
extractor composes this from the document; the chair
can override below. */}
<CitationBlock
precedent={data as Precedent}
editing={editingCitation}
draft={citationDraft}
onStartEdit={() => {
setCitationDraft(data.citation_formatted ?? "");
setEditingCitation(true);
}}
onCancel={() => setEditingCitation(false)}
onChange={setCitationDraft}
onSave={async () => {
try {
await update.mutateAsync({
id,
patch: { citation_formatted: citationDraft.trim() },
});
toast.success("מראה מקום עודכן");
setEditingCitation(false);
} catch (e) {
toast.error(
e instanceof Error ? e.message : "שמירה נכשלה",
);
}
}}
saving={update.isPending}
/>
<div className="flex items-center gap-2 flex-wrap">
{data.practice_area ? (
<Badge variant="outline" className="text-[0.7rem]">
@@ -178,3 +221,109 @@ export default function PrecedentDetailPage({
</AppShell>
);
}
function CitationBlock({
precedent,
editing,
draft,
onStartEdit,
onCancel,
onChange,
onSave,
saving,
}: {
precedent: Precedent;
editing: boolean;
draft: string;
onStartEdit: () => void;
onCancel: () => void;
onChange: (v: string) => void;
onSave: () => void;
saving: boolean;
}) {
const citation = (precedent.citation_formatted ?? "").trim();
if (editing) {
return (
<div className="rounded-md border border-gold/40 bg-gold-wash/30 p-3 space-y-2">
<div className="flex items-center justify-between gap-2">
<span className="text-[0.78rem] font-semibold text-navy">
עריכת מראה מקום
</span>
<span className="text-[0.7rem] text-ink-muted">
הקף את שמות הצדדים בכפול-כוכבית <code className="font-mono">**שם**</code> להדגשה
</span>
</div>
<Textarea
value={draft}
onChange={(e) => onChange(e.target.value)}
rows={3}
dir="rtl"
className="font-mono text-sm"
placeholder='ערר (ועדות ערר ...) 1234/24 **עורר נ&apos; הוועדה המקומית** (נבו 1.2.2025)'
disabled={saving}
/>
<div className="flex items-center gap-2">
<Button
size="sm"
onClick={onSave}
disabled={saving || !draft.trim()}
className="bg-navy text-parchment hover:bg-navy-soft"
>
<Check className="w-3.5 h-3.5 me-1" />
שמור
</Button>
<Button
size="sm"
variant="outline"
onClick={onCancel}
disabled={saving}
>
<X className="w-3.5 h-3.5 me-1" />
ביטול
</Button>
</div>
</div>
);
}
if (!citation) {
return (
<div className="rounded-md border border-dashed border-rule bg-rule-soft/30 p-3 flex items-center justify-between gap-2">
<span className="text-[0.78rem] text-ink-muted">
מראה מקום (כללי הציטוט האחיד) טרם חולץ
</span>
<Button size="sm" variant="outline" onClick={onStartEdit}>
<Pencil className="w-3.5 h-3.5 me-1" />
הוסף ידנית
</Button>
</div>
);
}
return (
<div className="rounded-md border border-rule bg-parchment-50 p-3 space-y-1.5">
<div className="flex items-center justify-between gap-2">
<span className="text-[0.7rem] uppercase tracking-wide text-ink-muted">
מראה מקום
</span>
<div className="flex items-center gap-1.5">
<CitationCopyButton citation={citation} size="xs" />
<button
type="button"
onClick={onStartEdit}
title="ערוך מראה מקום"
className="inline-flex items-center gap-1 rounded-md border border-rule bg-surface hover:bg-rule-soft/50 text-ink h-7 px-2 text-[0.72rem]"
>
<Pencil className="w-3.5 h-3.5" />
ערוך
</button>
</div>
</div>
<FormattedCitation
citation={citation}
className="block text-navy text-sm leading-relaxed"
/>
</div>
);
}