"use client"; import { use, useRef, useState } from "react"; import Link from "next/link"; import { FileText } from "lucide-react"; import { AppShell } from "@/components/app-shell"; import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@/components/ui/accordion"; import { SubsectionCard } from "@/components/compose/subsection-card"; import { PrecedentsSection } from "@/components/compose/precedents-section"; import { CitationVerificationPanel } from "@/components/compose/citation-verification-panel"; import { DecisionBlocksPanel } from "@/components/cases/decision-blocks-panel"; import { StatusBadge } from "@/components/cases/status-badge"; import { Markdown } from "@/components/ui/markdown"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useCase } from "@/lib/api/cases"; import { useCaseLearningStatus, type CaseLearningStatus, } from "@/lib/api/learning"; import { useResearchAnalysis } from "@/lib/api/research"; import { useCasePrecedents } from "@/lib/api/precedents"; import { APPEAL_SUBTYPES } from "@/lib/practice-area"; import { DOC_TYPE_LABELS, type DocType } from "@/lib/doc-types"; function subtypeLabel(subtype?: string | null): string | null { if (!subtype) return null; return APPEAL_SUBTYPES.find((s) => s.value === subtype)?.label ?? null; } // ── Staged-pipeline indicator text (mockup 03) — derived from the live // learning-status, same source as the drafts-panel LearningStatusBadges. ────── function voiceLearningText(s?: CaseLearningStatus): string { if (!s?.final_uploaded) return "ממתין להעלאת הסופי"; const v = s.voice_learning; if (v.outcome === "succeeded") { const bits = [`${v.lessons_count} לקחים הופקו`]; if (v.lessons_proposed > 0) bits.push(`${v.lessons_proposed} הוצעו לאישור`); return `✓ הושלם · ${bits.join(" · ")}`; } if (v.outcome === "failed") return v.error ? `✗ נכשל — ${v.error}` : "✗ נכשל"; return "ממתין להרצה"; } function halachaExtractionText(s?: CaseLearningStatus): string { if (!s?.final_uploaded) return "ממתין להעלאת הסופי"; const h = s.halacha_extraction; if (!h.enrolled_in_corpus) return h.not_enrolled_reason ?? "לא נכנס לקורפוס-הפסיקה"; switch (h.status) { case "completed": return `✓ הושלם · חולצו ${h.halachot_count} · ${h.approved} אושרו · ${h.rejected} נדחו`; case "processing": return "רץ עכשיו…"; case "pending": case "busy": return "בתור"; case "partial": return `חלקי · חולצו ${h.halachot_count}`; case "failed": case "extraction_failed": return "✗ נכשל"; case "no_chunks": return "אין טקסט לחילוץ"; default: return "ממתין להרצה"; } } function ProseSection({ title, content }: { title: string; content?: string }) { if (!content?.trim()) return null; return (

{title}

); } // ── "השלמה והעברה" rail card — DOCX export, upload, download (all real) ────── function FinishRail({ caseNumber, hasAnalysis, onUploaded, }: { caseNumber: string; hasAnalysis: boolean; onUploaded: () => void; }) { const fileRef = useRef(null); const [uploading, setUploading] = useState(false); const [uploadMsg, setUploadMsg] = useState<{ ok: boolean; text: string } | null>(null); const learning = useCaseLearningStatus(caseNumber); async function handleUpload(file: File) { setUploading(true); setUploadMsg(null); try { const form = new FormData(); form.append("file", file); const res = await fetch(`/api/cases/${caseNumber}/research/analysis/upload`, { method: "PUT", body: form, }); const data = await res.json(); if (!res.ok) { setUploadMsg({ ok: false, text: data.detail || "שגיאה בהעלאה" }); return; } setUploadMsg({ ok: true, text: `הקובץ הועלה — ${data.sections.threshold_claims} טענות סף, ${data.sections.issues} סוגיות`, }); onUploaded(); } catch { setUploadMsg({ ok: false, text: "שגיאת רשת" }); } finally { setUploading(false); if (fileRef.current) fileRef.current.value = ""; } } return (

השלמה והעברה

{ const f = e.target.files?.[0]; if (f) handleUpload(f); }} />
{hasAnalysis && ( )} {hasAnalysis && ( )}
{uploadMsg && (

{uploadMsg.text}

)} {/* mockup 03: stage indicators — informational pointers, not actions */}
הרץ למידת-קול — {voiceLearningText(learning.data)}
הרץ אימות-הלכות — {halachaExtractionText(learning.data)}
); } export default function ComposePage({ params, }: { params: Promise<{ caseNumber: string }>; }) { const { caseNumber } = use(params); const caseQuery = useCase(caseNumber); const analysis = useResearchAnalysis(caseNumber); const precedentsQuery = useCasePrecedents(caseNumber); /* Partition the flat list into scopes so each child renders its own slice * without re-fetching. Done once at the page level. */ const allPrecedents = precedentsQuery.data ?? []; const caseLevelPrecedents = allPrecedents.filter((p) => p.section_id === null); const precedentsBySection = new Map(); for (const p of allPrecedents) { if (p.section_id) { const existing = precedentsBySection.get(p.section_id) ?? []; existing.push(p); precedentsBySection.set(p.section_id, existing); } } const practiceArea = caseQuery.data?.practice_area ?? null; const subtype = subtypeLabel(caseQuery.data?.appeal_subtype); const parties = (() => { const c = caseQuery.data; if (!c) return null; const app = c.appellants?.length ? c.appellants.join(", ") : null; const resp = c.respondents?.length ? c.respondents.join(", ") : null; const out: string[] = []; if (app) out.push(`עוררים: ${app}`); if (resp) out.push(`משיבה: ${resp}`); return out.length ? out.join(" · ") : c.title || null; })(); const documents = caseQuery.data?.documents ?? []; const isNotFound = analysis.error instanceof Error && /404|לא נמצא|טרם בוצע/.test(analysis.error.message); return ( {/* ── Case header band (mockup 03) — parchment strip, full-bleed to the AppShell
edges (which pads px-10 py-10) ── */}
חזרה לדף התיק

ערר {caseNumber}

{/* read-only status chip — reuses the shared StatusBadge (G2: no parallel status map). Status is changed only in the overview hero. */} {caseQuery.data?.status && } {subtype && ( {subtype} )} {/* INV-G10: source-of-truth pill — the blocks are the canonical text */} מקור-אמת: בלוקים
{parties &&

{parties}

}
{caseQuery.isPending ? ( ) : ( /* ── Full-width workspace (mockup 03b): no side rail; the case-docs table is gone (DocumentsPanel in the overview is the sole owner), relocated here as a collapsible doc-strip inside the positions tab. ── */ עורך הבלוקים עמדות וטענות אימות פסיקה {/* Tab 1 — the 12-block decision editor (reused DecisionBlocksPanel) */} {/* Tab 3 — citation verification: per-argument support + verify gate (#154) */} {/* Tab 2 — chair positions on the analyst's threshold-claims + issues */} {/* ★ collapsible doc-strip at the top, closed by default (mockup 03b). Read-only pointer to the docs owned by the overview tab. */} מסמכי התיק {documents.length} מסמכים {documents.length === 0 ? (

אין מסמכים מצורפים

) : (
{documents.map((d) => ( {d.title || "מסמך"} {DOC_TYPE_LABELS[d.doc_type as DocType] ?? d.doc_type} ))}
)}

סוגיות-המחלוקת ועמדות הצדדים. עורך עמדת-היו״ר נשמר אוטומטית ומזין את בלוק י׳ (דיון והכרעה).

{analysis.isPending ? ( ) : isNotFound ? (

טרם בוצע ניתוח משפטי לתיק זה

לאחר שקובץ analysis-and-research.md ייווצר, תוכלי לערוך כאן את עמדת הוועדה לכל טענת סף וסוגיה.

) : analysis.error ? (

{analysis.error.message}

) : analysis.data ? (
{/* Threshold claims — laid out 2-up across the full width (mockup 03b) */} {analysis.data.threshold_claims && analysis.data.threshold_claims.length > 0 && (

טענות סף

{analysis.data.threshold_claims.length}
{analysis.data.threshold_claims.map((tc) => ( ))}
)} {/* Issues — 2-up grid */} {analysis.data.issues && analysis.data.issues.length > 0 && (

סוגיות להכרעה

{analysis.data.issues.length}
{analysis.data.issues.map((iss) => ( ))}
)} {!analysis.data.threshold_claims?.length && !analysis.data.issues?.length && ( לא נמצאו טענות סף או סוגיות בניתוח זה. )} {/* Background prose — supporting context after the decision points */}

רקע לניתוח

{analysis.data.conclusions?.trim() && (

מסקנות

)}
) : null} {/* ── case-level פסיקה מצורפת + finish/export gates — relocated from the removed side rail; kept inline (INV-IA3: gates not removed). ── */}
{/* פסיקה מצורפת (case-level) — stays inline */}

פסיקה מצורפת

ציטוטים התומכים בעמדה באופן רוחבי — ישולבו בפתיחת בלוק י (דיון).

{/* השלמה והעברה — export/upload gates, relocated not removed */} analysis.refetch()} />
)} ); }