All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 6s
אושר ב-Claude Design (כרטיס 20-halacha-followups). א׳ תיקון-חילוץ אמיתי ל-quote_unverified: - `update_halacha` מקבל `supporting_quote`; בעדכונו מריץ `_verify_quote` הקיים מול `case_law.full_text` השמור (דטרמיניסטי — בלי OCR/LLM מחדש, feedback_no_reocr_retrofit) ומסנכרן `quote_verified` + מוסיף/מסיר את הדגל `quote_unverified`. יו"ר שמדביק את הנוסח הנכון מהמקור → הדגל נמחק → ההלכה עוזבת את דלי-החילוץ. `HalachaUpdateRequest`+handler מעבירים את השדה; `HalachaPatch` + מצב-העריכה ב-HalachaCard כוללים textarea-ציטוט (נשלח רק כששונה) + hint. ב׳ דף-פרט פסיקה — ביטול כפילות-המשטח: - הלכה pending ב-`ExtractedHalachotSection` מציגה קישור "עבור לתור הלכות" במקום כפתורי אשר/דחה כפולים (שער-אישור יחיד, INV-IA/G10). - `/precedents` Tabs הפך נשלט וקורא `?tab=review` (post-mount, בלי hydration-mismatch) כדי שהקישור ינחת על טאב-התור. display-only ל-G10 (האימות מסנכרן מטא-איכות, לא review_status). ולידציה: py_compile + tsc + eslint נקיים. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
170 lines
7.4 KiB
TypeScript
170 lines
7.4 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useState } from "react";
|
||
import Link from "next/link";
|
||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||
import { AppShell } from "@/components/app-shell";
|
||
import { LibraryListPanel } from "@/components/precedents/library-list-panel";
|
||
import { LibrarySearchPanel } from "@/components/precedents/library-search-panel";
|
||
import { HalachaReviewPanel } from "@/components/precedents/halacha-review-panel";
|
||
import { LibraryStatsPanel } from "@/components/precedents/library-stats-panel";
|
||
import { useHalachotPending } from "@/lib/api/precedent-library";
|
||
import { useMissingPrecedents } from "@/lib/api/missing-precedents";
|
||
|
||
/**
|
||
* Precedent Library admin page.
|
||
*
|
||
* Four tabs:
|
||
* - ספרייה — browse all uploaded precedents (filters + upload + delete)
|
||
* - חיפוש סמנטי — semantic search across halachot + chunks
|
||
* - ממתין לאישור — chair review queue (PRIMARY tab; halachot from
|
||
* auto-extraction must be approved before agents can use them)
|
||
* - סטטיסטיקה — counts and coverage
|
||
*
|
||
* Distinct from /training (style corpus = Daphna's voice) and the
|
||
* per-case precedent attacher (chair-attached quotes scoped to a case).
|
||
*/
|
||
|
||
/** Colored count pill riding on a tab trigger (mockup 07: warn for review
|
||
* queue, info for incoming). Returns null when the queue is empty. */
|
||
function CountPill({ n, tone }: { n: number; tone: "warn" | "info" }) {
|
||
if (!n) return null;
|
||
const cls =
|
||
tone === "warn"
|
||
? "bg-warn text-white"
|
||
: "bg-info text-white";
|
||
return (
|
||
<span
|
||
className={`ms-1.5 inline-flex items-center justify-center rounded-full px-1.5 min-w-[1.15rem] h-[1.15rem] text-[0.68rem] font-semibold tabular-nums ${cls}`}
|
||
>
|
||
{n}
|
||
</span>
|
||
);
|
||
}
|
||
|
||
function PendingPill() {
|
||
const { data } = useHalachotPending();
|
||
return <CountPill n={data?.count ?? 0} tone="warn" />;
|
||
}
|
||
|
||
function IncomingPill() {
|
||
// "פסיקה נכנסת" = open missing-precedents waiting for the chair to upload.
|
||
const { data } = useMissingPrecedents({ status: "open", limit: 1 });
|
||
return <CountPill n={data?.by_status?.open ?? 0} tone="info" />;
|
||
}
|
||
|
||
const PRECEDENT_TABS = new Set(["library", "search", "review", "incoming", "stats"]);
|
||
|
||
export default function PrecedentsPage() {
|
||
// Controlled so a deep link like /precedents?tab=review (e.g. from a pending
|
||
// halacha on a precedent-detail page, #133) lands on the right tab. Read after
|
||
// mount to avoid an SSR/CSR mismatch and the useSearchParams Suspense rule.
|
||
const [tab, setTab] = useState("library");
|
||
useEffect(() => {
|
||
// read post-mount (not lazy init) to avoid an SSR/CSR hydration mismatch
|
||
const t = new URLSearchParams(window.location.search).get("tab");
|
||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||
if (t && PRECEDENT_TABS.has(t)) setTab(t);
|
||
}, []);
|
||
return (
|
||
<AppShell>
|
||
<Tabs value={tab} onValueChange={setTab} dir="rtl">
|
||
<section className="space-y-6">
|
||
<header className="space-y-3">
|
||
<nav className="text-[0.78rem] text-ink-muted">
|
||
<Link href="/" className="hover:text-gold-deep">בית</Link>
|
||
<span aria-hidden> · </span>
|
||
<span className="text-navy">ספריית פסיקה</span>
|
||
</nav>
|
||
<div className="space-y-1">
|
||
<h1 className="text-navy mb-0">ספריית הפסיקה הסמכותית</h1>
|
||
<p className="text-ink-muted text-sm mt-1 max-w-3xl leading-relaxed">
|
||
קורפוס הפסיקה והלכות המערכת — חיפוש סמנטי, תור-אישור והשלמת
|
||
פסיקה חסרה. כל קובץ עובר חילוץ הלכות אוטומטי, וההלכות ממתינות
|
||
לאישור היו"ר לפני שהן זמינות לסוכני הכתיבה.
|
||
</p>
|
||
</div>
|
||
|
||
{/* tabs as a dedicated row under the header — underline-style
|
||
triggers with colored count pills (mockup 07). */}
|
||
<TabsList className="flex w-full justify-start gap-1 rounded-none border-0 border-b border-rule bg-transparent p-0 h-auto">
|
||
{[
|
||
{ value: "library", label: "ספרייה", pill: null },
|
||
{ value: "search", label: "חיפוש בקורפוס", pill: null },
|
||
{ value: "review", label: "תור הלכות", pill: <PendingPill /> },
|
||
{
|
||
value: "incoming",
|
||
label: "פסיקה נכנסת",
|
||
pill: <IncomingPill />,
|
||
},
|
||
{ value: "stats", label: "סטטיסטיקה", pill: null },
|
||
].map((t) => (
|
||
<TabsTrigger
|
||
key={t.value}
|
||
value={t.value}
|
||
className="rounded-none border-0 border-b-2 border-transparent bg-transparent px-4 py-2.5 -mb-px text-sm font-medium text-ink-muted shadow-none data-[state=active]:border-gold data-[state=active]:bg-transparent data-[state=active]:font-semibold data-[state=active]:text-navy data-[state=active]:shadow-none"
|
||
>
|
||
{t.label}
|
||
{t.pill}
|
||
</TabsTrigger>
|
||
))}
|
||
</TabsList>
|
||
</header>
|
||
|
||
<TabsContent value="library" className="mt-0">
|
||
<LibraryListPanel />
|
||
</TabsContent>
|
||
|
||
<TabsContent value="search" className="mt-0">
|
||
<LibrarySearchPanel />
|
||
</TabsContent>
|
||
|
||
<TabsContent value="review" className="mt-0">
|
||
<HalachaReviewPanel />
|
||
</TabsContent>
|
||
|
||
{/* "פסיקה נכנסת" — the incoming/missing-precedent queue. Kept as a
|
||
tab per the mockup; full management lives on /missing-precedents. */}
|
||
<TabsContent value="incoming" className="mt-0">
|
||
<IncomingTab />
|
||
</TabsContent>
|
||
|
||
<TabsContent value="stats" className="mt-0">
|
||
<LibraryStatsPanel />
|
||
</TabsContent>
|
||
</section>
|
||
</Tabs>
|
||
</AppShell>
|
||
);
|
||
}
|
||
|
||
/** Lightweight in-tab pointer to the dedicated missing-precedents page,
|
||
* preserving the mockup's "פסיקה נכנסת" tab without duplicating the table. */
|
||
function IncomingTab() {
|
||
const { data } = useMissingPrecedents({ status: "open", limit: 1 });
|
||
const open = data?.by_status?.open ?? 0;
|
||
return (
|
||
<div className="rounded-lg border border-rule bg-surface shadow-sm p-6 space-y-3">
|
||
<div className="flex items-baseline gap-3 flex-wrap">
|
||
<h2 className="text-navy text-lg font-semibold m-0">פסיקה נכנסת</h2>
|
||
{open ? (
|
||
<span className="inline-flex items-baseline gap-1.5 rounded-lg border border-rule bg-warn-bg px-3 py-1">
|
||
<span className="text-base font-bold text-warn tabular-nums">{open}</span>
|
||
<span className="text-[0.8rem] text-ink-soft">פתוחים</span>
|
||
</span>
|
||
) : null}
|
||
</div>
|
||
<p className="text-ink-soft text-sm leading-relaxed max-w-2xl">
|
||
פסיקה שצוטטה בכתבי-הטענות אך אינה קיימת בקורפוס. השלמתה מאפשרת
|
||
אימות-הלכה ועיגון-מקור (INV-AH).
|
||
</p>
|
||
<Link
|
||
href="/missing-precedents"
|
||
className="inline-flex items-center rounded-md bg-gold px-4 py-2 text-sm font-semibold text-white hover:bg-gold-deep"
|
||
>
|
||
לניהול פסיקה חסרה ←
|
||
</Link>
|
||
</div>
|
||
);
|
||
}
|