feat(case-ui): merge /compose editor into case tabs (X17 #3)
Folds the standalone /compose decision-editor route into the main case page so there is no more "sub-page" (mockup 18f), with the final 6-tab workflow order: סקירה · טיעונים ועמדות · אימות פסיקה · ההחלטה · טיוטות והערות · סוכנים. - New PositionsPanel — the chair's editing surface (threshold claims + issues SubsectionCards + ChairEditor + background prose + case precedents + finish rail), extracted verbatim from the former /compose "עמדות וטענות" tab. Drops only the doc-strip (the overview DocumentsPanel is the sole owner — no dup). - "טיעונים ועמדות" tab = PositionsPanel above + the aggregated by-party LegalArgumentsPanel in a collapsible below. - "אימות פסיקה" promoted to its own top-level tab (CitationVerificationPanel, was /compose tab 3); placed BEFORE "ההחלטה" — verify citations, then write (INV-AH: the writer cites only verified precedents). - "ההחלטה" stays the single 12-block editor (DecisionBlocksPanel) — no longer duplicated between /compose and the case page. - Tabs are now controlled; the "פתח עורך החלטה" CTA switches to the ההחלטה tab instead of navigating to the deleted route. - /compose route deleted; header-context /compose special-case removed. No duplication remains (G2): blocks editor single, docs single (overview), citation-verify single. Design-gate: mockup 18f approved in Claude Design X17. tsc --noEmit + eslint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 (
|
|
||||||
<section className="space-y-2">
|
|
||||||
<h3 className="text-[0.78rem] uppercase tracking-[0.08em] text-gold-deep font-semibold">
|
|
||||||
{title}
|
|
||||||
</h3>
|
|
||||||
<Markdown content={content.trim()} />
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── "השלמה והעברה" rail card — DOCX export, upload, download (all real) ──────
|
|
||||||
function FinishRail({
|
|
||||||
caseNumber,
|
|
||||||
hasAnalysis,
|
|
||||||
onUploaded,
|
|
||||||
}: {
|
|
||||||
caseNumber: string;
|
|
||||||
hasAnalysis: boolean;
|
|
||||||
onUploaded: () => void;
|
|
||||||
}) {
|
|
||||||
const fileRef = useRef<HTMLInputElement>(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 (
|
|
||||||
<Card className="bg-surface border-rule shadow-sm">
|
|
||||||
<CardContent className="px-4 py-4">
|
|
||||||
<h3 className="text-navy text-[0.9rem] font-semibold mb-3">השלמה והעברה</h3>
|
|
||||||
|
|
||||||
<input
|
|
||||||
ref={fileRef}
|
|
||||||
type="file"
|
|
||||||
accept=".md"
|
|
||||||
className="hidden"
|
|
||||||
onChange={(e) => {
|
|
||||||
const f = e.target.files?.[0];
|
|
||||||
if (f) handleUpload(f);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
{hasAnalysis && (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="w-full justify-center"
|
|
||||||
onClick={() => {
|
|
||||||
const a = document.createElement("a");
|
|
||||||
a.href = `/api/cases/${caseNumber}/research/analysis/export-docx`;
|
|
||||||
a.click();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
ייצוא DOCX
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
className="w-full justify-center bg-gold text-white hover:bg-gold-deep"
|
|
||||||
disabled={uploading}
|
|
||||||
onClick={() => fileRef.current?.click()}
|
|
||||||
>
|
|
||||||
{uploading ? "מעלה…" : "העלאת ניתוח מעודכן"}
|
|
||||||
</Button>
|
|
||||||
{hasAnalysis && (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="w-full justify-center"
|
|
||||||
onClick={() => {
|
|
||||||
const a = document.createElement("a");
|
|
||||||
a.href = `/api/cases/${caseNumber}/research/analysis/download`;
|
|
||||||
a.download = `analysis-${caseNumber}.md`;
|
|
||||||
a.click();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
הורד ניתוח (MD)
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{uploadMsg && (
|
|
||||||
<p className={`text-xs mt-2 ${uploadMsg.ok ? "text-success" : "text-danger"}`}>
|
|
||||||
{uploadMsg.text}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* mockup 03: stage indicators — informational pointers, not actions */}
|
|
||||||
<div className="mt-3 space-y-0">
|
|
||||||
<div className="text-[0.78rem] text-ink-muted pt-2 border-t border-rule-soft">
|
|
||||||
<b className="text-navy">הרץ למידת-קול</b> — {voiceLearningText(learning.data)}
|
|
||||||
</div>
|
|
||||||
<div className="text-[0.78rem] text-ink-muted pt-2 mt-2 border-t border-rule-soft">
|
|
||||||
<b className="text-navy">הרץ אימות-הלכות</b> — {halachaExtractionText(learning.data)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button asChild variant="ghost" className="w-full justify-center mt-3 text-ink-muted">
|
|
||||||
<Link href={`/cases/${caseNumber}`}>חזרה לתיק</Link>
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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<string, typeof allPrecedents>();
|
|
||||||
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 (
|
|
||||||
<AppShell>
|
|
||||||
{/* ── Case header band (mockup 03) — parchment strip, full-bleed to the
|
|
||||||
AppShell <main> edges (which pads px-10 py-10) ── */}
|
|
||||||
<div className="-mx-10 -mt-10 mb-6 border-b border-rule bg-parchment px-10 py-5">
|
|
||||||
<nav className="text-[0.78rem] text-ink-muted flex items-center gap-2 mb-2">
|
|
||||||
<Link href="/" className="hover:text-gold-deep">בית</Link>
|
|
||||||
<span aria-hidden>·</span>
|
|
||||||
<Link href={`/cases/${caseNumber}`} className="hover:text-gold-deep">
|
|
||||||
ערר {caseNumber}
|
|
||||||
</Link>
|
|
||||||
<span aria-hidden>·</span>
|
|
||||||
<span className="text-navy">עורך החלטה</span>
|
|
||||||
</nav>
|
|
||||||
<Link
|
|
||||||
href={`/cases/${caseNumber}`}
|
|
||||||
className="inline-flex items-center gap-1.5 mb-3 rounded-md border border-rule bg-surface px-3 py-1.5 text-[0.82rem] font-semibold text-gold-deep hover:bg-gold-wash hover:text-navy transition-colors"
|
|
||||||
>
|
|
||||||
<span aria-hidden>→</span> חזרה לדף התיק
|
|
||||||
</Link>
|
|
||||||
<div className="flex items-center gap-3 flex-wrap">
|
|
||||||
<h1 className="text-navy text-2xl font-bold mb-0">ערר {caseNumber}</h1>
|
|
||||||
{/* read-only status chip — reuses the shared StatusBadge (G2: no
|
|
||||||
parallel status map). Status is changed only in the overview hero. */}
|
|
||||||
{caseQuery.data?.status && <StatusBadge status={caseQuery.data.status} />}
|
|
||||||
{subtype && (
|
|
||||||
<span className="rounded-full text-[0.78rem] font-semibold px-3 py-0.5 border border-rule bg-gold-wash text-gold-deep">
|
|
||||||
{subtype}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{/* INV-G10: source-of-truth pill — the blocks are the canonical text */}
|
|
||||||
<span className="ms-auto rounded-lg text-[0.8rem] font-semibold px-3.5 py-1.5 border border-gold bg-gold-wash text-gold-deep">
|
|
||||||
מקור-אמת: בלוקים
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{parties && <p className="text-ink-soft text-sm mt-2">{parties}</p>}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{caseQuery.isPending ? (
|
|
||||||
<Card className="bg-surface border-rule shadow-sm">
|
|
||||||
<CardContent className="px-6 py-5 space-y-3">
|
|
||||||
<Skeleton className="h-6 w-48" />
|
|
||||||
<Skeleton className="h-4 w-96" />
|
|
||||||
<Skeleton className="h-4 w-80" />
|
|
||||||
<Skeleton className="h-32 w-full" />
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
) : (
|
|
||||||
/* ── 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. ── */
|
|
||||||
<Tabs defaultValue="blocks" dir="rtl">
|
|
||||||
<TabsList className="bg-rule-soft/60">
|
|
||||||
<TabsTrigger value="blocks">עורך הבלוקים</TabsTrigger>
|
|
||||||
<TabsTrigger value="positions">עמדות וטענות</TabsTrigger>
|
|
||||||
<TabsTrigger value="verify">אימות פסיקה</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
{/* Tab 1 — the 12-block decision editor (reused DecisionBlocksPanel) */}
|
|
||||||
<TabsContent value="blocks" className="mt-5">
|
|
||||||
<DecisionBlocksPanel caseNumber={caseNumber} />
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
{/* Tab 3 — citation verification: per-argument support + verify gate (#154) */}
|
|
||||||
<TabsContent value="verify" className="mt-5">
|
|
||||||
<CitationVerificationPanel caseNumber={caseNumber} />
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
{/* Tab 2 — chair positions on the analyst's threshold-claims + issues */}
|
|
||||||
<TabsContent value="positions" className="mt-5 space-y-4">
|
|
||||||
{/* ★ collapsible doc-strip at the top, closed by default (mockup 03b).
|
|
||||||
Read-only pointer to the docs owned by the overview tab. */}
|
|
||||||
<Accordion type="single" collapsible>
|
|
||||||
<AccordionItem
|
|
||||||
value="docs"
|
|
||||||
className="overflow-hidden rounded-lg border border-rule bg-surface shadow-sm"
|
|
||||||
>
|
|
||||||
<AccordionTrigger className="px-4 py-3 hover:no-underline">
|
|
||||||
<span className="flex flex-1 items-center gap-2.5">
|
|
||||||
<FileText className="size-3.5 text-gold-deep" aria-hidden />
|
|
||||||
<span className="text-navy text-[0.85rem] font-semibold">מסמכי התיק</span>
|
|
||||||
<span className="text-ink-muted text-[0.78rem]">
|
|
||||||
{documents.length} מסמכים
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</AccordionTrigger>
|
|
||||||
<AccordionContent className="px-4 pb-3.5 pt-0">
|
|
||||||
{documents.length === 0 ? (
|
|
||||||
<p className="text-[0.78rem] text-ink-muted">אין מסמכים מצורפים</p>
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-wrap gap-2 border-t border-rule-soft pt-3">
|
|
||||||
{documents.map((d) => (
|
|
||||||
<span
|
|
||||||
key={d.id}
|
|
||||||
className="inline-flex items-center gap-1.5 rounded-full border border-rule bg-parchment px-3 py-1 text-[0.78rem] text-ink-soft"
|
|
||||||
title={d.title}
|
|
||||||
>
|
|
||||||
<FileText className="size-3 text-gold-deep shrink-0" aria-hidden />
|
|
||||||
<span className="truncate max-w-[14rem]">{d.title || "מסמך"}</span>
|
|
||||||
<span className="rounded bg-gold-wash text-gold-deep text-[0.66rem] font-semibold px-1.5 py-0.5">
|
|
||||||
{DOC_TYPE_LABELS[d.doc_type as DocType] ?? d.doc_type}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
</Accordion>
|
|
||||||
|
|
||||||
<p className="text-ink-muted text-[0.78rem]">
|
|
||||||
סוגיות-המחלוקת ועמדות הצדדים. עורך עמדת-היו״ר נשמר אוטומטית ומזין את בלוק י׳ (דיון והכרעה).
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{analysis.isPending ? (
|
|
||||||
<Card className="bg-surface border-rule shadow-sm">
|
|
||||||
<CardContent className="px-6 py-5 space-y-3">
|
|
||||||
<Skeleton className="h-6 w-48" />
|
|
||||||
<Skeleton className="h-4 w-96" />
|
|
||||||
<Skeleton className="h-32 w-full" />
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
) : isNotFound ? (
|
|
||||||
<Card className="bg-surface border-rule shadow-sm">
|
|
||||||
<CardContent className="px-6 py-12 text-center space-y-3">
|
|
||||||
<div className="text-gold text-3xl" aria-hidden>❦</div>
|
|
||||||
<h2 className="text-navy text-lg mb-0">
|
|
||||||
טרם בוצע ניתוח משפטי לתיק זה
|
|
||||||
</h2>
|
|
||||||
<p className="text-ink-muted text-sm max-w-md mx-auto">
|
|
||||||
לאחר שקובץ <code>analysis-and-research.md</code> ייווצר, תוכלי
|
|
||||||
לערוך כאן את עמדת הוועדה לכל טענת סף וסוגיה.
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
) : analysis.error ? (
|
|
||||||
<Card className="bg-danger-bg border-danger/40">
|
|
||||||
<CardContent className="px-6 py-5 text-center">
|
|
||||||
<p className="text-danger">{analysis.error.message}</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
) : analysis.data ? (
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* Threshold claims — laid out 2-up across the full width (mockup 03b) */}
|
|
||||||
{analysis.data.threshold_claims &&
|
|
||||||
analysis.data.threshold_claims.length > 0 && (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<h2 className="text-navy text-lg font-semibold mb-0">טענות סף</h2>
|
|
||||||
<span className="text-[0.72rem] rounded-full bg-gold-wash text-gold-deep px-2 py-0.5 border border-gold/40 tabular-nums">
|
|
||||||
{analysis.data.threshold_claims.length}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-3 lg:grid-cols-2 items-start">
|
|
||||||
{analysis.data.threshold_claims.map((tc) => (
|
|
||||||
<SubsectionCard
|
|
||||||
key={tc.id}
|
|
||||||
caseNumber={caseNumber}
|
|
||||||
item={tc}
|
|
||||||
precedents={precedentsBySection.get(tc.id) ?? []}
|
|
||||||
practiceArea={practiceArea}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Issues — 2-up grid */}
|
|
||||||
{analysis.data.issues && analysis.data.issues.length > 0 && (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<h2 className="text-navy text-lg font-semibold mb-0">סוגיות להכרעה</h2>
|
|
||||||
<span className="text-[0.72rem] rounded-full bg-gold-wash text-gold-deep px-2 py-0.5 border border-gold/40 tabular-nums">
|
|
||||||
{analysis.data.issues.length}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-3 lg:grid-cols-2 items-start">
|
|
||||||
{analysis.data.issues.map((iss) => (
|
|
||||||
<SubsectionCard
|
|
||||||
key={iss.id}
|
|
||||||
caseNumber={caseNumber}
|
|
||||||
item={iss}
|
|
||||||
precedents={precedentsBySection.get(iss.id) ?? []}
|
|
||||||
practiceArea={practiceArea}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!analysis.data.threshold_claims?.length &&
|
|
||||||
!analysis.data.issues?.length && (
|
|
||||||
<Card className="bg-surface border-rule">
|
|
||||||
<CardContent className="px-6 py-10 text-center text-ink-muted">
|
|
||||||
לא נמצאו טענות סף או סוגיות בניתוח זה.
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Background prose — supporting context after the decision points */}
|
|
||||||
<Card className="bg-surface border-rule shadow-sm">
|
|
||||||
<CardContent className="px-6 py-5 space-y-5">
|
|
||||||
<h2 className="text-navy text-lg font-semibold mb-0">רקע לניתוח</h2>
|
|
||||||
<ProseSection title="צד מיוצג" content={analysis.data.represented_party} />
|
|
||||||
<ProseSection title="רקע דיוני" content={analysis.data.procedural_background} />
|
|
||||||
<ProseSection title="עובדות מוסכמות" content={analysis.data.agreed_facts} />
|
|
||||||
<ProseSection title="עובדות במחלוקת" content={analysis.data.disputed_facts} />
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{analysis.data.conclusions?.trim() && (
|
|
||||||
<Card className="bg-gold-wash border-gold/40 shadow-sm">
|
|
||||||
<CardContent className="px-6 py-5 space-y-3">
|
|
||||||
<h2 className="text-gold-deep text-lg font-semibold mb-0">מסקנות</h2>
|
|
||||||
<Markdown content={analysis.data.conclusions.trim()} />
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{/* ── case-level פסיקה מצורפת + finish/export gates — relocated from
|
|
||||||
the removed side rail; kept inline (INV-IA3: gates not removed). ── */}
|
|
||||||
<div className="grid gap-4 lg:grid-cols-2 items-start pt-2">
|
|
||||||
{/* פסיקה מצורפת (case-level) — stays inline */}
|
|
||||||
<Card className="bg-surface border-rule shadow-sm">
|
|
||||||
<CardContent className="px-5 py-4">
|
|
||||||
<h3 className="text-navy text-[0.9rem] font-semibold mb-1">פסיקה מצורפת</h3>
|
|
||||||
<p className="text-[0.72rem] text-ink-muted mb-3">
|
|
||||||
ציטוטים התומכים בעמדה באופן רוחבי — ישולבו בפתיחת בלוק י (דיון).
|
|
||||||
</p>
|
|
||||||
<PrecedentsSection
|
|
||||||
caseNumber={caseNumber}
|
|
||||||
sectionId={null}
|
|
||||||
precedents={caseLevelPrecedents}
|
|
||||||
practiceArea={practiceArea}
|
|
||||||
emptyHelperText="עדיין לא צורפה פסיקה כללית לתיק"
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* השלמה והעברה — export/upload gates, relocated not removed */}
|
|
||||||
<FinishRail
|
|
||||||
caseNumber={caseNumber}
|
|
||||||
hasAnalysis={!!analysis.data}
|
|
||||||
onUploaded={() => analysis.refetch()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
)}
|
|
||||||
</AppShell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { use } from "react";
|
import { use, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { AppShell } from "@/components/app-shell";
|
import { AppShell } from "@/components/app-shell";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
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 { Button } from "@/components/ui/button";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { CaseHeader } from "@/components/cases/case-header";
|
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 { DraftsPanel } from "@/components/cases/drafts-panel";
|
||||||
import { DecisionBlocksPanel } from "@/components/cases/decision-blocks-panel";
|
import { DecisionBlocksPanel } from "@/components/cases/decision-blocks-panel";
|
||||||
import { LegalArgumentsPanel } from "@/components/cases/legal-arguments-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 { AgentActivityFeed } from "@/components/cases/agent-activity-feed";
|
||||||
import { AgentActivityPreview } from "@/components/cases/agent-activity-preview";
|
import { AgentActivityPreview } from "@/components/cases/agent-activity-preview";
|
||||||
import { UploadSheet } from "@/components/documents/upload-sheet";
|
import { UploadSheet } from "@/components/documents/upload-sheet";
|
||||||
import { useCase, useStartWorkflow } from "@/lib/api/cases";
|
import { useCase, useStartWorkflow } from "@/lib/api/cases";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
Play, Loader2, LayoutGrid, Scale, FileText, MessageSquare, Users,
|
Play, Loader2, LayoutGrid, Scale, BadgeCheck, FileText, MessageSquare, Users,
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
@@ -33,6 +38,7 @@ export default function CaseDetailPage({
|
|||||||
params: Promise<{ caseNumber: string }>;
|
params: Promise<{ caseNumber: string }>;
|
||||||
}) {
|
}) {
|
||||||
const { caseNumber } = use(params);
|
const { caseNumber } = use(params);
|
||||||
|
const [tab, setTab] = useState("overview");
|
||||||
const { data, isPending, error, refetch } = useCase(caseNumber);
|
const { data, isPending, error, refetch } = useCase(caseNumber);
|
||||||
const startWorkflow = useStartWorkflow(caseNumber);
|
const startWorkflow = useStartWorkflow(caseNumber);
|
||||||
const canStartWorkflow = data?.status === "new" || data?.status === "documents_ready";
|
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][] = [
|
const tabDefs: [string, string, LucideIcon][] = [
|
||||||
["overview", "סקירה", LayoutGrid],
|
["overview", "סקירה", LayoutGrid],
|
||||||
["arguments", "טיעונים", Scale],
|
["arguments", "טיעונים ועמדות", Scale],
|
||||||
|
["verify", "אימות פסיקה", BadgeCheck],
|
||||||
["decision", "ההחלטה", FileText],
|
["decision", "ההחלטה", FileText],
|
||||||
["drafts", "טיוטות והערות", MessageSquare],
|
["drafts", "טיוטות והערות", MessageSquare],
|
||||||
["agents", "סוכנים", Users],
|
["agents", "סוכנים", Users],
|
||||||
@@ -93,8 +102,11 @@ export default function CaseDetailPage({
|
|||||||
<>
|
<>
|
||||||
{data && <CaseEditDialog data={data} />}
|
{data && <CaseEditDialog data={data} />}
|
||||||
<UploadSheet caseNumber={caseNumber} />
|
<UploadSheet caseNumber={caseNumber} />
|
||||||
<Button asChild className="bg-gold text-white hover:bg-gold-deep border-transparent">
|
<Button
|
||||||
<Link href={`/cases/${caseNumber}/compose`}>פתח עורך החלטה</Link>
|
className="bg-gold text-white hover:bg-gold-deep border-transparent"
|
||||||
|
onClick={() => setTab("decision")}
|
||||||
|
>
|
||||||
|
פתח עורך החלטה
|
||||||
</Button>
|
</Button>
|
||||||
{canStartWorkflow && (
|
{canStartWorkflow && (
|
||||||
<Button
|
<Button
|
||||||
@@ -121,7 +133,7 @@ export default function CaseDetailPage({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell>
|
<AppShell>
|
||||||
<Tabs defaultValue="overview" dir="rtl">
|
<Tabs value={tab} onValueChange={setTab} dir="rtl">
|
||||||
{/* parchment band — header (title/chips/parties/actions) + tab strip */}
|
{/* parchment band — header (title/chips/parties/actions) + tab strip */}
|
||||||
{isPending ? (
|
{isPending ? (
|
||||||
<div className="-mx-10 -mt-10 mb-2 bg-parchment border-b border-rule px-10 pt-6 pb-4 space-y-3">
|
<div className="-mx-10 -mt-10 mb-2 bg-parchment border-b border-rule px-10 pt-6 pb-4 space-y-3">
|
||||||
@@ -144,10 +156,43 @@ export default function CaseDetailPage({
|
|||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="arguments" className="mt-0">
|
{/* טיעונים ועמדות — chair positions (editing) over the analyst's
|
||||||
|
issues, with the aggregated by-party arguments collapsible below
|
||||||
|
(merged from the deleted /compose editor, mockup 18f). */}
|
||||||
|
<TabsContent value="arguments" className="mt-0 space-y-4">
|
||||||
<Card className="bg-surface border-rule shadow-sm">
|
<Card className="bg-surface border-rule shadow-sm">
|
||||||
<CardContent className="px-6 py-5">
|
<CardContent className="px-6 py-5">
|
||||||
<LegalArgumentsPanel caseNumber={caseNumber} />
|
<PositionsPanel caseNumber={caseNumber} />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Accordion type="single" collapsible>
|
||||||
|
<AccordionItem
|
||||||
|
value="byparty"
|
||||||
|
className="overflow-hidden rounded-xl border border-rule bg-surface shadow-sm"
|
||||||
|
>
|
||||||
|
<AccordionTrigger className="px-6 py-4 hover:no-underline">
|
||||||
|
<span className="flex flex-1 flex-col items-start">
|
||||||
|
<span className="text-navy text-[0.95rem] font-bold">
|
||||||
|
טיעונים מאוגדים לפי צד
|
||||||
|
</span>
|
||||||
|
<span className="text-ink-muted text-[0.78rem]">
|
||||||
|
תצוגה מסונתזת של הטענות הגולמיות, מקובצת לפי צד וקדימות
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent className="px-6 pb-5 pt-0">
|
||||||
|
<LegalArgumentsPanel caseNumber={caseNumber} />
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* אימות פסיקה — per-argument supporting-precedent verify gate (#154),
|
||||||
|
relocated from the deleted /compose editor to a top-level tab. */}
|
||||||
|
<TabsContent value="verify" className="mt-0">
|
||||||
|
<Card className="bg-surface border-rule shadow-sm">
|
||||||
|
<CardContent className="px-6 py-5">
|
||||||
|
<CitationVerificationPanel caseNumber={caseNumber} />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
351
web-ui/src/components/cases/positions-panel.tsx
Normal file
351
web-ui/src/components/cases/positions-panel.tsx
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useRef, useState } from "react";
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { SubsectionCard } from "@/components/compose/subsection-card";
|
||||||
|
import { PrecedentsSection } from "@/components/compose/precedents-section";
|
||||||
|
import { Markdown } from "@/components/ui/markdown";
|
||||||
|
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";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* PositionsPanel — the chair's editing surface for the analyst's threshold
|
||||||
|
* claims + issues (chair positions feed block י). Extracted verbatim from the
|
||||||
|
* former /compose "עמדות וטענות" tab when the decision editor was merged into
|
||||||
|
* the main case page (X17 #3). Lives inside the "טיעונים ועמדות" tab above the
|
||||||
|
* collapsible by-party aggregated arguments. The 12-block editor + citation
|
||||||
|
* verification moved to their own top-level tabs; /compose was deleted.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ── Staged-pipeline indicator text — 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 (
|
||||||
|
<section className="space-y-2">
|
||||||
|
<h3 className="text-[0.78rem] uppercase tracking-[0.08em] text-gold-deep font-semibold">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
<Markdown content={content.trim()} />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── "השלמה והעברה" rail card — DOCX export, analysis upload/download (real) ──
|
||||||
|
function FinishRail({
|
||||||
|
caseNumber,
|
||||||
|
hasAnalysis,
|
||||||
|
onUploaded,
|
||||||
|
}: {
|
||||||
|
caseNumber: string;
|
||||||
|
hasAnalysis: boolean;
|
||||||
|
onUploaded: () => void;
|
||||||
|
}) {
|
||||||
|
const fileRef = useRef<HTMLInputElement>(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 (
|
||||||
|
<Card className="bg-surface border-rule shadow-sm">
|
||||||
|
<CardContent className="px-4 py-4">
|
||||||
|
<h3 className="text-navy text-[0.9rem] font-semibold mb-3">השלמה והעברה</h3>
|
||||||
|
|
||||||
|
<input
|
||||||
|
ref={fileRef}
|
||||||
|
type="file"
|
||||||
|
accept=".md"
|
||||||
|
className="hidden"
|
||||||
|
onChange={(e) => {
|
||||||
|
const f = e.target.files?.[0];
|
||||||
|
if (f) handleUpload(f);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
{hasAnalysis && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full justify-center"
|
||||||
|
onClick={() => {
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = `/api/cases/${caseNumber}/research/analysis/export-docx`;
|
||||||
|
a.click();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
ייצוא DOCX
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
className="w-full justify-center bg-gold text-white hover:bg-gold-deep"
|
||||||
|
disabled={uploading}
|
||||||
|
onClick={() => fileRef.current?.click()}
|
||||||
|
>
|
||||||
|
{uploading ? "מעלה…" : "העלאת ניתוח מעודכן"}
|
||||||
|
</Button>
|
||||||
|
{hasAnalysis && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full justify-center"
|
||||||
|
onClick={() => {
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = `/api/cases/${caseNumber}/research/analysis/download`;
|
||||||
|
a.download = `analysis-${caseNumber}.md`;
|
||||||
|
a.click();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
הורד ניתוח (MD)
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{uploadMsg && (
|
||||||
|
<p className={`text-xs mt-2 ${uploadMsg.ok ? "text-success" : "text-danger"}`}>
|
||||||
|
{uploadMsg.text}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* stage indicators — informational pointers, not actions */}
|
||||||
|
<div className="mt-3 space-y-0">
|
||||||
|
<div className="text-[0.78rem] text-ink-muted pt-2 border-t border-rule-soft">
|
||||||
|
<b className="text-navy">הרץ למידת-קול</b> — {voiceLearningText(learning.data)}
|
||||||
|
</div>
|
||||||
|
<div className="text-[0.78rem] text-ink-muted pt-2 mt-2 border-t border-rule-soft">
|
||||||
|
<b className="text-navy">הרץ אימות-הלכות</b> — {halachaExtractionText(learning.data)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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<string, typeof allPrecedents>();
|
||||||
|
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 (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-ink-muted text-[0.78rem]">
|
||||||
|
סוגיות-המחלוקת ועמדות הצדדים. עורך עמדת-היו״ר נשמר אוטומטית ומזין את בלוק י׳ (דיון והכרעה).
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{analysis.isPending ? (
|
||||||
|
<Card className="bg-surface border-rule shadow-sm">
|
||||||
|
<CardContent className="px-6 py-5 space-y-3">
|
||||||
|
<Skeleton className="h-6 w-48" />
|
||||||
|
<Skeleton className="h-4 w-96" />
|
||||||
|
<Skeleton className="h-32 w-full" />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : isNotFound ? (
|
||||||
|
<Card className="bg-surface border-rule shadow-sm">
|
||||||
|
<CardContent className="px-6 py-12 text-center space-y-3">
|
||||||
|
<div className="text-gold text-3xl" aria-hidden>❦</div>
|
||||||
|
<h2 className="text-navy text-lg mb-0">
|
||||||
|
טרם בוצע ניתוח משפטי לתיק זה
|
||||||
|
</h2>
|
||||||
|
<p className="text-ink-muted text-sm max-w-md mx-auto">
|
||||||
|
לאחר שקובץ <code>analysis-and-research.md</code> ייווצר, תוכלי
|
||||||
|
לערוך כאן את עמדת הוועדה לכל טענת סף וסוגיה.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : analysis.error ? (
|
||||||
|
<Card className="bg-danger-bg border-danger/40">
|
||||||
|
<CardContent className="px-6 py-5 text-center">
|
||||||
|
<p className="text-danger">{analysis.error.message}</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : analysis.data ? (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{analysis.data.threshold_claims &&
|
||||||
|
analysis.data.threshold_claims.length > 0 && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h2 className="text-navy text-lg font-semibold mb-0">טענות סף</h2>
|
||||||
|
<span className="text-[0.72rem] rounded-full bg-gold-wash text-gold-deep px-2 py-0.5 border border-gold/40 tabular-nums">
|
||||||
|
{analysis.data.threshold_claims.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-3 lg:grid-cols-2 items-start">
|
||||||
|
{analysis.data.threshold_claims.map((tc) => (
|
||||||
|
<SubsectionCard
|
||||||
|
key={tc.id}
|
||||||
|
caseNumber={caseNumber}
|
||||||
|
item={tc}
|
||||||
|
precedents={precedentsBySection.get(tc.id) ?? []}
|
||||||
|
practiceArea={practiceArea}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{analysis.data.issues && analysis.data.issues.length > 0 && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h2 className="text-navy text-lg font-semibold mb-0">סוגיות להכרעה</h2>
|
||||||
|
<span className="text-[0.72rem] rounded-full bg-gold-wash text-gold-deep px-2 py-0.5 border border-gold/40 tabular-nums">
|
||||||
|
{analysis.data.issues.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-3 lg:grid-cols-2 items-start">
|
||||||
|
{analysis.data.issues.map((iss) => (
|
||||||
|
<SubsectionCard
|
||||||
|
key={iss.id}
|
||||||
|
caseNumber={caseNumber}
|
||||||
|
item={iss}
|
||||||
|
precedents={precedentsBySection.get(iss.id) ?? []}
|
||||||
|
practiceArea={practiceArea}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!analysis.data.threshold_claims?.length &&
|
||||||
|
!analysis.data.issues?.length && (
|
||||||
|
<Card className="bg-surface border-rule">
|
||||||
|
<CardContent className="px-6 py-10 text-center text-ink-muted">
|
||||||
|
לא נמצאו טענות סף או סוגיות בניתוח זה.
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Card className="bg-surface border-rule shadow-sm">
|
||||||
|
<CardContent className="px-6 py-5 space-y-5">
|
||||||
|
<h2 className="text-navy text-lg font-semibold mb-0">רקע לניתוח</h2>
|
||||||
|
<ProseSection title="צד מיוצג" content={analysis.data.represented_party} />
|
||||||
|
<ProseSection title="רקע דיוני" content={analysis.data.procedural_background} />
|
||||||
|
<ProseSection title="עובדות מוסכמות" content={analysis.data.agreed_facts} />
|
||||||
|
<ProseSection title="עובדות במחלוקת" content={analysis.data.disputed_facts} />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{analysis.data.conclusions?.trim() && (
|
||||||
|
<Card className="bg-gold-wash border-gold/40 shadow-sm">
|
||||||
|
<CardContent className="px-6 py-5 space-y-3">
|
||||||
|
<h2 className="text-gold-deep text-lg font-semibold mb-0">מסקנות</h2>
|
||||||
|
<Markdown content={analysis.data.conclusions.trim()} />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{/* case-level פסיקה מצורפת + finish/export gates (INV-IA3: not removed) */}
|
||||||
|
<div className="grid gap-4 lg:grid-cols-2 items-start pt-2">
|
||||||
|
<Card className="bg-surface border-rule shadow-sm">
|
||||||
|
<CardContent className="px-5 py-4">
|
||||||
|
<h3 className="text-navy text-[0.9rem] font-semibold mb-1">פסיקה מצורפת</h3>
|
||||||
|
<p className="text-[0.72rem] text-ink-muted mb-3">
|
||||||
|
ציטוטים התומכים בעמדה באופן רוחבי — ישולבו בפתיחת בלוק י (דיון).
|
||||||
|
</p>
|
||||||
|
<PrecedentsSection
|
||||||
|
caseNumber={caseNumber}
|
||||||
|
sectionId={null}
|
||||||
|
precedents={caseLevelPrecedents}
|
||||||
|
practiceArea={practiceArea}
|
||||||
|
emptyHelperText="עדיין לא צורפה פסיקה כללית לתיק"
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<FinishRail
|
||||||
|
caseNumber={caseNumber}
|
||||||
|
hasAnalysis={!!analysis.data}
|
||||||
|
onUploaded={() => analysis.refetch()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,9 +3,8 @@
|
|||||||
* header. Reflects the current section so the user always sees where they
|
* header. Reflects the current section so the user always sees where they
|
||||||
* are without scanning the nav row.
|
* are without scanning the nav row.
|
||||||
*
|
*
|
||||||
* Special-cases case routes (`/cases/{caseNumber}` and `/compose`) so the
|
* Special-cases case routes (`/cases/{caseNumber}`) so the subtitle includes
|
||||||
* subtitle includes the case number — the most useful piece of context
|
* the case number — the most useful piece of context during decision drafting.
|
||||||
* during decision drafting.
|
|
||||||
*/
|
*/
|
||||||
export function headerSubtitle(pathname: string): string {
|
export function headerSubtitle(pathname: string): string {
|
||||||
if (pathname === "/") return "בית";
|
if (pathname === "/") return "בית";
|
||||||
@@ -13,9 +12,8 @@ export function headerSubtitle(pathname: string): string {
|
|||||||
if (pathname.startsWith("/cases/")) {
|
if (pathname.startsWith("/cases/")) {
|
||||||
const [, , slug] = pathname.split("/");
|
const [, , slug] = pathname.split("/");
|
||||||
if (!slug || slug === "new") return "תיק חדש";
|
if (!slug || slug === "new") return "תיק חדש";
|
||||||
const isCompose = pathname.includes("/compose");
|
|
||||||
const decoded = decodeURIComponent(slug);
|
const decoded = decodeURIComponent(slug);
|
||||||
return isCompose ? `ערר ${decoded} · ניסוח` : `ערר ${decoded}`;
|
return `ערר ${decoded}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pathname.startsWith("/archive")) return "ארכיון";
|
if (pathname.startsWith("/archive")) return "ארכיון";
|
||||||
|
|||||||
Reference in New Issue
Block a user