Merge pull request 'feat(ui): אינדיקטור התקדמות לחילוץ מטא-דאטה + מתג-מקטעים בספריית הפסיקה' (#70) from worktree-feat+metadata-extraction-progress into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m42s
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m42s
This commit was merged in pull request #70.
This commit is contained in:
@@ -610,6 +610,11 @@ ALTER TABLE case_law ADD COLUMN IF NOT EXISTS document_id UUID REFERENCES docume
|
|||||||
ALTER TABLE case_law ADD COLUMN IF NOT EXISTS extraction_status TEXT DEFAULT 'pending';
|
ALTER TABLE case_law ADD COLUMN IF NOT EXISTS extraction_status TEXT DEFAULT 'pending';
|
||||||
-- 'pending' | 'processing' | 'completed' | 'failed'
|
-- 'pending' | 'processing' | 'completed' | 'failed'
|
||||||
ALTER TABLE case_law ADD COLUMN IF NOT EXISTS halacha_extraction_status TEXT DEFAULT 'pending';
|
ALTER TABLE case_law ADD COLUMN IF NOT EXISTS halacha_extraction_status TEXT DEFAULT 'pending';
|
||||||
|
ALTER TABLE case_law ADD COLUMN IF NOT EXISTS metadata_extraction_status TEXT DEFAULT 'pending';
|
||||||
|
-- 'pending' | 'processing' | 'completed' | 'failed'. Mirrors the
|
||||||
|
-- text/halacha status columns so the UI can show a live badge while the
|
||||||
|
-- local-MCP worker drains the metadata queue (previously only the
|
||||||
|
-- metadata_extraction_requested_at timestamp existed — no 'processing').
|
||||||
ALTER TABLE case_law ADD COLUMN IF NOT EXISTS practice_area TEXT DEFAULT '';
|
ALTER TABLE case_law ADD COLUMN IF NOT EXISTS practice_area TEXT DEFAULT '';
|
||||||
ALTER TABLE case_law ADD COLUMN IF NOT EXISTS appeal_subtype TEXT DEFAULT '';
|
ALTER TABLE case_law ADD COLUMN IF NOT EXISTS appeal_subtype TEXT DEFAULT '';
|
||||||
ALTER TABLE case_law ADD COLUMN IF NOT EXISTS headnote TEXT DEFAULT '';
|
ALTER TABLE case_law ADD COLUMN IF NOT EXISTS headnote TEXT DEFAULT '';
|
||||||
@@ -3070,6 +3075,27 @@ async def set_case_law_halacha_status(case_law_id: UUID, status: str) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def set_case_law_metadata_status(case_law_id: UUID, status: str) -> None:
|
||||||
|
"""Set metadata-extraction status. Mirrors ``set_case_law_halacha_status``:
|
||||||
|
on terminal states ('completed'/'failed') we also clear
|
||||||
|
``metadata_extraction_requested_at`` so the local-MCP queue
|
||||||
|
(`process_pending_extractions`, which scans ``WHERE *_requested_at IS NOT
|
||||||
|
NULL``) stops re-picking the row and the UI's ``isPrecedentActive`` check
|
||||||
|
settles."""
|
||||||
|
pool = await get_pool()
|
||||||
|
if status in ("completed", "failed"):
|
||||||
|
await pool.execute(
|
||||||
|
"UPDATE case_law SET metadata_extraction_status = $2, "
|
||||||
|
"metadata_extraction_requested_at = NULL WHERE id = $1",
|
||||||
|
case_law_id, status,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await pool.execute(
|
||||||
|
"UPDATE case_law SET metadata_extraction_status = $2 WHERE id = $1",
|
||||||
|
case_law_id, status,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def list_external_case_law(
|
async def list_external_case_law(
|
||||||
practice_area: str = "",
|
practice_area: str = "",
|
||||||
court: str = "",
|
court: str = "",
|
||||||
@@ -3126,6 +3152,7 @@ async def list_external_case_law(
|
|||||||
summary, headnote, subject_tags, source_kind,
|
summary, headnote, subject_tags, source_kind,
|
||||||
chair_name, district, citation_formatted,
|
chair_name, district, citation_formatted,
|
||||||
extraction_status, halacha_extraction_status,
|
extraction_status, halacha_extraction_status,
|
||||||
|
metadata_extraction_status,
|
||||||
metadata_extraction_requested_at,
|
metadata_extraction_requested_at,
|
||||||
halacha_extraction_requested_at,
|
halacha_extraction_requested_at,
|
||||||
created_at,
|
created_at,
|
||||||
@@ -4274,8 +4301,12 @@ async def request_metadata_extraction(case_law_id: UUID) -> bool:
|
|||||||
fills empty fields), so this is safe.
|
fills empty fields), so this is safe.
|
||||||
"""
|
"""
|
||||||
pool = await get_pool()
|
pool = await get_pool()
|
||||||
|
# Reset the status to 'pending' alongside the timestamp so a re-request
|
||||||
|
# after a prior 'completed'/'failed' run shows "בתור" again in the UI
|
||||||
|
# instead of a stale terminal badge.
|
||||||
result = await pool.execute(
|
result = await pool.execute(
|
||||||
"UPDATE case_law SET metadata_extraction_requested_at = now() "
|
"UPDATE case_law SET metadata_extraction_requested_at = now(), "
|
||||||
|
"metadata_extraction_status = 'pending' "
|
||||||
"WHERE id = $1",
|
"WHERE id = $1",
|
||||||
case_law_id,
|
case_law_id,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -235,6 +235,12 @@ async def process_pending_extractions(kind: str = "metadata", limit: int = 20) -
|
|||||||
attempts = 0
|
attempts = 0
|
||||||
result: dict = {}
|
result: dict = {}
|
||||||
try:
|
try:
|
||||||
|
# Flip to 'processing' so the UI badge shows live progress while
|
||||||
|
# this row is being worked (metadata has no per-chunk status of
|
||||||
|
# its own — this is the only signal). Halacha already sets its own
|
||||||
|
# 'processing' inside the extractor.
|
||||||
|
if kind == "metadata":
|
||||||
|
await db.set_case_law_metadata_status(cid, "processing")
|
||||||
result = await _run_once(cid)
|
result = await _run_once(cid)
|
||||||
# Retry only on systematic extraction failure (rate-limit storm).
|
# Retry only on systematic extraction failure (rate-limit storm).
|
||||||
# Don't retry on 'no_halachot' — that means Claude looked and
|
# Don't retry on 'no_halachot' — that means Claude looked and
|
||||||
@@ -259,9 +265,15 @@ async def process_pending_extractions(kind: str = "metadata", limit: int = 20) -
|
|||||||
# Finalise: success or terminal failure both clear the request
|
# Finalise: success or terminal failure both clear the request
|
||||||
# so the queue moves on. (Use 'failed' DB state for terminal
|
# so the queue moves on. (Use 'failed' DB state for terminal
|
||||||
# extraction_failed so the UI shows the warning chip.)
|
# extraction_failed so the UI shows the warning chip.)
|
||||||
if kind == "halacha" and result.get("status") == "extraction_failed":
|
if kind == "halacha":
|
||||||
await db.set_case_law_halacha_status(cid, "failed")
|
if result.get("status") == "extraction_failed":
|
||||||
await db.clear_extraction_request(cid, kind=kind)
|
await db.set_case_law_halacha_status(cid, "failed")
|
||||||
|
await db.clear_extraction_request(cid, kind=kind)
|
||||||
|
else:
|
||||||
|
# metadata — set terminal 'completed' status (also clears the
|
||||||
|
# request timestamp) so the UI badge settles instead of
|
||||||
|
# lingering on 'processing'.
|
||||||
|
await db.set_case_law_metadata_status(cid, "completed")
|
||||||
processed += 1
|
processed += 1
|
||||||
results.append({
|
results.append({
|
||||||
"case_law_id": str(cid),
|
"case_law_id": str(cid),
|
||||||
@@ -273,6 +285,15 @@ async def process_pending_extractions(kind: str = "metadata", limit: int = 20) -
|
|||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("process_pending_extractions failed for %s: %s", cid, e)
|
logger.exception("process_pending_extractions failed for %s: %s", cid, e)
|
||||||
|
# Don't clear the request — it stays for the next run. But for
|
||||||
|
# metadata, revert the badge from 'processing' back to 'pending'
|
||||||
|
# (the timestamp is preserved) so the row shows "בתור" rather than
|
||||||
|
# a stuck "מחלץ" until the retry picks it up.
|
||||||
|
if kind == "metadata":
|
||||||
|
try:
|
||||||
|
await db.set_case_law_metadata_status(cid, "pending")
|
||||||
|
except Exception:
|
||||||
|
logger.exception("failed to revert metadata status for %s", cid)
|
||||||
results.append({
|
results.append({
|
||||||
"case_law_id": str(cid),
|
"case_law_id": str(cid),
|
||||||
"case_number": row.get("case_number", ""),
|
"case_number": row.get("case_number", ""),
|
||||||
@@ -280,7 +301,6 @@ async def process_pending_extractions(kind: str = "metadata", limit: int = 20) -
|
|||||||
"error": str(e),
|
"error": str(e),
|
||||||
"retry_attempts": attempts,
|
"retry_attempts": attempts,
|
||||||
})
|
})
|
||||||
# Don't clear the request — it stays for the next run.
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
@@ -314,12 +334,18 @@ async def reextract_metadata(
|
|||||||
raise ValueError("precedent not found")
|
raise ValueError("precedent not found")
|
||||||
# See note in db.request_metadata_extraction — opened to all source kinds.
|
# See note in db.request_metadata_extraction — opened to all source kinds.
|
||||||
|
|
||||||
|
# Mark 'processing' so a concurrent UI poll shows the live badge.
|
||||||
|
await db.set_case_law_metadata_status(case_law_id, "processing")
|
||||||
await progress("extracting_metadata", 40, "מחלץ מטא-דאטה (תקציר, תגיות)")
|
await progress("extracting_metadata", 40, "מחלץ מטא-דאטה (תקציר, תגיות)")
|
||||||
result = await precedent_metadata_extractor.extract_and_apply(case_law_id)
|
result = await precedent_metadata_extractor.extract_and_apply(case_law_id)
|
||||||
# Clear the queue timestamp so the UI / worker stop showing this row.
|
# Settle to terminal 'completed' (also NULLs the queue timestamp) so the
|
||||||
# See note in reextract_halachot.
|
# UI / worker stop showing this row. See note in reextract_halachot.
|
||||||
if result.get("status") in ("completed", "no_changes"):
|
if result.get("status") in ("completed", "no_changes"):
|
||||||
await db.clear_extraction_request(case_law_id, kind="metadata")
|
await db.set_case_law_metadata_status(case_law_id, "completed")
|
||||||
|
else:
|
||||||
|
# e.g. 'no_metadata' (no full_text) — don't leave the badge stuck on
|
||||||
|
# 'processing'; revert to 'pending' (preserves any queue timestamp).
|
||||||
|
await db.set_case_law_metadata_status(case_law_id, "pending")
|
||||||
fields = result.get("fields") or []
|
fields = result.get("fields") or []
|
||||||
msg = (
|
msg = (
|
||||||
f"מולאו {len(fields)} שדות: {', '.join(fields)}"
|
f"מולאו {len(fields)} שדות: {', '.join(fields)}"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Trash2, Plus, Pencil, Wand2 } from "lucide-react";
|
import { Trash2, Plus, Pencil, Wand2, Loader2 } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
|
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
|
||||||
@@ -10,7 +10,9 @@ import {
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Progress } from "@/components/ui/progress";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
@@ -87,6 +89,28 @@ function StatusPill({ p }: { p: Precedent }) {
|
|||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// Metadata extraction (orthogonal one-off re-extract). Surface it while
|
||||||
|
// active so the chair sees the queued button actually ran on something.
|
||||||
|
if (p.metadata_extraction_status === "processing") {
|
||||||
|
return <ActivePill label="מחלץ מטא-דאטה" />;
|
||||||
|
}
|
||||||
|
if (p.metadata_extraction_status === "failed") {
|
||||||
|
return (
|
||||||
|
<Badge variant="outline" className="bg-danger-bg text-danger border-danger/40">
|
||||||
|
מטא-דאטה נכשל
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
p.metadata_extraction_requested_at &&
|
||||||
|
p.metadata_extraction_status !== "completed"
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<Badge variant="outline" className="bg-rule-soft text-ink-muted">
|
||||||
|
ממתין למטא-דאטה
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
if (p.halacha_extraction_status === "processing") {
|
if (p.halacha_extraction_status === "processing") {
|
||||||
return <ActivePill label="מחלץ הלכות" />;
|
return <ActivePill label="מחלץ הלכות" />;
|
||||||
}
|
}
|
||||||
@@ -325,6 +349,92 @@ function CommitteeRow({ p, onEdit }: { p: Precedent; onEdit: (id: string) => voi
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Live banner for the metadata-extraction queue. The local-MCP worker
|
||||||
|
* (`precedent_process_pending kind=metadata`) drains stamped rows serially;
|
||||||
|
* while it runs, rows carry metadata_extraction_status='processing' (current
|
||||||
|
* item) or a request timestamp (still queued). We surface a real, advancing
|
||||||
|
* percentage by tracking the session high-water-mark of the queue depth:
|
||||||
|
* percent = (peak - remaining) / peak. Hidden when the queue is empty.
|
||||||
|
*/
|
||||||
|
function MetadataQueueBanner({ items }: { items: Precedent[] }) {
|
||||||
|
const processing = items.filter(
|
||||||
|
(p) => p.metadata_extraction_status === "processing",
|
||||||
|
).length;
|
||||||
|
const pending = items.filter(
|
||||||
|
(p) =>
|
||||||
|
p.metadata_extraction_status !== "processing" &&
|
||||||
|
p.metadata_extraction_requested_at !== null &&
|
||||||
|
p.metadata_extraction_status !== "completed" &&
|
||||||
|
p.metadata_extraction_status !== "failed",
|
||||||
|
).length;
|
||||||
|
const remaining = processing + pending;
|
||||||
|
|
||||||
|
// Session high-water-mark of the queue depth, using React's documented
|
||||||
|
// "adjust state during render" pattern (no effect, no ref-in-render). React
|
||||||
|
// re-renders immediately without committing the intermediate UI. Resets to 0
|
||||||
|
// when the queue drains, so the next batch starts a fresh 0→100% sweep.
|
||||||
|
const [peak, setPeak] = useState(0);
|
||||||
|
if (remaining === 0) {
|
||||||
|
if (peak !== 0) setPeak(0);
|
||||||
|
} else if (remaining > peak) {
|
||||||
|
setPeak(remaining);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remaining === 0) return null;
|
||||||
|
const done = Math.max(0, peak - remaining);
|
||||||
|
const percent = peak > 0 ? Math.round((done / peak) * 100) : 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border border-gold/40 bg-gold-wash/50 p-4 space-y-2">
|
||||||
|
<div className="flex items-center gap-2 text-sm text-gold-deep">
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin shrink-0" />
|
||||||
|
<span>
|
||||||
|
חילוץ מטא-דאטה פעיל — מעבד {processing}, נותרו {pending} בתור
|
||||||
|
{peak > 0 ? ` · ${done}/${peak}` : ""}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={percent} className="h-1.5" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SegButton({
|
||||||
|
active, onClick, label, count,
|
||||||
|
}: {
|
||||||
|
active: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
label: string;
|
||||||
|
count?: number;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClick}
|
||||||
|
aria-pressed={active}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 rounded-md px-4 py-1.5 text-sm font-semibold transition-colors",
|
||||||
|
active
|
||||||
|
? "bg-surface text-navy shadow-sm"
|
||||||
|
: "text-ink-muted hover:text-navy",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
{count !== undefined && (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
"text-[0.72rem]",
|
||||||
|
active ? "border-navy/30 text-navy" : "text-ink-muted",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{count}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function TableSkeleton({ cols }: { cols: number }) {
|
function TableSkeleton({ cols }: { cols: number }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -344,6 +454,9 @@ export function LibraryListPanel() {
|
|||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [uploadOpen, setUploadOpen] = useState(false);
|
const [uploadOpen, setUploadOpen] = useState(false);
|
||||||
const [editingId, setEditingId] = useState<string | null>(null);
|
const [editingId, setEditingId] = useState<string | null>(null);
|
||||||
|
// Which of the two corpora to show. Both queries stay mounted (counts +
|
||||||
|
// cross-table polling), but only the active table renders — no long scroll.
|
||||||
|
const [view, setView] = useState<"court" | "committee">("court");
|
||||||
|
|
||||||
const sharedFilters = {
|
const sharedFilters = {
|
||||||
practiceArea: practiceArea || undefined,
|
practiceArea: practiceArea || undefined,
|
||||||
@@ -354,6 +467,12 @@ export function LibraryListPanel() {
|
|||||||
const courts = usePrecedents({ ...sharedFilters, sourceKind: "external_upload", sourceType: "court_ruling" });
|
const courts = usePrecedents({ ...sharedFilters, sourceKind: "external_upload", sourceType: "court_ruling" });
|
||||||
const committee = usePrecedents({ ...sharedFilters, sourceKind: "all_committees" });
|
const committee = usePrecedents({ ...sharedFilters, sourceKind: "all_committees" });
|
||||||
|
|
||||||
|
// Metadata-queue banner draws on both corpora (a stamped row can be either).
|
||||||
|
const allItems = [
|
||||||
|
...(courts.data?.items ?? []),
|
||||||
|
...(committee.data?.items ?? []),
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{/* Shared filters */}
|
{/* Shared filters */}
|
||||||
@@ -385,16 +504,28 @@ export function LibraryListPanel() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Metadata-extraction queue progress (hidden when idle) */}
|
||||||
|
<MetadataQueueBanner items={allItems} />
|
||||||
|
|
||||||
|
{/* Segmented control — show one corpus at a time */}
|
||||||
|
<div className="inline-flex rounded-lg border border-rule bg-rule-soft/40 p-1 gap-1">
|
||||||
|
<SegButton
|
||||||
|
active={view === "court"}
|
||||||
|
onClick={() => setView("court")}
|
||||||
|
label="פסיקת בתי משפט"
|
||||||
|
count={courts.data?.count}
|
||||||
|
/>
|
||||||
|
<SegButton
|
||||||
|
active={view === "committee"}
|
||||||
|
onClick={() => setView("committee")}
|
||||||
|
label="החלטות ועדות ערר"
|
||||||
|
count={committee.data?.count}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Table 1 — Court rulings */}
|
{/* Table 1 — Court rulings */}
|
||||||
|
{view === "court" && (
|
||||||
<section>
|
<section>
|
||||||
<div className="flex items-center gap-2 mb-3">
|
|
||||||
<h3 className="text-base font-semibold text-navy">פסיקת בתי משפט</h3>
|
|
||||||
{courts.data && (
|
|
||||||
<Badge variant="outline" className="text-ink-muted text-[0.72rem]">
|
|
||||||
{courts.data.count}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{courts.error ? (
|
{courts.error ? (
|
||||||
<div className="rounded bg-danger-bg border border-danger/40 px-6 py-4 text-danger text-center text-sm">
|
<div className="rounded bg-danger-bg border border-danger/40 px-6 py-4 text-danger text-center text-sm">
|
||||||
{courts.error.message}
|
{courts.error.message}
|
||||||
@@ -433,17 +564,11 @@ export function LibraryListPanel() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Table 2 — Appeals committee decisions */}
|
{/* Table 2 — Appeals committee decisions */}
|
||||||
|
{view === "committee" && (
|
||||||
<section>
|
<section>
|
||||||
<div className="flex items-center gap-2 mb-3">
|
|
||||||
<h3 className="text-base font-semibold text-navy">החלטות ועדות ערר</h3>
|
|
||||||
{committee.data && (
|
|
||||||
<Badge variant="outline" className="text-ink-muted text-[0.72rem]">
|
|
||||||
{committee.data.count}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{committee.error ? (
|
{committee.error ? (
|
||||||
<div className="rounded bg-danger-bg border border-danger/40 px-6 py-4 text-danger text-center text-sm">
|
<div className="rounded bg-danger-bg border border-danger/40 px-6 py-4 text-danger text-center text-sm">
|
||||||
{committee.error.message}
|
{committee.error.message}
|
||||||
@@ -483,6 +608,7 @@ export function LibraryListPanel() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
<PrecedentUploadSheet open={uploadOpen} onOpenChange={setUploadOpen} />
|
<PrecedentUploadSheet open={uploadOpen} onOpenChange={setUploadOpen} />
|
||||||
<PrecedentEditSheet
|
<PrecedentEditSheet
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export type Precedent = {
|
|||||||
citation_formatted: string;
|
citation_formatted: string;
|
||||||
extraction_status: string;
|
extraction_status: string;
|
||||||
halacha_extraction_status: string;
|
halacha_extraction_status: string;
|
||||||
|
metadata_extraction_status: string;
|
||||||
metadata_extraction_requested_at: string | null;
|
metadata_extraction_requested_at: string | null;
|
||||||
halacha_extraction_requested_at: string | null;
|
halacha_extraction_requested_at: string | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
@@ -232,12 +233,14 @@ export function isPrecedentActive(p: Precedent): boolean {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Metadata extraction has no status column — only the timestamp.
|
// Metadata extraction now has its own status column. Active while the
|
||||||
// Treat as active only when extraction hasn't yet fully completed
|
// worker is processing the row, or while it's queued (timestamp set) and
|
||||||
// (otherwise stale timestamps linger after success).
|
// hasn't reached a terminal state yet.
|
||||||
|
if (p.metadata_extraction_status === "processing") return true;
|
||||||
if (
|
if (
|
||||||
p.metadata_extraction_requested_at !== null &&
|
p.metadata_extraction_requested_at !== null &&
|
||||||
p.extraction_status !== "completed"
|
p.metadata_extraction_status !== "completed" &&
|
||||||
|
p.metadata_extraction_status !== "failed"
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user