"use client"; import { useEffect, useMemo, useState } from "react"; import { Check, X, Edit2, ChevronDown, ChevronLeft, AlertTriangle, Clock } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Skeleton } from "@/components/ui/skeleton"; import { Textarea } from "@/components/ui/textarea"; import { CorroborationBadge } from "./corroboration-badge"; import { practiceAreaLabel } from "./practice-area"; import { useHalachotPending, useUpdateHalacha, useBatchReviewHalachot, type Halacha, } from "@/lib/api/precedent-library"; /** #81 strict-rubric flags — why an item was held back from auto-approval. */ const QUALITY_FLAG_LABELS: Record = { non_decision: "אי-הכרעה", truncated_quote: "ציטוט קטוע", thin_restatement: "ניסוח דק", quote_unverified: "ציטוט לא מאומת", }; /** * Halacha review queue — the chair-only path between automatic * extraction and agent visibility. Per the project's review policy, * NO halacha is auto-published; every row sits in pending_review until * approved. * * UX: items are grouped by precedent (case_law_id). Groups start * collapsed so the chair picks one ruling at a time. Within an open * group, J/K navigates, A approves, R rejects, E edits. Items inside * each group are sorted by confidence ascending so the doubtful ones * surface first. */ function formatDate(iso: string | null | undefined) { if (!iso) return "—"; try { return new Date(iso).toLocaleDateString("he-IL"); } catch { return iso; } } /* The upload form (and Nevo PDFs) embed Unicode bidi marks (RTL/LTR/embedding/ * isolate) inside the citation. They render as zero-width but visually push * the text away from where it should sit. Strip for display only. */ function cleanCitation(s: string | null | undefined): string { if (!s) return "—"; return s.replace(/[‎‏‪-‮⁦-⁩]/g, "").trim(); } const RULE_TYPE_LABELS: Record = { binding: "הלכה מחייבת", interpretive: "פרשני", procedural: "פרוצדורלי", obiter: "אמרת אגב", application: "יישום הלכה", persuasive: "משכנע", }; function ruleTypeLabel(t: string): string { return RULE_TYPE_LABELS[t] ?? t; } type EditState = { rule_statement: string; reasoning_summary: string }; function HalachaCard({ h, focused, onApprove, onReject, onDefer, onSave, }: { h: Halacha; focused: boolean; onApprove: () => void; onReject: () => void; onDefer: () => void; onSave: (patch: Partial) => Promise; }) { const [editing, setEditing] = useState(false); const [draft, setDraft] = useState({ rule_statement: h.rule_statement, reasoning_summary: h.reasoning_summary, }); // Reset draft when underlying row changes (focus moves to a new card). useEffect(() => { // eslint-disable-next-line react-hooks/set-state-in-effect setDraft({ rule_statement: h.rule_statement, reasoning_summary: h.reasoning_summary, }); }, [h.id, h.rule_statement, h.reasoning_summary]); const onSubmitEdit = async () => { await onSave(draft); setEditing(false); }; return (
{/* Header — status pills only (citation is in the group header) */}
{h.page_reference && ( {h.page_reference} )} {h.quote_verified ? ( ציטוט מאומת ) : ( ציטוט לא מאומת )} ביטחון {h.confidence.toFixed(2)} {ruleTypeLabel(h.rule_type)}
{/* #81 quality flags — explain why this item needs a human eye */} {h.quality_flags && h.quality_flags.length > 0 && (
{h.quality_flags.map((f) => ( {QUALITY_FLAG_LABELS[f] ?? f} ))}
)} {/* Side-by-side rule vs quote */}
ניסוח הכלל
{editing ? (