Files
legal-ai/web-ui/src/components/compose/precedent-attacher.tsx
Chaim 9482eb5a1e
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 5s
feat(ia): IA גל-3 — ניווט מבוסס-משימה + קידוד X17 ל-canonical (#132, X17)
גל-3 (האחרון) מבקלוג #127 — ניווט (INV-IA4) + השלמת קידוד-הספ. שינויי-UI קלים + docs.

א) הורדת /feedback מהניווט-הראשי (INV-IA4): "הערות יו״ר" ירד מ-WORK_LINKS.
   הוא משימה *מתוך* מרכז-האישורים — כרטיס chair_feedback ב-/api/chair/pending
   כבר deep-link ל-/feedback. הדף קיים ונגיש מהתיבה, לא יעד-ניווט מבוסס-פורמט.

ב) שמות-חיפוש עקביים (PRE-1, INV-IA6): ה-typeahead ב-precedent-attacher (חיפוש
   /api/precedents/search = פסיקה שכבר צורפה) קיבל כותרת מבהירה "פסיקה שכבר צירפת
   בעבר — לחץ למילוי אוטומטי", להבדילו מחיפוש-הקורפוס ב-/precedents (precedent-library).

ג) קידוד X17 ל-canonical: X17 נוסף לאינדקס-הספ ב-00-constitution §7 (טבלה + "X1–X17"),
   ול-CLAUDE.md (טבלת מסמכי-ייחוס, שורת ia-audit-redesign+X17). דלתות-הספ (X6 INV-UI7/8,
   07-learning §0.4, 00-constitution G2) כבר קודדו בגל-2.

בדיקות: tsc --noEmit ✓ · eslint ✓ (לבד מ-learning-panel:109 קיים-מראש). אין שינויי-backend.

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

235 lines
8.1 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 { Plus, Paperclip } from "lucide-react";
import { toast } from "sonner";
import {
Popover, PopoverContent, PopoverTrigger,
} from "@/components/ui/popover";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
import {
useCreatePrecedent,
usePrecedentLibrarySearch,
uploadPrecedentPdf,
} from "@/lib/api/precedents";
import type { PracticeArea } from "@/lib/practice-area";
/*
* Inline form for adding a new precedent. Opens in a Popover adjacent
* to the trigger button so the user can see the surrounding context
* (the threshold_claim body, the chair editor) while they fill it in.
*
* The citation field has cross-case typeahead: once the user types
* 2+ characters, we hit /api/precedents/search and show distinct
* matches. Picking one prefills quote + chair_note but keeps them
* editable — the new row is a copy, so a customized quote for this
* case doesn't affect the library.
*/
export function PrecedentAttacher({
caseNumber,
sectionId,
practiceArea,
}: {
caseNumber: string;
sectionId: string | null;
practiceArea: PracticeArea | null | undefined;
}) {
const [open, setOpen] = useState(false);
const [citation, setCitation] = useState("");
const [quote, setQuote] = useState("");
const [chairNote, setChairNote] = useState("");
const [pdfFile, setPdfFile] = useState<File | null>(null);
const [submitting, setSubmitting] = useState(false);
const [picked, setPicked] = useState(false);
const create = useCreatePrecedent(caseNumber);
const library = usePrecedentLibrarySearch(
citation,
practiceArea,
/* pause typeahead once the user has picked one and we're just editing */
!picked,
);
const reset = () => {
setCitation("");
setQuote("");
setChairNote("");
setPdfFile(null);
setPicked(false);
};
const onSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!quote.trim() || !citation.trim()) {
toast.error("ציטוט ומראה-מקום חובה");
return;
}
setSubmitting(true);
try {
let pdfDocumentId: string | undefined;
if (pdfFile) {
const res = await uploadPrecedentPdf(caseNumber, pdfFile);
pdfDocumentId = res.document_id;
}
await create.mutateAsync({
quote: quote.trim(),
citation: citation.trim(),
chair_note: chairNote.trim(),
section_id: sectionId ?? undefined,
pdf_document_id: pdfDocumentId,
});
toast.success("נוספה פסיקה");
reset();
setOpen(false);
} catch (err) {
toast.error(err instanceof Error ? err.message : "שגיאה בשמירה");
} finally {
setSubmitting(false);
}
};
return (
<Popover open={open} onOpenChange={(v) => { setOpen(v); if (!v) reset(); }}>
<PopoverTrigger asChild>
<Button
type="button"
variant="outline"
size="sm"
className="border-dashed border-gold/50 text-gold-deep hover:bg-gold-wash"
>
<Plus className="w-4 h-4 me-1" aria-hidden="true" />
הוסף פסיקה תומכת
</Button>
</PopoverTrigger>
<PopoverContent
className="w-[520px] max-w-[90vw] p-5"
align="start"
dir="rtl"
>
<form onSubmit={onSubmit} className="space-y-3" dir="rtl">
<div>
<Label htmlFor="prec-citation" className="text-navy">
מראה מקום <span className="text-danger">*</span>
</Label>
<Input
id="prec-citation"
value={citation}
onChange={(e) => {
setCitation(e.target.value);
setPicked(false);
}}
placeholder="ערר (ירושלים) 1126-08-25 ... נ' ... (נבו 9.3.2026)"
autoComplete="off"
className="mt-1"
/>
{!picked && library.data && library.data.length > 0 && citation.length >= 2 && (
<div className="mt-1 rounded border border-rule bg-surface shadow-sm overflow-hidden">
{/* PRE-1 (INV-IA6): this typeahead reuses precedents you already
attached to past cases — distinct from the corpus search in
/precedents (ספריית הפסיקה). Label it so the two don't blur. */}
<p className="px-3 py-1.5 text-[0.68rem] text-ink-muted bg-rule-soft/40 border-b border-rule">
פסיקה שכבר צירפת בעבר לחץ למילוי אוטומטי
</p>
<ul
className="max-h-44 overflow-y-auto divide-y divide-rule"
role="listbox"
>
{library.data.map((m) => (
<li key={m.id}>
<button
type="button"
onClick={() => {
setCitation(m.citation);
setQuote(m.quote);
setChairNote(m.chair_note || "");
setPicked(true);
}}
className="w-full text-right px-3 py-2 hover:bg-gold-wash/60 transition-colors"
>
<div className="text-[0.78rem] text-gold-deep font-semibold truncate">
{m.citation}
</div>
<div className="text-[0.72rem] text-ink-muted truncate">
{m.quote}
</div>
</button>
</li>
))}
</ul>
</div>
)}
</div>
<div>
<Label htmlFor="prec-quote" className="text-navy">
ציטוט <span className="text-danger">*</span>
</Label>
<Textarea
id="prec-quote"
value={quote}
onChange={(e) => setQuote(e.target.value)}
rows={5}
placeholder="הטקסט המדויק שישולב בהחלטה"
className="mt-1"
/>
</div>
<div>
<Label htmlFor="prec-note" className="text-navy">
הערה (אופציונלי)
</Label>
<Textarea
id="prec-note"
value={chairNote}
onChange={(e) => setChairNote(e.target.value)}
rows={2}
placeholder="למה הציטוט הזה תומך בעמדה"
className="mt-1"
/>
</div>
<div>
<Label className="text-navy flex items-center gap-2">
<Paperclip className="w-3.5 h-3.5" aria-hidden="true" />
צירוף קובץ המקור (אופציונלי, לארכיון)
</Label>
<input
type="file"
accept=".pdf,.docx,.doc"
onChange={(e) => setPdfFile(e.target.files?.[0] ?? null)}
className="mt-1 w-full text-sm file:me-3 file:rounded file:border-0 file:bg-rule-soft file:px-3 file:py-1.5 file:text-navy file:text-sm hover:file:bg-rule"
/>
{pdfFile && (
<p className="text-[0.72rem] text-ink-muted mt-1">
{pdfFile.name} · {(pdfFile.size / 1024).toFixed(1)} KB
</p>
)}
</div>
<div className="flex items-center justify-end gap-2 pt-2">
<Button
type="button"
variant="ghost"
onClick={() => { setOpen(false); reset(); }}
disabled={submitting}
>
ביטול
</Button>
<Button
type="submit"
disabled={submitting}
className="bg-navy hover:bg-navy-soft text-parchment"
>
{submitting ? "שומר…" : "שמור פסיקה"}
</Button>
</div>
</form>
</PopoverContent>
</Popover>
);
}