Files
legal-ai/web-ui/src/components/digests/digest-upload-dialog.tsx
Chaim 06281996ca feat(digests): Phase 2 — API endpoints + /digests UI (X12)
משטחי-משתמש לקורפוס היומונים: endpoints ב-FastAPI + דף UI נפרד /digests
(לדפדוף, חיפוש, העלאה, וקישור לפסק המקורי). היומון נשאר מקור-משני המצביע
על הפסק — אינו מצוטט בהחלטה (INV-DIG1) ואינו מחלץ הלכות (INV-DIG2).

Backend (container-safe + local split):
- digest_library: פוצל ל-create_pending_digest (CONTAINER-SAFE: stage+
  extract_text+create row 'pending', בלי LLM) ↔ enrich_digest/
  process_pending_digests (local: LLM+embed+autolink). ingest_digest מאחד.
- db.list_pending_digests; MCP digest_process_pending (tool+server) — חלופה
  ל-batch script לריקון התור.
- web/app.py: 10 endpoints /api/digests/* (upload/list/search/queue-pending/
  get/patch/delete/link/relink/unlink). upload=INSERT-only pending (ה-LLM רץ
  מקומית — claude_session local-only). כולם מחזירים dict בדפוס precedent.

Frontend (Next 16, ללא api:types — hooks עם טיפוסים hand-written כמו
precedent-library.ts):
- lib/api/digests.ts — hooks (useDigests/useDigestSearch/useDigestPending/
  useUploadDigest/useLink/Relink/Unlink/Delete/Update).
- דף /digests נפרד (לא כרטיסייה ב-/precedents — לשמור גבול סמכותי/משני,
  INV-DIG1): טאבים יומונים/חיפוש + DigestCard (badge קישור-לפסק) +
  DigestUploadDialog + pending badge. nav + header-context.

אומת: backend round-trip מלא (create_pending→list_pending→process_pending→
search→restore); web-ui מתקמפל (webpack/tsc נקי, route /digests נוצר).
הערה: build דיפולטי (turbopack) נכשל ב-worktree עקב symlink ל-node_modules —
ב-CI/Docker (node_modules אמיתי) עובד; אומת עם --webpack.

Invariants: מקיים INV-DIG1/2 (upload לא מחלץ הלכות, UI מציג "מצביע לא
מצוטט"), INV-DIG3 (link/relink/queue). G4 (אין בליעה — שגיאות→toast/HTTP),
G2 (מסלול נפרד, לא מקביל). X6 (חוזה UI↔API — endpoints בדפוס precedent;
hooks hand-written כמו שאר ה-domain modules).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 18:11:05 +00:00

153 lines
5.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 { useState } from "react";
import { Upload } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader,
DialogTitle, DialogTrigger,
} from "@/components/ui/dialog";
import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from "@/components/ui/select";
import { useUploadDigest, type DigestUploadInput } from "@/lib/api/digests";
import type { PracticeArea } from "@/lib/api/precedent-library";
import { PRACTICE_AREAS } from "@/components/precedents/practice-area";
/**
* Upload a "כל יום" digest PDF. The endpoint is container-safe: it only
* stages + extracts text, creating a row with status='pending'. The LLM
* enrichment (concept/headline/citation + embedding + autolink) runs locally
* via the MCP drainer ``digest_process_pending`` — so the toast tells the
* user the digest is queued, not yet searchable.
*/
export function DigestUploadDialog() {
const [open, setOpen] = useState(false);
const [file, setFile] = useState<File | null>(null);
const [yomonNumber, setYomonNumber] = useState("");
const [digestDate, setDigestDate] = useState("");
const [practiceArea, setPracticeArea] = useState<PracticeArea>("");
const upload = useUploadDigest();
const reset = () => {
setFile(null);
setYomonNumber("");
setDigestDate("");
setPracticeArea("");
};
const onSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!file) {
toast.error("בחר קובץ יומון (PDF)");
return;
}
const input: DigestUploadInput = { file };
if (yomonNumber.trim()) input.yomon_number = yomonNumber.trim();
if (digestDate) input.digest_date = digestDate;
if (practiceArea) input.practice_area = practiceArea;
upload.mutate(input, {
onSuccess: (res) => {
if (res.status === "exists") {
toast.info("יומון זהה כבר קיים — לא נוצר כפל");
} else {
toast.success(
"היומון נקלט וממתין לעיבוד מקומי (LLM). הרץ digest_process_pending " +
"או scripts/ingest_digests_batch.py כדי להשלים חילוץ + חיפוש.",
);
}
reset();
setOpen(false);
},
onError: (err) => {
toast.error(`שגיאה בהעלאה: ${err.message}`);
},
});
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button className="bg-navy text-parchment hover:bg-navy-soft">
<Upload className="w-4 h-4 me-1" />
העלאת יומון
</Button>
</DialogTrigger>
<DialogContent dir="rtl">
<DialogHeader>
<DialogTitle>העלאת יומון &quot;כל יום&quot;</DialogTitle>
<DialogDescription>
סיכום-עמוד של פסק דין. המערכת תחלץ אוטומטית את תג-המושג, ההלכה, ומראה-המקום
של הפסק המקורי. היומון משמש כ-radar בלבד אינו מצוטט בהחלטה.
</DialogDescription>
</DialogHeader>
<form onSubmit={onSubmit} className="space-y-4">
<div>
<Label htmlFor="digest-file">קובץ יומון (PDF)</Label>
<Input
id="digest-file"
type="file"
accept=".pdf,.docx,.doc,.rtf,.txt,.md"
onChange={(e) => setFile(e.target.files?.[0] ?? null)}
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<Label htmlFor="digest-num">מספר יומון (אופציונלי)</Label>
<Input
id="digest-num"
value={yomonNumber}
onChange={(e) => setYomonNumber(e.target.value)}
placeholder="5163"
dir="ltr"
/>
</div>
<div>
<Label htmlFor="digest-date">תאריך גיליון (אופציונלי)</Label>
<Input
id="digest-date"
type="date"
value={digestDate}
onChange={(e) => setDigestDate(e.target.value)}
/>
</div>
</div>
<div>
<Label>תחום (אופציונלי יחולץ אם ריק)</Label>
<Select
value={practiceArea || "_auto"}
onValueChange={(v) => setPracticeArea(v === "_auto" ? "" : (v as PracticeArea))}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="_auto">חילוץ אוטומטי</SelectItem>
{PRACTICE_AREAS.map((a) => (
<SelectItem key={a.value} value={a.value}>
{a.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<DialogFooter>
<Button
type="submit"
disabled={upload.isPending}
className="bg-navy text-parchment hover:bg-navy-soft"
>
{upload.isPending ? "מעלה…" : "העלה"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}