feat(precedents): show extracted halachot in library edit sheet
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 35s

The "ספרייה" tab only exposed approved/total counts in a status pill;
to inspect the actual extracted halachot per case the chair had to use
the global "ממתין לאישור" tab, which only surfaces pending items, or
the MCP tool. Now the per-precedent edit sheet renders a read-only
roll-up of every halacha (approved + pending + rejected) with status
filter tabs and counts. Review actions intentionally stay in the
review tab to avoid duplicate approve/reject UX.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-04 05:24:25 +00:00
parent f6bb46dc4a
commit bd1fb61655

View File

@@ -1,11 +1,12 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { Save, Sparkles } from "lucide-react"; import { Save, Sparkles } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { import {
Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription,
} from "@/components/ui/sheet"; } from "@/components/ui/sheet";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
@@ -18,6 +19,7 @@ import {
usePrecedent, usePrecedent,
useUpdatePrecedent, useUpdatePrecedent,
useRequestMetadataExtraction, useRequestMetadataExtraction,
type Halacha,
type PracticeArea, type PracticeArea,
type SourceType, type SourceType,
} from "@/lib/api/precedent-library"; } from "@/lib/api/precedent-library";
@@ -25,6 +27,200 @@ import {
PRACTICE_AREAS, PRECEDENT_LEVELS, SOURCE_TYPES, PRACTICE_AREAS, PRECEDENT_LEVELS, SOURCE_TYPES,
} from "./practice-area"; } from "./practice-area";
const RULE_TYPE_LABELS: Record<string, string> = {
binding: "הלכה מחייבת",
interpretive: "פרשני",
procedural: "פרוצדורלי",
obiter: "אמרת אגב",
application: "יישום הלכה",
persuasive: "משכנע",
};
type StatusFilter = "all" | "approved" | "pending" | "rejected";
function ReviewStatusPill({ status }: { status: Halacha["review_status"] }) {
if (status === "approved" || status === "published") {
return (
<Badge
variant="outline"
className="text-[0.65rem] bg-gold-wash text-gold-deep border-gold/40"
>
מאושרת
</Badge>
);
}
if (status === "pending_review") {
return (
<Badge
variant="outline"
className="text-[0.65rem] bg-rule-soft text-ink-muted"
>
ממתינה
</Badge>
);
}
return (
<Badge
variant="outline"
className="text-[0.65rem] bg-danger-bg text-danger border-danger/40"
>
נדחתה
</Badge>
);
}
/* Read-only roll-up of every halacha extracted from this precedent —
* approved + pending + rejected. The "ממתין לאישור" tab only surfaces
* pending items globally; this section is the per-case view. To act on
* an item (approve / edit / reject), go to the review tab — keeping the
* surfaces separated avoids duplicate review UX in two places. */
function ExtractedHalachotSection({ halachot }: { halachot: Halacha[] }) {
const [filter, setFilter] = useState<StatusFilter>("all");
const counts = useMemo(() => {
const c = { all: halachot.length, approved: 0, pending: 0, rejected: 0 };
for (const h of halachot) {
if (h.review_status === "approved" || h.review_status === "published") {
c.approved++;
} else if (h.review_status === "pending_review") {
c.pending++;
} else if (h.review_status === "rejected") {
c.rejected++;
}
}
return c;
}, [halachot]);
const sorted = useMemo(() => {
const matches = (h: Halacha) => {
if (filter === "all") return true;
if (filter === "approved") {
return h.review_status === "approved" || h.review_status === "published";
}
if (filter === "pending") return h.review_status === "pending_review";
return h.review_status === "rejected";
};
return halachot
.filter(matches)
.sort((a, b) => a.halacha_index - b.halacha_index);
}, [halachot, filter]);
if (!halachot.length) {
return (
<div className="rounded-lg border border-rule bg-rule-soft/40 p-4 text-center text-ink-muted text-sm">
עדיין לא חולצו הלכות מהפסיקה הזו.
</div>
);
}
const tabs: { key: StatusFilter; label: string; count: number }[] = [
{ key: "all", label: "הכל", count: counts.all },
{ key: "approved", label: "מאושרות", count: counts.approved },
{ key: "pending", label: "ממתינות", count: counts.pending },
{ key: "rejected", label: "נדחו", count: counts.rejected },
];
return (
<div className="space-y-3">
<div className="flex items-center justify-between gap-2 flex-wrap">
<h3 className="text-navy text-base font-semibold m-0">
הלכות שחולצו ({counts.all})
</h3>
<div className="flex items-center gap-1 flex-wrap" role="tablist">
{tabs.map((t) => {
const active = filter === t.key;
return (
<button
key={t.key}
type="button"
role="tab"
aria-selected={active}
onClick={() => setFilter(t.key)}
className={`text-[0.78rem] px-2.5 py-1 rounded border transition-colors tabular-nums ${
active
? "bg-navy text-parchment border-navy"
: "bg-surface text-ink-muted border-rule hover:bg-rule-soft"
}`}
>
{t.label} ({t.count})
</button>
);
})}
</div>
</div>
{!sorted.length ? (
<div className="text-center text-ink-muted text-sm py-6">
אין הלכות בקטגוריה זו.
</div>
) : (
<ol className="space-y-3 list-none ps-0 m-0">
{sorted.map((h) => (
<li
key={h.id}
className="rounded-lg border border-rule bg-surface p-3 space-y-2"
>
<div className="flex items-start gap-2 flex-wrap">
<span className="text-[0.7rem] text-ink-muted tabular-nums font-mono">
#{h.halacha_index}
</span>
<ReviewStatusPill status={h.review_status} />
<Badge variant="outline" className="text-[0.65rem]">
{RULE_TYPE_LABELS[h.rule_type] ?? h.rule_type}
</Badge>
<Badge variant="outline" className="text-[0.65rem] tabular-nums">
ביטחון {h.confidence.toFixed(2)}
</Badge>
{h.page_reference ? (
<span className="text-[0.7rem] text-ink-muted ms-auto">
{h.page_reference}
</span>
) : null}
</div>
<p
className="text-navy font-medium text-sm leading-relaxed m-0"
dir="rtl"
>
{h.rule_statement}
</p>
{h.reasoning_summary ? (
<p
className="text-ink-soft text-[0.82rem] leading-relaxed m-0"
dir="rtl"
>
<span className="text-ink-muted text-[0.7rem]">היגיון: </span>
{h.reasoning_summary}
</p>
) : null}
{h.supporting_quote ? (
<blockquote
className="text-ink-soft text-[0.82rem] leading-relaxed border-r-2 border-gold pr-3 m-0"
dir="rtl"
>
&ldquo;{h.supporting_quote}&rdquo;
</blockquote>
) : null}
{h.subject_tags?.length ? (
<div className="flex items-center gap-1 flex-wrap pt-1">
{h.subject_tags.map((t) => (
<Badge key={t} variant="outline" className="text-[0.65rem]">
{t}
</Badge>
))}
</div>
) : null}
</li>
))}
</ol>
)}
</div>
);
}
type Props = { type Props = {
caseLawId: string | null; caseLawId: string | null;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
@@ -144,6 +340,7 @@ export function PrecedentEditSheet({ caseLawId, onOpenChange }: Props) {
{[...Array(6)].map((_, i) => <Skeleton key={i} className="h-10 w-full" />)} {[...Array(6)].map((_, i) => <Skeleton key={i} className="h-10 w-full" />)}
</div> </div>
) : ( ) : (
<>
<form onSubmit={onSubmit} className="px-6 pb-6 space-y-4 mt-4"> <form onSubmit={onSubmit} className="px-6 pb-6 space-y-4 mt-4">
<div className="rounded-lg border border-rule bg-rule-soft/40 p-3 flex items-start gap-3"> <div className="rounded-lg border border-rule bg-rule-soft/40 p-3 flex items-start gap-3">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
@@ -278,6 +475,10 @@ export function PrecedentEditSheet({ caseLawId, onOpenChange }: Props) {
</Button> </Button>
</div> </div>
</form> </form>
<div className="px-6 pb-8 pt-2 border-t border-rule">
<ExtractedHalachotSection halachot={record.halachot ?? []} />
</div>
</>
)} )}
</SheetContent> </SheetContent>
</Sheet> </Sheet>