feat(precedents): UI button queues extraction for local MCP worker
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m27s

The chair wanted a one-click "extract metadata" button on the edit sheet.
The constraint stays the same — claude_session needs the local CLI which
the container doesn't have, so the button can't run the extractor itself.
Compromise: button stamps a queue marker; the local MCP server drains the
queue on demand.

DB (V8): two nullable timestamps on case_law,
metadata_extraction_requested_at and halacha_extraction_requested_at,
with partial indexes for cheap "find pending" scans.

API:
  POST /api/precedent-library/{id}/request-metadata   → stamp the row
  POST /api/precedent-library/{id}/request-halachot   → same for halacha
  GET  /api/precedent-library/queue/pending?kind=...  → read-only view

UI: Sparkles button in the edit sheet header. Click → toast tells the
chair what to run from Claude Code. The button never triggers the
extractor directly from the container.

MCP tool: precedent_process_pending(kind, limit) — runs from Claude Code
with the local CLI, picks up everything stamped, calls the extractor for
each, clears the timestamp on success. Failures keep the timestamp so the
next invocation retries them.

Architectural rule (claude_session local-only) is preserved end-to-end
and called out in the new endpoint comment + tool docstring.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-03 12:32:25 +00:00
parent 8e1384b897
commit 4a9a6b7970
7 changed files with 293 additions and 21 deletions

View File

@@ -1,7 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { Save } from "lucide-react";
import { Save, Sparkles } from "lucide-react";
import { toast } from "sonner";
import {
Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription,
@@ -17,6 +17,7 @@ import {
import {
usePrecedent,
useUpdatePrecedent,
useRequestMetadataExtraction,
type PracticeArea,
type SourceType,
} from "@/lib/api/precedent-library";
@@ -59,6 +60,7 @@ export function PrecedentEditSheet({ caseLawId, onOpenChange }: Props) {
const open = caseLawId !== null;
const { data: record, isPending } = usePrecedent(caseLawId);
const update = useUpdatePrecedent();
const requestMetadata = useRequestMetadataExtraction();
const [form, setForm] = useState<FormState>(EMPTY);
@@ -112,6 +114,18 @@ export function PrecedentEditSheet({ caseLawId, onOpenChange }: Props) {
}
};
const onRequestMetadata = async () => {
if (!caseLawId) return;
try {
await requestMetadata.mutateAsync(caseLawId);
toast.success(
"סומן לחילוץ מטא-דאטה. הריצי מ-Claude Code: precedent_process_pending",
);
} catch (err) {
toast.error(err instanceof Error ? err.message : "שגיאה");
}
};
return (
<Sheet open={open} onOpenChange={(o) => { if (!o) onOpenChange(false); }}>
<SheetContent side="left" className="w-full sm:max-w-2xl overflow-y-auto" dir="rtl">
@@ -119,9 +133,9 @@ export function PrecedentEditSheet({ caseLawId, onOpenChange }: Props) {
<SheetTitle className="text-navy">עריכת פרטי פסיקה</SheetTitle>
<SheetDescription className="text-ink-muted">
כל השדות ניתנים לעריכה חוץ ממראה המקום (מזהה ייחודי).
לחילוץ מטא-דאטה אוטומטי או הלכות להפעיל מ-Claude Code את
ה-MCP tools <code>precedent_extract_metadata</code> /{" "}
<code>precedent_extract_halachot</code>.
כפתור &quot;חלץ מטא-דאטה&quot; שולח בקשה לתור מקומי שאני מרוקן
מ-Claude Code (ה-LLM רץ מקומית עם <code>claude session</code>,
לא ב-API).
</SheetDescription>
</SheetHeader>
@@ -131,11 +145,23 @@ export function PrecedentEditSheet({ caseLawId, onOpenChange }: Props) {
</div>
) : (
<form onSubmit={onSubmit} className="px-6 pb-6 space-y-4 mt-4">
<div className="rounded-lg border border-rule bg-rule-soft/40 p-3">
<div className="text-[0.78rem] text-ink-muted">מראה מקום (לא ניתן לעריכה)</div>
<div className="text-navy font-mono text-sm break-all" dir="ltr">
{record.case_number}
<div className="rounded-lg border border-rule bg-rule-soft/40 p-3 flex items-start gap-3">
<div className="flex-1 min-w-0">
<div className="text-[0.78rem] text-ink-muted">מראה מקום (לא ניתן לעריכה)</div>
<div className="text-navy font-mono text-sm break-all" dir="ltr">
{record.case_number}
</div>
</div>
<Button
type="button" size="sm" variant="outline"
onClick={onRequestMetadata}
disabled={requestMetadata.isPending}
className="shrink-0"
title="שולח בקשה לחילוץ מטא-דאטה לתור המקומי"
>
<Sparkles className="w-3.5 h-3.5 me-1" />
חלץ מטא-דאטה
</Button>
</div>
<div className="grid grid-cols-2 gap-3">

View File

@@ -336,12 +336,41 @@ export function useUpdatePrecedent() {
});
}
// Halacha + metadata extraction are not exposed as HTTP mutations because
// they call the local `claude` CLI through the MCP server — see the rule
// in mcp-server/src/legal_mcp/services/claude_session.py. The chair
// triggers them from Claude Code via:
// mcp__legal-ai__precedent_extract_halachot <case_law_id>
// mcp__legal-ai__precedent_extract_metadata <case_law_id>
/* Extraction can't run inside the container (no `claude` CLI). The
* "request" endpoints below stamp a queue marker in case_law; the chair
* (or me) drains the queue from Claude Code by invoking the MCP tool
* `precedent_process_pending`, which runs the actual extractor locally.
* See the rule in mcp-server/src/legal_mcp/services/claude_session.py. */
export function useRequestMetadataExtraction() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: string) =>
apiRequest<{ queued: boolean }>(
`/api/precedent-library/${encodeURIComponent(id)}/request-metadata`,
{ method: "POST" },
),
onSuccess: (_, id) => {
qc.invalidateQueries({ queryKey: libraryKeys.detail(id) });
qc.invalidateQueries({ queryKey: libraryKeys.all });
},
});
}
export function useRequestHalachotExtraction() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: string) =>
apiRequest<{ queued: boolean }>(
`/api/precedent-library/${encodeURIComponent(id)}/request-halachot`,
{ method: "POST" },
),
onSuccess: (_, id) => {
qc.invalidateQueries({ queryKey: libraryKeys.detail(id) });
qc.invalidateQueries({ queryKey: libraryKeys.all });
},
});
}
export function useHalachotPending(limit = 200) {
return useQuery({