Files
legal-ai/web-ui/src/app/precedents/page.tsx
Chaim 2962538c09
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 6s
feat: תיקון-ציטוט בדלי-החילוץ + קישור-לתור מדף-פרט (#133 follow-ups)
אושר ב-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>
2026-06-12 09:03:29 +00:00

170 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 { 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">
קורפוס הפסיקה והלכות המערכת חיפוש סמנטי, תור-אישור והשלמת
פסיקה חסרה. כל קובץ עובר חילוץ הלכות אוטומטי, וההלכות ממתינות
לאישור היו&quot;ר לפני שהן זמינות לסוכני הכתיבה.
</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>
);
}