diff --git a/web-ui/src/app/cases/[caseNumber]/compose/page.tsx b/web-ui/src/app/cases/[caseNumber]/compose/page.tsx deleted file mode 100644 index f6890df..0000000 --- a/web-ui/src/app/cases/[caseNumber]/compose/page.tsx +++ /dev/null @@ -1,502 +0,0 @@ -"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()} - /> -
-
-
- )} - - ); -} diff --git a/web-ui/src/app/cases/[caseNumber]/page.tsx b/web-ui/src/app/cases/[caseNumber]/page.tsx index 229400b..f72b0e8 100644 --- a/web-ui/src/app/cases/[caseNumber]/page.tsx +++ b/web-ui/src/app/cases/[caseNumber]/page.tsx @@ -1,10 +1,13 @@ "use client"; -import { use } from "react"; +import { use, useState } from "react"; import Link from "next/link"; import { AppShell } from "@/components/app-shell"; import { Card, CardContent } from "@/components/ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { + Accordion, AccordionContent, AccordionItem, AccordionTrigger, +} from "@/components/ui/accordion"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; import { CaseHeader } from "@/components/cases/case-header"; @@ -13,13 +16,15 @@ import { DocumentsPanel } from "@/components/cases/documents-panel"; import { DraftsPanel } from "@/components/cases/drafts-panel"; import { DecisionBlocksPanel } from "@/components/cases/decision-blocks-panel"; import { LegalArgumentsPanel } from "@/components/cases/legal-arguments-panel"; +import { PositionsPanel } from "@/components/cases/positions-panel"; +import { CitationVerificationPanel } from "@/components/compose/citation-verification-panel"; import { AgentActivityFeed } from "@/components/cases/agent-activity-feed"; import { AgentActivityPreview } from "@/components/cases/agent-activity-preview"; import { UploadSheet } from "@/components/documents/upload-sheet"; import { useCase, useStartWorkflow } from "@/lib/api/cases"; import { toast } from "sonner"; import { - Play, Loader2, LayoutGrid, Scale, FileText, MessageSquare, Users, + Play, Loader2, LayoutGrid, Scale, BadgeCheck, FileText, MessageSquare, Users, type LucideIcon, } from "lucide-react"; @@ -33,6 +38,7 @@ export default function CaseDetailPage({ params: Promise<{ caseNumber: string }>; }) { const { caseNumber } = use(params); + const [tab, setTab] = useState("overview"); const { data, isPending, error, refetch } = useCase(caseNumber); const startWorkflow = useStartWorkflow(caseNumber); const canStartWorkflow = data?.status === "new" || data?.status === "documents_ready"; @@ -62,9 +68,12 @@ export default function CaseDetailPage({ ); } + // Workflow order (X17): intake → arguments+positions → verify citations → + // write decision → drafts/final → agents (monitoring). const tabDefs: [string, string, LucideIcon][] = [ ["overview", "סקירה", LayoutGrid], - ["arguments", "טיעונים", Scale], + ["arguments", "טיעונים ועמדות", Scale], + ["verify", "אימות פסיקה", BadgeCheck], ["decision", "ההחלטה", FileText], ["drafts", "טיוטות והערות", MessageSquare], ["agents", "סוכנים", Users], @@ -93,8 +102,11 @@ export default function CaseDetailPage({ <> {data && } - {canStartWorkflow && ( + )} + + {hasAnalysis && ( + + )} + + + {uploadMsg && ( +

+ {uploadMsg.text} +

+ )} + + {/* stage indicators — informational pointers, not actions */} +
+
+ הרץ למידת-קול — {voiceLearningText(learning.data)} +
+
+ הרץ אימות-הלכות — {halachaExtractionText(learning.data)} +
+
+ + + ); +} + +export function PositionsPanel({ caseNumber }: { caseNumber: string }) { + const caseQuery = useCase(caseNumber); + const analysis = useResearchAnalysis(caseNumber); + const precedentsQuery = useCasePrecedents(caseNumber); + + 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 isNotFound = + analysis.error instanceof Error && + /404|לא נמצא|טרם בוצע/.test(analysis.error.message); + + return ( +
+

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

+ + {analysis.isPending ? ( + + + + + + + + ) : isNotFound ? ( + + +
+

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

+

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

+
+
+ ) : analysis.error ? ( + + +

{analysis.error.message}

+
+
+ ) : analysis.data ? ( +
+ {analysis.data.threshold_claims && + analysis.data.threshold_claims.length > 0 && ( +
+
+

טענות סף

+ + {analysis.data.threshold_claims.length} + +
+
+ {analysis.data.threshold_claims.map((tc) => ( + + ))} +
+
+ )} + + {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 && ( + + + לא נמצאו טענות סף או סוגיות בניתוח זה. + + + )} + + + +

רקע לניתוח

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

מסקנות

+ +
+
+ )} +
+ ) : null} + + {/* case-level פסיקה מצורפת + finish/export gates (INV-IA3: not removed) */} +
+ + +

פסיקה מצורפת

+

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

+ +
+
+ + analysis.refetch()} + /> +
+
+ ); +} diff --git a/web-ui/src/components/header-context.ts b/web-ui/src/components/header-context.ts index e74a41d..9dba1f8 100644 --- a/web-ui/src/components/header-context.ts +++ b/web-ui/src/components/header-context.ts @@ -3,9 +3,8 @@ * header. Reflects the current section so the user always sees where they * are without scanning the nav row. * - * Special-cases case routes (`/cases/{caseNumber}` and `/compose`) so the - * subtitle includes the case number — the most useful piece of context - * during decision drafting. + * Special-cases case routes (`/cases/{caseNumber}`) so the subtitle includes + * the case number — the most useful piece of context during decision drafting. */ export function headerSubtitle(pathname: string): string { if (pathname === "/") return "בית"; @@ -13,9 +12,8 @@ export function headerSubtitle(pathname: string): string { if (pathname.startsWith("/cases/")) { const [, , slug] = pathname.split("/"); if (!slug || slug === "new") return "תיק חדש"; - const isCompose = pathname.includes("/compose"); const decoded = decodeURIComponent(slug); - return isCompose ? `ערר ${decoded} · ניסוח` : `ערר ${decoded}`; + return `ערר ${decoded}`; } if (pathname.startsWith("/archive")) return "ארכיון";