Files
legal-ai/web-ui/src/components/precedents/extracted-halachot.tsx
Chaim 1aadd3b455 feat(web-ui): sort corroborated halachot first in extracted list (X11)
Halachot carrying a corroboration badge (positive citation count or a
negative treatment) float to the top of 'הלכות שחולצו', ordered by
corroboration strength; the rest keep document order by halacha_index.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 05:50:12 +00:00

214 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useMemo, useState } from "react";
import { Badge } from "@/components/ui/badge";
import { CorroborationBadge } from "@/components/precedents/corroboration-badge";
import type { Halacha } from "@/lib/api/precedent-library";
const RULE_TYPE_LABELS: Record<string, string> = {
binding: "הלכה מחייבת",
interpretive: "פרשני",
procedural: "פרוצדורלי",
obiter: "אמרת אגב",
application: "יישום הלכה",
persuasive: "משכנע",
};
type StatusFilter = "all" | "approved" | "pending" | "rejected";
export 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 a 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. */
export 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";
};
/* Surface citation-corroborated halachot first: any with a badge
* (positive count or a negative treatment) float to the top, ordered
* by corroboration strength; the rest keep document order by index. */
const hasTag = (h: Halacha) =>
(h.corroboration_count ?? 0) > 0 || (h.corroboration_negative ?? false);
return halachot
.filter(matches)
.sort((a, b) => {
const tagDelta = Number(hasTag(b)) - Number(hasTag(a));
if (tagDelta !== 0) return tagDelta;
const countDelta =
(b.corroboration_count ?? 0) - (a.corroboration_count ?? 0);
if (countDelta !== 0) return countDelta;
return 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>
<CorroborationBadge halacha={h} />
{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>
);
}