feat: תיקון-ציטוט בדלי-החילוץ + קישור-לתור מדף-פרט (#133 follow-ups) #230
@@ -4658,12 +4658,40 @@ async def update_halacha(
|
||||
reasoning_summary: str | None = None,
|
||||
subject_tags: list[str] | None = None,
|
||||
practice_areas: list[str] | None = None,
|
||||
supporting_quote: str | None = None,
|
||||
) -> dict | None:
|
||||
"""Update a halacha — used by the chair to approve/reject/edit."""
|
||||
"""Update a halacha — used by the chair to approve/reject/edit.
|
||||
|
||||
#133 follow-up — extraction repair: when ``supporting_quote`` is edited it is
|
||||
RE-VERIFIED against the stored source text (deterministic; no re-OCR / no LLM —
|
||||
see feedback_no_reocr_retrofit) and ``quote_verified`` + the ``quote_unverified``
|
||||
quality flag are synced. A chair who pastes the correct quote from the source
|
||||
clears the flag, and the item leaves the 'fix' bucket."""
|
||||
pool = await get_pool()
|
||||
set_parts: list[str] = []
|
||||
params: list = [halacha_id]
|
||||
idx = 2
|
||||
if supporting_quote is not None:
|
||||
set_parts.append(f"supporting_quote = ${idx}")
|
||||
params.append(supporting_quote)
|
||||
idx += 1
|
||||
# re-verify against the stored full_text, sync quote_verified + the flag
|
||||
src = await pool.fetchrow(
|
||||
"SELECT cl.full_text, h.quality_flags FROM halachot h "
|
||||
"JOIN case_law cl ON cl.id = h.case_law_id WHERE h.id = $1", halacha_id)
|
||||
if src is not None:
|
||||
from legal_mcp.services.halacha_extractor import _verify_quote
|
||||
verified = _verify_quote(supporting_quote, src["full_text"] or "")
|
||||
set_parts.append(f"quote_verified = ${idx}")
|
||||
params.append(verified)
|
||||
idx += 1
|
||||
flags = [f for f in (src["quality_flags"] or [])
|
||||
if f != halacha_quality.FLAG_QUOTE_UNVERIFIED]
|
||||
if not verified:
|
||||
flags.append(halacha_quality.FLAG_QUOTE_UNVERIFIED)
|
||||
set_parts.append(f"quality_flags = ${idx}")
|
||||
params.append(flags)
|
||||
idx += 1
|
||||
if review_status is not None:
|
||||
set_parts.append(f"review_status = ${idx}")
|
||||
params.append(review_status)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"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";
|
||||
@@ -52,10 +53,22 @@ function IncomingPill() {
|
||||
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 defaultValue="library" dir="rtl">
|
||||
<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">
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { Check, X, RotateCcw } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { Check, X, RotateCcw, ArrowLeft } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -250,23 +251,19 @@ export function ExtractedHalachotSection({ halachot }: { halachot: Halacha[] })
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{/* #133 — the chair approves in the unified queue, not here:
|
||||
a pending halacha links to the queue instead of duplicating the
|
||||
approve/reject card (single gate, INV-IA/G10). */}
|
||||
{h.review_status === "pending_review" && (
|
||||
<>
|
||||
<Button size="sm" variant="ghost" disabled={update.isPending}
|
||||
onClick={() => {
|
||||
if (window.confirm("לדחות הלכה זו?")) setStatus(h, "rejected");
|
||||
}}
|
||||
className="text-danger hover:text-danger hover:bg-danger-bg">
|
||||
<X className="w-3.5 h-3.5 me-1" />
|
||||
דחה
|
||||
</Button>
|
||||
<Button size="sm" disabled={update.isPending}
|
||||
onClick={() => setStatus(h, "approved")}
|
||||
className="bg-gold text-navy hover:bg-gold-deep">
|
||||
<Check className="w-3.5 h-3.5 me-1" />
|
||||
אשר
|
||||
</Button>
|
||||
</>
|
||||
<Link
|
||||
href="/precedents?tab=review"
|
||||
className="inline-flex items-center gap-1.5 text-[0.78rem] font-semibold
|
||||
text-gold-deep bg-gold-wash border border-gold/40 rounded-md px-3 py-1.5
|
||||
hover:bg-gold-wash/70 transition-colors"
|
||||
>
|
||||
עבור לתור הלכות
|
||||
<ArrowLeft className="w-3.5 h-3.5" />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -42,7 +42,11 @@ function cleanCitation(s: string | null | undefined): string {
|
||||
return s.replace(/[--]/g, "").trim();
|
||||
}
|
||||
|
||||
type EditState = { rule_statement: string; reasoning_summary: string };
|
||||
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
|
||||
@@ -143,6 +147,7 @@ function HalachaCard({
|
||||
const [draft, setDraft] = useState<EditState>({
|
||||
rule_statement: h.rule_statement,
|
||||
reasoning_summary: h.reasoning_summary,
|
||||
supporting_quote: h.supporting_quote,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -150,11 +155,21 @@ function HalachaCard({
|
||||
setDraft({
|
||||
rule_statement: h.rule_statement,
|
||||
reasoning_summary: h.reasoning_summary,
|
||||
supporting_quote: h.supporting_quote,
|
||||
});
|
||||
}, [h.id, h.rule_statement, h.reasoning_summary]);
|
||||
}, [h.id, h.rule_statement, h.reasoning_summary, h.supporting_quote]);
|
||||
|
||||
const onSubmitEdit = async () => {
|
||||
await onSave(draft);
|
||||
// Only send the quote when it actually changed — that triggers server-side
|
||||
// re-verification + flag sync (#133); unchanged edits keep the old behavior.
|
||||
const patch: Partial<EditState> = {
|
||||
rule_statement: draft.rule_statement,
|
||||
reasoning_summary: draft.reasoning_summary,
|
||||
};
|
||||
if (draft.supporting_quote !== h.supporting_quote) {
|
||||
patch.supporting_quote = draft.supporting_quote;
|
||||
}
|
||||
await onSave(patch);
|
||||
setEditing(false);
|
||||
};
|
||||
|
||||
@@ -232,9 +247,22 @@ function HalachaCard({
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[0.7rem] text-ink-muted mb-1">ציטוט תומך</div>
|
||||
{editing ? (
|
||||
<>
|
||||
<Textarea
|
||||
value={draft.supporting_quote} rows={4} dir="rtl"
|
||||
onChange={(e) => setDraft({ ...draft, supporting_quote: e.target.value })}
|
||||
className="bg-gold-wash/50 border-gold/30"
|
||||
/>
|
||||
<p className="text-[0.66rem] text-info mt-1 leading-snug">
|
||||
השמירה מאמתת את הציטוט מול טקסט-המקור; אם תואם — הדגל ״ציטוט לא-מאומת״ יוסר.
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<blockquote className="text-ink-soft text-sm leading-relaxed border-r-2 border-gold pr-3" dir="rtl">
|
||||
“{h.supporting_quote}”
|
||||
</blockquote>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -650,6 +650,9 @@ export type HalachaPatch = Partial<{
|
||||
reasoning_summary: string;
|
||||
subject_tags: string[];
|
||||
practice_areas: string[];
|
||||
// #133 — editing the quote re-verifies it against the source server-side and
|
||||
// clears/sets the quote_unverified flag (extraction repair).
|
||||
supporting_quote: string;
|
||||
}>;
|
||||
|
||||
export function useUpdateHalacha() {
|
||||
|
||||
@@ -5960,6 +5960,7 @@ class HalachaUpdateRequest(BaseModel):
|
||||
reasoning_summary: str | None = None
|
||||
subject_tags: list[str] | None = None
|
||||
practice_areas: list[str] | None = None
|
||||
supporting_quote: str | None = None # #133 — edited quote → re-verify + sync flag
|
||||
|
||||
|
||||
class HalachaBatchReviewRequest(BaseModel):
|
||||
@@ -7253,6 +7254,7 @@ async def halacha_update(halacha_id: str, req: HalachaUpdateRequest):
|
||||
reasoning_summary=req.reasoning_summary,
|
||||
subject_tags=req.subject_tags,
|
||||
practice_areas=req.practice_areas,
|
||||
supporting_quote=req.supporting_quote,
|
||||
)
|
||||
if not row:
|
||||
raise HTTPException(404, "הלכה לא נמצאה")
|
||||
|
||||
Reference in New Issue
Block a user