"use client"; import { useEffect, useMemo, useState, type ReactNode } from "react"; import { Check, X, Edit2, ChevronDown, ChevronLeft, AlertTriangle, Clock, RotateCcw, Info, Search } 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, useHalachotByStatus, useUpdateHalacha, useBatchReviewHalachot, useLibraryStats, isExtractionFixItem, useCanonicalInstances, type Halacha, type CanonicalInstance, } from "@/lib/api/precedent-library"; import { AuthorityBadge, ruleTypeLabel } from "./halacha-meta"; /** #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: "ציטוט לא מאומת", nli_unsupported: "כלל לא נגזר מהציטוט", application: "יישום תלוי-עובדות", near_duplicate: "כפילות-קרובה", nevo_preamble_leak: "דליפת רציו נבו", }; function formatDate(iso: string | null | undefined) { if (!iso) return "—"; try { return new Date(iso).toLocaleDateString("he-IL"); } catch { return iso; } } /* Strip Unicode bidi marks that render as zero-width but shift visual position. */ function cleanCitation(s: string | null | undefined): string { if (!s) return "—"; return s.replace(/[‎‏‪-‮⁦-⁩]/g, "").trim(); } type EditState = { rule_statement: string; reasoning_summary: string; supporting_quote: string; // #133 — editing this re-verifies vs the source }; // ─── Panel deliberation (#133/FU-2) ─────────────────────────────────────────── // Surfaces the 3-judge panel's vote+rationale inside the chair's review card so // her decision — the gold label the active-learning loop learns from — is informed // by WHY the panel split. Display-only; never affects review_status (INV-G10). const JUDGE_META: Record = { claude: { name: "Claude", lineage: "Anthropic" }, deepseek: { name: "DeepSeek", lineage: "DeepSeek" }, gemini: { name: "Gemini", lineage: "Google" }, }; const VERDICT_META: Record = { unanimous_yes: { label: "פה-אחד", cls: "bg-success text-white" }, unanimous_no: { label: "פה-אחד נגד", cls: "bg-danger text-white" }, split: { label: "פיצול", cls: "bg-warn text-white" }, incomplete: { label: "חלקי", cls: "bg-ink-muted text-white" }, }; function PanelDeliberation({ round }: { round: NonNullable }) { const isEntail = round.question === "entailed"; const yesLabel = isEntail ? "נתמך" : "לשמירה"; const noLabel = isEntail ? "הכלל חורג" : "לפסילה"; const question = isEntail ? "האם הציטוט תומך בכלל ואינו מרחיב מעבר לו?" : "האם זו הלכה בת-הכללה לשמירה?"; const votes = round.judges.map((j) => j.vote).filter((v): v is boolean => v !== null); const yes = votes.filter(Boolean).length; const no = votes.length - yes; const v = VERDICT_META[round.verdict] ?? VERDICT_META.incomplete; const ratio = round.verdict === "split" ? ` ${Math.max(yes, no)}:${Math.min(yes, no)}` : ""; // The strict-vs-gist explainer only makes sense on a split over entailment. const showHint = round.verdict === "split" && isEntail; return (
התלבטות הפאנל {v.label}{ratio} השאלה: ״{question}״
{round.judges.map((j) => { const yesVote = j.vote === true; const noVote = j.vote === false; const meta = JUDGE_META[j.model] ?? { name: j.model, lineage: "" }; return (
{meta.name} {meta.lineage}
{j.vote === null ? "— ללא" : yesVote ? `✓ ${yesLabel}` : `✗ ${noLabel}`}

{j.reason || "—"}

); })} {showHint && (

שורש המחלוקת:{" "} סטנדרט קפדני (כל פרט בכלל חייב להופיע בציטוט) מול סטנדרט-תמצית (הגרעין המשפטי נתמך). הכרעתך קובעת איזה סטנדרט הפאנל יאמץ.

)} {/* seedline (mockup 18): the chair's call IS the gold label the active-learning loop trains on — surfaced on every deliberation. */}

הכרעתך תיקלט כתווית-הזהב שממנה לומדת לולאת-הלמידה-הפעילה.

); } // ─── V41: Canonical section (principle statement + instances accordion) ────── const INSTANCE_TYPE_LABELS: Record = { original: "עיקרון מקורי", citation: "ציטוט", application: "יישום", }; const INSTANCE_TYPE_CLS: Record = { original: "bg-navy text-parchment", citation: "bg-info text-white", application: "bg-ink-muted text-white", }; function CanonicalSection({ h, onSaveCanonical, }: { h: Halacha; onSaveCanonical: (stmt: string) => Promise; }) { const [editingCanon, setEditingCanon] = useState(false); const [canonDraft, setCanonDraft] = useState(h.canonical_statement ?? ""); const [showInstances, setShowInstances] = useState(false); const { data: instances, isLoading: instLoading } = useCanonicalInstances( showInstances ? h.canonical_id : null, ); useEffect(() => { // eslint-disable-next-line react-hooks/set-state-in-effect setCanonDraft(h.canonical_statement ?? ""); }, [h.canonical_id, h.canonical_statement]); const handleSave = async () => { await onSaveCanonical(canonDraft); setEditingCanon(false); }; const instanceCount = h.instance_count ?? 1; return (
כ ניסוח קנוני — העיקרון הרחב (V41) {instanceCount > 1 ? `מאחד ${instanceCount} פסיקות` : "instance יחיד"} {h.review_status && ` · ${h.review_status}`}
{editingCanon ? ( <>