feat(precedent-library): add halacha-extract button to library list rows
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 3m8s

When a precedent has not had successful halacha extraction yet, show a
small wand icon between the edit and delete buttons. Clicking it queues
the precedent for the local MCP worker (request-halachot endpoint).

Visibility rule (`needsHalachaExtraction`): show when text extraction is
complete AND halacha status is "pending without requested_at" (never
tried) or "failed" (allow retry). Hide while processing, after
completion, or when already queued — to avoid duplicate requests.

Pairs with the metadata-extract button on the edit sheet.
This commit is contained in:
2026-05-07 06:30:03 +00:00
parent afcc4818a4
commit 171da84680

View File

@@ -1,7 +1,7 @@
"use client";
import { useState } from "react";
import { Trash2, Plus, Pencil } from "lucide-react";
import { Trash2, Plus, Pencil, Wand2 } from "lucide-react";
import { toast } from "sonner";
import {
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
@@ -16,6 +16,7 @@ import {
import {
usePrecedents,
useDeletePrecedent,
useRequestHalachotExtraction,
isPrecedentActive,
type Precedent,
type PracticeArea,
@@ -38,6 +39,20 @@ function cleanCitation(s: string | null | undefined): string {
return s.replace(/[--]/g, "").trim();
}
// Show the "extract halachot" button only when the precedent hasn't had a
// successful (or even attempted) extraction yet. Hide while processing or
// after completion to avoid duplicate requests.
function needsHalachaExtraction(p: Precedent): boolean {
if (p.extraction_status !== "completed") return false; // text not ready
if (p.halacha_extraction_status === "processing") return false;
if (p.halacha_extraction_status === "completed") return false;
if (p.halacha_extraction_status === "pending" && p.halacha_extraction_requested_at) {
return false; // already queued
}
// Remaining cases: pending+no-requested_at (never tried) or failed (retry).
return true;
}
function ActivePill({ label }: { label: string }) {
return (
<Badge
@@ -103,7 +118,9 @@ function StatusPill({ p }: { p: Precedent }) {
function CourtRow({ p, onEdit }: { p: Precedent; onEdit: (id: string) => void }) {
const del = useDeletePrecedent();
const reqHalachot = useRequestHalachotExtraction();
const active = isPrecedentActive(p);
const showExtractHalachot = needsHalachaExtraction(p);
const onDelete = async () => {
if (active) {
@@ -119,6 +136,15 @@ function CourtRow({ p, onEdit }: { p: Precedent; onEdit: (id: string) => void })
}
};
const onExtractHalachot = async () => {
try {
await reqHalachot.mutateAsync(p.id);
toast.success("סומן לחילוץ הלכות. הריצי מ-Claude Code: precedent_process_pending_halachot");
} catch (e) {
toast.error(e instanceof Error ? e.message : "שגיאה");
}
};
return (
<TableRow className="border-rule hover:bg-gold-wash/30 align-top">
<TableCell
@@ -150,6 +176,15 @@ function CourtRow({ p, onEdit }: { p: Precedent; onEdit: (id: string) => void })
className="text-ink-muted hover:text-navy">
<Pencil className="w-4 h-4" />
</Button>
{showExtractHalachot && (
<Button variant="ghost" size="sm" onClick={onExtractHalachot}
disabled={reqHalachot.isPending}
aria-label={`חלץ הלכות מ-${p.case_number}`}
title="חלץ הלכות"
className="text-gold-deep hover:text-gold-deep hover:bg-gold-wash disabled:opacity-30">
<Wand2 className="w-4 h-4" />
</Button>
)}
<Button variant="ghost" size="sm" onClick={onDelete}
disabled={del.isPending || active}
aria-label={`מחק את ${p.case_number}`}
@@ -165,7 +200,9 @@ function CourtRow({ p, onEdit }: { p: Precedent; onEdit: (id: string) => void })
function CommitteeRow({ p, onEdit }: { p: Precedent; onEdit: (id: string) => void }) {
const del = useDeletePrecedent();
const reqHalachot = useRequestHalachotExtraction();
const active = isPrecedentActive(p);
const showExtractHalachot = needsHalachaExtraction(p);
const onDelete = async () => {
if (active) {
@@ -181,6 +218,15 @@ function CommitteeRow({ p, onEdit }: { p: Precedent; onEdit: (id: string) => voi
}
};
const onExtractHalachot = async () => {
try {
await reqHalachot.mutateAsync(p.id);
toast.success("סומן לחילוץ הלכות. הריצי מ-Claude Code: precedent_process_pending_halachot");
} catch (e) {
toast.error(e instanceof Error ? e.message : "שגיאה");
}
};
return (
<TableRow className="border-rule hover:bg-gold-wash/30 align-top">
<TableCell
@@ -214,6 +260,15 @@ function CommitteeRow({ p, onEdit }: { p: Precedent; onEdit: (id: string) => voi
className="text-ink-muted hover:text-navy">
<Pencil className="w-4 h-4" />
</Button>
{showExtractHalachot && (
<Button variant="ghost" size="sm" onClick={onExtractHalachot}
disabled={reqHalachot.isPending}
aria-label={`חלץ הלכות מ-${p.case_number}`}
title="חלץ הלכות"
className="text-gold-deep hover:text-gold-deep hover:bg-gold-wash disabled:opacity-30">
<Wand2 className="w-4 h-4" />
</Button>
)}
<Button variant="ghost" size="sm" onClick={onDelete}
disabled={del.isPending || active}
aria-label={`מחק את ${p.case_number}`}