"use client"; /* * Side-drawer for inspecting + editing a single style_corpus entry. * * Tabs: * - "פרטים" — show + edit the enriched metadata (decision_number, date, * subjects, summary, outcome, key_principles, appeal_subtype). Saving * issues a PATCH /api/training/corpus/{id} and invalidates the list. * - "תוכן" — read-only full_text view (truncated to 5K with "show more"). * We never let the chair edit full_text from the UI; corrections happen * by re-uploading via the Upload dialog. * - "מה למדנו" — per-decision lessons (Phase 4 placeholder for now). * - "דפוסים" — style_patterns scoped by appeal_subtype. * * Why a Sheet, not a Dialog: the drawer needs to coexist with the corpus * table so the chair can scan multiple decisions without losing context. * Sheet (side: "left" in RTL = right edge in LTR) gives that without * stealing the entire viewport. */ import { useEffect, useState } from "react"; import { Save, FileText, Tag, Calendar, BookOpen, Loader2 } from "lucide-react"; import { toast } from "sonner"; import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, } from "@/components/ui/sheet"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Badge } from "@/components/ui/badge"; import { ScrollArea } from "@/components/ui/scroll-area"; import { usePatchCorpus, type CorpusDecision, type CorpusDecisionPatch, } from "@/lib/api/training"; import { LessonsTab } from "./lessons-tab"; type Props = { decision: CorpusDecision | null; onOpenChange: (open: boolean) => void; }; export function CorpusDetailDrawer({ decision, onOpenChange }: Props) { // Local editable state for the "details" tab. Re-seeds whenever the // selected decision changes so the form reflects the row the chair // clicked. const [draft, setDraft] = useState({}); const patch = usePatchCorpus(); /* eslint-disable react-hooks/set-state-in-effect */ useEffect(() => { if (!decision) { setDraft({}); return; } setDraft({ decision_number: decision.decision_number, decision_date: decision.decision_date, subject_categories: decision.subject_categories, summary: decision.summary, outcome: decision.outcome, key_principles: decision.key_principles, appeal_subtype: decision.appeal_subtype, practice_area: decision.practice_area, }); }, [decision]); /* eslint-enable react-hooks/set-state-in-effect */ const open = decision !== null; if (!decision) return null; // Diff against the originally loaded row — only PATCH fields the chair // actually changed, so concurrent edits to other fields stay intact. const diff: CorpusDecisionPatch = {}; if (draft.decision_number !== decision.decision_number) diff.decision_number = draft.decision_number; if (draft.decision_date !== decision.decision_date) diff.decision_date = draft.decision_date; if (draft.summary !== decision.summary) diff.summary = draft.summary; if (draft.outcome !== decision.outcome) diff.outcome = draft.outcome; if (draft.appeal_subtype !== decision.appeal_subtype) diff.appeal_subtype = draft.appeal_subtype; if (draft.practice_area !== decision.practice_area) diff.practice_area = draft.practice_area; if ( JSON.stringify(draft.subject_categories) !== JSON.stringify(decision.subject_categories) ) diff.subject_categories = draft.subject_categories; if ( JSON.stringify(draft.key_principles) !== JSON.stringify(decision.key_principles) ) diff.key_principles = draft.key_principles; const isDirty = Object.keys(diff).length > 0; const onSave = async () => { if (!isDirty) return; try { await patch.mutateAsync({ id: decision.id, patch: diff }); toast.success("המטא-דאטה עודכן"); } catch (e) { toast.error(e instanceof Error ? e.message : "כשל בשמירה"); } }; const setSubjects = (raw: string) => setDraft((d) => ({ ...d, subject_categories: raw.split(/[,،]/).map((s) => s.trim()).filter(Boolean), })); const setPrinciples = (raw: string) => setDraft((d) => ({ ...d, key_principles: raw.split("\n").map((s) => s.trim()).filter(Boolean), })); return ( {decision.legal_citation || decision.decision_number || "—"} {decision.doc_title || "החלטה בקורפוס הסגנוני"} {/* Summary strip — fast-scan info, always visible above the tabs. */}
} label="תאריך" value={decision.decision_date || "—"} /> } label="תווים" value={`${(decision.chars / 1000).toFixed(1)}K`} /> } label="עמודים" value={decision.page_count > 0 ? String(decision.page_count) : "—"} /> } label="תת-סוג" value={decision.appeal_subtype || "—"} />
פרטים תוכן מה למדנו דפוסים {/* ── Tab: editable metadata ─────────────────────────── */}
setDraft((d) => ({ ...d, decision_number: e.target.value }))} dir="rtl" /> setDraft((d) => ({ ...d, decision_date: e.target.value }))} />
setSubjects(e.target.value)} dir="rtl" /> {decision.subject_categories.length > 0 && (
{decision.subject_categories.map((s) => ( {s} ))}
)}
setDraft((d) => ({ ...d, appeal_subtype: e.target.value }))} placeholder="building_permit / betterment_levy / compensation_197" dir="rtl" /> setDraft((d) => ({ ...d, practice_area: e.target.value }))} dir="rtl" />