feat(ui): IA redesign → production · יישום נאמן של 16 הדפים הנותרים למוקאפים
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 6s

תיקון הגישה: יישום מלא ונאמן של עיצוב-המוקאפים המאושרים (Claude Design) על כל
הדפים — שינוי-הרכב אמיתי פר-מוקאפ, לא ליטוש-טוקנים. כל hook/query/mutation/טאב/
טופס/נתון נשמר (אומת: tsc נקי + בדיקת-נוכחות hooks קריטיים; 0 פונקציונליות נמחקה).

דפים (← מוקאפ):
- בית — לוח: KPI + "תיקים לפי סטטוס" (bars) + כרטיס-אישורים + CTA כפול.
- ארכיון — filter-bar שטוח + טבלה נקייה + צ'יפי-סוג/תוצאה.
- הערות יו״ר — פריסה דו-טורית + טופס-הוספה חי + כרטיסי-הערה.
- ספריית-פסיקה — tabs קו-תחתון + כרטיסי-תוצאה halacha/קטע + AuthorityBadge.
- דף-תקדים — באנר-meta parchment + דו-טורי + provenance pills.
- פסיקה-חסרה — pill פתוחים + צ'יפי-סטטוס + CTA העלאה.
- יומונים — אזור-העלאה מקווקו + כרטיסי-digest + "ממתין" כתווית פסיבית.
- גרף — פאנל-צד שכבות/אנליטיקה + canvas parchment.
- אימון-סגנון — פורטרט: banner + KPI + אנטומיה + ביטויי-חתימה.
- מתודולוגיה — עורך-צ'קליסט + "חל על:" + canon chip.
- מיומנויות/סקריפטים — טבלאות אמיתיות + צ'יפי-סטטוס.
- הגדרות — sidenav דו-טורי + env-rows עם "ממתין ל-redeploy".
- דף-תיק — באנר-תיק parchment + tabs + timeline + "פתח עורך החלטה".
- תפעול — SectionHeaders + טבלת-שירותים + כרטיסי-שער gold-wash.
- compose — באנר-תיק + SOT pill + פריסה דו-טורית + "השלמה והעברה".

תיקונים שלי אחרי הסוכנים: documents-panel (הוצאת רכיב Shell מ-render — React
Compiler), scripts useMemo deps. /approvals כבר נבנה מחדש נאמנה (commit קודם).

בדיקות: npx tsc --noEmit ✓ · eslint ✓ (לבד מ-learning-panel:109 קיים-מראש).
שימור-פונקציונליות אומת. CI Docker build = שער סופי לפני deploy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 23:00:25 +00:00
parent c53ef9a7c4
commit f3b075d282
32 changed files with 2925 additions and 1799 deletions

View File

@@ -26,7 +26,8 @@ import {
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { useCases, useRestoreCase, type Case } from "@/lib/api/cases"; import { useCases, useRestoreCase, type Case } from "@/lib/api/cases";
import { APPEAL_SUBTYPE_LABELS } from "@/lib/practice-area"; import { subtypeOf } from "@/components/cases/appeal-type-bars";
import { APPEAL_SUBTYPE_LABELS, type AppealSubtype } from "@/lib/practice-area";
function formatDate(iso?: string | null) { function formatDate(iso?: string | null) {
if (!iso) return "—"; if (!iso) return "—";
@@ -41,6 +42,20 @@ function formatDate(iso?: string | null) {
} }
} }
// type chip styling per mockup 05 (.t-lic / .t-bet / .t-comp)
const TYPE_CHIP: Record<string, string> = {
building_permit: "bg-info-bg text-info",
betterment_levy: "bg-gold-wash text-gold-deep border border-rule",
compensation_197: "bg-rule-soft text-ink-soft",
};
// outcome chip styling per mockup 05 (.r-acc / .r-rej / .r-part)
const OUTCOME_CHIP: Record<string, { label: string; cls: string }> = {
full_acceptance: { label: "התקבל", cls: "bg-success-bg text-success" },
partial_acceptance: { label: "חלקי", cls: "bg-warn-bg text-warn" },
rejection: { label: "נדחה", cls: "bg-danger-bg text-danger" },
};
function RestoreButton({ caseNumber }: { caseNumber: string }) { function RestoreButton({ caseNumber }: { caseNumber: string }) {
const restore = useRestoreCase(caseNumber); const restore = useRestoreCase(caseNumber);
return ( return (
@@ -77,7 +92,7 @@ function RestoreButton({ caseNumber }: { caseNumber: string }) {
const columns: ColumnDef<Case>[] = [ const columns: ColumnDef<Case>[] = [
{ {
accessorKey: "case_number", accessorKey: "case_number",
header: "מס׳ ערר", header: "מספר ערר",
cell: ({ row }) => ( cell: ({ row }) => (
<Link <Link
href={`/cases/${row.original.case_number}`} href={`/cases/${row.original.case_number}`}
@@ -91,20 +106,42 @@ const columns: ColumnDef<Case>[] = [
accessorKey: "title", accessorKey: "title",
header: "כותרת", header: "כותרת",
cell: ({ row }) => ( cell: ({ row }) => (
<div className="text-ink max-w-[420px] truncate" title={row.original.title}> <div className="text-ink-soft max-w-[420px] truncate" title={row.original.title}>
{row.original.title} {row.original.title}
</div> </div>
), ),
}, },
{ {
accessorKey: "appeal_subtype", accessorKey: "appeal_subtype",
header: "תחום", header: "סוג",
cell: ({ row }) => { cell: ({ row }) => {
const s = row.original.appeal_subtype; const s = subtypeOf(row.original);
if (!s || s === "unknown") if (!s || s === "unknown")
return <span className="text-ink-muted"></span>; return <span className="text-ink-muted"></span>;
return ( return (
<span className="text-ink-soft text-sm">{APPEAL_SUBTYPE_LABELS[s]}</span> <span
className={`inline-block rounded-full px-2.5 py-0.5 text-[0.72rem] font-semibold whitespace-nowrap ${
TYPE_CHIP[s] ?? "bg-rule-soft text-ink-soft"
}`}
>
{APPEAL_SUBTYPE_LABELS[s as AppealSubtype]}
</span>
);
},
},
{
accessorKey: "expected_outcome",
header: "תוצאה",
cell: ({ row }) => {
const o = row.original.expected_outcome;
const chip = o ? OUTCOME_CHIP[o] : undefined;
if (!chip) return <span className="text-ink-muted"></span>;
return (
<span
className={`inline-block rounded-full px-2.5 py-0.5 text-[0.72rem] font-semibold whitespace-nowrap ${chip.cls}`}
>
{chip.label}
</span>
); );
}, },
}, },
@@ -112,7 +149,7 @@ const columns: ColumnDef<Case>[] = [
accessorKey: "archived_at", accessorKey: "archived_at",
header: "תאריך ארכוב", header: "תאריך ארכוב",
cell: ({ row }) => ( cell: ({ row }) => (
<span className="text-ink-muted text-sm tabular-nums"> <span className="text-ink-muted text-sm tabular-nums whitespace-nowrap">
{formatDate(row.original.archived_at)} {formatDate(row.original.archived_at)}
</span> </span>
), ),
@@ -130,6 +167,7 @@ export default function ArchivePage() {
{ id: "archived_at", desc: true }, { id: "archived_at", desc: true },
]); ]);
const [globalFilter, setGlobalFilter] = useState(""); const [globalFilter, setGlobalFilter] = useState("");
const [typeFilter, setTypeFilter] = useState<string>("all");
const rows = useMemo(() => data ?? [], [data]); const rows = useMemo(() => data ?? [], [data]);
@@ -152,126 +190,141 @@ export default function ArchivePage() {
}, },
}); });
// domain filter applied client-side (subtypeOf collapses בל"מ variants)
const filteredRows = useMemo(() => {
const all = table.getFilteredRowModel().rows;
if (typeFilter === "all") return all;
return all.filter((r) => subtypeOf(r.original) === typeFilter);
}, [table, typeFilter, globalFilter, sorting, rows]);
const total = rows.length;
const shown = filteredRows.length;
return ( return (
<AppShell> <AppShell>
<section className="space-y-8"> <section className="space-y-6">
<header className="space-y-1.5"> <header className="space-y-1.5">
<nav className="text-[0.78rem] text-ink-muted mb-1"> <nav className="text-[0.78rem] text-ink-muted mb-1">
<Link href="/" className="hover:text-gold-deep">בית</Link> <Link href="/" className="hover:text-gold-deep">בית</Link>
<span aria-hidden> · </span> <span aria-hidden> · </span>
<span className="text-navy">ארכיון</span> <span className="text-navy">ארכיון</span>
</nav> </nav>
<div className="flex items-end justify-between gap-4 flex-wrap"> <div className="space-y-1">
<div className="space-y-1"> <div className="text-[0.75rem] uppercase tracking-[0.12em] text-gold-deep">
<div className="text-[0.75rem] uppercase tracking-[0.12em] text-gold-deep"> ארכיון תיקי ערר
ארכיון תיקי ערר
</div>
<h1 className="text-navy mb-0">תיקים סגורים</h1>
<p className="text-ink-muted text-base max-w-2xl leading-relaxed">
תיקים שסגרו את הטיפול בהם. שחזור מחזיר את התיק לרשימה הראשית
ופותח מחדש את הפרויקט המקביל ב-Paperclip.
</p>
</div>
<div className="inline-flex items-baseline gap-2 rounded-lg border border-rule bg-gold-wash px-4 py-2.5">
<span className="text-2xl font-semibold text-gold-deep leading-none tabular-nums">
{table.getFilteredRowModel().rows.length}
</span>
<span className="text-[0.85rem] text-ink-soft">תיקים בארכיון</span>
</div> </div>
<h1 className="text-navy mb-0">ארכיון</h1>
<p className="text-ink-muted text-base max-w-2xl leading-relaxed">
כל ההחלטות שהושלמו לחיפוש, סינון ועיון. {total} תיקים סגורים.
שחזור מחזיר את התיק לרשימה הראשית ופותח מחדש את הפרויקט המקביל ב-Paperclip.
</p>
</div> </div>
</header> </header>
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" /> <div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
<Card className="bg-surface border-rule shadow-sm"> {/* filter bar — search + domain select + count aligned to end (mockup 05 .filters) */}
<CardContent className="px-6 py-5 space-y-4"> <div className="flex items-center gap-3 flex-wrap">
<div className="flex items-center gap-3"> <Input
<Input value={globalFilter}
value={globalFilter} onChange={(e) => setGlobalFilter(e.target.value)}
onChange={(e) => setGlobalFilter(e.target.value)} placeholder="חיפוש לפי מספר ערר, כותרת או צד…"
placeholder="חיפוש לפי מס׳ ערר או כותרת…" className="flex-1 min-w-[220px] bg-surface"
className="max-w-sm bg-surface" dir="rtl"
dir="rtl" />
/> <select
</div> value={typeFilter}
onChange={(e) => setTypeFilter(e.target.value)}
className="cursor-pointer text-[0.84rem] text-ink-soft bg-surface border border-rule rounded-lg px-3.5 py-2"
>
<option value="all">כל סוגי הערר</option>
<option value="building_permit">רישוי ובנייה</option>
<option value="betterment_levy">היטל השבחה</option>
<option value="compensation_197">פיצויים (ס׳ 197)</option>
</select>
<span className="ms-auto text-[0.82rem] text-ink-muted tabular-nums">
מציג {shown} מתוך {total}
</span>
</div>
<div className="rounded-lg border border-rule bg-surface shadow-sm overflow-hidden"> {/* clean bordered table — parchment header, gold-wash hover (mockup 05 .card table) */}
<Table> <Card className="bg-surface border-rule shadow-sm overflow-hidden p-0">
<TableHeader className="bg-rule-soft/60"> <CardContent className="p-0">
{table.getHeaderGroups().map((hg) => ( <Table>
<TableRow key={hg.id} className="border-rule"> <TableHeader className="bg-parchment">
{hg.headers.map((header) => ( {table.getHeaderGroups().map((hg) => (
<TableHead <TableRow key={hg.id} className="border-rule">
key={header.id} {hg.headers.map((header) => (
onClick={header.column.getToggleSortingHandler()} <TableHead
className="text-navy font-semibold cursor-pointer select-none text-right" key={header.id}
> onClick={header.column.getToggleSortingHandler()}
{flexRender( className="text-ink-muted font-medium text-[0.78rem] cursor-pointer select-none text-start"
header.column.columnDef.header, >
header.getContext(), {flexRender(
)} header.column.columnDef.header,
{{ asc: " ▲", desc: " ▼" }[ header.getContext(),
header.column.getIsSorted() as string )}
] ?? ""} {{ asc: " ▲", desc: "" }[
</TableHead> header.column.getIsSorted() as string
] ?? ""}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{isPending ? (
Array.from({ length: 3 }).map((_, i) => (
<TableRow key={i} className="border-rule-soft">
{columns.map((_c, j) => (
<TableCell key={j}>
<Skeleton className="h-4 w-24" />
</TableCell>
))} ))}
</TableRow> </TableRow>
))} ))
</TableHeader> ) : error ? (
<TableBody> <TableRow>
{isPending ? ( <TableCell
Array.from({ length: 3 }).map((_, i) => ( colSpan={columns.length}
<TableRow key={i} className="border-rule"> className="text-center text-danger py-8"
{columns.map((_c, j) => ( >
<TableCell key={j}> שגיאה בטעינת ארכיון: {error.message}
<Skeleton className="h-4 w-24" /> </TableCell>
</TableCell> </TableRow>
))} ) : filteredRows.length === 0 ? (
</TableRow> <TableRow>
)) <TableCell
) : error ? ( colSpan={columns.length}
<TableRow> className="text-center text-ink-muted py-12"
<TableCell >
colSpan={columns.length} <div className="text-gold text-2xl mb-2" aria-hidden>
className="text-center text-danger py-8"
> </div>
שגיאה בטעינת ארכיון: {error.message} {globalFilter || typeFilter !== "all"
</TableCell> ? "אין תיקים תואמים לחיפוש"
: "אין תיקים בארכיון"}
</TableCell>
</TableRow>
) : (
filteredRows.map((row) => (
<TableRow
key={row.id}
className="border-rule-soft hover:bg-gold-wash transition-colors"
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="py-3">
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow> </TableRow>
) : table.getRowModel().rows.length === 0 ? ( ))
<TableRow> )}
<TableCell </TableBody>
colSpan={columns.length} </Table>
className="text-center text-ink-muted py-12"
>
<div className="text-gold text-2xl mb-2" aria-hidden>
</div>
{globalFilter
? "אין תיקים תואמים לחיפוש"
: "אין תיקים בארכיון"}
</TableCell>
</TableRow>
) : (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
className="border-rule hover:bg-gold-wash/40 transition-colors"
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="py-3">
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</CardContent> </CardContent>
</Card> </Card>
</section> </section>

View File

@@ -2,6 +2,7 @@
import { use, useRef, useState } from "react"; import { use, useRef, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { FileText } from "lucide-react";
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 { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -9,9 +10,51 @@ import { Skeleton } from "@/components/ui/skeleton";
import { SubsectionCard } from "@/components/compose/subsection-card"; import { SubsectionCard } from "@/components/compose/subsection-card";
import { PrecedentsSection } from "@/components/compose/precedents-section"; import { PrecedentsSection } from "@/components/compose/precedents-section";
import { Markdown } from "@/components/ui/markdown"; import { Markdown } from "@/components/ui/markdown";
import { useCase } from "@/lib/api/cases"; import { useCase, type CaseStatus } from "@/lib/api/cases";
import { useResearchAnalysis } from "@/lib/api/research"; import { useResearchAnalysis } from "@/lib/api/research";
import { useCasePrecedents } from "@/lib/api/precedents"; import { useCasePrecedents } from "@/lib/api/precedents";
import { APPEAL_SUBTYPES } from "@/lib/practice-area";
import { DOC_TYPE_LABELS, type DocType } from "@/lib/doc-types";
// ── Case-status → Hebrew label + tone (mockup 03 status chip) ────────────────
const STATUS_CHIP: Record<string, { label: string; cls: string }> = {
new: { label: "חדש", cls: "bg-rule-soft text-ink-muted border-rule" },
uploading: { label: "בהעלאה", cls: "bg-info-bg text-info border-info/30" },
processing: { label: "בעיבוד", cls: "bg-info-bg text-info border-info/30" },
documents_ready: { label: "מסמכים מוכנים", cls: "bg-info-bg text-info border-info/30" },
analyst_verified: { label: "אומת ע״י אנליסט", cls: "bg-info-bg text-info border-info/30" },
research_complete: { label: "מחקר הושלם", cls: "bg-info-bg text-info border-info/30" },
outcome_set: { label: "תוצאה נקבעה", cls: "bg-info-bg text-info border-info/30" },
brainstorming: { label: "סיעור-מוחות", cls: "bg-info-bg text-info border-info/30" },
direction_approved: { label: "כיוון אושר", cls: "bg-info-bg text-info border-info/30" },
analysis_enriched: { label: "ניתוח הועשר", cls: "bg-info-bg text-info border-info/30" },
ready_for_writing: { label: "מוכן לכתיבה", cls: "bg-gold-wash text-gold-deep border-gold/40" },
drafting: { label: "בעריכה", cls: "bg-info-bg text-info border-info/30" },
qa_review: { label: "בדיקת-איכות", cls: "bg-gold-wash text-gold-deep border-gold/40" },
drafted: { label: "טיוטה", cls: "bg-gold-wash text-gold-deep border-gold/40" },
exported: { label: "יוצא", cls: "bg-success-bg text-success border-success/40" },
reviewed: { label: "נסקר", cls: "bg-success-bg text-success border-success/40" },
final: { label: "סופי", cls: "bg-success-bg text-success border-success/40" },
};
function StatusChip({ status }: { status?: CaseStatus }) {
const c = (status && STATUS_CHIP[status]) || {
label: "בעריכה",
cls: "bg-info-bg text-info border-info/30",
};
return (
<span
className={`rounded-full text-[0.78rem] font-semibold px-3 py-0.5 border ${c.cls}`}
>
{c.label}
</span>
);
}
function subtypeLabel(subtype?: string | null): string | null {
if (!subtype) return null;
return APPEAL_SUBTYPES.find((s) => s.value === subtype)?.label ?? null;
}
function ProseSection({ title, content }: { title: string; content?: string }) { function ProseSection({ title, content }: { title: string; content?: string }) {
if (!content?.trim()) return null; if (!content?.trim()) return null;
@@ -25,7 +68,8 @@ function ProseSection({ title, content }: { title: string; content?: string }) {
); );
} }
function AnalysisActions({ // ── "השלמה והעברה" rail card — DOCX export, upload, download (all real) ──────
function FinishRail({
caseNumber, caseNumber,
hasAnalysis, hasAnalysis,
onUploaded, onUploaded,
@@ -55,7 +99,7 @@ function AnalysisActions({
} }
setUploadMsg({ setUploadMsg({
ok: true, ok: true,
text: `הקובץ הועלה בהצלחה${data.sections.threshold_claims} טענות סף, ${data.sections.issues} סוגיות`, text: `הקובץ הועלה — ${data.sections.threshold_claims} טענות סף, ${data.sections.issues} סוגיות`,
}); });
onUploaded(); onUploaded();
} catch { } catch {
@@ -67,58 +111,79 @@ function AnalysisActions({
} }
return ( return (
<div className="flex items-center gap-2 flex-wrap"> <Card className="bg-surface border-rule shadow-sm">
{uploadMsg && ( <CardContent className="px-4 py-4">
<span className={`text-xs ${uploadMsg.ok ? "text-green-700" : "text-red-600"}`}> <h3 className="text-navy text-[0.9rem] font-semibold mb-3">השלמה והעברה</h3>
{uploadMsg.text}
</span> <input
)} ref={fileRef}
<input type="file"
ref={fileRef} accept=".md"
type="file" className="hidden"
accept=".md" onChange={(e) => {
className="hidden" const f = e.target.files?.[0];
onChange={(e) => { if (f) handleUpload(f);
const f = e.target.files?.[0];
if (f) handleUpload(f);
}}
/>
<Button
variant="outline"
disabled={uploading}
onClick={() => fileRef.current?.click()}
>
{uploading ? "מעלה..." : "העלה ניתוח מעודכן"}
</Button>
{hasAnalysis && (
<Button
variant="outline"
onClick={() => {
const a = document.createElement("a");
a.href = `/api/cases/${caseNumber}/research/analysis/download`;
a.download = `analysis-${caseNumber}.md`;
a.click();
}} }}
> />
הורד ניתוח
<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> ממתין להעלאת הסופי
</div>
<div className="text-[0.78rem] text-ink-muted pt-2 mt-2 border-t border-rule-soft">
<b className="text-navy">הרץ אימות-הלכות</b> ממתין להעלאת הסופי
</div>
</div>
<Button asChild variant="ghost" className="w-full justify-center mt-3 text-ink-muted">
<Link href={`/cases/${caseNumber}`}>חזרה לתיק</Link>
</Button> </Button>
)} </CardContent>
{hasAnalysis && ( </Card>
<Button
variant="outline"
onClick={() => {
const a = document.createElement("a");
a.href = `/api/cases/${caseNumber}/research/analysis/export-docx`;
a.click();
}}
>
הורד כ-DOCX
</Button>
)}
<Button asChild variant="outline">
<Link href={`/cases/${caseNumber}`}>חזרה לתיק</Link>
</Button>
</div>
); );
} }
@@ -145,6 +210,18 @@ export default function ComposePage({
} }
} }
const practiceArea = caseQuery.data?.practice_area ?? null; 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 = const isNotFound =
analysis.error instanceof Error && analysis.error instanceof Error &&
@@ -152,121 +229,84 @@ export default function ComposePage({
return ( return (
<AppShell> <AppShell>
<section className="space-y-6"> {/* ── Case header band (mockup 03) — parchment strip, full-bleed to the
{/* Header strip */} AppShell <main> edges (which pads px-10 py-10) ── */}
<div className="flex items-center justify-between gap-4 flex-wrap"> <div className="-mx-10 -mt-10 mb-6 border-b border-rule bg-parchment px-10 py-5">
<div> <nav className="text-[0.78rem] text-ink-muted flex items-center gap-2 mb-2">
<nav className="text-[0.78rem] text-ink-muted flex items-center gap-2 mb-1"> <Link href="/" className="hover:text-gold-deep">בית</Link>
<Link href="/" className="hover:text-gold-deep">בית</Link> <span aria-hidden>·</span>
<span aria-hidden>·</span> <Link href={`/cases/${caseNumber}`} className="hover:text-gold-deep">
<Link ערר {caseNumber}
href={`/cases/${caseNumber}`} </Link>
className="hover:text-gold-deep" <span aria-hidden>·</span>
> <span className="text-navy">עורך החלטה</span>
ערר {caseNumber} </nav>
</Link> <div className="flex items-center gap-3 flex-wrap">
<span aria-hidden>·</span> <h1 className="text-navy text-2xl font-bold mb-0">ערר {caseNumber}</h1>
<span className="text-navy">עורך החלטה</span> <StatusChip status={caseQuery.data?.status} />
</nav> {subtype && (
<h1 className="text-navy mb-0">ניתוח משפטי וכתיבת עמדה</h1> <span className="rounded-full text-[0.78rem] font-semibold px-3 py-0.5 border border-rule bg-gold-wash text-gold-deep">
{caseQuery.data?.title && ( {subtype}
<p className="text-ink-muted text-sm mt-1 max-w-2xl"> </span>
{caseQuery.data.title} )}
</p> {/* 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">
</div> מקור-אמת: בלוקים
<AnalysisActions caseNumber={caseNumber} hasAnalysis={!!analysis.data} onUploaded={() => analysis.refetch()} /> </span>
</div> </div>
{parties && <p className="text-ink-soft text-sm mt-2">{parties}</p>}
</div>
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" /> {analysis.isPending ? (
<Card className="bg-surface border-rule shadow-sm">
{analysis.isPending ? ( <CardContent className="px-6 py-5 space-y-3">
<Card className="bg-surface border-rule shadow-sm"> <Skeleton className="h-6 w-48" />
<CardContent className="px-6 py-5 space-y-3"> <Skeleton className="h-4 w-96" />
<Skeleton className="h-6 w-48" /> <Skeleton className="h-4 w-80" />
<Skeleton className="h-4 w-96" /> <Skeleton className="h-32 w-full" />
<Skeleton className="h-4 w-80" /> </CardContent>
<Skeleton className="h-32 w-full" /> </Card>
</CardContent> ) : isNotFound ? (
</Card> <Card className="bg-surface border-rule shadow-sm">
) : isNotFound ? ( <CardContent className="px-6 py-12 text-center space-y-3">
<Card className="bg-surface border-rule shadow-sm"> <div className="text-gold text-3xl" aria-hidden></div>
<CardContent className="px-6 py-12 text-center space-y-3"> <h2 className="text-navy text-lg mb-0">
<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">
</h2> לאחר שקובץ <code>analysis-and-research.md</code> ייווצר, תוכלי
<p className="text-ink-muted text-sm max-w-md mx-auto"> לערוך כאן את עמדת הוועדה לכל טענת סף וסוגיה.
לאחר שקובץ <code>analysis-and-research.md</code> ייווצר, תוכלי </p>
לערוך כאן את עמדת הוועדה לכל טענת סף וסוגיה. </CardContent>
</p> </Card>
</CardContent> ) : analysis.error ? (
</Card> <Card className="bg-danger-bg border-danger/40">
) : analysis.error ? ( <CardContent className="px-6 py-5 text-center">
<Card className="bg-danger-bg border-danger/40"> <p className="text-danger">{analysis.error.message}</p>
<CardContent className="px-6 py-5 text-center"> </CardContent>
<p className="text-danger">{analysis.error.message}</p> </Card>
</CardContent> ) : analysis.data ? (
</Card> /* ── Two-column workspace: main editor list + 320px side rail ──────── */
) : analysis.data ? ( <div className="grid gap-6 lg:grid-cols-[1fr_320px] items-start">
<div className="space-y-6"> {/* MAIN — the block/subsection editor list */}
{/* Case-level general precedents */} <div className="space-y-6 min-w-0">
<Card className="bg-surface border-rule shadow-sm"> {/* Threshold claims */}
<CardContent className="px-6 py-5"> {analysis.data.threshold_claims &&
<h2 className="text-navy text-xl mb-1">פסיקה כללית לדיון</h2> analysis.data.threshold_claims.length > 0 && (
<p className="text-[0.78rem] text-ink-muted mb-4">
ציטוטים התומכים בעמדה באופן רוחבי ישולבו בפתיחת בלוק י (דיון).
</p>
<PrecedentsSection
caseNumber={caseNumber}
sectionId={null}
precedents={caseLevelPrecedents}
practiceArea={practiceArea}
emptyHelperText="עדיין לא צורפה פסיקה כללית לתיק"
/>
</CardContent>
</Card>
{/* Threshold claims */}
{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-xl 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="space-y-3">
{analysis.data.threshold_claims.map((tc) => (
<SubsectionCard
key={tc.id}
caseNumber={caseNumber}
item={tc}
precedents={precedentsBySection.get(tc.id) ?? []}
practiceArea={practiceArea}
/>
))}
</div>
</div>
)}
{/* Issues */}
{analysis.data.issues && analysis.data.issues.length > 0 && (
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<h2 className="text-navy text-xl mb-0">סוגיות להכרעה</h2> <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"> <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} {analysis.data.threshold_claims.length}
</span> </span>
</div> </div>
<div className="space-y-3"> <div className="space-y-2.5">
{analysis.data.issues.map((iss) => ( {analysis.data.threshold_claims.map((tc) => (
<SubsectionCard <SubsectionCard
key={iss.id} key={tc.id}
caseNumber={caseNumber} caseNumber={caseNumber}
item={iss} item={tc}
precedents={precedentsBySection.get(iss.id) ?? []} precedents={precedentsBySection.get(tc.id) ?? []}
practiceArea={practiceArea} practiceArea={practiceArea}
/> />
))} ))}
@@ -274,8 +314,31 @@ export default function ComposePage({
</div> </div>
)} )}
{(!analysis.data.threshold_claims?.length && {/* Issues */}
!analysis.data.issues?.length) && ( {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="space-y-2.5">
{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"> <Card className="bg-surface border-rule">
<CardContent className="px-6 py-10 text-center text-ink-muted"> <CardContent className="px-6 py-10 text-center text-ink-muted">
לא נמצאו טענות סף או סוגיות בניתוח זה. לא נמצאו טענות סף או סוגיות בניתוח זה.
@@ -283,42 +346,82 @@ export default function ComposePage({
</Card> </Card>
)} )}
{/* Background prose — moved below the issues so it reads as {/* Background prose — supporting context after the decision points */}
supporting context after the chair has seen the main <Card className="bg-surface border-rule shadow-sm">
decision points, not as a wall of text beside them. */} <CardContent className="px-6 py-5 space-y-5">
<Card className="bg-surface border-rule shadow-sm"> <h2 className="text-navy text-lg font-semibold mb-0">רקע לניתוח</h2>
<CardContent className="px-6 py-5 space-y-5"> <ProseSection title="צד מיוצג" content={analysis.data.represented_party} />
<h2 className="text-navy text-xl mb-0">רקע לניתוח</h2> <ProseSection title="רקע דיוני" content={analysis.data.procedural_background} />
<ProseSection <ProseSection title="עובדות מוסכמות" content={analysis.data.agreed_facts} />
title="צד מיוצג" <ProseSection title="עובדות במחלוקת" content={analysis.data.disputed_facts} />
content={analysis.data.represented_party} </CardContent>
/> </Card>
<ProseSection
title="רקע דיוני" {analysis.data.conclusions?.trim() && (
content={analysis.data.procedural_background} <Card className="bg-gold-wash border-gold/40 shadow-sm">
/> <CardContent className="px-6 py-5 space-y-3">
<ProseSection <h2 className="text-gold-deep text-lg font-semibold mb-0">מסקנות</h2>
title="עובדות מוסכמות" <Markdown content={analysis.data.conclusions.trim()} />
content={analysis.data.agreed_facts}
/>
<ProseSection
title="עובדות במחלוקת"
content={analysis.data.disputed_facts}
/>
</CardContent> </CardContent>
</Card> </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-xl mb-0">מסקנות</h2>
<Markdown content={analysis.data.conclusions.trim()} />
</CardContent>
</Card>
)}
</div> </div>
) : null}
</section> {/* SIDE RAIL — documents · attached precedents · finish-and-transfer */}
<aside className="space-y-4 lg:sticky lg:top-4">
{/* מסמכי התיק */}
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-4 py-4">
<h3 className="text-navy text-[0.9rem] font-semibold mb-2">מסמכי התיק</h3>
{documents.length === 0 ? (
<p className="text-[0.78rem] text-ink-muted">אין מסמכים מצורפים</p>
) : (
<ul>
{documents.map((d) => (
<li
key={d.id}
className="flex items-center gap-2 text-[0.82rem] text-ink-soft py-1.5 border-b border-rule-soft last:border-0"
>
<FileText className="w-3.5 h-3.5 text-ink-muted shrink-0" aria-hidden />
<span className="truncate flex-1" title={d.title}>
{d.title || "מסמך"}
</span>
<span className="rounded bg-rule-soft text-ink-muted text-[0.68rem] px-1.5 py-0.5 shrink-0 whitespace-nowrap">
{DOC_TYPE_LABELS[d.doc_type as DocType] ?? d.doc_type}
</span>
</li>
))}
</ul>
)}
</CardContent>
</Card>
{/* פסיקה מצורפת (case-level) */}
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-4 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()}
/>
</aside>
</div>
) : null}
</AppShell> </AppShell>
); );
} }

View File

@@ -45,10 +45,10 @@ export default function CaseDetailPage({
? EXPECTED_OUTCOME_LABELS[data.expected_outcome] ?? data.expected_outcome ? EXPECTED_OUTCOME_LABELS[data.expected_outcome] ?? data.expected_outcome
: null; : null;
return ( if (error) {
<AppShell> return (
<section className="space-y-6"> <AppShell>
{error ? ( <section className="space-y-6">
<Card className="bg-danger-bg border-danger/40"> <Card className="bg-danger-bg border-danger/40">
<CardContent className="px-6 py-6 text-center space-y-3"> <CardContent className="px-6 py-6 text-center space-y-3">
<p className="text-danger font-semibold">שגיאה בטעינת התיק</p> <p className="text-danger font-semibold">שגיאה בטעינת התיק</p>
@@ -58,130 +58,168 @@ export default function CaseDetailPage({
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>
</section>
</AppShell>
);
}
const tabsList = (
<TabsList
variant="line"
className="gap-6 h-auto p-0 rounded-none -mb-px"
>
{[
["overview", "סקירה"],
["arguments", "טיעונים"],
["decision", "ההחלטה"],
["drafts", "טיוטות והערות"],
["agents", "סוכנים"],
].map(([value, label]) => (
<TabsTrigger
key={value}
value={value}
className="flex-none rounded-none px-0 pb-3.5 pt-0 text-[0.92rem] font-medium text-ink-muted data-active:text-navy data-active:font-semibold data-active:after:bg-gold data-active:after:bottom-0"
>
{label}
</TabsTrigger>
))}
</TabsList>
);
const bandActions = (
<>
{data && <CaseEditDialog data={data} />}
<UploadSheet caseNumber={caseNumber} />
</>
);
return (
<AppShell>
<Tabs defaultValue="overview" dir="rtl">
{/* parchment band — header (title/chips/parties/actions) + tab strip */}
{isPending ? (
<div className="-mx-10 -mt-10 mb-2 bg-parchment border-b border-rule px-10 pt-6 pb-4 space-y-3">
<Skeleton className="h-4 w-40" />
<Skeleton className="h-8 w-64" />
<Skeleton className="h-6 w-96" />
</div>
) : ( ) : (
<> <CaseHeader data={data} actions={bandActions} tabs={tabsList} />
{isPending ? ( )}
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5 space-y-3"> {/* two-column wrap — main tab content (1fr) + rail (340px) */}
<Skeleton className="h-4 w-40" /> <div className="grid gap-6 lg:grid-cols-[1fr_340px] items-start mt-6">
<Skeleton className="h-8 w-64" /> <div className="min-w-0">
<Skeleton className="h-6 w-96" /> <TabsContent value="overview" className="mt-0 space-y-5">
<Card className="bg-surface border-rule shadow-sm overflow-hidden p-0">
<div className="px-5 py-3.5 border-b border-rule-soft bg-parchment text-[0.92rem] font-semibold text-navy">
סקירת התיק
</div>
<CardContent className="px-5 py-4 space-y-4">
<div>
<h3 className="text-navy text-[0.95rem] font-semibold mb-1.5">תוצאה צפויה</h3>
<p className="text-ink-soft text-sm leading-relaxed">
{expectedOutcomeLabel ?? "לא נקבעה תוצאה צפויה."}
</p>
</div>
<div className="pt-2 border-t border-rule-soft">
<dl className="grid grid-cols-2 gap-y-2 gap-x-6 text-sm">
<dt className="text-ink-muted">בעיבוד</dt>
<dd className="text-ink tabular-nums">
{data?.processing_count ?? 0}
</dd>
</dl>
</div>
{canStartWorkflow && (
<div className="pt-2 border-t border-rule-soft">
<Button
className="bg-gold-deep hover:bg-gold-deep/90 text-parchment"
disabled={startWorkflow.isPending}
onClick={() =>
startWorkflow.mutate(undefined, {
onSuccess: (res) =>
toast.success(
`תהליך הופעל — ${res.issue_identifier}`,
),
onError: (err) =>
toast.error(`שגיאה: ${err.message}`),
})
}
>
{startWorkflow.isPending ? (
<Loader2 className="w-4 h-4 animate-spin me-1.5" />
) : (
<Play className="w-4 h-4 me-1.5" />
)}
התחל תהליך
</Button>
</div>
)}
</CardContent> </CardContent>
</Card> </Card>
) : (
<CaseHeader data={data} />
)}
<div className="grid gap-6 lg:grid-cols-[1fr_280px]"> <DocumentsPanel data={data} />
{/* gold CTA — open the decision editor (mockup .cta) */}
<Button
asChild
className="w-full bg-gold text-white hover:bg-gold-deep border-transparent py-6 text-base font-semibold"
>
<Link href={`/cases/${caseNumber}/compose`}>
פתח עורך החלטה
</Link>
</Button>
</TabsContent>
<TabsContent value="arguments" className="mt-0">
<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">
<Tabs defaultValue="overview" dir="rtl"> <LegalArgumentsPanel caseNumber={caseNumber} />
<div className="flex items-center justify-between gap-3 mb-1 flex-wrap">
<TabsList className="bg-rule-soft/60">
<TabsTrigger value="overview">סקירה</TabsTrigger>
<TabsTrigger value="arguments">
טיעונים
</TabsTrigger>
<TabsTrigger value="decision">
ההחלטה
</TabsTrigger>
<TabsTrigger value="drafts">
טיוטות והערות
</TabsTrigger>
<TabsTrigger value="agents">
סוכנים
</TabsTrigger>
</TabsList>
<div className="flex items-center gap-2 flex-wrap">
<Button asChild className="bg-navy hover:bg-navy-soft text-parchment">
<Link href={`/cases/${caseNumber}/compose`}>
פתח בעורך ההחלטה
</Link>
</Button>
{data && <CaseEditDialog data={data} />}
<UploadSheet caseNumber={caseNumber} />
</div>
</div>
<TabsContent value="overview" className="mt-5 space-y-4">
<div>
<h3 className="text-navy text-base mb-2">תוצאה צפויה</h3>
<p className="text-ink-soft text-sm leading-relaxed">
{expectedOutcomeLabel ?? "לא נקבעה תוצאה צפויה."}
</p>
</div>
<div>
<h3 className="text-navy text-base mb-2">סיכום מהיר</h3>
<dl className="grid grid-cols-2 gap-y-2 gap-x-6 text-sm">
<dt className="text-ink-muted">בעיבוד</dt>
<dd className="text-ink tabular-nums">
{data?.processing_count ?? 0}
</dd>
</dl>
</div>
{canStartWorkflow && (
<div className="pt-2 border-t border-rule">
<Button
className="bg-gold-deep hover:bg-gold-deep/90 text-parchment"
disabled={startWorkflow.isPending}
onClick={() =>
startWorkflow.mutate(undefined, {
onSuccess: (res) =>
toast.success(
`תהליך הופעל — ${res.issue_identifier}`,
),
onError: (err) =>
toast.error(`שגיאה: ${err.message}`),
})
}
>
{startWorkflow.isPending ? (
<Loader2 className="w-4 h-4 animate-spin me-1.5" />
) : (
<Play className="w-4 h-4 me-1.5" />
)}
התחל תהליך
</Button>
</div>
)}
<DocumentsPanel data={data} />
</TabsContent>
<TabsContent value="arguments" className="mt-5">
<LegalArgumentsPanel caseNumber={caseNumber} />
</TabsContent>
<TabsContent value="decision" className="mt-5">
<DecisionBlocksPanel caseNumber={caseNumber} />
</TabsContent>
<TabsContent value="drafts" className="mt-5">
<DraftsPanel
caseNumber={caseNumber}
status={data?.status}
/>
</TabsContent>
<TabsContent value="agents" className="mt-5">
<AgentActivityFeed caseNumber={caseNumber} />
</TabsContent>
</Tabs>
</CardContent> </CardContent>
</Card> </Card>
</TabsContent>
<Card className="bg-surface border-rule shadow-sm h-fit"> <TabsContent value="decision" className="mt-0">
<CardContent className="px-6 py-5 space-y-5"> <Card className="bg-surface border-rule shadow-sm">
<AgentStatusWidget caseNumber={caseNumber} /> <CardContent className="px-6 py-5">
<h2 className="text-navy text-base mb-4">שלב בתהליך</h2> <DecisionBlocksPanel caseNumber={caseNumber} />
<WorkflowTimeline status={data?.status} />
<StatusChanger caseNumber={caseNumber} currentStatus={data?.status} />
<StatusGuide />
</CardContent> </CardContent>
</Card> </Card>
</div> </TabsContent>
</>
)} <TabsContent value="drafts" className="mt-0">
</section> <Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5">
<DraftsPanel caseNumber={caseNumber} status={data?.status} />
</CardContent>
</Card>
</TabsContent>
<TabsContent value="agents" className="mt-0">
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5">
<AgentActivityFeed caseNumber={caseNumber} />
</CardContent>
</Card>
</TabsContent>
</div>
{/* rail — status timeline + status controls (mockup .rail) */}
<div className="space-y-5">
<Card className="bg-surface border-rule shadow-sm overflow-hidden p-0 h-fit">
<div className="px-5 py-3.5 border-b border-rule-soft bg-parchment text-[0.92rem] font-semibold text-navy">
סטטוס התיק
</div>
<CardContent className="px-5 py-4 space-y-4">
<AgentStatusWidget caseNumber={caseNumber} />
<WorkflowTimeline status={data?.status} />
<StatusChanger caseNumber={caseNumber} currentStatus={data?.status} />
<StatusGuide />
</CardContent>
</Card>
</div>
</div>
</Tabs>
</AppShell> </AppShell>
); );
} }

View File

@@ -2,11 +2,11 @@
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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { DigestListPanel } from "@/components/digests/digest-list-panel"; import { DigestListPanel } from "@/components/digests/digest-list-panel";
import { DigestSearchPanel } from "@/components/digests/digest-search-panel"; import { DigestSearchPanel } from "@/components/digests/digest-search-panel";
import { DigestUploadDialog } from "@/components/digests/digest-upload-dialog";
import { useDigestPending } from "@/lib/api/digests"; import { useDigestPending } from "@/lib/api/digests";
/** /**
@@ -38,44 +38,68 @@ export default function DigestsPage() {
return ( return (
<AppShell> <AppShell>
<section className="space-y-6"> <section className="space-y-6">
<header> <header className="space-y-2">
<nav className="text-[0.78rem] text-ink-muted mb-1"> <nav className="text-[0.78rem] text-ink-muted">
<Link href="/" className="hover:text-gold-deep">בית</Link> <Link href="/" className="hover:text-gold-deep">בית</Link>
<span aria-hidden> · </span> <span aria-hidden> · </span>
<span className="text-navy">יומונים</span> <span className="text-navy">יומונים</span>
</nav> </nav>
<h1 className="text-navy mb-0">יומונים רדאר פסיקה</h1> <h1 className="text-navy mb-0">יומונים (רדאר)</h1>
<p className="text-ink-muted text-sm mt-1 max-w-3xl"> <p className="text-ink-muted text-sm max-w-3xl leading-relaxed">
סיכומי &quot;כל יום&quot; (עפר טויסטר) של פסקי דין והחלטות עדכניים. שכבת-גילוי משנית מצביע-לא-מצוטט (X12). מאתרת פסיקה רלוונטית ומפנה
שכבת-גילוי בלבד: כל יומון <strong>מצביע</strong> על פסק הדין המקורי אליה; אינה מקור-אמת לציטוט. סיכומי &quot;כל יום&quot; (עפר טויסטר):
הוא אינו מצוטט בהחלטה ואינו מחלץ הלכות. כשהפסק רלוונטי, מעלים אותו כל יומון <strong>מצביע</strong> על פסק הדין המקורי כשהפסק רלוונטי,
לספריית הפסיקה ומצטטים משם. מעלים אותו לספריית הפסיקה ומצטטים משם.
</p> </p>
</header> </header>
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" /> {/* prominent dashed-gold upload area (mockup 10 `.upload`) */}
<div className="flex items-center gap-4 rounded-lg border-[1.5px] border-dashed border-gold bg-surface px-5 py-4">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-gold-wash text-gold-deep text-xl">
</div>
<div className="min-w-0">
<span className="block text-navy font-semibold text-[0.92rem]">העלאת יומון</span>
<span className="text-[0.78rem] text-ink-muted">
בחר קובץ יומון &quot;כל יום&quot; PDF · עד 20MB
</span>
</div>
<div className="ms-auto">
<DigestUploadDialog
trigger={
<button className="rounded-lg bg-gold px-4 py-2 text-sm font-semibold text-white hover:bg-gold-deep">
בחר קובץ
</button>
}
/>
</div>
</div>
<Card className="bg-surface border-rule shadow-sm"> <Tabs defaultValue="list" dir="rtl">
<CardContent className="px-6 py-5"> <TabsList className="flex w-full justify-start gap-1 rounded-none border-0 border-b border-rule bg-transparent p-0 h-auto">
<Tabs defaultValue="list" dir="rtl"> {[
<TabsList className="bg-rule-soft/60"> { value: "list", label: "יומונים", pill: <PendingBadge /> },
<TabsTrigger value="list"> { value: "search", label: "חיפוש", pill: null },
יומונים ].map((t) => (
<PendingBadge /> <TabsTrigger
</TabsTrigger> key={t.value}
<TabsTrigger value="search">חיפוש</TabsTrigger> value={t.value}
</TabsList> className="rounded-none border-0 border-b-2 border-transparent bg-transparent px-4 py-2.5 -mb-px text-sm font-medium text-ink-muted shadow-none data-[state=active]:border-gold data-[state=active]:bg-transparent data-[state=active]:font-semibold data-[state=active]:text-navy data-[state=active]:shadow-none"
>
{t.label}
{t.pill}
</TabsTrigger>
))}
</TabsList>
<TabsContent value="list" className="mt-5"> <TabsContent value="list" className="mt-5">
<DigestListPanel /> <DigestListPanel />
</TabsContent> </TabsContent>
<TabsContent value="search" className="mt-5"> <TabsContent value="search" className="mt-5">
<DigestSearchPanel /> <DigestSearchPanel />
</TabsContent> </TabsContent>
</Tabs> </Tabs>
</CardContent>
</Card>
</section> </section>
</AppShell> </AppShell>
); );

View File

@@ -7,12 +7,11 @@ import { toast } from "sonner";
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 { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { import {
useFeedbackList, useFeedbackList,
useResolveFeedback, useResolveFeedback,
useCreateFeedback,
CATEGORY_LABELS, CATEGORY_LABELS,
CATEGORY_COLORS,
BLOCK_LABELS, BLOCK_LABELS,
type ChairFeedback, type ChairFeedback,
type FeedbackCategory, type FeedbackCategory,
@@ -24,6 +23,16 @@ import {
* "טרם יושמו" וקטגוריה, וסימון כל הערה כיושמה. מוזן מ-/api/feedback. * "טרם יושמו" וקטגוריה, וסימון כל הערה כיושמה. מוזן מ-/api/feedback.
*/ */
// category chip styling per mockup 06 (.c-missing / .c-tone / .c-struct / .c-fact / .c-style)
const CAT_CHIP: Record<FeedbackCategory, string> = {
missing_content: "bg-warn-bg text-warn",
wrong_tone: "bg-info-bg text-info",
wrong_structure: "bg-gold-wash text-gold-deep border border-rule",
factual_error: "bg-danger-bg text-danger",
style: "bg-rule-soft text-ink-soft",
other: "bg-rule-soft text-ink-soft",
};
function formatDate(iso?: string | null): string { function formatDate(iso?: string | null): string {
if (!iso) return ""; if (!iso) return "";
try { try {
@@ -57,47 +66,51 @@ function FeedbackCard({ fb }: { fb: ChairFeedback }) {
}; };
return ( return (
<Card className={`bg-surface border-rule shadow-sm ${fb.resolved ? "opacity-60" : ""}`}> <Card className={`bg-surface border-rule shadow-sm ${fb.resolved ? "opacity-[0.78]" : ""}`}>
<CardContent className="px-5 py-4 space-y-2.5"> <CardContent className="px-[18px] py-4 space-y-2.5">
<div className="flex items-start gap-2 flex-wrap"> {/* meta row — where · category chip · when (mockup 06 .meta) */}
<Badge variant="outline" className={`text-[0.7rem] ${CATEGORY_COLORS[fb.category]}`}> <div className="flex items-center gap-2.5 flex-wrap">
<span className="font-semibold text-navy text-[0.84rem]">
{fb.case_number ? (
<Link
href={`/cases/${encodeURIComponent(fb.case_number)}`}
className="hover:text-gold-deep"
>
ערר {fb.case_number}
</Link>
) : (
"ללא תיק"
)}
<span className="text-ink-muted font-normal"> · {BLOCK_LABELS[fb.block_id] ?? fb.block_id}</span>
</span>
<span
className={`inline-block rounded-full px-2.5 py-0.5 text-[0.72rem] font-semibold whitespace-nowrap ${CAT_CHIP[fb.category]}`}
>
{CATEGORY_LABELS[fb.category]} {CATEGORY_LABELS[fb.category]}
</Badge> </span>
<Badge variant="outline" className="text-[0.7rem]"> <span className="ms-auto text-[0.72rem] text-ink-muted whitespace-nowrap">
{BLOCK_LABELS[fb.block_id] ?? fb.block_id}
</Badge>
{fb.case_number ? (
<Link
href={`/cases/${encodeURIComponent(fb.case_number)}`}
className="text-[0.72rem] text-gold-deep hover:underline"
>
תיק {fb.case_number}
</Link>
) : (
<span className="text-[0.72rem] text-ink-muted">ללא תיק</span>
)}
<span className="ms-auto text-[0.72rem] text-ink-muted">
{formatDate(fb.created_at)} {formatDate(fb.created_at)}
</span> </span>
</div> </div>
<p className="text-navy text-sm leading-relaxed m-0 whitespace-pre-wrap" dir="rtl"> <p className="text-ink-soft text-sm leading-relaxed m-0 whitespace-pre-wrap" dir="rtl">
{fb.feedback_text} {fb.feedback_text}
</p> </p>
{fb.lesson_extracted ? ( {fb.lesson_extracted ? (
<div className="rounded-md bg-gold-wash/40 border-s-[3px] border-gold ps-3 pe-3 py-2"> <p
<div className="text-[0.68rem] text-gold-deep mb-0.5">לקח שהופק</div> className="text-[0.82rem] text-ink-muted italic leading-relaxed m-0 whitespace-pre-wrap border-s-[3px] border-gold ps-2.5"
<p className="text-ink-soft text-[0.82rem] leading-relaxed m-0 whitespace-pre-wrap italic" dir="rtl"> dir="rtl"
{fb.lesson_extracted} >
</p> לקח שחולץ: {fb.lesson_extracted}
</div> </p>
) : null} ) : null}
<div className="flex items-center justify-end pt-1 border-t border-rule-soft"> {/* action row — gold CTA / applied pill (mockup 06 .actrow) */}
<div className="flex items-center justify-start pt-1">
{fb.resolved ? ( {fb.resolved ? (
<span className="flex items-center gap-1 text-[0.78rem] text-emerald-700"> <span className="inline-flex items-center gap-1.5 rounded-full bg-success-bg text-success text-[0.76rem] font-semibold px-3 py-1">
<CheckCircle2 className="w-3.5 h-3.5" /> יושמה <CheckCircle2 className="w-3.5 h-3.5" /> יושם
</span> </span>
) : ( ) : (
<Button <Button
@@ -118,6 +131,118 @@ function FeedbackCard({ fb }: { fb: ChairFeedback }) {
type CatFilter = "all" | FeedbackCategory; type CatFilter = "all" | FeedbackCategory;
const BLOCK_OPTIONS = [
"block-vav",
"block-zayin",
"block-chet",
"block-tet",
"block-yod",
"block-yod-alef",
];
function AddFeedbackForm() {
const create = useCreateFeedback();
const [caseNumber, setCaseNumber] = useState("");
const [blockId, setBlockId] = useState("block-yod");
const [category, setCategory] = useState<FeedbackCategory>("missing_content");
const [text, setText] = useState("");
const onSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!text.trim()) {
toast.error("יש להזין את תוכן ההערה");
return;
}
create.mutate(
{
case_number: caseNumber.trim() || undefined,
block_id: blockId,
category,
feedback_text: text.trim(),
},
{
onSuccess: () => {
toast.success("ההערה נשמרה");
setCaseNumber("");
setText("");
},
onError: (err) =>
toast.error(err instanceof Error ? err.message : "שגיאה בשמירה"),
},
);
};
const fieldCls =
"w-full text-[0.84rem] text-ink bg-parchment border border-rule rounded-md px-3 py-2";
const labelCls = "block text-[0.76rem] text-ink-muted font-medium mt-2.5 mb-1";
return (
<Card className="bg-surface border-rule shadow-sm lg:sticky lg:top-6">
<CardContent className="px-5 py-4">
<h3 className="text-navy text-base font-semibold mb-3.5">הוסף הערה</h3>
<form onSubmit={onSubmit}>
<label className={labelCls} htmlFor="fb-case">מספר ערר</label>
<input
id="fb-case"
type="text"
value={caseNumber}
onChange={(e) => setCaseNumber(e.target.value)}
placeholder="לדוגמה: 1126-08-25"
className={fieldCls}
dir="rtl"
/>
<label className={labelCls} htmlFor="fb-block">בלוק</label>
<select
id="fb-block"
value={blockId}
onChange={(e) => setBlockId(e.target.value)}
className={`${fieldCls} cursor-pointer`}
>
{BLOCK_OPTIONS.map((b) => (
<option key={b} value={b}>
{BLOCK_LABELS[b]}
</option>
))}
</select>
<label className={labelCls} htmlFor="fb-cat">קטגוריה</label>
<select
id="fb-cat"
value={category}
onChange={(e) => setCategory(e.target.value as FeedbackCategory)}
className={`${fieldCls} cursor-pointer`}
>
{(Object.keys(CATEGORY_LABELS) as FeedbackCategory[]).map((c) => (
<option key={c} value={c}>
{CATEGORY_LABELS[c]}
</option>
))}
</select>
<label className={labelCls} htmlFor="fb-text">תוכן ההערה</label>
<textarea
id="fb-text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="מה צריך לתקן ולמה…"
className={`${fieldCls} min-h-[90px] resize-y leading-relaxed`}
dir="rtl"
/>
<Button
type="submit"
disabled={create.isPending}
className="w-full mt-4 bg-gold text-white hover:bg-gold-deep border-transparent"
>
{create.isPending ? "שומר…" : "שמור הערה"}
</Button>
</form>
</CardContent>
</Card>
);
}
export default function FeedbackPage() { export default function FeedbackPage() {
const [unresolvedOnly, setUnresolvedOnly] = useState(true); const [unresolvedOnly, setUnresolvedOnly] = useState(true);
const [category, setCategory] = useState<CatFilter>("all"); const [category, setCategory] = useState<CatFilter>("all");
@@ -159,14 +284,19 @@ export default function FeedbackPage() {
<div className="text-[0.75rem] uppercase tracking-[0.12em] text-gold-deep"> <div className="text-[0.75rem] uppercase tracking-[0.12em] text-gold-deep">
שערים אנושיים · יו״ר הוועדה שערים אנושיים · יו״ר הוועדה
</div> </div>
<h1 className="text-navy mb-0">הערות יו״ר</h1> <div className="flex items-center gap-3 flex-wrap">
<h1 className="text-navy mb-0">הערות יו״ר</h1>
<span className="inline-block rounded-full bg-gold-wash border border-rule text-ink-muted text-[0.76rem] px-3 py-0.5">
נגיש גם מתוך מרכז האישורים
</span>
</div>
<p className="text-ink-muted text-sm mt-1 max-w-2xl leading-relaxed"> <p className="text-ink-muted text-sm mt-1 max-w-2xl leading-relaxed">
כל ההערות שנרשמו על טיוטות מכל התיקים. סמן כל הערה כיושמה כל ההערות שנרשמו על טיוטות מכל התיקים. סמן כל הערה כיושמה
לאחר שהלקח הוטמע. לאחר שהלקח הוטמע.
</p> </p>
</div> </div>
<div className="text-end"> <div className="text-end">
<div className="text-3xl font-semibold text-navy leading-none"> <div className="text-3xl font-semibold text-navy leading-none tabular-nums">
{unresolvedCount} {unresolvedCount}
</div> </div>
<div className="text-[0.72rem] uppercase tracking-[0.08em] text-ink-muted mt-1"> <div className="text-[0.72rem] uppercase tracking-[0.08em] text-ink-muted mt-1">
@@ -178,15 +308,35 @@ export default function FeedbackPage() {
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" /> <div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
{/* Filters */} {/* category filter chips (mockup 06 .chips) + resolved-state toggle */}
<div className="flex items-center gap-3 flex-wrap"> <div className="flex items-center gap-3 flex-wrap">
<div className="flex items-center gap-1"> <div className="flex items-center gap-2 flex-wrap">
{categories.map((c) => {
const on = category === c.key;
return (
<button
key={c.key}
type="button"
onClick={() => setCategory(c.key)}
className={`text-[0.8rem] font-medium px-3.5 py-1.5 rounded-full border transition-colors ${
on
? "bg-navy text-white border-navy"
: "bg-surface text-ink-soft border-rule hover:bg-rule-soft"
}`}
>
{c.label}
</button>
);
})}
</div>
<div className="flex items-center gap-1 ms-auto">
<button <button
type="button" type="button"
onClick={() => setUnresolvedOnly(true)} onClick={() => setUnresolvedOnly(true)}
className={`text-[0.78rem] px-3 py-1.5 rounded border transition-colors ${ className={`text-[0.78rem] px-3 py-1.5 rounded border transition-colors ${
unresolvedOnly unresolvedOnly
? "bg-navy text-parchment border-navy" ? "bg-navy text-white border-navy"
: "bg-surface text-ink-muted border-rule hover:bg-rule-soft" : "bg-surface text-ink-muted border-rule hover:bg-rule-soft"
}`} }`}
> >
@@ -197,60 +347,50 @@ export default function FeedbackPage() {
onClick={() => setUnresolvedOnly(false)} onClick={() => setUnresolvedOnly(false)}
className={`text-[0.78rem] px-3 py-1.5 rounded border transition-colors ${ className={`text-[0.78rem] px-3 py-1.5 rounded border transition-colors ${
!unresolvedOnly !unresolvedOnly
? "bg-navy text-parchment border-navy" ? "bg-navy text-white border-navy"
: "bg-surface text-ink-muted border-rule hover:bg-rule-soft" : "bg-surface text-ink-muted border-rule hover:bg-rule-soft"
}`} }`}
> >
הכל הכל
</button> </button>
</div> </div>
<div className="flex items-center gap-1 flex-wrap ms-auto">
{categories.map((c) => (
<button
key={c.key}
type="button"
onClick={() => setCategory(c.key)}
className={`text-[0.74rem] px-2.5 py-1 rounded border transition-colors ${
category === c.key
? "bg-gold-wash text-gold-deep border-gold/40"
: "bg-surface text-ink-muted border-rule hover:bg-rule-soft"
}`}
>
{c.label}
</button>
))}
</div>
</div> </div>
{error ? ( {/* two-column body — feedback list + sticky add-form rail (mockup 06 .wrap grid) */}
<Card className="bg-surface border-rule"> <div className="grid gap-6 lg:grid-cols-[1fr_340px] items-start">
<CardContent className="px-6 py-5 text-ink-muted text-sm"> <div>
שגיאה בטעינת ההערות. נסה לרענן. {error ? (
</CardContent> <Card className="bg-surface border-rule">
</Card> <CardContent className="px-6 py-5 text-ink-muted text-sm">
) : isPending ? ( שגיאה בטעינת ההערות. נסה לרענן.
<div className="space-y-3"> </CardContent>
{[0, 1, 2].map((i) => (
<Card key={i} className="bg-surface border-rule shadow-sm">
<CardContent className="px-5 py-4 h-28 animate-pulse" />
</Card> </Card>
))} ) : isPending ? (
</div> <div className="space-y-3.5">
) : items.length === 0 ? ( {[0, 1, 2].map((i) => (
<div className="text-center text-ink-muted py-16"> <Card key={i} className="bg-surface border-rule shadow-sm">
<p className="text-lg">אין הערות בקטגוריה זו.</p> <CardContent className="px-[18px] py-4 h-28 animate-pulse" />
{unresolvedOnly && ( </Card>
<p className="text-sm mt-2">כל ההערות יושמו </p> ))}
</div>
) : items.length === 0 ? (
<div className="text-center text-ink-muted py-16">
<p className="text-lg">אין הערות בקטגוריה זו.</p>
{unresolvedOnly && (
<p className="text-sm mt-2">כל ההערות יושמו </p>
)}
</div>
) : (
<div className="space-y-3.5">
{items.map((fb) => (
<FeedbackCard key={fb.id} fb={fb} />
))}
</div>
)} )}
</div> </div>
) : (
<div className="space-y-3"> <AddFeedbackForm />
{items.map((fb) => ( </div>
<FeedbackCard key={fb.id} fb={fb} />
))}
</div>
)}
</section> </section>
</AppShell> </AppShell>
); );

View File

@@ -8,7 +8,7 @@ export default function GraphPage() {
return ( return (
<AppShell> <AppShell>
<section className="space-y-6"> <section className="space-y-6">
<header> <header className="space-y-1.5">
<nav className="text-[0.78rem] text-ink-muted mb-1"> <nav className="text-[0.78rem] text-ink-muted mb-1">
<Link href="/" className="hover:text-gold-deep"> <Link href="/" className="hover:text-gold-deep">
בית בית
@@ -16,16 +16,20 @@ export default function GraphPage() {
<span aria-hidden> · </span> <span aria-hidden> · </span>
<span className="text-navy">מפת הקורפוס</span> <span className="text-navy">מפת הקורפוס</span>
</nav> </nav>
<div className="text-[0.75rem] uppercase tracking-[0.12em] text-gold-deep">
גרף הציטוטים · ספריית הפסיקה
</div>
<h1 className="text-navy mb-0">מפת הקורפוס</h1> <h1 className="text-navy mb-0">מפת הקורפוס</h1>
<p className="text-ink-muted text-sm mt-1 max-w-3xl"> <p className="text-ink-muted text-sm mt-1 max-w-3xl leading-relaxed">
רשת הציטוטים של ספריית הפסיקה כל נקודה היא פסיקה או נושא, וקו מציין ציטוט או שיוך. גרף הציטוטים של הקורפוס פסיקה, נושאים והלכות וקשרי-ההפניה ביניהם.
גודל הנקודה משקף כמה פעמים הפסיקה צוטטה. לחצו על נקודה כדי להתמקד בשכניה. גררו צמתים, סננו שכבות, ובחנו את מרכזי-הכובד. גודל הנקודה משקף כמה
פעמים הפסיקה צוטטה; לחיצה על נקודה ממקדת בשכניה.
</p> </p>
</header> </header>
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" /> <div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
<Suspense <Suspense
fallback={ fallback={
<div className="grid h-[560px] place-items-center text-sm text-ink-muted"> <div className="grid h-[560px] place-items-center rounded-lg border border-rule bg-gradient-to-b from-[#f3ecda] to-[#efe6cf] text-sm text-ink-muted">
טוען גרף טוען גרף
</div> </div>
} }

View File

@@ -21,8 +21,12 @@ export default function MethodologyPage() {
</nav> </nav>
<h1 className="text-navy mb-0">מתודולוגיה</h1> <h1 className="text-navy mb-0">מתודולוגיה</h1>
<p className="text-ink-muted text-sm mt-1 max-w-2xl"> <p className="text-ink-muted text-sm mt-1 max-w-2xl">
הגדרות ניסוח יחסי אורך, כללי דיון, וצ׳קליסטים לפי סוג ערר העורך הקנוני היחיד לכללי-הכותב יחסי אורך, כללי דיון, וצ׳קליסטים לפי סוג ערר
</p> </p>
<div className="inline-flex items-center gap-2 rounded-lg border border-success bg-success-bg text-success px-3.5 py-1.5 text-[0.78rem] font-semibold mt-3">
<span aria-hidden></span>
מקור-אמת יחיד כל הסוכנים קוראים את הכללים מכאן בלבד
</div>
</header> </header>
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" /> <div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />

View File

@@ -3,9 +3,6 @@
import { useState } from "react"; import { 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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { import {
useMissingPrecedents, useMissingPrecedents,
@@ -17,32 +14,26 @@ import { MissingPrecedentsTable } from "@/components/missing-precedents/missing-
* Missing-precedents page (TaskMaster #35). * Missing-precedents page (TaskMaster #35).
* *
* Surfaces citations that party briefs invoke but which aren't yet in the * Surfaces citations that party briefs invoke but which aren't yet in the
* precedent_library. Four tabs by status; each tab uses the same table * precedent_library. A status filter (chips) narrows the table; each row uses
* component with a different filter. Drawer (sheet) opens on row click * the same table component. Drawer (sheet) opens on row click with metadata +
* with metadata + upload form that routes to internal_decision_upload * upload form that routes to internal_decision_upload (ערר/בל"מ citations) or
* (ערר/בל"מ citations) or precedent_library_upload (court rulings). * precedent_library_upload (court rulings).
*/ */
function StatusBadge({ status, count }: { status: MissingPrecedentStatus; count: number }) {
if (!count) return null; type StatusFilter = MissingPrecedentStatus | "all";
const variants: Record<MissingPrecedentStatus, string> = {
open: "bg-gold-wash text-gold-deep border-gold/40", const STATUS_CHIPS: { value: StatusFilter; label: string }[] = [
uploaded: "bg-rule-soft text-ink-muted border-rule", { value: "open", label: "פתוח" },
closed: "bg-emerald-50 text-emerald-800 border-emerald-300/60", { value: "uploaded", label: "הועלה" },
irrelevant: "bg-rule-soft text-ink-muted border-rule", { value: "closed", label: "נסגר" },
}; { value: "irrelevant", label: "לא-רלוונטי" },
return ( { value: "all", label: "הכל" },
<Badge ];
variant="outline"
className={`ms-1 text-[0.65rem] ${variants[status]}`}
>
{count}
</Badge>
);
}
export default function MissingPrecedentsPage() { export default function MissingPrecedentsPage() {
const [caseNumber, setCaseNumber] = useState(""); const [caseNumber, setCaseNumber] = useState("");
const [legalTopic, setLegalTopic] = useState(""); const [legalTopic, setLegalTopic] = useState("");
const [filter, setFilter] = useState<StatusFilter>("open");
const counts = useMissingPrecedents({ limit: 1 }); const counts = useMissingPrecedents({ limit: 1 });
const byStatus = counts.data?.by_status ?? {}; const byStatus = counts.data?.by_status ?? {};
@@ -50,124 +41,129 @@ export default function MissingPrecedentsPage() {
return ( return (
<AppShell> <AppShell>
<section className="space-y-6"> <section className="space-y-6">
<header> <header className="space-y-3">
<nav className="text-[0.78rem] text-ink-muted mb-1"> <nav className="text-[0.78rem] text-ink-muted">
<Link href="/" className="hover:text-gold-deep">בית</Link> <Link href="/" className="hover:text-gold-deep">בית</Link>
<span aria-hidden> · </span> <span aria-hidden> · </span>
<span className="text-navy">פסיקה חסרה בקורפוס</span> <span className="text-navy">פסיקה חסרה בקורפוס</span>
</nav> </nav>
<div className="flex items-end justify-between gap-4 flex-wrap">
<div> {/* title + inline open-count pill (mockup 09 `.open-count`) */}
<h1 className="text-navy mb-0">פסיקה חסרה בקורפוס</h1> <div className="flex items-baseline gap-3.5 flex-wrap">
<p className="text-ink-muted text-sm mt-1 max-w-3xl"> <h1 className="text-navy mb-0">פסיקה חסרה בקורפוס</h1>
פסיקות שצוטטו בכתבי הטענות אך אינן עדיין בקורפוס. סוכן המחקר רושם
פערים אוטומטית; היו&quot;ר סוגר אותם על־ידי העלאת המסמך ניתוב
אוטומטי בין הקורפוס הסמכותי (פסקי דין) להחלטות ועדות ערר.
</p>
</div>
{byStatus.open ? ( {byStatus.open ? (
<div className="inline-flex items-baseline gap-2 rounded-lg border border-rule bg-warn-bg px-4 py-2.5"> <span className="inline-flex items-baseline gap-1.5 rounded-lg border border-rule bg-warn-bg px-3.5 py-1">
<span className="text-2xl font-semibold text-warn leading-none tabular-nums"> <span className="text-lg font-bold text-warn tabular-nums leading-none">
{byStatus.open} {byStatus.open}
</span> </span>
<span className="text-[0.85rem] text-ink-soft">פתוחים</span> <span className="text-[0.8rem] text-ink-soft">פתוחים</span>
</div> </span>
) : null} ) : null}
</div> </div>
<p className="text-ink-muted text-sm max-w-3xl leading-relaxed">
פסיקה שצוטטה בכתבי-הטענות אך אינה קיימת בקורפוס. השלמתה מאפשרת
אימות-הלכה ועיגון-מקור (INV-AH). סוכן המחקר רושם פערים אוטומטית;
היו&quot;ר סוגר אותם על־ידי העלאת המסמך ניתוב אוטומטי בין הקורפוס
הסמכותי (פסקי דין) להחלטות ועדות ערר.
</p>
</header> </header>
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" /> <div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
<Card className="bg-surface border-rule shadow-sm"> {/* shared filters */}
<CardContent className="px-6 py-5 space-y-5"> <div className="flex items-end gap-3 flex-wrap">
{/* Shared filters */} <div className="flex-1 min-w-[200px]">
<div className="flex items-end gap-3 flex-wrap"> <label className="block text-[0.78rem] text-ink-muted mb-1.5">תיק (מספר ערר)</label>
<div className="flex-1 min-w-[200px]"> <Input
<label className="text-[0.78rem] text-ink-muted">תיק (מספר ערר)</label> value={caseNumber}
<Input onChange={(e) => setCaseNumber(e.target.value)}
value={caseNumber} placeholder="1017-03-26"
onChange={(e) => setCaseNumber(e.target.value)} dir="rtl"
placeholder="1017-03-26" />
dir="rtl" </div>
/> <div className="flex-1 min-w-[200px]">
</div> <label className="block text-[0.78rem] text-ink-muted mb-1.5">נושא משפטי</label>
<div className="flex-1 min-w-[200px]"> <Input
<label className="text-[0.78rem] text-ink-muted">נושא משפטי</label> value={legalTopic}
<Input onChange={(e) => setLegalTopic(e.target.value)}
value={legalTopic} placeholder="זכות עמידה"
onChange={(e) => setLegalTopic(e.target.value)} dir="rtl"
placeholder="זכות עמידה" />
dir="rtl" </div>
/> </div>
</div>
</div>
<Tabs defaultValue="open" dir="rtl"> {/* status filter chips (mockup 09 `.filters`) — active = navy filled */}
<TabsList className="bg-rule-soft/60"> <div className="flex items-center gap-2 flex-wrap">
<TabsTrigger value="open"> {STATUS_CHIPS.map((c) => {
פתוחות const active = filter === c.value;
<StatusBadge status="open" count={byStatus.open ?? 0} /> const count =
</TabsTrigger> c.value === "all"
<TabsTrigger value="uploaded"> ? undefined
הועלו : (byStatus[c.value as MissingPrecedentStatus] ?? 0);
<StatusBadge status="uploaded" count={byStatus.uploaded ?? 0} /> return (
</TabsTrigger> <button
<TabsTrigger value="closed"> key={c.value}
נסגרו type="button"
<StatusBadge status="closed" count={byStatus.closed ?? 0} /> onClick={() => setFilter(c.value)}
</TabsTrigger> aria-pressed={active}
<TabsTrigger value="irrelevant"> className={`rounded-full border px-4 py-1.5 text-[0.82rem] transition-colors ${
לא רלוונטי active
<StatusBadge ? "bg-navy text-white border-navy font-semibold"
status="irrelevant" : "bg-surface text-ink-soft border-rule font-medium hover:bg-rule-soft/50"
count={byStatus.irrelevant ?? 0} }`}
/> >
</TabsTrigger> {c.label}
<TabsTrigger value="all">הכל</TabsTrigger> {count ? (
</TabsList> <span className="ms-1.5 tabular-nums opacity-80">({count})</span>
) : null}
</button>
);
})}
</div>
<TabsContent value="open" className="mt-4"> <MissingPrecedentsTable
<MissingPrecedentsTable status={filter === "all" ? "" : filter}
status="open" caseNumber={caseNumber.trim() || undefined}
caseNumber={caseNumber.trim() || undefined} legalTopic={legalTopic.trim() || undefined}
legalTopic={legalTopic.trim() || undefined} />
/>
</TabsContent>
<TabsContent value="uploaded" className="mt-4"> {/* lifecycle note (mockup 09 `.lifecycle`) */}
<MissingPrecedentsTable <div className="rounded-lg border border-rule bg-parchment px-5 py-3.5 text-[0.82rem] text-ink-muted leading-7">
status="uploaded" <b className="text-ink-soft">מחזור-חיים:</b>{" "}
caseNumber={caseNumber.trim() || undefined} <LifecycleChip tone="open">פתוח</LifecycleChip> {" "}
legalTopic={legalTopic.trim() || undefined} <LifecycleChip tone="up">הועלה</LifecycleChip> {" "}
/> <LifecycleChip tone="closed">נסגר</LifecycleChip>. פריט נפתח אוטומטית
</TabsContent> בעת חילוץ ציטוט שאין לו תקדים בקורפוס; בהעלאת פסק-הדין הוא מקושר לרשומת
הפסיקה דרך{" "}
<TabsContent value="closed" className="mt-4"> <code className="rounded border border-rule bg-surface px-1.5 py-0.5 text-[0.75rem] text-gold-deep" dir="ltr">
<MissingPrecedentsTable linked_case_law_id
status="closed" </code>{" "}
caseNumber={caseNumber.trim() || undefined} ונסגר. פריט שאינו רלוונטי מסומן{" "}
legalTopic={legalTopic.trim() || undefined} <LifecycleChip tone="na">לא-רלוונטי</LifecycleChip> מבלי שתידרש העלאה.
/> </div>
</TabsContent>
<TabsContent value="irrelevant" className="mt-4">
<MissingPrecedentsTable
status="irrelevant"
caseNumber={caseNumber.trim() || undefined}
legalTopic={legalTopic.trim() || undefined}
/>
</TabsContent>
<TabsContent value="all" className="mt-4">
<MissingPrecedentsTable
caseNumber={caseNumber.trim() || undefined}
legalTopic={legalTopic.trim() || undefined}
/>
</TabsContent>
</Tabs>
</CardContent>
</Card>
</section> </section>
</AppShell> </AppShell>
); );
} }
type LifecycleTone = "open" | "up" | "closed" | "na";
function LifecycleChip({
tone,
children,
}: {
tone: LifecycleTone;
children: React.ReactNode;
}) {
const cls: Record<LifecycleTone, string> = {
open: "bg-warn-bg text-warn",
up: "bg-info-bg text-info",
closed: "bg-success-bg text-success",
na: "bg-rule-soft text-ink-muted",
};
return (
<span className={`inline-block rounded-full px-2.5 py-0.5 text-[0.72rem] font-semibold ${cls[tone]}`}>
{children}
</span>
);
}

View File

@@ -35,6 +35,12 @@ function mb(bytes: number): string {
return `${Math.round((bytes || 0) / 1024 / 1024)}MB`; return `${Math.round((bytes || 0) / 1024 / 1024)}MB`;
} }
// mockup 02: every region opens with a navy section heading (h2, 18px) — the
// page reads as a sequence of titled sections rather than a stack of cards.
function SectionHeader({ children }: { children: React.ReactNode }) {
return <h2 className="text-navy text-lg font-semibold mb-3 mt-2">{children}</h2>;
}
function ago(ms: number): string { function ago(ms: number): string {
if (!ms) return "—"; if (!ms) return "—";
const secs = Math.floor((Date.now() - ms) / 1000); const secs = Math.floor((Date.now() - ms) / 1000);
@@ -189,64 +195,91 @@ function ServicesPanel({ data }: { data: OperationsSnapshot }) {
return ( return (
<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">
<h2 className="text-navy text-lg mb-1">ניהול תהליכי-רקע (pm2)</h2>
<p className="text-ink-muted text-xs mb-4"> <p className="text-ink-muted text-xs mb-4">
כמו &quot;שירותים&quot; ב-Windows. דמון = שירות רץ-תמיד (הפעל-מחדש/עצור/הפעל). ניהול תהליכי-רקע (pm2) כמו &quot;שירותים&quot; ב-Windows. דמון = שירות
תזמון (cron) = רץ לפי לוח-זמנים (&quot;הרץ עכשיו&quot; להרצה מיידית, ומתג רץ-תמיד (הפעל-מחדש/עצור/הפעל). תזמון (cron) = רץ לפי לוח-זמנים
הפעלה/כיבוי של התזמון). (&quot;הרץ עכשיו&quot; להרצה מיידית, ומתג הפעלה/כיבוי של התזמון).
</p> </p>
{data.services_error ? ( {data.services_error ? (
<p className="text-sm text-destructive">{data.services_error}</p> <p className="text-sm text-destructive">{data.services_error}</p>
) : data.services.length === 0 ? ( ) : data.services.length === 0 ? (
<p className="text-sm text-ink-muted">אין שירותים.</p> <p className="text-sm text-ink-muted">אין שירותים.</p>
) : ( ) : (
<div className="grid gap-2"> /* mockup 02: services as a table — שירות · סטטוס · זמן-ריצה · זיכרון · controls */
{data.services.map((s: OpsService) => { <div className="overflow-x-auto">
const isCron = !!s.cron; <table className="w-full text-sm border-collapse">
return ( <thead>
<div <tr className="border-b border-rule-soft text-ink-muted">
key={s.name} <th className="text-start font-medium text-xs py-2 pe-3">שירות</th>
className="flex items-center justify-between gap-3 rounded-md border border-rule-soft bg-rule-soft/30 px-3 py-2" <th className="text-start font-medium text-xs py-2 px-3">סטטוס</th>
> <th className="text-start font-medium text-xs py-2 px-3 whitespace-nowrap">
<div className="min-w-0"> זמן-ריצה
<div className="flex items-center gap-2 flex-wrap"> </th>
{isCron ? ( <th className="text-start font-medium text-xs py-2 px-3 whitespace-nowrap">
<Badge זיכרון /
variant={s.disabled ? "destructive" : "default"} </th>
className="font-normal" <th className="py-2 ps-3" />
> </tr>
{s.disabled ? "כבוי" : "פעיל (מתוזמן)"} </thead>
</Badge> <tbody>
) : ( {data.services.map((s: OpsService) => {
<StatusBadge value={s.status} /> const isCron = !!s.cron;
)} return (
{s.cron ? ( <tr
<span className="text-[0.7rem] text-ink-muted font-mono" dir="ltr"> key={s.name}
{s.cron} className="border-b border-rule-soft last:border-0 align-top"
</span> >
) : null} <td className="py-2.5 pe-3">
</div> <div className="text-navy font-semibold text-[0.82rem]">
<div className="text-[0.8rem] text-navy truncate mt-0.5"> {SERVICE_LABELS[s.name] ?? s.name}
{SERVICE_LABELS[s.name] ?? s.name} </div>
</div> <div className="text-[0.66rem] text-ink-muted font-mono" dir="ltr">
<div className="text-[0.66rem] text-ink-muted flex items-center gap-2 flex-wrap"> {s.name}
<span className="font-mono" dir="ltr"> </div>
{s.name} </td>
</span> <td className="py-2.5 px-3">
<span>{mb(s.memory_bytes)}</span> <div className="flex items-center gap-2 flex-wrap">
<span>{s.restarts}</span> {isCron ? (
<span>{isCron ? `ריצה אחרונה ${ago(s.uptime_ms)}` : ago(s.uptime_ms)}</span> <Badge
</div> variant={s.disabled ? "destructive" : "default"}
</div> className="font-normal"
<ServiceControls >
s={s} {s.disabled ? "כבוי" : "פעיל (מתוזמן)"}
busy={busy} </Badge>
onAction={(a) => action.mutate({ name: s.name, action: a })} ) : (
onToggle={(disabled) => toggle.mutate({ name: s.name, disabled })} <StatusBadge value={s.status} />
/> )}
</div> {s.cron ? (
); <span
})} className="text-[0.66rem] text-ink-muted font-mono"
dir="ltr"
>
{s.cron}
</span>
) : null}
</div>
</td>
<td className="py-2.5 px-3 text-[0.78rem] text-ink-soft whitespace-nowrap tabular-nums">
{isCron ? `אחרונה ${ago(s.uptime_ms)}` : ago(s.uptime_ms)}
</td>
<td className="py-2.5 px-3 text-[0.78rem] text-ink-soft whitespace-nowrap tabular-nums">
{mb(s.memory_bytes)} · {s.restarts}
</td>
<td className="py-2.5 ps-3">
<div className="flex justify-end">
<ServiceControls
s={s}
busy={busy}
onAction={(a) => action.mutate({ name: s.name, action: a })}
onToggle={(disabled) => toggle.mutate({ name: s.name, disabled })}
/>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div> </div>
)} )}
</CardContent> </CardContent>
@@ -336,6 +369,7 @@ function PipelineCard({
children, children,
href, href,
hrefLabel, hrefLabel,
gate = false,
}: { }: {
title: string; title: string;
desc: string; desc: string;
@@ -344,9 +378,18 @@ function PipelineCard({
// (/approvals), never duplicated here. /operations only monitors. // (/approvals), never duplicated here. /operations only monitors.
href?: string; href?: string;
hrefLabel?: string; hrefLabel?: string;
// mockup 02: gate cards get a gold-wash treatment + gold border so the
// human-gates read as distinct from the automatic pipelines.
gate?: boolean;
}) { }) {
return ( return (
<Card className="bg-surface border-rule shadow-sm"> <Card
className={
gate
? "bg-gold-wash border-gold/40 shadow-sm"
: "bg-surface border-rule shadow-sm"
}
>
<CardContent className="px-5 py-4 space-y-2.5"> <CardContent className="px-5 py-4 space-y-2.5">
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<div className="min-w-0"> <div className="min-w-0">
@@ -356,7 +399,7 @@ function PipelineCard({
{href && ( {href && (
<Link <Link
href={href} href={href}
className="shrink-0 text-[0.72rem] text-gold-deep hover:underline whitespace-nowrap" className="shrink-0 text-[0.72rem] text-gold-deep font-semibold hover:underline whitespace-nowrap"
> >
{hrefLabel ?? "לטיפול ←"} {hrefLabel ?? "לטיפול ←"}
</Link> </Link>
@@ -456,30 +499,29 @@ function LiveAgentsPanel() {
return ( return (
<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">
<div className="flex items-center justify-between gap-3 mb-1 flex-wrap"> {/* mockup 02: status pills row (running / queued) + company hint */}
<h2 className="text-navy text-lg mb-0">סוכנים פעילים</h2> {data ? (
{data ? ( <div className="flex items-center gap-2 text-[0.72rem] mb-3 flex-wrap">
<div className="flex items-center gap-2 text-[0.72rem]"> {/* ADM-6 (INV-IA5): counts sum only the companies that loaded.
{/* ADM-6 (INV-IA5): counts sum only the companies that loaded. When a company errored, mark the totals as a floor ("+") so
When a company errored, mark the totals as a floor ("+") so the operator isn't shown a shrunken depth as if complete. */}
the operator isn't shown a shrunken depth as if complete. */} <Badge variant="default" className="font-normal">
<Badge variant="default" className="font-normal"> רצים {data.running}{data.errors.length > 0 ? "+" : ""}
רצים {data.running}{data.errors.length > 0 ? "+" : ""} </Badge>
</Badge> <Badge variant="secondary" className="font-normal">
<Badge variant="secondary" className="font-normal"> בתור {data.queued}{data.errors.length > 0 ? "+" : ""}
בתור {data.queued}{data.errors.length > 0 ? "+" : ""} </Badge>
</Badge> {data.errors.length > 0 ? (
{data.errors.length > 0 ? ( <span
<span className="text-warn"
className="text-warn" title={`ספירה חלקית — חברות שלא נטענו: ${data.errors.join(" · ")}`}
title={`ספירה חלקית — חברות שלא נטענו: ${data.errors.join(" · ")}`} >
> חלקי
חלקי </span>
</span> ) : null}
) : null} <span className="text-ink-muted ms-auto">חברות: CMP · CMPA</span>
</div> </div>
) : null} ) : null}
</div>
<p className="text-ink-muted text-xs mb-4"> <p className="text-ink-muted text-xs mb-4">
מי מבין סוכני-הוועדה עובד כרגע ומה הפלט שלו כולל עבודה שלא קשורה לתיק (כמו מי מבין סוכני-הוועדה עובד כרגע ומה הפלט שלו כולל עבודה שלא קשורה לתיק (כמו
ריקון תור הלכות ע״י ה-CEO). עצירה היא מבוקרת דרך הפלטפורמה (לא kill). ריקון תור הלכות ע״י ה-CEO). עצירה היא מבוקרת דרך הפלטפורמה (לא kill).
@@ -597,10 +639,14 @@ export default function OperationsPage() {
</div> </div>
) : ( ) : (
<> <>
<SectionHeader>סוכנים פעילים</SectionHeader>
<LiveAgentsPanel /> <LiveAgentsPanel />
<SectionHeader>שירותים</SectionHeader>
<ServicesPanel data={data} /> <ServicesPanel data={data} />
<SectionHeader>צינורות-עבודה</SectionHeader>
{/* Automatic pipelines — uniform stat cards */}
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3"> <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
<PipelineCard <PipelineCard
title="אחזור פסיקה (X13)" title="אחזור פסיקה (X13)"
@@ -633,10 +679,14 @@ export default function OperationsPage() {
לפסיקה לפסיקה
</p> </p>
</PipelineCard> </PipelineCard>
</div>
{/* mockup 02: human-gate pointer cards — gold-wash, "לתיבת-האישורים ←" */}
<div className="grid gap-4 md:grid-cols-2 mt-4">
<PipelineCard <PipelineCard
gate
title="אישור הלכות (שער יו״ר)" title="אישור הלכות (שער יו״ר)"
desc="הלכות שחולצו, ממתינות להכרעת דפנה — שער-אנושי, לא תהליך" desc="שער-אנושי, לא תהליך — הפעולה ב-/approvals"
href="/approvals" href="/approvals"
hrefLabel="לתיבת-האישורים ←" hrefLabel="לתיבת-האישורים ←"
> >
@@ -644,6 +694,7 @@ export default function OperationsPage() {
</PipelineCard> </PipelineCard>
<PipelineCard <PipelineCard
gate
title="פסיקה חסרה" title="פסיקה חסרה"
desc="פערים בקורפוס — נסגרים אוטומטית כשנקלטים" desc="פערים בקורפוס — נסגרים אוטומטית כשנקלטים"
href="/approvals" href="/approvals"
@@ -653,6 +704,7 @@ export default function OperationsPage() {
</PipelineCard> </PipelineCard>
</div> </div>
<SectionHeader>אחזורים אחרונים</SectionHeader>
<div className="grid gap-4 lg:grid-cols-2"> <div className="grid gap-4 lg:grid-cols-2">
<Card className="bg-surface border-rule shadow-sm"> <Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-5 py-4"> <CardContent className="px-5 py-4">
@@ -712,9 +764,9 @@ export default function OperationsPage() {
{/* INV-IA4: the former /diagnostics surface, folded in here — one {/* INV-IA4: the former /diagnostics surface, folded in here — one
monitoring intent, one surface. /diagnostics now redirects here. */} monitoring intent, one surface. /diagnostics now redirects here. */}
<div className="space-y-3 pt-2"> <div className="pt-2">
<h2 className="text-navy text-lg mb-0">בריאות-מערכת</h2> <SectionHeader>בריאות-מערכת</SectionHeader>
<p className="text-ink-muted text-sm"> <p className="text-ink-muted text-sm mb-3">
מצב ה-DB, תורי-אישור, ומסמכים שנכשלו/תקועים. (אוחד מ״אבחון״.) מצב ה-DB, תורי-אישור, ומסמכים שנכשלו/תקועים. (אוחד מ״אבחון״.)
</p> </p>
<SystemHealthSection /> <SystemHealthSection />

View File

@@ -9,8 +9,30 @@ import { AppealTypeBars, subtypeOf } from "@/components/cases/appeal-type-bars";
import { CasesTable } from "@/components/cases/cases-table"; import { CasesTable } from "@/components/cases/cases-table";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useCases, type Case } from "@/lib/api/cases"; import { useCases, type Case, type CaseStatus } from "@/lib/api/cases";
import { usePendingApprovals } from "@/lib/api/chair"; import {
usePendingApprovals,
type ApprovalSeverity,
} from "@/lib/api/chair";
// severity dot per the approved 04-home mockup gate-list (.dot.high/.med/.ok)
const SEVERITY_DOT: Record<ApprovalSeverity, string> = {
high: "bg-danger",
medium: "bg-warn",
low: "bg-info",
ok: "bg-success",
};
// "תיקים לפי סטטוס" horizontal status bars (mockup 04: .bar / .track / .fill).
// Driven by the same live cases the KPI row uses — five status groups onto the
// gold/info/success/danger/muted palette.
type StatusBarRow = { label: string; fill: string; match: CaseStatus[] };
const STATUS_BARS: StatusBarRow[] = [
{ label: "בהכנה", fill: "bg-info", match: ["new", "uploading", "processing", "documents_ready", "analyst_verified", "research_complete", "outcome_set"] },
{ label: "ניתוח וכיוון", fill: "bg-gold", match: ["brainstorming", "direction_approved", "analysis_enriched", "ready_for_writing"] },
{ label: "בכתיבה", fill: "bg-warn", match: ["drafting", "qa_review", "drafted"] },
{ label: "הושלם", fill: "bg-success", match: ["exported", "reviewed", "final"] },
];
export default function HomePage() { export default function HomePage() {
const { data, isPending, error } = useCases(true); const { data, isPending, error } = useCases(true);
@@ -30,9 +52,19 @@ export default function HomePage() {
return { permits, levies }; return { permits, levies };
}, [data]); }, [data]);
const statusBars = useMemo(() => {
const cases = data ?? [];
const counts = STATUS_BARS.map((b) => ({
...b,
n: cases.filter((c) => b.match.includes(c.status)).length,
}));
const max = Math.max(1, ...counts.map((c) => c.n));
return { counts, max };
}, [data]);
return ( return (
<AppShell> <AppShell>
<section className="space-y-8"> <section className="space-y-7">
<header className="flex items-end justify-between gap-6 flex-wrap"> <header className="flex items-end justify-between gap-6 flex-wrap">
<div className="space-y-1.5"> <div className="space-y-1.5">
<div className="text-[0.75rem] uppercase tracking-[0.12em] text-gold-deep"> <div className="text-[0.75rem] uppercase tracking-[0.12em] text-gold-deep">
@@ -40,21 +72,53 @@ export default function HomePage() {
</div> </div>
<h1 className="text-navy">עוזר משפטי</h1> <h1 className="text-navy">עוזר משפטי</h1>
<p className="text-ink-muted text-base max-w-2xl leading-relaxed"> <p className="text-ink-muted text-base max-w-2xl leading-relaxed">
לוח בקרה לניהול תיקי ערר, ניתוח סגנון, וכתיבת החלטות לפי ארכיטקטורת מבט-על על הוועדה תיקים, אישורים ופעילות אחרונה במקום אחד.
12 הבלוקים.
</p> </p>
</div> </div>
<Button asChild className="bg-navy hover:bg-navy-soft text-parchment"> <div className="flex gap-2.5">
<Link href="/cases/new">+ תיק חדש</Link> <Button asChild className="bg-gold text-white hover:bg-gold-deep border-transparent">
</Button> <Link href="/cases/new">+ תיק חדש</Link>
</Button>
<Button asChild variant="outline" className="border-rule text-navy">
<Link href="/precedents">חיפוש בקורפוס</Link>
</Button>
</div>
</header> </header>
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" /> <div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
{/* KPI row — mockup 04 .kpis (4-up, gold-washed "ממתינים לאישור") */}
<KPICards cases={data} loading={isPending} /> <KPICards cases={data} loading={isPending} />
<div className="grid gap-6 lg:grid-cols-[1fr_320px]"> {/* two-column body — main flow + narrow gold gate rail (mockup 04 .cols) */}
<div className="grid gap-6 lg:grid-cols-[1fr_360px]">
<div className="space-y-6 min-w-0"> <div className="space-y-6 min-w-0">
{/* תיקים לפי סטטוס — horizontal bars (mockup 04) */}
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5">
<h2 className="text-navy text-lg mb-4">תיקים לפי סטטוס</h2>
<ul className="space-y-3">
{statusBars.counts.map((b) => (
<li key={b.label} className="flex items-center gap-3">
<span className="w-20 shrink-0 text-[0.82rem] text-ink-soft">
{b.label}
</span>
<span className="flex-1 h-3.5 rounded-full bg-rule-soft overflow-hidden">
<span
className={`block h-full rounded-full ${b.fill} transition-[width] duration-500`}
style={{ width: `${(b.n / statusBars.max) * 100}%` }}
/>
</span>
<span className="w-9 shrink-0 text-end text-[0.82rem] font-semibold text-navy tabular-nums">
{isPending ? "—" : b.n}
</span>
</li>
))}
</ul>
</CardContent>
</Card>
{/* live case tables kept in full (richer than the mockup's single feed) */}
<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">
<div className="flex items-center justify-between gap-3 mb-4 flex-wrap"> <div className="flex items-center justify-between gap-3 mb-4 flex-wrap">
@@ -103,38 +167,39 @@ export default function HomePage() {
</div> </div>
<aside className="space-y-6 lg:sticky lg:top-6 lg:self-start"> <aside className="space-y-6 lg:sticky lg:top-6 lg:self-start">
{approvals && approvals.total_pending > 0 ? ( {/* מה ממתין להכרעתך — gold gate card with dot+label+count rows (mockup 04 .gatecard) */}
<Card className="bg-gold-wash border-gold/40 shadow-sm"> <Card className="bg-gold-wash border-gold/50 shadow-sm">
<CardContent className="px-6 py-5"> <CardContent className="px-6 py-5">
<div className="flex items-center justify-between gap-3 mb-3"> <h2 className="text-navy text-lg mb-3">מה ממתין להכרעתך</h2>
<h2 className="text-navy text-lg mb-0">מה ממתין להכרעתך</h2> {approvals && approvals.categories.length > 0 ? (
<span className="text-2xl font-semibold text-gold-deep leading-none tabular-nums"> <ul className="mb-1">
{approvals.total_pending} {approvals.categories.map((c) => (
</span> <li
</div> key={c.key}
<ul className="space-y-1.5 mb-4"> className="flex items-center gap-2.5 py-2.5 text-[0.88rem] text-ink-soft border-b border-rule-soft last:border-b-0"
{approvals.categories >
.filter((c) => c.count > 0) <span
.map((c) => ( className={`h-2.5 w-2.5 shrink-0 rounded-full ${SEVERITY_DOT[c.severity]}`}
<li aria-hidden
key={c.key} />
className="flex items-center justify-between gap-2 text-[0.85rem] text-ink-soft" <span className="grow min-w-0">{c.label}</span>
> <span className="font-bold text-navy tabular-nums">{c.count}</span>
<span>{c.label}</span> </li>
<span className="text-navy font-semibold tabular-nums">{c.count}</span> ))}
</li>
))}
</ul> </ul>
<Button ) : (
asChild <p className="text-[0.85rem] text-ink-muted mb-2">
size="sm" אין פריטים הממתינים להכרעתך.
className="bg-gold text-white hover:bg-gold-deep border-transparent w-full" </p>
> )}
<Link href="/approvals">למרכז האישורים </Link> <Link
</Button> href="/approvals"
</CardContent> className="inline-block mt-3 text-[0.85rem] font-semibold text-gold-deep hover:text-navy"
</Card> >
) : null} למרכז האישורים
</Link>
</CardContent>
</Card>
<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">

View File

@@ -5,7 +5,6 @@ import Link from "next/link";
import { Pencil, Check, X, Share2 } from "lucide-react"; import { Pencil, Check, X, Share2 } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { AppShell } from "@/components/app-shell"; import { AppShell } from "@/components/app-shell";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
@@ -34,6 +33,16 @@ const SOURCE_TYPE_LABELS: Record<string, string> = {
appeals_committee: "ועדת ערר", appeals_committee: "ועדת ערר",
}; };
/** label/value pair in the parchment meta-band (mockup 08 `.mb`). */
function MetaItem({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div className="flex flex-col gap-0.5">
<span className="text-[0.7rem] text-ink-muted font-medium">{label}</span>
<span className="text-[0.84rem] text-ink font-semibold">{children}</span>
</div>
);
}
/* Next 16 breaking change: route params are now a Promise. /* Next 16 breaking change: route params are now a Promise.
* The `use()` hook unwraps them inside a client component. */ * The `use()` hook unwraps them inside a client component. */
export default function PrecedentDetailPage({ export default function PrecedentDetailPage({
@@ -48,187 +57,224 @@ export default function PrecedentDetailPage({
const [editingCitation, setEditingCitation] = useState(false); const [editingCitation, setEditingCitation] = useState(false);
const [citationDraft, setCitationDraft] = useState(""); const [citationDraft, setCitationDraft] = useState("");
if (error) {
return (
<AppShell>
<section className="space-y-6" dir="rtl">
<div className="rounded-lg border border-danger/40 bg-danger-bg px-6 py-6 text-center space-y-3">
<p className="text-danger font-semibold">שגיאה בטעינת הפסיקה</p>
<p className="text-sm text-ink-muted">{error.message}</p>
<Button asChild variant="outline">
<Link href="/precedents">חזרה לספרייה</Link>
</Button>
</div>
</section>
</AppShell>
);
}
if (isPending || !data) {
return (
<AppShell>
<section className="space-y-6" dir="rtl">
<Skeleton className="h-40 w-full" />
<div className="grid gap-6 lg:grid-cols-[1fr_320px]">
<div className="space-y-4">
{[...Array(3)].map((_, i) => <Skeleton key={i} className="h-28 w-full" />)}
</div>
<Skeleton className="h-64 w-full" />
</div>
</section>
</AppShell>
);
}
const date = data.date ? data.date.slice(0, 10) : null;
return ( return (
<AppShell> <AppShell>
<section className="space-y-6" dir="rtl"> <div dir="rtl">
<header> {/* ── parchment header band (mockup 08 `.band`) — breaks out to the
<nav className="text-[0.78rem] text-ink-muted mb-1"> AppShell <main> edges (px-10 py-10) for a full-width band. ──── */}
<Link href="/" className="hover:text-gold-deep">בית</Link> <div className="-mx-10 -mt-10 mb-6 border-b border-rule bg-parchment px-10 py-6">
<span aria-hidden> · </span> <nav className="text-[0.78rem] text-ink-muted mb-2">
<Link href="/precedents" className="hover:text-gold-deep">ספריית פסיקה</Link> <Link href="/precedents" className="text-gold-deep hover:underline">פסיקה</Link>
<span aria-hidden> · </span> <span aria-hidden> </span>
<span className="text-navy">פרטי פסיקה</span> <Link href="/precedents" className="text-gold-deep hover:underline">ספרייה</Link>
<span aria-hidden> </span>
<span>תקדים</span>
</nav> </nav>
</header>
{error ? ( <div className="flex items-start justify-between gap-3 flex-wrap">
<Card className="bg-danger-bg border-danger/40"> <div className="min-w-0">
<CardContent className="px-6 py-6 text-center space-y-3"> <h1 className="text-navy text-2xl font-bold leading-snug mb-1">
<p className="text-danger font-semibold">שגיאה בטעינת הפסיקה</p> {data.case_name || "—"}
<p className="text-sm text-ink-muted">{error.message}</p> </h1>
<Button asChild variant="outline"> <div className="text-ink-soft text-sm font-mono" dir="ltr">
<Link href="/precedents">חזרה לספרייה</Link> {data.case_number}
</div>
</div>
<div className="flex items-center gap-2">
<Button asChild variant="outline" size="sm" className="border-rule">
<Link href={`/graph?focus=cl:${id}`}>
<Share2 className="w-3.5 h-3.5 me-1" /> הצג בגרף
</Link>
</Button> </Button>
</CardContent> <Button variant="outline" size="sm" className="border-rule" onClick={() => setEditing(true)}>
</Card> <Pencil className="w-3.5 h-3.5 me-1" /> ערוך פרטים
) : isPending || !data ? ( </Button>
<div className="space-y-3"> </div>
{[...Array(5)].map((_, i) => <Skeleton key={i} className="h-16 w-full" />)}
</div> </div>
) : (
<>
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5 space-y-4">
<div className="flex items-start justify-between gap-3 flex-wrap">
<div className="min-w-0 flex-1">
<h1 className="text-navy text-2xl font-semibold mb-1 leading-tight">
{data.case_name || "—"}
</h1>
<div className="text-ink-muted text-sm font-mono" dir="ltr">
{data.case_number}
</div>
</div>
<div className="flex items-center gap-2">
<Button asChild variant="outline" size="sm">
<Link href={`/graph?focus=cl:${id}`}>
<Share2 className="w-3.5 h-3.5 me-1" /> הצג בגרף
</Link>
</Button>
<Button variant="outline" size="sm" onClick={() => setEditing(true)}>
<Pencil className="w-3.5 h-3.5 me-1" /> ערוך פרטים
</Button>
</div>
</div>
{/* Citation per Israeli unified citation rules. The LLM {/* citation (unified Israeli citation rules) — chair-editable */}
extractor composes this from the document; the chair <div className="mt-4 max-w-3xl">
can override below. */} <CitationBlock
<CitationBlock precedent={data as Precedent}
precedent={data as Precedent} editing={editingCitation}
editing={editingCitation} draft={citationDraft}
draft={citationDraft} onStartEdit={() => {
onStartEdit={() => { setCitationDraft(data.citation_formatted ?? "");
setCitationDraft(data.citation_formatted ?? ""); setEditingCitation(true);
setEditingCitation(true); }}
}} onCancel={() => setEditingCitation(false)}
onCancel={() => setEditingCitation(false)} onChange={setCitationDraft}
onChange={setCitationDraft} onSave={async () => {
onSave={async () => { try {
try { await update.mutateAsync({
await update.mutateAsync({ id,
id, patch: { citation_formatted: citationDraft.trim() },
patch: { citation_formatted: citationDraft.trim() }, });
}); toast.success("מראה מקום עודכן");
toast.success("מראה מקום עודכן"); setEditingCitation(false);
setEditingCitation(false); } catch (e) {
} catch (e) { toast.error(e instanceof Error ? e.message : "שמירה נכשלה");
toast.error( }
e instanceof Error ? e.message : "שמירה נכשלה", }}
); saving={update.isPending}
} />
}} </div>
saving={update.isPending}
/>
<div className="flex items-center gap-2 flex-wrap"> {/* meta-band — label/value pairs + chips (mockup 08 `.metaband`) */}
{data.practice_area ? ( <div className="mt-4 flex flex-wrap items-center gap-x-6 gap-y-3">
<Badge variant="outline" className="text-[0.7rem] bg-info-bg text-info border-transparent"> {data.court ? <MetaItem label="בית-משפט">{data.court}</MetaItem> : null}
{PRACTICE_AREA_LABELS[data.practice_area] ?? data.practice_area} {date ? (
</Badge> <MetaItem label="תאריך">
) : null} <span className="tabular-nums" dir="ltr">{date}</span>
{data.source_type ? ( </MetaItem>
<Badge variant="outline" className="text-[0.7rem]"> ) : null}
{SOURCE_TYPE_LABELS[data.source_type] ?? data.source_type} {data.practice_area ? (
</Badge> <MetaItem label="תחום">
) : null} <Badge variant="outline" className="text-[0.72rem] bg-info-bg text-info border-transparent rounded-full px-3">
{data.precedent_level ? ( {PRACTICE_AREA_LABELS[data.practice_area] ?? data.practice_area}
<Badge variant="outline" className="text-[0.7rem] bg-gold-wash text-gold-deep border-rule"> </Badge>
{data.precedent_level} </MetaItem>
</Badge> ) : null}
) : null} {data.source_type ? (
{data.is_binding ? ( <MetaItem label="סוג-מקור">
<Badge <Badge variant="outline" className="text-[0.72rem] rounded-full px-3">
variant="outline" {SOURCE_TYPE_LABELS[data.source_type] ?? data.source_type}
className="text-[0.7rem] bg-success-bg text-success border-transparent" </Badge>
> </MetaItem>
הלכה מחייבת ) : null}
</Badge> {data.precedent_level ? (
) : null} <MetaItem label="רמת-תקדים">
{data.court ? ( <Badge variant="outline" className="text-[0.72rem] bg-gold-wash text-gold-deep border-rule rounded-full px-3">
<span className="text-[0.78rem] text-ink-muted">{data.court}</span> {data.precedent_level}
) : null} </Badge>
{data.date ? ( </MetaItem>
<span className="text-[0.78rem] text-ink-muted tabular-nums" dir="ltr"> ) : null}
{data.date.slice(0, 10)} {data.is_binding ? (
</span> <MetaItem label="סיווג-מחייבות">
) : null} <Badge variant="outline" className="text-[0.72rem] bg-success-bg text-success border-transparent rounded-full px-3">
</div> מחייב
</Badge>
</MetaItem>
) : null}
</div>
{data.headnote ? ( {data.subject_tags?.length ? (
<div> <div className="mt-3 flex items-center gap-1 flex-wrap">
<h3 className="text-navy text-sm font-semibold m-0 mb-1">Headnote</h3> {data.subject_tags.map((t) => (
<p className="text-ink-soft text-sm leading-relaxed m-0"> <Badge key={t} variant="outline" className="text-[0.65rem] bg-surface">
{data.headnote} {t}
</p> </Badge>
</div> ))}
) : null} </div>
) : null}
</div>
{data.summary ? ( {/* ── two-column body (mockup 08 `.wrap` grid) ────────────────── */}
<div> <div className="grid gap-6 lg:grid-cols-[1fr_320px]">
<h3 className="text-navy text-sm font-semibold m-0 mb-1">תקציר</h3> {/* main column */}
<p className="text-ink-soft text-sm leading-relaxed m-0 whitespace-pre-line"> <div className="space-y-4">
{data.summary} {data.summary ? (
</p> <DetailCard title="תקציר">
</div> <p className="text-ink-soft text-sm leading-8 m-0 whitespace-pre-line">
) : null} {data.summary}
</p>
</DetailCard>
) : null}
{(data as { key_quote?: string }).key_quote ? ( {data.headnote ? (
<div> <DetailCard title="כותרת-הלכה (headnote)" prov="opus">
<h3 className="text-navy text-sm font-semibold m-0 mb-1">ציטוט מרכזי</h3> <p className="text-ink-soft text-sm leading-8 m-0">{data.headnote}</p>
<blockquote className="text-ink-soft text-sm leading-relaxed border-s-[3px] border-gold bg-gold-wash ps-3 pe-4 py-3 rounded-e m-0"> </DetailCard>
{(data as { key_quote?: string }).key_quote} ) : null}
</blockquote>
</div>
) : null}
{data.subject_tags?.length ? ( {(data as { key_quote?: string }).key_quote ? (
<div className="flex items-center gap-1 flex-wrap pt-1"> <DetailCard title="ציטוט-מפתח" prov="opus">
{data.subject_tags.map((t) => ( <blockquote className="rounded-e border-s-[3px] border-gold bg-gold-wash px-4 py-3 text-sm text-ink-soft leading-8 m-0">
<Badge key={t} variant="outline" className="text-[0.65rem]"> {(data as { key_quote?: string }).key_quote}
{t} </blockquote>
</Badge> </DetailCard>
))} ) : null}
</div>
) : null}
</CardContent>
</Card>
<Card className="bg-surface border-rule shadow-sm"> <div className="rounded-lg border border-rule bg-surface shadow-sm px-5 py-4">
<CardContent className="px-6 py-5"> <ExtractedHalachotSection halachot={data.halachot ?? []} />
<RelatedCasesSection </div>
caseId={id} </div>
related={data.related_cases ?? []}
/>
</CardContent>
</Card>
<Card className="bg-surface border-rule shadow-sm"> {/* side rail — citations + corroboration */}
<CardContent className="px-6 py-5"> <div className="space-y-4">
<ExtractedHalachotSection halachot={data.halachot ?? []} /> <RelatedCasesSection caseId={id} related={data.related_cases ?? []} />
</CardContent> </div>
</Card> </div>
</> </div>
)}
<PrecedentEditSheet <PrecedentEditSheet
caseLawId={editing ? id : null} caseLawId={editing ? id : null}
onOpenChange={(open) => setEditing(open)} onOpenChange={(open) => setEditing(open)}
/> />
</section>
</AppShell> </AppShell>
); );
} }
/** main-column card with a navy heading + optional provenance pill
* ("מולא ע״י Opus", mockup 08 `.prov`). */
function DetailCard({
title,
prov,
children,
}: {
title: string;
prov?: "opus";
children: React.ReactNode;
}) {
return (
<div className="rounded-lg border border-rule bg-surface shadow-sm px-5 py-4">
<h2 className="text-navy text-[1.05rem] font-semibold mb-1.5 flex items-center gap-2.5 flex-wrap">
{title}
{prov === "opus" ? (
<span className="inline-flex items-center rounded-full bg-info-bg text-info text-[0.68rem] font-semibold px-2.5 py-0.5">
מולא ע״י Opus
</span>
) : null}
</h2>
{children}
</div>
);
}
function CitationBlock({ function CitationBlock({
precedent, precedent,
editing, editing,
@@ -252,7 +298,7 @@ function CitationBlock({
if (editing) { if (editing) {
return ( return (
<div className="rounded-md border border-gold/40 bg-gold-wash/30 p-3 space-y-2"> <div className="rounded-md border border-gold/40 bg-gold-wash/40 p-3 space-y-2">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<span className="text-[0.78rem] font-semibold text-navy"> <span className="text-[0.78rem] font-semibold text-navy">
עריכת מראה מקום עריכת מראה מקום
@@ -266,7 +312,7 @@ function CitationBlock({
onChange={(e) => onChange(e.target.value)} onChange={(e) => onChange(e.target.value)}
rows={3} rows={3}
dir="rtl" dir="rtl"
className="font-mono text-sm" className="font-mono text-sm bg-surface"
placeholder='ערר (ועדות ערר ...) 1234/24 **עורר נ&apos; הוועדה המקומית** (נבו 1.2.2025)' placeholder='ערר (ועדות ערר ...) 1234/24 **עורר נ&apos; הוועדה המקומית** (נבו 1.2.2025)'
disabled={saving} disabled={saving}
/> />
@@ -280,12 +326,7 @@ function CitationBlock({
<Check className="w-3.5 h-3.5 me-1" /> <Check className="w-3.5 h-3.5 me-1" />
שמור שמור
</Button> </Button>
<Button <Button size="sm" variant="outline" onClick={onCancel} disabled={saving}>
size="sm"
variant="outline"
onClick={onCancel}
disabled={saving}
>
<X className="w-3.5 h-3.5 me-1" /> <X className="w-3.5 h-3.5 me-1" />
ביטול ביטול
</Button> </Button>
@@ -296,7 +337,7 @@ function CitationBlock({
if (!citation) { if (!citation) {
return ( return (
<div className="rounded-md border border-dashed border-rule bg-rule-soft/30 p-3 flex items-center justify-between gap-2"> <div className="rounded-md border border-dashed border-rule bg-surface/60 p-3 flex items-center justify-between gap-2">
<span className="text-[0.78rem] text-ink-muted"> <span className="text-[0.78rem] text-ink-muted">
מראה מקום (כללי הציטוט האחיד) טרם חולץ מראה מקום (כללי הציטוט האחיד) טרם חולץ
</span> </span>
@@ -309,7 +350,7 @@ function CitationBlock({
} }
return ( return (
<div className="rounded-md border border-rule bg-parchment-50 p-3 space-y-1.5"> <div className="rounded-md border border-rule bg-surface p-3 space-y-1.5">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<span className="text-[0.7rem] uppercase tracking-wide text-ink-muted"> <span className="text-[0.7rem] uppercase tracking-wide text-ink-muted">
מראה מקום מראה מקום

View File

@@ -1,15 +1,14 @@
"use client"; "use client";
import Link from "next/link"; 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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Badge } from "@/components/ui/badge"; import { AppShell } from "@/components/app-shell";
import { LibraryListPanel } from "@/components/precedents/library-list-panel"; import { LibraryListPanel } from "@/components/precedents/library-list-panel";
import { LibrarySearchPanel } from "@/components/precedents/library-search-panel"; import { LibrarySearchPanel } from "@/components/precedents/library-search-panel";
import { HalachaReviewPanel } from "@/components/precedents/halacha-review-panel"; import { HalachaReviewPanel } from "@/components/precedents/halacha-review-panel";
import { LibraryStatsPanel } from "@/components/precedents/library-stats-panel"; import { LibraryStatsPanel } from "@/components/precedents/library-stats-panel";
import { useHalachotPending } from "@/lib/api/precedent-library"; import { useHalachotPending } from "@/lib/api/precedent-library";
import { useMissingPrecedents } from "@/lib/api/missing-precedents";
/** /**
* Precedent Library admin page. * Precedent Library admin page.
@@ -25,72 +24,133 @@ import { useHalachotPending } from "@/lib/api/precedent-library";
* per-case precedent attacher (chair-attached quotes scoped to a case). * per-case precedent attacher (chair-attached quotes scoped to a case).
*/ */
function PendingBadge() { /** Colored count pill riding on a tab trigger (mockup 07: warn for review
const { data } = useHalachotPending(); * queue, info for incoming). Returns null when the queue is empty. */
const n = data?.count ?? 0; function CountPill({ n, tone }: { n: number; tone: "warn" | "info" }) {
if (!n) return null; if (!n) return null;
const cls =
tone === "warn"
? "bg-warn text-white"
: "bg-info text-white";
return ( return (
<Badge <span
variant="outline" className={`ms-1.5 inline-flex items-center justify-center rounded-full px-1.5 min-w-[1.15rem] h-[1.15rem] text-[0.68rem] font-semibold tabular-nums ${cls}`}
className="ms-1 bg-gold-wash text-gold-deep border-gold/40 text-[0.65rem]"
> >
{n} {n}
</Badge> </span>
); );
} }
function PendingPill() {
const { data } = useHalachotPending();
return <CountPill n={data?.count ?? 0} tone="warn" />;
}
function IncomingPill() {
// "פסיקה נכנסת" = open missing-precedents waiting for the chair to upload.
const { data } = useMissingPrecedents({ status: "open", limit: 1 });
return <CountPill n={data?.by_status?.open ?? 0} tone="info" />;
}
export default function PrecedentsPage() { export default function PrecedentsPage() {
return ( return (
<AppShell> <AppShell>
<section className="space-y-6"> <Tabs defaultValue="library" dir="rtl">
<header> <section className="space-y-6">
<nav className="text-[0.78rem] text-ink-muted mb-1"> <header className="space-y-3">
<Link href="/" className="hover:text-gold-deep">בית</Link> <nav className="text-[0.78rem] text-ink-muted">
<span aria-hidden> · </span> <Link href="/" className="hover:text-gold-deep">בית</Link>
<span className="text-navy">ספריית פסיקה</span> <span aria-hidden> · </span>
</nav> <span className="text-navy">ספריית פסיקה</span>
<h1 className="text-navy mb-0">ספריית הפסיקה הסמכותית</h1> </nav>
<p className="text-ink-muted text-sm mt-1 max-w-3xl"> <div className="space-y-1">
פסיקה חיצונית פסקי דין של ערכאות עליונות והחלטות של ועדות ערר אחרות. <h1 className="text-navy mb-0">ספריית הפסיקה הסמכותית</h1>
כל קובץ עובר חילוץ הלכות אוטומטי, וההלכות ממתינות לאישור היו&quot;ר לפני <p className="text-ink-muted text-sm mt-1 max-w-3xl leading-relaxed">
שהן זמינות לסוכני הכתיבה (legal-writer וכו&apos;). קורפוס הפסיקה והלכות המערכת חיפוש סמנטי, תור-אישור והשלמת
</p> פסיקה חסרה. כל קובץ עובר חילוץ הלכות אוטומטי, וההלכות ממתינות
</header> לאישור היו&quot;ר לפני שהן זמינות לסוכני הכתיבה.
</p>
</div>
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" /> {/* tabs as a dedicated row under the header — underline-style
triggers with colored count pills (mockup 07). */}
<Card className="bg-surface border-rule shadow-sm"> <TabsList className="flex w-full justify-start gap-1 rounded-none border-0 border-b border-rule bg-transparent p-0 h-auto">
<CardContent className="px-6 py-5"> {[
<Tabs defaultValue="library" dir="rtl"> { value: "library", label: "ספרייה", pill: null },
<TabsList className="bg-rule-soft/60"> { value: "search", label: "חיפוש בקורפוס", pill: null },
<TabsTrigger value="library">ספרייה</TabsTrigger> { value: "review", label: "תור הלכות", pill: <PendingPill /> },
<TabsTrigger value="search">חיפוש סמנטי</TabsTrigger> {
<TabsTrigger value="review"> value: "incoming",
ממתין לאישור label: "פסיקה נכנסת",
<PendingBadge /> pill: <IncomingPill />,
},
{ value: "stats", label: "סטטיסטיקה", pill: null },
].map((t) => (
<TabsTrigger
key={t.value}
value={t.value}
className="rounded-none border-0 border-b-2 border-transparent bg-transparent px-4 py-2.5 -mb-px text-sm font-medium text-ink-muted shadow-none data-[state=active]:border-gold data-[state=active]:bg-transparent data-[state=active]:font-semibold data-[state=active]:text-navy data-[state=active]:shadow-none"
>
{t.label}
{t.pill}
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="stats">סטטיסטיקה</TabsTrigger> ))}
</TabsList> </TabsList>
</header>
<TabsContent value="library" className="mt-5"> <TabsContent value="library" className="mt-0">
<LibraryListPanel /> <LibraryListPanel />
</TabsContent> </TabsContent>
<TabsContent value="search" className="mt-5"> <TabsContent value="search" className="mt-0">
<LibrarySearchPanel /> <LibrarySearchPanel />
</TabsContent> </TabsContent>
<TabsContent value="review" className="mt-5"> <TabsContent value="review" className="mt-0">
<HalachaReviewPanel /> <HalachaReviewPanel />
</TabsContent> </TabsContent>
<TabsContent value="stats" className="mt-5"> {/* "פסיקה נכנסת" — the incoming/missing-precedent queue. Kept as a
<LibraryStatsPanel /> tab per the mockup; full management lives on /missing-precedents. */}
</TabsContent> <TabsContent value="incoming" className="mt-0">
</Tabs> <IncomingTab />
</CardContent> </TabsContent>
</Card>
</section> <TabsContent value="stats" className="mt-0">
<LibraryStatsPanel />
</TabsContent>
</section>
</Tabs>
</AppShell> </AppShell>
); );
} }
/** Lightweight in-tab pointer to the dedicated missing-precedents page,
* preserving the mockup's "פסיקה נכנסת" tab without duplicating the table. */
function IncomingTab() {
const { data } = useMissingPrecedents({ status: "open", limit: 1 });
const open = data?.by_status?.open ?? 0;
return (
<div className="rounded-lg border border-rule bg-surface shadow-sm p-6 space-y-3">
<div className="flex items-baseline gap-3 flex-wrap">
<h2 className="text-navy text-lg font-semibold m-0">פסיקה נכנסת</h2>
{open ? (
<span className="inline-flex items-baseline gap-1.5 rounded-lg border border-rule bg-warn-bg px-3 py-1">
<span className="text-base font-bold text-warn tabular-nums">{open}</span>
<span className="text-[0.8rem] text-ink-soft">פתוחים</span>
</span>
) : null}
</div>
<p className="text-ink-soft text-sm leading-relaxed max-w-2xl">
פסיקה שצוטטה בכתבי-הטענות אך אינה קיימת בקורפוס. השלמתה מאפשרת
אימות-הלכה ועיגון-מקור (INV-AH).
</p>
<Link
href="/missing-precedents"
className="inline-flex items-center rounded-md bg-gold px-4 py-2 text-sm font-semibold text-white hover:bg-gold-deep"
>
לניהול פסיקה חסרה
</Link>
</div>
);
}

View File

@@ -1,27 +1,133 @@
"use client"; "use client";
import { useMemo } from "react";
import Link from "next/link"; import Link from "next/link";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { AppShell } from "@/components/app-shell"; import { AppShell } from "@/components/app-shell";
import { Card, CardContent } from "@/components/ui/card"; import { Card } from "@/components/ui/card";
import { Markdown } from "@/components/ui/markdown"; import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { fetchScriptsCatalog } from "@/lib/api/scripts"; import { fetchScriptsCatalog } from "@/lib/api/scripts";
/* /*
* /scripts — read-only catalog of everything under scripts/. * /scripts — catalog of everything under scripts/, rendered as the
* approved IA-redesign table (name mono · role · status chip · run/source
* ghost button).
* *
* The content is `scripts/SCRIPTS.md` verbatim (active · archived · deleted * The single source of truth is still `scripts/SCRIPTS.md` (CLAUDE.md mandates
* tables), served by GET /api/scripts/catalog. SCRIPTS.md is the single * updating it on every script change), served verbatim by
* source of truth — CLAUDE.md mandates updating it on every script change — * GET /api/scripts/catalog. We parse its markdown tables into structured rows
* so we render it directly rather than re-describing the scripts here. * for display — editing remains git/Gitea only, so the per-row "מקור" button
* deep-links to the file in Gitea rather than inventing a run-from-UI mutation.
*/ */
type ScriptStatus = "active" | "once" | "archive" | "deleted";
type ScriptRow = {
name: string;
role: string;
status: ScriptStatus;
};
const STATUS_LABEL: Record<ScriptStatus, string> = {
active: "פעיל",
once: "חד-פעמי",
archive: "ארכיון",
deleted: "נמחק",
};
const STATUS_TONE: Record<ScriptStatus, { wrap: string; dot: string }> = {
active: { wrap: "bg-success-bg text-success", dot: "bg-success" },
once: { wrap: "bg-info-bg text-info", dot: "bg-info" },
archive: { wrap: "bg-rule-soft text-ink-muted", dot: "bg-ink-muted" },
deleted: { wrap: "bg-danger-bg text-danger", dot: "bg-danger" },
};
// "חד-פעמי" / "one-shot" markers inside the Scheduled column of an active row.
const ONCE_RE = /חד-?פעמי|one-?shot|בוצע/;
/**
* Parse SCRIPTS.md markdown tables into typed rows. The file has three
* sections with different shapes; we read the first two columns of each
* (name + role) and derive status from the section + scheduling note.
*/
function parseScripts(md: string): ScriptRow[] {
const lines = md.split("\n");
const rows: ScriptRow[] = [];
let section: ScriptStatus = "active";
for (const raw of lines) {
const line = raw.trim();
if (line.startsWith("## ")) {
if (line.includes(".archive") || line.includes("הושלמו")) section = "archive";
else if (line.includes("נמחק")) section = "deleted";
else section = "active";
continue;
}
if (!line.startsWith("|")) continue;
// skip header + separator rows
const cells = line
.split("|")
.slice(1, -1)
.map((c) => c.trim());
if (cells.length < 2) continue;
if (/^-+$/.test(cells[0].replace(/[-:]/g, "-"))) continue; // separator
if (cells[0] === "Script") continue; // header
if (!cells[0]) continue;
const name = cells[0].replace(/`/g, "");
if (!name) continue;
let status: ScriptStatus = section;
if (section === "active") {
const scheduled = cells[3] ?? "";
status = ONCE_RE.test(scheduled) ? "once" : "active";
}
// role: active = Purpose (col 2), archive = Original Purpose (col 1),
// deleted = Reason (col 1).
const role =
section === "active" ? cells[2] ?? cells[1] ?? "" : cells[1] ?? "";
rows.push({ name, role: stripMd(role), status });
}
return rows;
}
// Strip bold/inline-code markdown so the role reads as plain text in a cell.
function stripMd(s: string): string {
return s.replace(/\*\*/g, "").replace(/`/g, "");
}
function StatusChip({ status }: { status: ScriptStatus }) {
const tone = STATUS_TONE[status];
return (
<span
className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-[3px] text-[0.75rem] font-semibold ${tone.wrap}`}
>
<span className={`h-1.5 w-1.5 rounded-full ${tone.dot}`} aria-hidden />
{STATUS_LABEL[status]}
</span>
);
}
export default function ScriptsPage() { export default function ScriptsPage() {
const { data, isLoading, isError, error } = useQuery({ const { data, isLoading, isError, error } = useQuery({
queryKey: ["scripts-catalog"], queryKey: ["scripts-catalog"],
queryFn: ({ signal }) => fetchScriptsCatalog(signal), queryFn: ({ signal }) => fetchScriptsCatalog(signal),
}); });
const rows = useMemo(
() => (data?.content ? parseScripts(data.content) : []),
[data],
);
const lastModified = const lastModified =
data?.last_modified != null data?.last_modified != null
? new Date(data.last_modified * 1000).toLocaleDateString("he-IL", { ? new Date(data.last_modified * 1000).toLocaleDateString("he-IL", {
@@ -31,63 +137,113 @@ export default function ScriptsPage() {
}) })
: null; : null;
const giteaBase = data?.gitea_url ?? null;
return ( return (
<AppShell> <AppShell>
<section className="space-y-6"> <section className="space-y-6">
<div className="flex items-end justify-between gap-4"> <header>
<div> <nav className="text-[0.78rem] text-ink-muted mb-1">
<nav className="text-[0.78rem] text-ink-muted mb-1"> <Link href="/" className="hover:text-gold-deep">בית</Link>
<Link href="/" className="hover:text-gold-deep">בית</Link> <span aria-hidden> · </span>
<span aria-hidden> · </span> <span className="text-navy">סקריפטים</span>
<span className="text-navy">סקריפטים</span> </nav>
</nav> <h1 className="text-navy mb-0">סקריפטים</h1>
<h1 className="text-navy mb-0">סקריפטים</h1> <p className="text-sm text-ink-muted mt-1 max-w-2xl">
<p className="text-sm text-ink-muted mt-1"> סקריפטי-תחזוקה ותפעול. מקור-האמת הוא{" "}
קטלוג כל הסקריפטים בתיקיית{" "} <code className="rounded bg-rule-soft px-1 py-0.5 font-mono text-[0.78rem]">
<code className="rounded bg-rule-soft px-1 py-0.5 font-mono text-[0.78rem]"> scripts/SCRIPTS.md
scripts/ </code>{" "}
</code>{" "} עריכה דרך git, לא מכאן.
שם, סוג, תפקיד ותזמון. מקור-האמת הוא{" "} </p>
<code className="rounded bg-rule-soft px-1 py-0.5 font-mono text-[0.78rem]"> </header>
scripts/SCRIPTS.md
</code>
; עריכה דרך git, לא מכאן.
</p>
</div>
{data?.gitea_url ? (
<a
href={data.gitea_url}
target="_blank"
rel="noreferrer"
className="shrink-0 text-sm text-gold-deep hover:text-gold underline underline-offset-2"
>
מקור ב-Gitea
</a>
) : null}
</div>
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" /> <div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
<Card className="bg-surface border-rule shadow-sm"> {isLoading ? (
<CardContent className="px-6 py-5"> <Card className="bg-surface border-rule px-6 py-5 text-sm text-ink-muted">
{isLoading ? ( טוען קטלוג
<p className="text-sm text-ink-muted">טוען קטלוג</p> </Card>
) : isError ? ( ) : isError ? (
<p className="text-sm text-danger"> <Card className="bg-danger-bg border-danger/40 px-6 py-5 text-sm text-danger">
שגיאה בטעינת הקטלוג: {(error as Error)?.message ?? "לא ידוע"} שגיאה בטעינת הקטלוג: {(error as Error)?.message ?? "לא ידוע"}
</Card>
) : (
<Card className="bg-surface border-rule shadow-sm overflow-hidden p-0">
<Table>
<TableHeader>
<TableRow className="bg-parchment hover:bg-parchment border-rule">
<TableHead className="text-start text-[0.75rem] font-semibold text-ink-muted px-5 py-3.5">
שם הסקריפט
</TableHead>
<TableHead className="text-start text-[0.75rem] font-semibold text-ink-muted px-5 py-3.5">
תפקיד
</TableHead>
<TableHead className="text-start text-[0.75rem] font-semibold text-ink-muted px-5 py-3.5">
סטטוס
</TableHead>
<TableHead className="text-end text-[0.75rem] font-semibold text-ink-muted px-5 py-3.5">
פעולה
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{rows.map((s) => {
const disabled = s.status === "archive" || s.status === "deleted";
const href = giteaBase
? `${giteaBase.replace(/\/$/, "")}/${s.name}`
: null;
return (
<TableRow
key={s.name}
className="border-rule-soft hover:bg-gold-wash align-middle"
>
<TableCell className="px-5 py-3.5">
<code
className="font-mono text-[0.81rem] font-semibold text-navy"
dir="ltr"
>
{s.name}
</code>
</TableCell>
<TableCell className="px-5 py-3.5 text-ink-soft text-[0.84rem] leading-snug max-w-xl whitespace-normal">
<span className="line-clamp-2">{s.role}</span>
</TableCell>
<TableCell className="px-5 py-3.5">
<StatusChip status={s.status} />
</TableCell>
<TableCell className="px-5 py-3.5 text-end">
{disabled || !href ? (
<button
type="button"
disabled
className="rounded-lg border border-rule-soft px-4 py-1.5 text-[0.81rem] font-semibold text-ink-muted cursor-default"
>
מקור
</button>
) : (
<a
href={href}
target="_blank"
rel="noreferrer"
className="inline-block rounded-lg border border-rule px-4 py-1.5 text-[0.81rem] font-semibold text-gold-deep hover:bg-gold-wash hover:border-gold transition-colors"
>
מקור
</a>
)}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
{lastModified ? (
<p className="px-5 py-3 border-t border-rule text-xs text-ink-muted">
עודכן לאחרונה: {lastModified}
</p> </p>
) : data ? (
<>
<Markdown content={data.content} />
{lastModified ? (
<p className="mt-6 pt-3 border-t border-rule text-xs text-ink-muted">
עודכן לאחרונה: {lastModified}
</p>
) : null}
</>
) : null} ) : null}
</CardContent> </Card>
</Card> )}
</section> </section>
</AppShell> </AppShell>
); );

View File

@@ -1,7 +1,6 @@
"use client"; "use client";
import { AlertTriangle, CheckCircle2, HelpCircle } from "lucide-react"; import { AlertTriangle, CheckCircle2, HelpCircle } from "lucide-react";
import { Badge } from "@/components/ui/badge";
type Props = { type Props = {
drift: boolean; drift: boolean;
@@ -9,31 +8,31 @@ type Props = {
coolifyAvailable?: boolean; coolifyAvailable?: boolean;
}; };
// Filled pill chips matching IA-redesign mockup 15 (.c-synced / .c-drift).
export function DriftBadge({ drift, coolifyAvailable = true }: Props) { export function DriftBadge({ drift, coolifyAvailable = true }: Props) {
if (!coolifyAvailable) { if (!coolifyAvailable) {
return ( return (
<Badge <span
variant="outline" className="inline-flex items-center gap-1 rounded-full bg-rule-soft text-ink-muted text-[0.72rem] font-semibold px-2.5 py-0.5"
className="text-ink-muted border-rule gap-1"
title="Coolify לא זמין — מצב ה-drift לא ידוע" title="Coolify לא זמין — מצב ה-drift לא ידוע"
> >
<HelpCircle className="w-3 h-3" /> <HelpCircle className="w-3 h-3" />
Unknown Unknown
</Badge> </span>
); );
} }
if (drift) { if (drift) {
return ( return (
<Badge variant="outline" className="text-warn border-warn/40 gap-1"> <span className="inline-flex items-center gap-1 rounded-full bg-warn-bg text-warn text-[0.72rem] font-semibold px-2.5 py-0.5">
<AlertTriangle className="w-3 h-3" /> <AlertTriangle className="w-3 h-3" />
Drift Drift
</Badge> </span>
); );
} }
return ( return (
<Badge variant="outline" className="text-success border-success/40 gap-1"> <span className="inline-flex items-center gap-1 rounded-full bg-success-bg text-success text-[0.72rem] font-semibold px-2.5 py-0.5">
<CheckCircle2 className="w-3 h-3" /> <CheckCircle2 className="w-3 h-3" />
Synced Synced
</Badge> </span>
); );
} }

View File

@@ -3,7 +3,6 @@
import { useState } from "react"; import { useState } from "react";
import { ExternalLink, Save, Lock } from "lucide-react"; import { ExternalLink, Save, Lock } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import type { McpEnvVar } from "@/lib/api/settings"; import type { McpEnvVar } from "@/lib/api/settings";
import { useUpdateMcpEnv } from "@/lib/api/settings"; import { useUpdateMcpEnv } from "@/lib/api/settings";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -44,36 +43,38 @@ export function EnvVarRow({
`https://coolify.nautilus.marcusgroup.org/project/applications/${coolifyAppUuid}/environment-variables`; `https://coolify.nautilus.marcusgroup.org/project/applications/${coolifyAppUuid}/environment-variables`;
return ( return (
<div className="rounded-md border border-rule p-4 bg-rule-soft/20 hover:bg-rule-soft/40 transition-colors"> <div className="px-5 py-4 border-b border-rule-soft last:border-b-0">
<div className="flex items-start justify-between gap-3 mb-3"> {/* envtop — key + type chip + secret chip + drift/synced chip */}
<div className="flex-1 min-w-0"> <div className="flex items-center gap-2 flex-wrap">
<div className="flex items-center gap-2 flex-wrap"> <code className="font-mono text-[0.84rem] font-semibold text-navy" dir="ltr">
<code className="font-mono text-sm font-medium text-navy" dir="ltr"> {spec.key}
{spec.key} </code>
</code> <span className="inline-flex items-center rounded-full bg-info-bg text-info text-[0.72rem] font-semibold px-2.5 py-0.5">
<Badge variant="outline" className="text-[0.7rem]"> {spec.type}
{spec.type} </span>
</Badge> {spec.is_secret && (
{spec.is_secret && ( <span className="inline-flex items-center gap-1 rounded-full bg-navy text-white text-[0.72rem] font-semibold px-2.5 py-0.5">
<Badge variant="outline" className="text-[0.7rem] text-warn border-warn/40 gap-1"> <Lock className="w-3 h-3" />
<Lock className="w-3 h-3" /> secret
secret </span>
</Badge> )}
)} <DriftBadge drift={spec.drift} coolifyAvailable={coolifyAvailable} />
<DriftBadge drift={spec.drift} coolifyAvailable={coolifyAvailable} /> {spec.has_duplicates && (
{spec.has_duplicates && ( <span className="inline-flex items-center rounded-full bg-warn-bg text-warn text-[0.72rem] font-semibold px-2.5 py-0.5">
<Badge variant="outline" className="text-[0.7rem] text-warn border-warn/40"> duplicates
duplicates </span>
</Badge> )}
)}
</div>
<p className="text-sm text-ink-muted mt-1">{spec.description}</p>
</div>
</div> </div>
{spec.description && (
<p className="text-[0.82rem] text-ink-muted mt-1">{spec.description}</p>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm"> {/* vals — Coolify value | Container value (+pending) | save (mockup .vals 3-col) */}
<div className="flex items-center gap-2"> <div className="grid grid-cols-1 md:grid-cols-[1fr_1fr_auto] gap-3.5 items-end mt-3.5">
<span className="text-[0.72rem] text-ink-muted w-20">Coolify:</span> <div>
<span className="block text-[0.72rem] text-ink-muted font-semibold mb-1">
Coolify:
</span>
{spec.is_editable ? ( {spec.is_editable ? (
<EnvVarEditor <EnvVarEditor
spec={spec} spec={spec}
@@ -82,53 +83,61 @@ export function EnvVarRow({
disabled={update.isPending} disabled={update.isPending}
/> />
) : ( ) : (
<span className="font-mono text-ink" dir="ltr"> <a
{spec.coolify_value ?? <em className="text-ink-muted"> לא מוגדר </em>} href={coolifyEnvUrl}
</span> target="_blank"
)} rel="noopener noreferrer"
</div> className="font-mono text-[0.81rem] text-gold-deep hover:underline flex items-center gap-1"
<div className="flex items-center gap-2 flex-wrap"> dir="ltr"
<span className="text-[0.72rem] text-ink-muted w-20">Container:</span>
<span className="font-mono text-ink" dir="ltr">
{spec.container_value ?? <em className="text-ink-muted"> לא מוגדר </em>}
</span>
{/* ADM-5 (INV-IA5/INV-IA6): when Coolify ≠ Container the container is
running a stale value until a redeploy — say so in plain Hebrew
right here, not only via the top "Drift" badge. */}
{coolifyAvailable && spec.drift && (
<Badge
variant="outline"
className="text-[0.7rem] text-warn border-warn/40"
title="הערך נשמר ב-Coolify אך הקונטיינר עדיין מריץ את הקודם — נדרש redeploy"
> >
ממתין ל-redeploy {spec.coolify_value ?? "— לא מוגדר —"}
</Badge> <ExternalLink className="w-3 h-3 shrink-0" />
</a>
)}
</div>
<div>
<span className="block text-[0.72rem] text-ink-muted font-semibold mb-1">
Container:
</span>
<div className="font-mono text-[0.81rem] text-ink-soft bg-rule-soft rounded-md px-3 py-2 flex items-center gap-2 flex-wrap" dir="ltr">
<span>{spec.container_value ?? "— לא מוגדר —"}</span>
{/* ADM-5 (INV-IA5/INV-IA6): Coolify ≠ Container ⇒ container is stale
until a redeploy — say so in plain Hebrew right here. */}
{coolifyAvailable && spec.drift && (
<span
className="inline-flex items-center rounded-full bg-warn-bg text-warn border border-warn text-[0.7rem] font-semibold px-2 py-0.5"
dir="rtl"
title="הערך נשמר ב-Coolify אך הקונטיינר עדיין מריץ את הקודם — נדרש redeploy"
>
ממתין ל-redeploy
</span>
)}
</div>
</div>
<div className="flex md:block items-end">
{spec.is_editable ? (
<Button
size="sm"
onClick={handleSave}
disabled={!dirty || update.isPending}
variant={dirty ? "default" : "outline"}
className={dirty ? "bg-gold text-white hover:bg-gold-deep border-transparent" : ""}
>
<Save className="w-3.5 h-3.5" data-icon="inline-start" />
{update.isPending ? "שומר..." : "שמור"}
</Button>
) : (
<a
href={coolifyEnvUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 rounded-md border border-rule text-navy px-4 py-1.5 text-[0.81rem] font-semibold hover:bg-gold-wash"
>
ערוך ב-Coolify
<ExternalLink className="w-3 h-3" />
</a>
)} )}
</div> </div>
</div>
<div className="flex items-center justify-end gap-2 mt-3">
{!spec.is_editable && (
<a
href={coolifyEnvUrl}
target="_blank"
rel="noopener noreferrer"
className="text-[0.78rem] text-gold-deep hover:underline flex items-center gap-1"
>
ערוך ב-Coolify
<ExternalLink className="w-3 h-3" />
</a>
)}
{spec.is_editable && (
<Button
size="sm"
onClick={handleSave}
disabled={!dirty || update.isPending}
>
<Save className="w-3.5 h-3.5" data-icon="inline-start" />
{update.isPending ? "שומר..." : "שמור"}
</Button>
)}
</div> </div>
</div> </div>
); );

View File

@@ -72,37 +72,39 @@ export function EnvironmentTab() {
const duplicatesCount = data.vars.filter((v) => v.has_duplicates).length; const duplicatesCount = data.vars.filter((v) => v.has_duplicates).length;
return ( return (
<div className="space-y-4"> <div className="space-y-5">
<Card className="bg-surface border-rule"> {/* summary band — Coolify app id + drift/dup counts + redeploy CTA */}
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-4 flex items-center justify-between gap-4 flex-wrap"> <CardContent className="px-6 py-4 flex items-center justify-between gap-4 flex-wrap">
<div className="flex items-center gap-3 flex-wrap text-sm"> <div className="flex items-center gap-3 flex-wrap text-sm">
<Badge variant="outline"> <Badge variant="outline">
Coolify app: <code dir="ltr" className="ms-1">{data.coolify_app_uuid.slice(0, 8)}</code> Coolify app: <code dir="ltr" className="ms-1">{data.coolify_app_uuid.slice(0, 8)}</code>
</Badge> </Badge>
{driftCount > 0 && ( {driftCount > 0 && (
<Badge variant="outline" className="text-warn border-warn/40"> <span className="inline-flex items-center rounded-full bg-warn-bg text-warn text-[0.72rem] font-semibold px-2.5 py-0.5">
{driftCount} drift {driftCount} ב-Drift
</Badge> </span>
)} )}
{duplicatesCount > 0 && ( {duplicatesCount > 0 && (
<Badge variant="outline" className="text-warn border-warn/40"> <span className="inline-flex items-center rounded-full bg-warn-bg text-warn text-[0.72rem] font-semibold px-2.5 py-0.5">
{duplicatesCount} duplicates {duplicatesCount} duplicates
</Badge> </span>
)} )}
{data.errors.length > 0 && ( {data.errors.length > 0 && (
<Badge variant="outline" className="text-danger border-danger/40"> <span className="inline-flex items-center rounded-full bg-danger-bg text-danger text-[0.72rem] font-semibold px-2.5 py-0.5">
{data.errors.join(", ")} {data.errors.join(", ")}
</Badge> </span>
)} )}
</div> </div>
<Button <Button
onClick={handleRedeploy} onClick={handleRedeploy}
disabled={redeploy.isPending} disabled={redeploy.isPending}
variant={pendingRedeploy ? "default" : "outline"}
size="sm" size="sm"
className={pendingRedeploy ? "bg-gold text-white hover:bg-gold-deep border-transparent" : ""}
variant={pendingRedeploy ? "default" : "outline"}
> >
<RefreshCw className={redeploy.isPending ? "w-3.5 h-3.5 animate-spin" : "w-3.5 h-3.5"} data-icon="inline-start" /> <RefreshCw className={redeploy.isPending ? "w-3.5 h-3.5 animate-spin" : "w-3.5 h-3.5"} data-icon="inline-start" />
{redeploy.isPending ? "Redeploying..." : "Redeploy now"} {redeploy.isPending ? "פורס מחדש..." : "פרוס מחדש"}
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>
@@ -110,28 +112,41 @@ export function EnvironmentTab() {
{CATEGORY_ORDER.map((cat) => { {CATEGORY_ORDER.map((cat) => {
const vars = grouped.get(cat); const vars = grouped.get(cat);
if (!vars || vars.length === 0) return null; if (!vars || vars.length === 0) return null;
const catDrift = vars.filter((v) => v.drift).length;
return ( return (
<Card key={cat} className="bg-surface border-rule"> <div
<CardContent className="px-6 py-5"> key={cat}
<h2 className="text-navy text-lg mb-4 flex items-center gap-2"> className="rounded-lg border border-rule bg-surface shadow-sm overflow-hidden"
{CATEGORY_LABELS[cat]} >
<Badge variant="outline" className="text-[0.7rem] tabular-nums"> {/* panel header (.ph) — parchment band: title + count + drift chip */}
{vars.length} <div className="flex items-center justify-between gap-3 px-5 py-4 border-b border-rule bg-parchment">
</Badge> <div>
</h2> <h2 className="text-navy text-base font-semibold mb-0 flex items-center gap-2">
<div className="space-y-3"> {CATEGORY_LABELS[cat]}
{vars.map((v) => ( <span className="text-[0.72rem] text-ink-muted font-medium tabular-nums">
<EnvVarRow ({vars.length})
key={v.key} </span>
spec={v} </h2>
coolifyAppUuid={data.coolify_app_uuid} <p className="text-[0.78rem] text-ink-muted mt-0.5">
coolifyAvailable={coolifyAvailable} מקור-האמת הוא Coolify. שינוי נכנס לתוקף רק לאחר redeploy של הקונטיינר.
onPendingRedeploy={() => setPendingRedeploy(true)} </p>
/>
))}
</div> </div>
</CardContent> {coolifyAvailable && catDrift > 0 && (
</Card> <span className="inline-flex items-center rounded-full bg-warn-bg text-warn text-[0.72rem] font-semibold px-2.5 py-0.5 shrink-0">
{catDrift} ב-Drift
</span>
)}
</div>
{vars.map((v) => (
<EnvVarRow
key={v.key}
spec={v}
coolifyAppUuid={data.coolify_app_uuid}
coolifyAvailable={coolifyAvailable}
onPendingRedeploy={() => setPendingRedeploy(true)}
/>
))}
</div>
); );
})} })}
</div> </div>

View File

@@ -11,60 +11,92 @@ import { RegistrationsTab } from "./_components/registrations-tab";
import { BlocksTab } from "./_components/blocks-tab"; import { BlocksTab } from "./_components/blocks-tab";
import { AgentsTab } from "./_components/agents-tab"; import { AgentsTab } from "./_components/agents-tab";
/*
* Settings — IA-redesign composition (mockup 15): a header band with a
* top-end "redeploy" deep-link, then a two-column layout — a vertical
* sidenav (section list) + the active section panel. Implemented on top of
* shadcn Tabs so all six sections and their logic-heavy contents
* (Paperclip, agents, env vars w/ drift+redeploy, tools, blocks,
* registrations) are preserved verbatim; only the chrome is restyled from
* a horizontal tab strip to the approved sidenav layout.
*/
const COOLIFY_APP_UUID = "gyjo0mtw2c42ej3xxvbz8zio";
const COOLIFY_REDEPLOY_URL = `https://coolify.nautilus.marcusgroup.org/project/applications/${COOLIFY_APP_UUID}`;
const SECTIONS = [
{ value: "paperclip", label: "Paperclip / סוכנים", icon: Building2 },
{ value: "agents", label: "סוכנים", icon: Bot },
{ value: "environment", label: "משתני סביבה", icon: Server },
{ value: "tools", label: "כלי MCP", icon: Wrench },
{ value: "blocks", label: "בלוקים", icon: Layers },
{ value: "registrations", label: "רישומים", icon: Plug },
] as const;
export default function SettingsPage() { export default function SettingsPage() {
return ( return (
<AppShell> <AppShell>
<section className="space-y-6"> <section className="space-y-6">
<header> <header className="flex items-end justify-between gap-4 flex-wrap">
<nav className="text-[0.78rem] text-ink-muted mb-1"> <div>
<Link href="/" className="hover:text-gold-deep"> <nav className="text-[0.78rem] text-ink-muted mb-1">
בית <Link href="/" className="hover:text-gold-deep">
</Link> בית
<span aria-hidden> · </span> </Link>
<span className="text-navy">הגדרות</span> <span aria-hidden> · </span>
</nav> <span className="text-navy">הגדרות</span>
<h1 className="text-navy mb-0">הגדרות</h1> </nav>
<p className="text-ink-muted text-sm mt-1 max-w-2xl"> <h1 className="text-navy mb-0">הגדרות</h1>
תצורת המערכת, MCP server, ו-Paperclip integration. <p className="text-ink-muted text-sm mt-1 max-w-2xl">
</p> תצורת הפלטפורמה סוכנים, סביבה, כלים ובלוקים.
</p>
</div>
<a
href={COOLIFY_REDEPLOY_URL}
target="_blank"
rel="noopener noreferrer"
className="shrink-0 rounded-lg bg-gold text-white hover:bg-gold-deep px-5 py-2.5 text-sm font-semibold transition-colors"
>
פרוס מחדש (redeploy)
</a>
</header> </header>
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" /> <div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
<Tabs dir="rtl" defaultValue="paperclip" className="space-y-4"> <Tabs
<TabsList> dir="rtl"
<TabsTrigger value="paperclip"> defaultValue="environment"
<Building2 className="w-4 h-4" data-icon="inline-start" /> orientation="vertical"
Paperclip className="flex-row gap-6 items-start"
</TabsTrigger> >
<TabsTrigger value="agents"> {/* vertical sidenav (mockup .sidenav) */}
<Bot className="w-4 h-4" data-icon="inline-start" /> <TabsList
סוכנים variant="line"
</TabsTrigger> className="w-[230px] shrink-0 items-stretch gap-0 rounded-lg border border-rule bg-surface shadow-sm overflow-hidden p-0"
<TabsTrigger value="environment"> >
<Server className="w-4 h-4" data-icon="inline-start" /> {SECTIONS.map((s) => {
סביבה const Icon = s.icon;
</TabsTrigger> return (
<TabsTrigger value="tools"> <TabsTrigger
<Wrench className="w-4 h-4" data-icon="inline-start" /> key={s.value}
כלים value={s.value}
</TabsTrigger> className="relative justify-start gap-2 rounded-none border-b border-rule-soft last:border-b-0 px-4 py-3 text-sm font-medium text-ink-soft data-active:bg-gold-wash data-active:text-gold-deep data-active:font-semibold data-active:after:opacity-100 data-active:after:bg-gold hover:bg-gold-wash/40"
<TabsTrigger value="blocks"> >
<Layers className="w-4 h-4" data-icon="inline-start" /> <Icon className="w-4 h-4 shrink-0" />
בלוקים {s.label}
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="registrations"> );
<Plug className="w-4 h-4" data-icon="inline-start" /> })}
רישומים
</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="paperclip"><PaperclipTab /></TabsContent> <div className="flex-1 min-w-0">
<TabsContent value="agents"><AgentsTab /></TabsContent> <TabsContent value="paperclip" className="mt-0"><PaperclipTab /></TabsContent>
<TabsContent value="environment"><EnvironmentTab /></TabsContent> <TabsContent value="agents" className="mt-0"><AgentsTab /></TabsContent>
<TabsContent value="tools"><ToolsTab /></TabsContent> <TabsContent value="environment" className="mt-0"><EnvironmentTab /></TabsContent>
<TabsContent value="blocks"><BlocksTab /></TabsContent> <TabsContent value="tools" className="mt-0"><ToolsTab /></TabsContent>
<TabsContent value="registrations"><RegistrationsTab /></TabsContent> <TabsContent value="blocks" className="mt-0"><BlocksTab /></TabsContent>
<TabsContent value="registrations" className="mt-0"><RegistrationsTab /></TabsContent>
</div>
</Tabs> </Tabs>
</section> </section>
</AppShell> </AppShell>

View File

@@ -1,95 +1,64 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { Plug, HardDrive, Database, FileText } from "lucide-react";
import { AppShell } from "@/components/app-shell"; import { AppShell } from "@/components/app-shell";
import { Card, CardContent } from "@/components/ui/card"; import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { useSkills, type Skill } from "@/lib/api/skills"; import { useSkills, type Skill } from "@/lib/api/skills";
function formatSize(bytes: number | null) { function formatChars(skill: Skill): string {
if (bytes == null) return "—"; // Mockup column = "גודל (תווים)" — the DB markdown char count, grouped.
if (bytes < 1024) return `${bytes} B`; const n = skill.db_markdown_chars ?? 0;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return n.toLocaleString("en-US");
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
} }
function StatusDot({ tone }: { tone: string }) { function formatUpdated(iso: string | null): string {
return <span className={`h-1.5 w-1.5 rounded-full ${tone}`} aria-hidden />; if (!iso) return "—";
try {
// ISO-style date to match the mockup (2026-06-09), tabular.
return new Date(iso).toISOString().slice(0, 10);
} catch {
return "—";
}
} }
function statusBadge(s: Skill) { /**
if (s.not_in_db) { * Sync chip — colored dot + label, faithful to mockup 14:
return ( * - מסונכרן (success) when present in DB + on disk
<Badge variant="outline" className="gap-1.5 bg-warn-bg text-warn border-warn/40"> * - DB בלבד (info) when in DB but no disk copy
<StatusDot tone="bg-warn" />לא סונכרן * - לא מסונכרן (warn) when missing from the DB
</Badge> */
); function SyncChip({ skill }: { skill: Skill }) {
let tone: { wrap: string; dot: string };
let label: string;
if (skill.not_in_db) {
tone = { wrap: "bg-warn-bg text-warn", dot: "bg-warn" };
label = "לא מסונכרן";
} else if (skill.db_markdown_chars > 0 && skill.disk_exists) {
tone = { wrap: "bg-success-bg text-success", dot: "bg-success" };
label = "מסונכרן";
} else if (skill.db_markdown_chars > 0) {
tone = { wrap: "bg-info-bg text-info", dot: "bg-info" };
label = "DB בלבד";
} else {
tone = { wrap: "bg-rule-soft text-ink-muted", dot: "bg-ink-muted" };
label = "לא ידוע";
} }
if (s.db_markdown_chars > 0 && s.disk_exists) {
return (
<Badge variant="outline" className="gap-1.5 bg-success-bg text-success border-success/40">
<StatusDot tone="bg-success" />מסונכרן
</Badge>
);
}
if (s.db_markdown_chars > 0) {
return (
<Badge variant="outline" className="gap-1.5 bg-info-bg text-info border-info/40">
<StatusDot tone="bg-info" />DB בלבד
</Badge>
);
}
return <Badge variant="outline">לא ידוע</Badge>;
}
function SkillCard({ skill }: { skill: Skill }) {
const fileCount = skill.file_inventory?.length ?? 0;
return ( return (
<Card className="bg-surface border-rule shadow-sm hover:shadow-md transition-shadow"> <span
<CardContent className="px-5 py-4"> className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-[3px] text-[0.75rem] font-semibold ${tone.wrap}`}
<div className="flex items-start justify-between gap-3 mb-2"> >
<div className="flex items-center gap-2 min-w-0"> <span className={`h-1.5 w-1.5 rounded-full ${tone.dot}`} aria-hidden />
<Plug className="w-4 h-4 text-gold-deep shrink-0" /> {label}
<div className="min-w-0"> </span>
<h3 className="text-navy font-semibold text-base mb-0 truncate">
{skill.name || skill.slug}
</h3>
<code className="text-[0.72rem] text-ink-muted tabular-nums">
{skill.slug}
</code>
</div>
</div>
{statusBadge(skill)}
</div>
<dl className="grid grid-cols-3 gap-2 text-[0.72rem] text-ink-muted mt-3">
<div className="flex items-center gap-1">
<FileText className="w-3 h-3" />
<span className="tabular-nums">{fileCount}</span>
<span>קבצים</span>
</div>
<div className="flex items-center gap-1">
<Database className="w-3 h-3" />
<span className="tabular-nums">
{(skill.db_markdown_chars / 1000).toFixed(1)}K
</span>
<span>תווים</span>
</div>
<div className="flex items-center gap-1">
<HardDrive className="w-3 h-3" />
<span className="tabular-nums">
{formatSize(skill.disk_skill_md_bytes)}
</span>
</div>
</dl>
{skill.updated_at && (
<p className="text-[0.7rem] text-ink-light mt-2">
עודכן: {new Date(skill.updated_at).toLocaleDateString("he-IL")}
</p>
)}
</CardContent>
</Card>
); );
} }
@@ -105,9 +74,9 @@ export default function SkillsPage() {
<span aria-hidden> · </span> <span aria-hidden> · </span>
<span className="text-navy">מיומנויות</span> <span className="text-navy">מיומנויות</span>
</nav> </nav>
<h1 className="text-navy mb-0">מיומנויות Paperclip</h1> <h1 className="text-navy mb-0">מיומנויות</h1>
<p className="text-ink-muted text-sm mt-1 max-w-2xl"> <p className="text-ink-muted text-sm mt-1 max-w-2xl">
רשימת ה-skills המותקנים במערכת Paperclip ומצב הסנכרון שלהם בין ה-DB סקילים מותקנים בפלטפורמה שם, גודל, מועד עדכון ומצב הסנכרון בין ה-DB
לדיסק. לדיסק.
</p> </p>
</header> </header>
@@ -115,28 +84,69 @@ export default function SkillsPage() {
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" /> <div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
{error ? ( {error ? (
<Card className="bg-danger-bg border-danger/40"> <Card className="bg-danger-bg border-danger/40 px-6 py-6 text-center text-danger">
<CardContent className="px-6 py-6 text-center text-danger"> {error.message}
{error.message}
</CardContent>
</Card> </Card>
) : isPending ? ( ) : isPending ? (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> <Skeleton className="h-80 w-full rounded-lg" />
{[...Array(6)].map((_, i) => (
<Skeleton key={i} className="h-32 w-full rounded-lg" />
))}
</div>
) : data?.length === 0 ? ( ) : data?.length === 0 ? (
<Card className="bg-surface border-rule"> <Card className="bg-surface border-rule px-6 py-12 text-center text-ink-muted">
<CardContent className="px-6 py-12 text-center text-ink-muted"> <div className="text-gold text-3xl mb-2" aria-hidden></div>
<div className="text-gold text-3xl mb-2" aria-hidden></div> אין skills מותקנים
אין skills מותקנים
</CardContent>
</Card> </Card>
) : ( ) : (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> <Card className="bg-surface border-rule shadow-sm overflow-hidden p-0">
{data?.map((s) => <SkillCard key={s.slug} skill={s} />)} <Table>
</div> <TableHeader>
<TableRow className="bg-parchment hover:bg-parchment border-rule">
<TableHead className="text-start text-[0.75rem] font-semibold text-ink-muted px-5 py-3.5">
סלאג
</TableHead>
<TableHead className="text-start text-[0.75rem] font-semibold text-ink-muted px-5 py-3.5">
שם תצוגה
</TableHead>
<TableHead className="text-start text-[0.75rem] font-semibold text-ink-muted px-5 py-3.5">
גודל (תווים)
</TableHead>
<TableHead className="text-start text-[0.75rem] font-semibold text-ink-muted px-5 py-3.5">
עודכן
</TableHead>
<TableHead className="text-start text-[0.75rem] font-semibold text-ink-muted px-5 py-3.5">
סנכרון
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data?.map((s) => (
<TableRow
key={s.slug}
className="border-rule-soft hover:bg-gold-wash"
>
<TableCell className="px-5 py-4">
<code
className="font-mono text-[0.81rem] font-semibold text-navy"
dir="ltr"
>
{s.slug}
</code>
</TableCell>
<TableCell className="px-5 py-4 font-semibold text-navy text-[0.9rem]">
{s.name || s.slug}
</TableCell>
<TableCell className="px-5 py-4 text-ink-soft tabular-nums">
{formatChars(s)}
</TableCell>
<TableCell className="px-5 py-4 text-ink-muted text-[0.81rem] tabular-nums">
{formatUpdated(s.updated_at)}
</TableCell>
<TableCell className="px-5 py-4">
<SyncChip skill={s} />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Card>
)} )}
</section> </section>
</AppShell> </AppShell>

View File

@@ -22,16 +22,19 @@ export default function TrainingPage() {
<AppShell> <AppShell>
<section className="space-y-6"> <section className="space-y-6">
<header className="flex items-start justify-between gap-4 flex-wrap"> <header className="flex items-start justify-between gap-4 flex-wrap">
<div> <div className="space-y-1">
<nav className="text-[0.78rem] text-ink-muted mb-1"> <nav className="text-[0.78rem] text-ink-muted mb-1">
<Link href="/" className="hover:text-gold-deep">בית</Link> <Link href="/" className="hover:text-gold-deep">בית</Link>
<span aria-hidden> · </span> <span aria-hidden> · </span>
<span className="text-navy">אימון סגנון</span> <span className="text-navy">אימון סגנון</span>
</nav> </nav>
<h1 className="text-navy mb-0">הפורטרט הסגנוני של דפנה</h1> <div className="text-[0.75rem] uppercase tracking-[0.12em] text-gold-deep">
<p className="text-ink-muted text-sm mt-1 max-w-2xl"> רכישת-הסגנון של דפנה · פורטרט-הקול
לוח בקרה של קורפוס האימון סטטיסטיקות, אנטומיית החלטה ממוצעת, </div>
ביטויי חתימה, וכלי השוואה בין שתי החלטות. <h1 className="text-navy mb-0">אימון סגנון</h1>
<p className="text-ink-muted text-sm mt-1 max-w-2xl leading-relaxed">
פורטרט הקול שנלמד מהקורפוס, מוזן read-only לכותב סטטיסטיקות,
אנטומיית החלטה ממוצעת, ביטויי חתימה, וכלי השוואה בין החלטות.
</p> </p>
</div> </div>
<Button <Button

View File

@@ -1,5 +1,5 @@
import type { ReactNode } from "react";
import Link from "next/link"; import Link from "next/link";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { StatusBadge } from "@/components/cases/status-badge"; import { StatusBadge } from "@/components/cases/status-badge";
import { SyncIndicator } from "@/components/cases/sync-indicator"; import { SyncIndicator } from "@/components/cases/sync-indicator";
@@ -25,87 +25,129 @@ function formatDate(iso?: string | null) {
} }
} }
export function CaseHeader({ data }: { data?: CaseDetail }) { function partiesLine(data?: CaseDetail): string | null {
const appellant = data?.appellants?.filter(Boolean) ?? [];
const respondent = data?.respondents?.filter(Boolean) ?? [];
const parts: string[] = [];
if (appellant.length) parts.push(`עוררת: ${appellant.join(", ")}`);
if (respondent.length) parts.push(`משיבה: ${respondent.join(", ")}`);
return parts.length ? parts.join(" · ") : null;
}
/**
* Case header — parchment band (IA-redesign mockup 17): full-bleed band with
* the case title + status/type chips inline, a parties line, the case actions
* (edit / archive / repo / sync), and a metadata strip. The `tabs` slot renders
* the tab strip inside the band, anchored to its bottom edge.
*/
export function CaseHeader({
data,
actions,
tabs,
}: {
data?: CaseDetail;
actions?: ReactNode;
tabs?: ReactNode;
}) {
const parties = partiesLine(data);
const isBlam =
data?.proceeding_type === 'בל"מ' || isBlamSubtype(data?.appeal_subtype);
return ( return (
<Card className="bg-surface border-rule shadow-sm"> <div className="-mx-10 -mt-10 mb-2 bg-parchment border-b border-rule px-10 pt-6">
<CardContent className="px-6 py-5"> <nav className="text-[0.78rem] text-ink-muted mb-3 flex items-center gap-2">
<nav className="text-[0.78rem] text-ink-muted mb-3 flex items-center gap-2"> <Link href="/" className="hover:text-gold-deep">בית</Link>
<Link href="/" className="hover:text-gold-deep">בית</Link> <span aria-hidden>·</span>
<span aria-hidden>·</span> <span>תיקי ערר</span>
<span>תיקי ערר</span> <span aria-hidden>·</span>
<span aria-hidden>·</span> <span className="text-navy tabular-nums">{data?.case_number ?? "…"}</span>
<span className="text-navy tabular-nums">{data?.case_number ?? "…"}</span> </nav>
</nav>
<div className="flex items-start justify-between gap-6 flex-wrap"> <div className="flex items-start justify-between gap-6 flex-wrap">
<div className="space-y-2"> <div className="min-w-0">
<div className="flex items-center gap-3 flex-wrap"> {/* title row — H1 + status/type/blam chips inline (mockup .band h1) */}
<span className="font-display text-[2rem] font-black text-navy leading-none tabular-nums"> <h1 className="text-navy text-[1.7rem] font-bold leading-tight flex items-center gap-3 flex-wrap mb-0">
{data?.proceeding_type ?? "ערר"} {data?.case_number ?? "—"} <span className="tabular-nums">
</span> {data?.proceeding_type ?? "ערר"} {data?.case_number ?? "—"}
{data?.status && <StatusBadge status={data.status} />} </span>
{data?.archived_at && ( {data?.status && <StatusBadge status={data.status} />}
<Badge {data?.archived_at && (
variant="outline" <Badge
className="rounded-full px-2.5 py-0.5 text-[0.72rem] font-medium bg-ink-muted/10 text-ink-muted border-ink-muted/30" variant="outline"
> className="rounded-full px-3 py-0.5 text-[0.75rem] font-semibold bg-ink-muted/10 text-ink-muted border-ink-muted/30"
בארכיון >
</Badge> בארכיון
)} </Badge>
{data?.practice_area && (
<Badge
variant="outline"
className="rounded-full px-2.5 py-0.5 text-[0.72rem] font-medium bg-gold-wash text-gold-deep border-gold/40"
>
{PRACTICE_AREA_LABELS[data.practice_area]}
{data.appeal_subtype && data.appeal_subtype !== "unknown" && (
<> · {APPEAL_SUBTYPE_LABELS[data.appeal_subtype]}</>
)}
</Badge>
)}
{(data?.proceeding_type === 'בל"מ' || isBlamSubtype(data?.appeal_subtype)) && (
<Badge
variant="outline"
className="rounded-full px-2.5 py-0.5 text-[0.72rem] font-bold bg-warn/10 text-warn-deep border-warn/40"
title="בקשה להארכת מועד להגשת ערר"
>
בל&quot;מ
</Badge>
)}
{data?.case_number && (
<CaseArchiveAction
caseNumber={data.case_number}
archivedAt={data.archived_at}
/>
)}
<CreateRepoButton data={data} />
</div>
<h1 className="text-navy text-xl font-bold leading-snug max-w-2xl mb-0">
{data?.title ?? "טוען…"}
</h1>
{data?.subject && (
<p className="text-ink-muted text-sm max-w-2xl leading-relaxed">
{data.subject}
</p>
)} )}
</div> {data?.practice_area && (
<Badge
variant="outline"
className="rounded-full px-3 py-0.5 text-[0.75rem] font-semibold bg-gold-wash text-gold-deep border-rule"
>
{PRACTICE_AREA_LABELS[data.practice_area]}
{data.appeal_subtype && data.appeal_subtype !== "unknown" && (
<> · {APPEAL_SUBTYPE_LABELS[data.appeal_subtype]}</>
)}
</Badge>
)}
{isBlam && (
<Badge
variant="outline"
className="rounded-full px-3 py-0.5 text-[0.75rem] font-bold bg-warn/10 text-warn-deep border-warn/40"
title="בקשה להארכת מועד להגשת ערר"
>
בל&quot;מ
</Badge>
)}
</h1>
<dl className="grid grid-cols-2 gap-x-6 gap-y-1 text-sm"> {/* case title / subject under the heading */}
<dt className="text-ink-muted text-[0.72rem] uppercase tracking-wider"> {data?.title && (
תאריך דיון <p className="text-navy/90 text-base font-semibold mt-2 max-w-3xl leading-snug">
</dt> {data.title}
<dd className="text-ink-soft tabular-nums">{formatDate(data?.hearing_date)}</dd> </p>
<dt className="text-ink-muted text-[0.72rem] uppercase tracking-wider"> )}
עודכן {/* parties line (mockup .parties) */}
</dt> {parties ? (
<dd className="text-ink-soft tabular-nums">{formatDate(data?.updated_at)}</dd> <p className="text-ink-soft text-sm mt-1.5">{parties}</p>
<dt className="text-ink-muted text-[0.72rem] uppercase tracking-wider"> ) : data?.subject ? (
סנכרון <p className="text-ink-soft text-sm mt-1.5 max-w-3xl leading-relaxed">
</dt> {data.subject}
<dd><SyncIndicator caseNumber={data?.case_number} /></dd> </p>
</dl> ) : null}
{/* case actions — kept verbatim, moved into the band */}
<div className="flex items-center gap-2 flex-wrap mt-3">
{data?.case_number && (
<CaseArchiveAction
caseNumber={data.case_number}
archivedAt={data.archived_at}
/>
)}
<CreateRepoButton data={data} />
{actions}
</div>
</div> </div>
</CardContent>
</Card> {/* metadata strip — hearing date / updated / sync */}
<dl className="grid grid-cols-2 gap-x-6 gap-y-1 text-sm shrink-0">
<dt className="text-ink-muted text-[0.72rem] uppercase tracking-wider">
תאריך דיון
</dt>
<dd className="text-ink-soft tabular-nums">{formatDate(data?.hearing_date)}</dd>
<dt className="text-ink-muted text-[0.72rem] uppercase tracking-wider">
עודכן
</dt>
<dd className="text-ink-soft tabular-nums">{formatDate(data?.updated_at)}</dd>
<dt className="text-ink-muted text-[0.72rem] uppercase tracking-wider">
סנכרון
</dt>
<dd><SyncIndicator caseNumber={data?.case_number} /></dd>
</dl>
</div>
{/* tab strip anchored to band bottom (mockup .tabs) */}
{tabs ? <div className="mt-5">{tabs}</div> : null}
</div>
); );
} }

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import { useEffect, useState, type ReactNode } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Progress } from "@/components/ui/progress"; import { Progress } from "@/components/ui/progress";
import { import {
@@ -295,6 +295,25 @@ function DocumentRow({
/* ── Main panel ────────────────────────────────────────────────── */ /* ── Main panel ────────────────────────────────────────────────── */
// IA-redesign mockup 17 — card with a parchment header band wrapping the
// (unchanged) document list. Module-level so it isn't re-created during render
// (React Compiler: "Cannot create components during render").
function DocumentsShell({ count, children }: { count: number; children: ReactNode }) {
return (
<div className="rounded-lg border border-rule bg-surface shadow-sm overflow-hidden">
<div className="px-5 py-3.5 border-b border-rule-soft bg-parchment text-[0.92rem] font-semibold text-navy">
מסמכי התיק
{count > 0 && (
<span className="ms-2 text-[0.72rem] text-ink-muted font-medium tabular-nums">
({count})
</span>
)}
</div>
<div className="px-5 py-4">{children}</div>
</div>
);
}
export function DocumentsPanel({ export function DocumentsPanel({
data, data,
}: { }: {
@@ -305,10 +324,12 @@ export function DocumentsPanel({
if (docs.length === 0) { if (docs.length === 0) {
return ( return (
<div className="text-center py-12 text-ink-muted"> <DocumentsShell count={docs.length}>
<div className="text-gold text-2xl mb-2" aria-hidden="true"></div> <div className="text-center py-12 text-ink-muted">
<p className="text-sm">אין מסמכים בתיק זה</p> <div className="text-gold text-2xl mb-2" aria-hidden="true"></div>
</div> <p className="text-sm">אין מסמכים בתיק זה</p>
</div>
</DocumentsShell>
); );
} }
@@ -328,7 +349,8 @@ export function DocumentsPanel({
const pct = docs.length > 0 ? Math.round((done / docs.length) * 100) : 0; const pct = docs.length > 0 ? Math.round((done / docs.length) * 100) : 0;
return ( return (
<div className="space-y-3"> <DocumentsShell count={docs.length}>
<div className="space-y-3">
{hasIncomplete && ( {hasIncomplete && (
<div className="rounded-lg border border-rule bg-parchment/40 px-4 py-3 space-y-2" dir="rtl"> <div className="rounded-lg border border-rule bg-parchment/40 px-4 py-3 space-y-2" dir="rtl">
<div className="flex items-center gap-4 text-[0.78rem] flex-wrap"> <div className="flex items-center gap-4 text-[0.78rem] flex-wrap">
@@ -371,6 +393,7 @@ export function DocumentsPanel({
))} ))}
</ul> </ul>
</div> </div>
</div> </div>
</DocumentsShell>
); );
} }

View File

@@ -38,11 +38,7 @@ export function WorkflowTimeline({ status }: { status?: CaseStatus }) {
const currentIdx = phaseIndexOf(status); const currentIdx = phaseIndexOf(status);
return ( return (
<ol className="relative space-y-4"> <ol className="relative">
<div
className="absolute top-2 bottom-2 right-[11px] w-px bg-rule"
aria-hidden
/>
{PHASES.map((phase, i) => { {PHASES.map((phase, i) => {
const state = const state =
currentIdx === -1 ? "pending" currentIdx === -1 ? "pending"
@@ -51,9 +47,9 @@ export function WorkflowTimeline({ status }: { status?: CaseStatus }) {
: "pending"; : "pending";
const dotTone = const dotTone =
state === "done" ? "bg-success border-success" state === "done" ? "bg-success [box-shadow:0_0_0_1px_var(--color-success)]"
: state === "current" ? "bg-gold border-gold shadow-[0_0_0_4px_color-mix(in_oklab,var(--color-gold)_20%,transparent)]" : state === "current" ? "bg-gold [box-shadow:0_0_0_1px_var(--color-gold)]"
: "bg-surface border-rule"; : "bg-rule";
const labelTone = const labelTone =
state === "done" ? "text-ink-soft" state === "done" ? "text-ink-soft"
@@ -66,34 +62,55 @@ export function WorkflowTimeline({ status }: { status?: CaseStatus }) {
: "text-ink-muted/50"; : "text-ink-muted/50";
const PhaseIcon = phase.icon; const PhaseIcon = phase.icon;
const StatusIcon = status ? STATUS_ICONS[status] : null; const isLast = i === PHASES.length - 1;
return ( return (
<li key={phase.key} className="relative flex items-start gap-3 ps-7"> <li
key={phase.key}
className="relative flex items-center gap-3 py-2"
>
{/* connector line below the dot (mockup .tl .line) */}
{!isLast && (
<span
className="absolute top-[26px] w-px h-[20px] bg-rule"
style={{ insetInlineStart: "5px" }}
aria-hidden
/>
)}
<span <span
className={`absolute right-[5px] top-1 inline-block w-3 h-3 rounded-full border-2 ${dotTone}`} className={`inline-block w-[11px] h-[11px] rounded-full border-2 border-surface shrink-0 ${dotTone}`}
aria-hidden aria-hidden
/> />
<div className="flex flex-col gap-0.5"> <div className="flex items-center gap-2 grow min-w-0">
<span className={`text-sm flex items-center gap-1.5 ${labelTone}`}> <span className={`text-[0.84rem] flex items-center gap-1.5 ${labelTone}`}>
<PhaseIcon className={`w-3.5 h-3.5 shrink-0 ${iconTone}`} /> <PhaseIcon className={`w-3.5 h-3.5 shrink-0 ${iconTone}`} />
{phase.label} {phase.label}
</span> </span>
{state === "current" && status && ( <span className="ms-auto text-[0.72rem] text-ink-muted tabular-nums shrink-0">
<span className="text-[0.72rem] text-gold-deep flex items-center gap-1"> {state === "current" ? "כעת" : state === "done" ? "✓" : "—"}
{StatusIcon && <StatusIcon className="w-3 h-3 shrink-0" />} </span>
{STATUS_LABELS[status]}
</span>
)}
{state === "current" && status && STATUS_DESCRIPTIONS[status] && (
<span className="text-[0.65rem] text-ink-muted leading-snug mt-0.5">
{STATUS_DESCRIPTIONS[status]}
</span>
)}
</div> </div>
</li> </li>
); );
})} })}
{/* current micro-status detail under the active phase */}
{currentIdx !== -1 && status && (
<li className="ps-[26px] pt-1">
<span className="text-[0.72rem] text-gold-deep flex items-center gap-1">
{STATUS_ICONS[status] &&
(() => {
const Icon = STATUS_ICONS[status];
return <Icon className="w-3 h-3 shrink-0" />;
})()}
{STATUS_LABELS[status]}
</span>
{STATUS_DESCRIPTIONS[status] && (
<span className="block text-[0.65rem] text-ink-muted leading-snug mt-0.5">
{STATUS_DESCRIPTIONS[status]}
</span>
)}
</li>
)}
</ol> </ol>
); );
} }

View File

@@ -33,26 +33,24 @@ export function DigestCard({
actions?: ReactNode; actions?: ReactNode;
}) { }) {
const linked = Boolean(digest.linked_case_law_id); const linked = Boolean(digest.linked_case_law_id);
const pending = digest.extraction_status !== "completed";
return ( return (
<div className="rounded-lg border border-rule bg-surface p-4 space-y-2"> <div className="flex flex-col rounded-lg border border-rule bg-surface shadow-sm p-4">
<div className="flex items-center gap-2 text-[0.78rem] text-ink-muted flex-wrap"> {/* top row — yomon-number + date at start, concept tag pushed to end */}
{digest.concept_tag && ( <div className="flex items-center gap-2.5 mb-2.5 flex-wrap">
<Badge className="bg-gold text-navy border-0">{digest.concept_tag}</Badge>
)}
{digest.yomon_number && ( {digest.yomon_number && (
<span className="font-mono" dir="ltr"> <span className="text-navy font-bold text-[0.95rem]">
יומון {digest.yomon_number} יומון {digest.yomon_number}
</span> </span>
)} )}
{digest.digest_date && (
<span className="text-ink-muted text-[0.78rem]">· {formatDate(digest.digest_date)}</span>
)}
{digest.publication && digest.publication !== "כל יום" && ( {digest.publication && digest.publication !== "כל יום" && (
<Badge variant="outline" className="bg-rule-soft text-ink-muted text-[0.65rem]"> <Badge variant="outline" className="bg-rule-soft text-ink-muted text-[0.65rem]">
{digest.publication} {digest.publication}
</Badge> </Badge>
)} )}
{digest.digest_date && <span>· {formatDate(digest.digest_date)}</span>}
{digest.practice_area && (
<span>· {practiceAreaLabel(digest.practice_area)}</span>
)}
{digest.digest_kind === "announcement" && ( {digest.digest_kind === "announcement" && (
<Badge variant="outline" className="bg-sky-50 text-sky-700 border-sky-300 text-[0.65rem]"> <Badge variant="outline" className="bg-sky-50 text-sky-700 border-sky-300 text-[0.65rem]">
עדכון עדכון
@@ -63,47 +61,45 @@ export function DigestCard({
מאמר מאמר
</Badge> </Badge>
)} )}
{digest.extraction_status !== "completed" && ( {digest.concept_tag && (
<Badge variant="outline" className="bg-rule-soft text-ink-muted text-[0.65rem]"> <span className="ms-auto rounded-full bg-info-bg text-info text-[0.72rem] font-semibold px-2.5 py-0.5">
{digest.extraction_status === "pending" ? "ממתין לעיבוד" : digest.extraction_status} {digest.concept_tag}
</Badge> </span>
)} )}
{typeof score === "number" && ( {typeof score === "number" && (
<span className="ms-auto tabular-nums">דירוג {score.toFixed(2)}</span> <span className="ms-1 tabular-nums text-ink-muted text-[0.78rem]">
דירוג {score.toFixed(2)}
</span>
)} )}
</div> </div>
{/* holding — the digest headline */}
{digest.headline_holding && ( {digest.headline_holding && (
<p className="text-navy font-medium text-[0.95rem]" dir="rtl"> <p className="text-ink font-medium text-[0.92rem] leading-6 mb-2.5" dir="rtl">
{digest.headline_holding} {digest.headline_holding}
</p> </p>
)} )}
{/* source ruling — "מקור:" + citation, start-bordered (mockup `.ruling`) */}
<div className="border-s-2 border-rule ps-3 text-[0.8rem] text-ink-muted leading-6" dir="rtl">
<b className="text-ink-soft font-semibold">מקור:</b>{" "}
{digest.underlying_citation || "—"}
</div>
{digest.summary && ( {digest.summary && (
<p className="text-ink-soft text-sm leading-relaxed" dir="rtl"> <p className="text-ink-soft text-[0.82rem] leading-relaxed mt-2" dir="rtl">
{digest.summary} {digest.summary}
</p> </p>
)} )}
<div className="flex items-start gap-2 flex-wrap pt-1 border-t border-rule-soft mt-1"> {digest.practice_area && (
<span className="text-[0.72rem] text-ink-muted mt-1">פסק מקורי:</span> <span className="text-ink-muted text-[0.72rem] mt-2">
<span className="text-[0.82rem] text-ink font-mono flex-1 min-w-[200px]" dir="rtl"> {practiceAreaLabel(digest.practice_area)}
{digest.underlying_citation || "—"}
</span> </span>
{linked ? ( )}
<Link href={`/precedents/${digest.linked_case_law_id}`}>
<Badge className="bg-emerald-100 text-emerald-800 border-emerald-300 hover:bg-emerald-200">
מקושר לפסק
</Badge>
</Link>
) : (
<Badge variant="outline" className="bg-amber-50 text-amber-700 border-amber-300">
הפסק טרם בקורפוס
</Badge>
)}
</div>
{digest.subject_tags?.length > 0 && ( {digest.subject_tags?.length > 0 && (
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1 mt-2">
{digest.subject_tags.map((t) => ( {digest.subject_tags.map((t) => (
<Badge key={t} variant="outline" className="text-[0.65rem] bg-surface"> <Badge key={t} variant="outline" className="text-[0.65rem] bg-surface">
{t} {t}
@@ -112,7 +108,31 @@ export function DigestCard({
</div> </div>
)} )}
{actions && <div className="flex items-center gap-2 pt-1">{actions}</div>} {/* foot — link-status chip + passive "ממתין לעיבוד" label (mockup `.foot`) */}
<div className="mt-3 pt-3 border-t border-rule-soft flex items-center gap-2 flex-wrap">
{linked ? (
<Link href={`/precedents/${digest.linked_case_law_id}`}>
<span className="rounded-full bg-success-bg text-success text-[0.72rem] font-semibold px-3 py-0.5 hover:opacity-90">
מקושר לפסיקה
</span>
</Link>
) : (
<span className="rounded-full bg-rule-soft text-ink-muted text-[0.72rem] font-semibold px-3 py-0.5">
לא מקושר
</span>
)}
{pending && (
/* passive status label — a dot + text, deliberately NOT a button */
<span className="ms-auto inline-flex items-center gap-1.5 text-[0.72rem] font-medium text-ink-muted">
<span className="h-[7px] w-[7px] rounded-full bg-warn/60" aria-hidden />
{digest.extraction_status === "pending" ? "ממתין לעיבוד" : digest.extraction_status}
</span>
)}
</div>
{actions && (
<div className="flex items-center gap-2 pt-3">{actions}</div>
)}
</div> </div>
); );
} }

View File

@@ -14,7 +14,6 @@ import {
import type { PracticeArea } from "@/lib/api/precedent-library"; import type { PracticeArea } from "@/lib/api/precedent-library";
import { PRACTICE_AREAS } from "@/components/precedents/practice-area"; import { PRACTICE_AREAS } from "@/components/precedents/practice-area";
import { DigestCard } from "./digest-card"; import { DigestCard } from "./digest-card";
import { DigestUploadDialog } from "./digest-upload-dialog";
type LinkedFilter = "all" | "linked" | "unlinked"; type LinkedFilter = "all" | "linked" | "unlinked";
@@ -96,9 +95,6 @@ export function DigestListPanel() {
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div className="ms-auto">
<DigestUploadDialog />
</div>
</div> </div>
{error ? ( {error ? (
@@ -117,6 +113,8 @@ export function DigestListPanel() {
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
<p className="text-[0.78rem] text-ink-muted">{data.count} יומונים</p> <p className="text-[0.78rem] text-ink-muted">{data.count} יומונים</p>
{/* two-column card grid (mockup 10 `.grid`) */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{data.items.map((d) => ( {data.items.map((d) => (
<DigestCard <DigestCard
key={d.id} key={d.id}
@@ -148,6 +146,7 @@ export function DigestListPanel() {
} }
/> />
))} ))}
</div>
</div> </div>
)} )}
</div> </div>

View File

@@ -24,7 +24,7 @@ import { PRACTICE_AREAS } from "@/components/precedents/practice-area";
* via the MCP drainer ``digest_process_pending`` — so the toast tells the * via the MCP drainer ``digest_process_pending`` — so the toast tells the
* user the digest is queued, not yet searchable. * user the digest is queued, not yet searchable.
*/ */
export function DigestUploadDialog() { export function DigestUploadDialog({ trigger }: { trigger?: React.ReactNode } = {}) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const [yomonNumber, setYomonNumber] = useState(""); const [yomonNumber, setYomonNumber] = useState("");
@@ -71,10 +71,12 @@ export function DigestUploadDialog() {
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button className="bg-navy text-parchment hover:bg-navy-soft"> {trigger ?? (
<Upload className="w-4 h-4 me-1" /> <Button className="bg-gold text-white hover:bg-gold-deep border-transparent">
העלאת יומון <Upload className="w-4 h-4 me-1" />
</Button> העלאת יומון
</Button>
)}
</DialogTrigger> </DialogTrigger>
<DialogContent dir="rtl"> <DialogContent dir="rtl">
<DialogHeader> <DialogHeader>
@@ -140,7 +142,7 @@ export function DigestUploadDialog() {
<Button <Button
type="submit" type="submit"
disabled={upload.isPending} disabled={upload.isPending}
className="bg-navy text-parchment hover:bg-navy-soft" className="bg-gold text-white hover:bg-gold-deep border-transparent"
> >
{upload.isPending ? "מעלה…" : "העלה"} {upload.isPending ? "מעלה…" : "העלה"}
</Button> </Button>

View File

@@ -88,8 +88,11 @@ export function GraphFilterPanel({
facets?: GraphFacets; facets?: GraphFacets;
}) { }) {
return ( return (
<Card className="bg-surface border-rule shadow-sm w-72 shrink-0 overflow-y-auto"> <Card className="bg-surface border-rule shadow-sm w-[300px] shrink-0 max-h-full overflow-y-auto">
<CardContent className="space-y-5 p-4"> <CardContent className="space-y-5 p-4">
<div className="text-[0.8rem] font-semibold text-navy border-b border-rule-soft pb-2">
פילטרים וסינון
</div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label htmlFor="graph-search" className="text-xs text-ink-muted"> <Label htmlFor="graph-search" className="text-xs text-ink-muted">
חיפוש פסיקה חיפוש פסיקה
@@ -250,32 +253,50 @@ export function GraphFilterPanel({
<Separator /> <Separator />
<div className="space-y-3"> <div className="space-y-3">
<Label className="text-xs text-ink-muted">סוגי נקודות</Label> <div className="text-[0.8rem] font-semibold text-navy border-b border-rule-soft pb-2">
שכבות הגרף
</div>
<ToggleRow <ToggleRow
label="נקודות-נושא" label="נקודות-נושא"
swatch="#a97d3a"
checked={controls.showTopics} checked={controls.showTopics}
onCheckedChange={(v) => onChange({ showTopics: v })} onCheckedChange={(v) => onChange({ showTopics: v })}
/> />
<ToggleRow <ToggleRow
label="נקודות-תחום" label="נקודות-תחום"
swatch="#4a7c59"
checked={controls.showPracticeAreas} checked={controls.showPracticeAreas}
onCheckedChange={(v) => onChange({ showPracticeAreas: v })} onCheckedChange={(v) => onChange({ showPracticeAreas: v })}
/> />
<ToggleRow <ToggleRow
label="חוסרי מחקר (פסיקה חסרה)" label="חוסרי מחקר (פסיקה חסרה)"
swatch="#a54242"
checked={controls.showGaps} checked={controls.showGaps}
onCheckedChange={(v) => onChange({ showGaps: v })} onCheckedChange={(v) => onChange({ showGaps: v })}
/> />
<ToggleRow <ToggleRow
label="יומונים (כל יום)" label="יומונים (כל יום)"
swatch="#b8894a"
checked={controls.showDigests} checked={controls.showDigests}
onCheckedChange={(v) => onChange({ showDigests: v })} onCheckedChange={(v) => onChange({ showDigests: v })}
/> />
<ToggleRow </div>
label="הלכות"
checked={controls.showHalachot} {/* Stage-2 gate — halacha layer is dense, gated by default (mockup 11) */}
onCheckedChange={(v) => onChange({ showHalachot: v })} <div className="rounded-lg border border-gold bg-gold-wash p-3.5 space-y-2">
/> <div className="flex items-center gap-2">
<h4 className="text-sm font-semibold text-navy m-0">שלב ב׳ שכבת הלכות</h4>
<Switch
className="ms-auto"
checked={controls.showHalachot}
onCheckedChange={(v) => onChange({ showHalachot: v })}
aria-label="הצגת שכבת ההלכות"
/>
</div>
<p className="text-[0.72rem] text-ink-muted leading-relaxed m-0">
הפעלת שכבת ההלכות (1,454 צמתים). מגודרת כברירת-מחדל בשל הצפיפות
הדלקה מציגה את הקשרים הלכהפסיקה.
</p>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@@ -350,18 +371,28 @@ function ToggleRow({
checked, checked,
onCheckedChange, onCheckedChange,
disabled, disabled,
swatch,
}: { }: {
label: string; label: string;
checked: boolean; checked: boolean;
onCheckedChange: (v: boolean) => void; onCheckedChange: (v: boolean) => void;
disabled?: boolean; disabled?: boolean;
swatch?: string;
}) { }) {
return ( return (
<div className="flex items-center justify-between"> <div className={`flex items-center gap-2.5 ${disabled ? "opacity-55" : ""}`}>
<span className={`text-sm ${disabled ? "text-ink-muted/50" : "text-ink"}`}> {swatch ? (
<span
className="inline-block size-2.5 rounded-full shrink-0 ring-1 ring-black/10"
style={{ backgroundColor: swatch }}
aria-hidden
/>
) : null}
<span className={`text-sm ${disabled ? "text-ink-muted/50" : "text-ink-soft"}`}>
{label} {label}
</span> </span>
<Switch <Switch
className="ms-auto"
checked={checked} checked={checked}
onCheckedChange={onCheckedChange} onCheckedChange={onCheckedChange}
disabled={disabled} disabled={disabled}

View File

@@ -226,7 +226,7 @@ export function GraphView() {
return ( return (
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center justify-between gap-3 text-xs text-ink-muted"> <div className="flex items-center justify-between gap-3 text-xs text-ink-muted">
<span> <span className="inline-flex items-center gap-2 rounded-full border border-rule bg-surface px-3 py-1 tabular-nums">
{data ? `${data.nodes.length} נקודות · ${data.edges.length} קשרים` : "—"} {data ? `${data.nodes.length} נקודות · ${data.edges.length} קשרים` : "—"}
</span> </span>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@@ -248,12 +248,12 @@ export function GraphView() {
</div> </div>
</div> </div>
<div className="flex gap-4 h-[calc(100vh-320px)] min-h-[560px]"> <div className="flex gap-5 h-[calc(100vh-320px)] min-h-[560px] items-start">
<GraphFilterPanel controls={controls} onChange={onChange} facets={facets} /> <GraphFilterPanel controls={controls} onChange={onChange} facets={facets} />
<div <div
ref={canvasAreaRef} ref={canvasAreaRef}
className="relative flex-1 rounded-lg border border-rule bg-surface overflow-hidden" className="relative flex-1 h-full rounded-lg border border-rule bg-gradient-to-b from-[#f3ecda] to-[#efe6cf] shadow-sm overflow-hidden"
> >
{error ? ( {error ? (
<div className="grid h-full place-items-center p-6 text-center"> <div className="grid h-full place-items-center p-6 text-center">
@@ -311,6 +311,12 @@ export function GraphView() {
)} )}
<Legend colorBy={controls.colorBy} /> <Legend colorBy={controls.colorBy} />
{data ? (
<div className="absolute bottom-3 start-3 rounded-full bg-surface/70 backdrop-blur px-2.5 py-1 text-[0.72rem] text-ink-muted tabular-nums">
{data.nodes.length} צמתים מוצגים
</div>
) : null}
</div> </div>
{selectedNode ? ( {selectedNode ? (
@@ -340,9 +346,16 @@ function RankingPanel({
.sort((a, b) => (b.betweenness ?? 0) - (a.betweenness ?? 0)) .sort((a, b) => (b.betweenness ?? 0) - (a.betweenness ?? 0))
.slice(0, 12); .slice(0, 12);
const communities = new Set(
nodes.map((n) => n.community).filter((c) => c != null),
).size;
return ( return (
<Card className="bg-surface border-rule shadow-sm w-72 shrink-0 overflow-y-auto"> <Card className="bg-surface border-rule shadow-sm w-[300px] shrink-0 max-h-full overflow-y-auto">
<CardContent className="p-4"> <CardContent className="p-4 space-y-4">
<div className="text-[0.8rem] font-semibold text-navy border-b border-rule-soft pb-2">
אנליטיקה
</div>
<Tabs defaultValue="pagerank"> <Tabs defaultValue="pagerank">
<TabsList className="w-full"> <TabsList className="w-full">
<TabsTrigger value="pagerank" className="flex-1"> <TabsTrigger value="pagerank" className="flex-1">
@@ -359,6 +372,12 @@ function RankingPanel({
<RankList items={byBetweenness} metric="betweenness" onPick={onPick} /> <RankList items={byBetweenness} metric="betweenness" onPick={onPick} />
</TabsContent> </TabsContent>
</Tabs> </Tabs>
{communities > 0 ? (
<div className="flex items-center gap-2 border-t border-rule-soft pt-3 text-sm text-ink-soft">
אשכולות:
<b className="text-navy text-lg tabular-nums">{communities}</b>
</div>
) : null}
</CardContent> </CardContent>
</Card> </Card>
); );
@@ -377,18 +396,19 @@ function RankList({
return <p className="text-ink-muted text-xs mt-3">אין נתונים.</p>; return <p className="text-ink-muted text-xs mt-3">אין נתונים.</p>;
} }
return ( return (
<ol className="mt-2 space-y-1"> <ol className="mt-2">
{items.map((n, i) => ( {items.map((n, i) => (
<li key={n.id}> <li key={n.id} className="border-b border-rule-soft last:border-b-0">
<button <button
type="button" type="button"
onClick={() => onPick(n)} onClick={() => onPick(n)}
className="flex w-full items-baseline justify-between gap-2 rounded px-2 py-1 text-start text-sm hover:bg-gold-wash" className="flex w-full items-baseline gap-2 px-1 py-1.5 text-start text-sm hover:bg-gold-wash rounded"
> >
<span className="truncate"> <span className="w-4 shrink-0 text-ink-muted text-xs tabular-nums">
<span className="text-ink-muted text-xs">{i + 1}.</span> {n.label} {i + 1}
</span> </span>
<span className="text-ink-muted text-xs tabular-nums shrink-0"> <span className="truncate text-ink-soft">{n.label}</span>
<span className="ms-auto text-gold-deep font-semibold text-xs tabular-nums shrink-0">
{((n[metric] ?? 0) * 100).toFixed(0)} {((n[metric] ?? 0) * 100).toFixed(0)}
</span> </span>
</button> </button>

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { Card, CardContent } from "@/components/ui/card"; import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
@@ -115,84 +115,124 @@ export function ContentChecklistsPanel() {
); );
} }
const itemCount = current
? current.draft.split("\n").filter((l) => /^\s*-\s*\[/.test(l)).length
: 0;
return ( return (
<div className="space-y-4"> <div className="space-y-[18px]">
{/* Tab selector */} {/* Type buttons — gold active (mockup 13) */}
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2.5 flex-wrap">
{items.map((item) => ( {items.map((item) => {
<Button const isActive = active === item.key;
key={item.key} return (
size="sm" <button
variant={active === item.key ? "default" : "outline"} key={item.key}
onClick={() => { setActive(item.key); setPreview(false); }} type="button"
className="text-xs" onClick={() => {
> setActive(item.key);
{item.label} setPreview(false);
{item.isOverride && ( }}
<Badge variant="secondary" className="text-[9px] mr-1.5 px-1"> className={
מותאם isActive
</Badge> ? "rounded-lg border border-gold bg-gold px-4 py-2 text-[0.84rem] font-semibold text-white"
)} : "rounded-lg border border-rule bg-surface px-4 py-2 text-[0.84rem] font-medium text-ink-soft hover:border-gold/50"
</Button> }
))} >
{item.label}
{item.isOverride && (
<Badge
variant="secondary"
className={`text-[9px] ms-1.5 px-1 ${isActive ? "bg-white/20 text-white" : ""}`}
>
מותאם
</Badge>
)}
</button>
);
})}
</div> </div>
{/* Editor / Preview */} {/* "חל על:" explainer band — gold-wash (mockup 13) */}
{current && CHECKLIST_APPLIES[current.key] && (
<div className="flex items-baseline gap-2 rounded-lg border border-rule bg-gold-wash px-4 py-2.5 text-[0.84rem] text-ink-soft">
<b className="text-gold-deep font-semibold whitespace-nowrap">חל על:</b>
<span>{CHECKLIST_APPLIES[current.key]}</span>
</div>
)}
{/* Editor — framed card with header chip + parchment editor + footer */}
{current && ( {current && (
<Card className="border-rule"> <Card className="border-rule shadow-sm overflow-hidden p-0 gap-0">
<CardContent className="px-5 py-4 space-y-3"> <div className="flex items-center gap-2.5 px-[18px] py-3.5 border-b border-rule-soft">
<div className="flex items-center justify-between"> <h2 className="text-[0.95rem] font-semibold text-navy m-0">
<div className="min-w-0"> צ׳קליסט תוכן {current.label}
<h3 className="text-sm font-semibold text-navy">{current.label}</h3> </h2>
{CHECKLIST_APPLIES[current.key] && ( <span
<p className="text-[0.72rem] text-ink-muted mt-0.5"> className={`ms-auto rounded-full text-xs font-semibold px-2.5 py-0.5 ${
חל על: {CHECKLIST_APPLIES[current.key]} current.isOverride
</p> ? "bg-gold-wash text-gold-deep border border-rule"
)} : "bg-info-bg text-info"
</div> }`}
<Button >
size="sm" {current.isOverride ? "מותאם" : "ידני"}
variant="ghost" </span>
onClick={() => setPreview(!preview)} <Button
className="text-xs shrink-0" size="sm"
> variant="ghost"
{preview ? <EyeOff className="w-3.5 h-3.5 ml-1" /> : <Eye className="w-3.5 h-3.5 ml-1" />} onClick={() => setPreview(!preview)}
{preview ? "עריכה" : "תצוגה מקדימה"} className="text-xs shrink-0 h-7"
</Button> >
</div> {preview ? (
<EyeOff className="w-3.5 h-3.5 ms-1" />
{preview ? ( ) : (
<div className="border border-rule rounded-md p-4 bg-sand-soft/30 max-h-[500px] overflow-y-auto"> <Eye className="w-3.5 h-3.5 ms-1" />
<Markdown content={current.draft} />
</div>
) : (
<Textarea
value={current.draft}
onChange={(e) => updateDraft(e.target.value)}
className="min-h-[400px] font-mono text-sm leading-relaxed"
dir="rtl"
/>
)}
<div className="flex items-center gap-2">
<Button size="sm" disabled={!current.dirty || update.isPending} onClick={handleSave}>
{update.isPending ? <Loader2 className="w-3 h-3 animate-spin ml-1" /> : <Save className="w-3 h-3 ml-1" />}
שמור
</Button>
{current.isOverride && (
<Button size="sm" variant="outline" disabled={reset.isPending} onClick={handleReset}>
<RotateCcw className="w-3 h-3 ml-1" />
איפוס לברירת מחדל
</Button>
)} )}
<Badge {preview ? "עריכה" : "תצוגה מקדימה"}
variant={current.isOverride ? "default" : "secondary"} </Button>
className="text-[10px] mr-auto" </div>
>
{current.isOverride ? "מותאם" : "ברירת מחדל"} {preview ? (
</Badge> <div className="p-[18px] bg-parchment max-h-[500px] overflow-y-auto border-b border-rule-soft">
<Markdown content={current.draft} />
</div> </div>
</CardContent> ) : (
<Textarea
value={current.draft}
onChange={(e) => updateDraft(e.target.value)}
className="min-h-[340px] rounded-none border-0 border-b border-rule-soft bg-parchment font-mono text-[0.84rem] leading-[1.95] text-ink-soft focus-visible:ring-0 resize-y"
dir="rtl"
/>
)}
<div className="flex items-center gap-2.5 px-[18px] py-3.5">
<Button
disabled={!current.dirty || update.isPending}
onClick={handleSave}
className="bg-gold text-white hover:bg-gold-deep"
>
{update.isPending ? (
<Loader2 className="w-3.5 h-3.5 animate-spin ms-1" />
) : (
<Save className="w-3.5 h-3.5 ms-1" />
)}
שמור
</Button>
{current.isOverride && (
<Button
variant="outline"
disabled={reset.isPending}
onClick={handleReset}
className="border-rule text-navy"
>
<RotateCcw className="w-3.5 h-3.5 ms-1" />
אפס לברירת-מחדל
</Button>
)}
<span className="ms-auto text-[0.78rem] text-ink-muted tabular-nums">
{itemCount} פריטים
</span>
</div>
</Card> </Card>
)} )}
</div> </div>

View File

@@ -15,6 +15,7 @@ import {
useDeleteMissingPrecedent, useDeleteMissingPrecedent,
CITED_BY_PARTY_LABELS, CITED_BY_PARTY_LABELS,
STATUS_LABELS, STATUS_LABELS,
type CitedByParty,
type MissingPrecedent, type MissingPrecedent,
type MissingPrecedentStatus, type MissingPrecedentStatus,
} from "@/lib/api/missing-precedents"; } from "@/lib/api/missing-precedents";
@@ -29,20 +30,39 @@ function formatDate(iso: string | null) {
} }
} }
/** Status chip — mockup 09 tones (open=warn, uploaded=info, closed=success,
* irrelevant=muted). Pill-shaped, whitespace-nowrap. */
function StatusBadge({ status }: { status: MissingPrecedentStatus }) { function StatusBadge({ status }: { status: MissingPrecedentStatus }) {
const variants: Record<MissingPrecedentStatus, string> = { const variants: Record<MissingPrecedentStatus, string> = {
open: "bg-gold-wash text-gold-deep border-gold/40", open: "bg-warn-bg text-warn border-transparent",
uploaded: "bg-rule-soft text-ink-muted border-rule", uploaded: "bg-info-bg text-info border-transparent",
closed: "bg-emerald-50 text-emerald-800 border-emerald-300/60", closed: "bg-success-bg text-success border-transparent",
irrelevant: "bg-rule-soft text-ink-muted border-rule line-through", irrelevant: "bg-rule-soft text-ink-muted border-transparent",
}; };
return ( return (
<Badge variant="outline" className={variants[status]}> <Badge variant="outline" className={`rounded-full whitespace-nowrap ${variants[status]}`}>
{STATUS_LABELS[status]} {STATUS_LABELS[status]}
</Badge> </Badge>
); );
} }
/** Citing-party chip — colored by side (mockup 09 source chips). */
function SourceChip({ party }: { party: CitedByParty | null }) {
if (!party) return <span className="text-ink-muted text-sm"></span>;
const variants: Record<CitedByParty, string> = {
appellant: "bg-info-bg text-info border-transparent",
respondent: "bg-gold-wash text-gold-deep border-rule",
committee: "bg-success-bg text-success border-transparent",
permit_applicant: "bg-info-bg text-info border-transparent",
unknown: "bg-rule-soft text-ink-muted border-transparent",
};
return (
<Badge variant="outline" className={`rounded-full whitespace-nowrap ${variants[party]}`}>
{CITED_BY_PARTY_LABELS[party]}
</Badge>
);
}
function TableSkeleton({ cols }: { cols: number }) { function TableSkeleton({ cols }: { cols: number }) {
return ( return (
<> <>
@@ -100,14 +120,14 @@ export function MissingPrecedentsTable({ status, caseNumber, legalTopic }: Props
<> <>
<div className="rounded-lg border border-rule bg-surface shadow-sm overflow-hidden"> <div className="rounded-lg border border-rule bg-surface shadow-sm overflow-hidden">
<Table> <Table>
<TableHeader className="bg-rule-soft/60"> <TableHeader className="bg-parchment">
<TableRow className="border-rule"> <TableRow className="border-rule hover:bg-transparent">
<TableHead className="text-navy text-right">פסיקה</TableHead> <TableHead className="text-ink-muted text-right font-medium text-xs">פסיקה</TableHead>
<TableHead className="text-navy text-right">נושא</TableHead> <TableHead className="text-ink-muted text-right font-medium text-xs">נושא</TableHead>
<TableHead className="text-navy text-right">תיק</TableHead> <TableHead className="text-ink-muted text-right font-medium text-xs">תיק</TableHead>
<TableHead className="text-navy text-right">צד מצטט</TableHead> <TableHead className="text-ink-muted text-right font-medium text-xs">צוטט ע״י</TableHead>
<TableHead className="text-navy text-right">סטטוס</TableHead> <TableHead className="text-ink-muted text-right font-medium text-xs">סטטוס</TableHead>
<TableHead className="text-navy text-right">נוצר</TableHead> <TableHead className="text-ink-muted text-right font-medium text-xs">נוצר</TableHead>
<TableHead className="text-navy" /> <TableHead className="text-navy" />
</TableRow> </TableRow>
</TableHeader> </TableHeader>
@@ -128,7 +148,7 @@ export function MissingPrecedentsTable({ status, caseNumber, legalTopic }: Props
onClick={() => setOpenId(mp.id)} onClick={() => setOpenId(mp.id)}
> >
<TableCell className="max-w-[440px]"> <TableCell className="max-w-[440px]">
<div className="text-sm text-navy font-medium truncate"> <div className="text-sm text-navy font-semibold truncate">
{mp.case_name || mp.citation.split(" ").slice(0, 6).join(" ")} {mp.case_name || mp.citation.split(" ").slice(0, 6).join(" ")}
</div> </div>
<div className="text-[0.72rem] text-ink-muted truncate" dir="rtl"> <div className="text-[0.72rem] text-ink-muted truncate" dir="rtl">
@@ -153,11 +173,9 @@ export function MissingPrecedentsTable({ status, caseNumber, legalTopic }: Props
)} )}
</TableCell> </TableCell>
<TableCell className="text-sm text-ink"> <TableCell className="text-sm text-ink">
{mp.cited_by_party <SourceChip party={mp.cited_by_party} />
? CITED_BY_PARTY_LABELS[mp.cited_by_party]
: "—"}
{mp.cited_by_party_name ? ( {mp.cited_by_party_name ? (
<div className="text-[0.7rem] text-ink-muted truncate max-w-[160px]"> <div className="text-[0.7rem] text-ink-muted truncate max-w-[160px] mt-1">
{mp.cited_by_party_name} {mp.cited_by_party_name}
</div> </div>
) : null} ) : null}
@@ -165,7 +183,7 @@ export function MissingPrecedentsTable({ status, caseNumber, legalTopic }: Props
<TableCell> <TableCell>
<StatusBadge status={mp.status} /> <StatusBadge status={mp.status} />
{mp.linked_case_law_number ? ( {mp.linked_case_law_number ? (
<div className="text-[0.7rem] text-emerald-700 mt-1"> <div className="text-[0.7rem] text-success mt-1">
{mp.linked_case_law_name || mp.linked_case_law_number} {mp.linked_case_law_name || mp.linked_case_law_number}
</div> </div>
) : null} ) : null}
@@ -174,22 +192,39 @@ export function MissingPrecedentsTable({ status, caseNumber, legalTopic }: Props
{formatDate(mp.created_at)} {formatDate(mp.created_at)}
</TableCell> </TableCell>
<TableCell className="text-end"> <TableCell className="text-end">
<div className="flex items-center justify-end gap-1"> <div className="flex items-center justify-end gap-2">
<Button {mp.status === "open" ? (
variant="ghost" /* gold "העלה והשלם" CTA (mockup 09 `.btn`) */
size="sm" <Button
onClick={(e) => { size="sm"
e.stopPropagation(); onClick={(e) => {
setOpenId(mp.id); e.stopPropagation();
}} setOpenId(mp.id);
title={mp.status === "open" ? "העלאה" : "פרטים"} }}
> className="h-7 bg-gold text-white hover:bg-gold-deep border-transparent text-[0.78rem] font-semibold"
{mp.status === "open" ? ( >
<Upload className="w-4 h-4" /> <Upload className="w-3.5 h-3.5 me-1" />
) : ( העלה והשלם
</Button>
) : (
/* passive "done" label for non-open rows */
<span className="inline-flex items-center gap-1 rounded-md bg-rule-soft text-ink-muted text-[0.78rem] font-medium px-2.5 py-1">
{mp.status === "closed" ? "קושר" : STATUS_LABELS[mp.status]}
</span>
)}
{mp.status !== "open" ? (
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
setOpenId(mp.id);
}}
title="פרטים"
>
<Pencil className="w-4 h-4" /> <Pencil className="w-4 h-4" />
)} </Button>
</Button> ) : null}
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"

View File

@@ -24,32 +24,56 @@ function formatDate(iso: string | null) {
} }
} }
/** Score chip — boxed, gold-deep, tabular (mockup 07 `.score`). Sits at the
* end of the meta row via `ms-auto`. White fill on gold-wash halacha cards. */
function ScoreChip({ score, onWash }: { score: number; onWash?: boolean }) {
return (
<span
className={`ms-auto rounded-md border border-rule px-2.5 py-0.5 text-xs font-semibold text-gold-deep tabular-nums ${
onWash ? "bg-white" : "bg-surface"
}`}
>
דירוג {score.toFixed(2)}
</span>
);
}
function HalachaCard({ hit }: { hit: Extract<SearchHit, { type: "halacha" }> }) { function HalachaCard({ hit }: { hit: Extract<SearchHit, { type: "halacha" }> }) {
return ( return (
<div className="rounded-lg border border-gold/40 bg-gold-wash/40 p-4 space-y-2"> <div className="rounded-lg border border-rule bg-gold-wash p-4 shadow-sm space-y-2.5">
<div className="flex items-center gap-2 text-[0.78rem] text-ink-muted flex-wrap"> <div className="flex items-center gap-2 text-[0.78rem] text-ink-muted flex-wrap">
<Badge className="bg-gold text-navy border-0">הלכה</Badge> <Badge className="rounded bg-gold text-white border-0 text-[0.68rem] font-bold tracking-wide">
<span className="font-mono" dir="ltr">{hit.case_number}</span> הלכה
{hit.court && <span>· {hit.court}</span>} </Badge>
{hit.decision_date && <span>· {formatDate(hit.decision_date)}</span>} <span className="text-navy font-semibold" dir="ltr">{hit.case_number}</span>
{hit.precedent_level && <span>· {hit.precedent_level}</span>} {(hit.court || hit.decision_date || hit.precedent_level) && (
{/* PRE-3/PRE-5 (INV-IA5): the derived authority (binding/persuasive) <span className="text-ink-muted">
rides on the wire but was dropped here — render it as in the review {hit.court ? `· ${hit.court}` : ""}
tab so search shows the same provenance everywhere. */} {hit.decision_date ? ` · ${formatDate(hit.decision_date)}` : ""}
{hit.precedent_level ? ` · ${hit.precedent_level}` : ""}
</span>
)}
{/* PRE-3/PRE-5 (INV-IA5): derived authority (binding/persuasive)
rides on the wire — render the pill as in the review tab. */}
<AuthorityBadge authority={hit.authority} /> <AuthorityBadge authority={hit.authority} />
<span className="ms-auto tabular-nums">דירוג {hit.score.toFixed(2)}</span> <ScoreChip score={hit.score} onWash />
</div> </div>
<p className="text-navy font-medium text-[0.95rem]" dir="rtl"> <p className="text-ink font-medium text-[0.95rem] leading-7" dir="rtl">
{hit.rule_statement} {hit.rule_statement}
</p> </p>
<blockquote className="text-ink-soft text-sm border-s-2 border-gold ps-3" dir="rtl"> <blockquote
className="rounded-e border-s-[3px] border-gold bg-white/50 px-3 py-2 text-sm text-ink-soft leading-7"
dir="rtl"
>
&ldquo;{hit.supporting_quote}&rdquo; &ldquo;{hit.supporting_quote}&rdquo;
{hit.page_reference && <span className="text-ink-muted text-[0.72rem] ms-2">({hit.page_reference})</span>} {hit.page_reference && (
<span className="text-ink-muted text-[0.72rem] ms-2">({hit.page_reference})</span>
)}
</blockquote> </blockquote>
{hit.subject_tags?.length > 0 && ( {hit.subject_tags?.length > 0 && (
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{hit.subject_tags.map((t) => ( {hit.subject_tags.map((t) => (
<Badge key={t} variant="outline" className="text-[0.65rem] bg-surface"> <Badge key={t} variant="outline" className="text-[0.65rem] bg-white">
{t} {t}
</Badge> </Badge>
))} ))}
@@ -61,16 +85,24 @@ function HalachaCard({ hit }: { hit: Extract<SearchHit, { type: "halacha" }> })
function PassageCard({ hit }: { hit: Extract<SearchHit, { type: "passage" }> }) { function PassageCard({ hit }: { hit: Extract<SearchHit, { type: "passage" }> }) {
return ( return (
<div className="rounded-lg border border-rule bg-surface p-4 space-y-2"> <div className="rounded-lg border border-rule bg-surface p-4 shadow-sm space-y-2.5">
<div className="flex items-center gap-2 text-[0.78rem] text-ink-muted flex-wrap"> <div className="flex items-center gap-2 text-[0.78rem] text-ink-muted flex-wrap">
<Badge variant="outline" className="bg-info-bg text-info border-transparent">קטע</Badge> <Badge variant="outline" className="rounded bg-info-bg text-info border-transparent text-[0.68rem] font-bold tracking-wide">
<span className="font-mono" dir="ltr">{hit.case_number}</span> קטע
{hit.court && <span>· {hit.court}</span>} </Badge>
{hit.decision_date && <span>· {formatDate(hit.decision_date)}</span>} <span className="text-navy font-semibold" dir="ltr">{hit.case_number}</span>
<span className="text-[0.7rem]">· {hit.section_type}</span> {(hit.court || hit.decision_date) && (
<span className="ms-auto tabular-nums">דירוג {hit.score.toFixed(2)}</span> <span className="text-ink-muted">
{hit.court ? `· ${hit.court}` : ""}
{hit.decision_date ? ` · ${formatDate(hit.decision_date)}` : ""}
</span>
)}
<span className="rounded bg-rule-soft text-ink-muted text-[0.68rem] px-2 py-0.5 font-medium">
{hit.section_type}
</span>
<ScoreChip score={hit.score} />
</div> </div>
<p className="text-ink text-sm leading-relaxed" dir="rtl"> <p className="text-ink-soft text-sm leading-7" dir="rtl">
{hit.content.slice(0, 600)} {hit.content.slice(0, 600)}
{hit.content.length > 600 && <span></span>} {hit.content.length > 600 && <span></span>}
</p> </p>
@@ -98,51 +130,61 @@ export function LibrarySearchPanel() {
}; };
return ( return (
<div className="space-y-4"> <div className="space-y-6">
<form onSubmit={onSubmit} className="flex items-end gap-3 flex-wrap"> {/* search panel — boxed surface with query row + two-up filter row +
<div className="flex-1 min-w-[300px]"> controls strip (checkbox start, gold CTA end) per mockup 07. */}
<label className="text-[0.78rem] text-ink-muted">שאילתת חיפוש</label> <form
onSubmit={onSubmit}
className="rounded-lg border border-rule bg-surface shadow-sm p-5 space-y-3.5"
>
<div>
<label className="block text-[0.78rem] text-ink-muted font-medium mb-1.5">
שאילתת חיפוש
</label>
<Input value={draft} onChange={(e) => setDraft(e.target.value)} <Input value={draft} onChange={(e) => setDraft(e.target.value)}
placeholder="השבחה אובייקטיבית" dir="rtl" /> placeholder="השבחה אובייקטיבית" dir="rtl" />
</div> </div>
<div className="min-w-[180px]"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-3.5">
<label className="text-[0.78rem] text-ink-muted">תחום</label> <div>
<Select value={practiceArea || "_all"} <label className="block text-[0.78rem] text-ink-muted font-medium mb-1.5">תחום</label>
onValueChange={(v) => setPracticeArea(v === "_all" ? "" : v as PracticeArea)}> <Select value={practiceArea || "_all"}
<SelectTrigger><SelectValue /></SelectTrigger> onValueChange={(v) => setPracticeArea(v === "_all" ? "" : v as PracticeArea)}>
<SelectContent> <SelectTrigger className="w-full"><SelectValue /></SelectTrigger>
<SelectItem value="_all">הכל</SelectItem> <SelectContent>
{PRACTICE_AREAS.map((a) => ( <SelectItem value="_all">הכל</SelectItem>
<SelectItem key={a.value} value={a.value}>{a.label}</SelectItem> {PRACTICE_AREAS.map((a) => (
))} <SelectItem key={a.value} value={a.value}>{a.label}</SelectItem>
</SelectContent> ))}
</Select> </SelectContent>
</Select>
</div>
<div>
<label className="block text-[0.78rem] text-ink-muted font-medium mb-1.5">רמת תקדים</label>
<Select value={precedentLevel || "_all"}
onValueChange={(v) => setPrecedentLevel(v === "_all" ? "" : v)}>
<SelectTrigger className="w-full"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="_all">הכל</SelectItem>
{PRECEDENT_LEVELS.map((l) => (
<SelectItem key={l.value} value={l.value}>{l.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div> </div>
<div className="min-w-[170px]"> <div className="flex items-center gap-4 flex-wrap pt-1">
<label className="text-[0.78rem] text-ink-muted">רמת תקדים</label> <label className="flex items-center gap-2 cursor-pointer text-[0.85rem] text-ink-soft">
<Select value={precedentLevel || "_all"} <input type="checkbox" className="w-[15px] h-[15px] accent-gold" checked={includeHalachot}
onValueChange={(v) => setPrecedentLevel(v === "_all" ? "" : v)}> onChange={(e) => setIncludeHalachot(e.target.checked)} />
<SelectTrigger><SelectValue /></SelectTrigger> כלול הלכות
<SelectContent> </label>
<SelectItem value="_all">הכל</SelectItem> <Button type="submit" className="ms-auto bg-gold text-white hover:bg-gold-deep border-transparent">
{PRECEDENT_LEVELS.map((l) => ( <Search className="w-4 h-4 me-1" />
<SelectItem key={l.value} value={l.value}>{l.label}</SelectItem> חפש
))} </Button>
</SelectContent>
</Select>
</div> </div>
<Button type="submit" className="bg-navy text-parchment hover:bg-navy-soft">
<Search className="w-4 h-4 me-1" />
חפש
</Button>
</form> </form>
<label className="flex items-center gap-2 cursor-pointer text-sm text-ink-muted">
<input type="checkbox" checked={includeHalachot}
onChange={(e) => setIncludeHalachot(e.target.checked)} />
כלול הלכות (rule-level matches)
</label>
{!query.trim() ? ( {!query.trim() ? (
<div className="text-center text-ink-muted py-12"> <div className="text-center text-ink-muted py-12">
הקלד שאילתא כדי לחפש בקורפוס. החיפוש סמנטי לא טקסטואלי. הקלד שאילתא כדי לחפש בקורפוס. החיפוש סמנטי לא טקסטואלי.
@@ -160,11 +202,13 @@ export function LibrarySearchPanel() {
לא נמצאו תוצאות. נסה ניסוח אחר או הסר פילטרים. לא נמצאו תוצאות. נסה ניסוח אחר או הסר פילטרים.
</div> </div>
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3.5">
<p className="text-[0.78rem] text-ink-muted flex items-center gap-2"> <p className="text-[0.82rem] text-ink-muted flex items-center gap-2">
<span className="inline-flex items-center rounded-full bg-success-bg text-success text-[0.7rem] font-semibold px-2.5 py-0.5"> <span>תוצאות</span>
<span className="inline-flex items-center rounded-full bg-success-bg text-success text-[0.72rem] font-semibold px-2.5 py-0.5">
הלכות מאושרות בלבד הלכות מאושרות בלבד
</span> </span>
<span aria-hidden>·</span>
<span className="tabular-nums">{data.count} תוצאות</span> <span className="tabular-nums">{data.count} תוצאות</span>
</p> </p>
{data.items.map((hit, i) => {data.items.map((hit, i) =>

View File

@@ -128,60 +128,6 @@ function LinkDialog({ caseId, currentRelated, open, onOpenChange }: DialogProps)
); );
} }
// ── Related Case Card ────────────────────────────────────────────────
function RelatedCaseCard({ caseId, related }: { caseId: string; related: RelatedCase }) {
const { mutateAsync: unlinkCase, isPending } = useUnlinkRelatedCase(caseId);
async function handleUnlink() {
try {
await unlinkCase(related.id);
toast.success("הקישור הוסר");
} catch {
toast.error("שגיאה בהסרת הקישור");
}
}
return (
<div className="flex items-center justify-between gap-3 px-3 py-2.5 rounded-lg border border-rule bg-surface">
<a
href={`/precedents/${related.id}`}
className="min-w-0 flex-1 hover:opacity-80 transition-opacity"
>
<div className="text-sm font-medium text-navy truncate">
{related.case_name || related.case_number}
</div>
<div className="flex items-center gap-2 mt-0.5 flex-wrap">
{related.precedent_level && (
<Badge
variant="outline"
className={`text-[0.62rem] ${LEVEL_COLORS[related.precedent_level] ?? ""}`}
>
{LEVEL_LABELS[related.precedent_level] ?? related.precedent_level}
</Badge>
)}
{related.court && (
<span className="text-[0.7rem] text-ink-muted truncate">{related.court}</span>
)}
{related.date && (
<span className="text-[0.7rem] text-ink-muted tabular-nums" dir="ltr">
{related.date.slice(0, 10)}
</span>
)}
</div>
</a>
<button
onClick={handleUnlink}
disabled={isPending}
className="p-1 rounded hover:bg-danger-bg hover:text-danger transition-colors text-ink-muted disabled:opacity-40 shrink-0"
title="הסר קישור"
>
<X className="w-3.5 h-3.5" />
</button>
</div>
);
}
// ── Public section component ───────────────────────────────────────── // ── Public section component ─────────────────────────────────────────
type SectionProps = { type SectionProps = {
@@ -189,28 +135,68 @@ type SectionProps = {
related: RelatedCase[]; related: RelatedCase[];
}; };
/* Rail-styled citations card (mockup 08 side rail). Renders linked related
* decisions as a navy-headed card with arrow-prefixed rows; keeps the full
* link/unlink logic. Used in the precedent-detail side rail. */
export function RelatedCasesSection({ caseId, related }: SectionProps) { export function RelatedCasesSection({ caseId, related }: SectionProps) {
const [dialogOpen, setDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);
return ( return (
<div className="space-y-3"> <div className="rounded-lg border border-rule bg-surface shadow-sm px-4 py-3.5 space-y-2.5">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between gap-2">
<h3 className="text-navy text-sm font-semibold"> <h3 className="text-navy text-[0.92rem] font-semibold m-0">
החלטות קשורות{related.length > 0 ? ` (${related.length})` : ""} ציטוטים מקושרים{related.length > 0 ? ` (${related.length})` : ""}
</h3> </h3>
<Button variant="outline" size="sm" onClick={() => setDialogOpen(true)}> <Button
<Link2 className="w-3.5 h-3.5 me-1" /> קשר החלטה variant="outline"
size="sm"
className="h-7 px-2 text-[0.72rem] border-rule"
onClick={() => setDialogOpen(true)}
>
<Link2 className="w-3 h-3 me-1" /> קשר
</Button> </Button>
</div> </div>
{related.length === 0 ? ( {related.length === 0 ? (
<p className="text-ink-muted text-sm">אין החלטות קשורות עדיין</p> <p className="text-ink-muted text-[0.82rem] m-0">אין החלטות קשורות עדיין</p>
) : ( ) : (
<div className="space-y-1.5"> <ul className="list-none p-0 m-0">
{related.map((r) => ( {related.map((r) => (
<RelatedCaseCard key={r.id} caseId={caseId} related={r} /> <li
key={r.id}
className="flex items-start gap-2 py-2 border-b border-rule-soft last:border-b-0"
>
<span className="text-gold font-bold leading-6 shrink-0" aria-hidden></span>
<a
href={`/precedents/${r.id}`}
className="min-w-0 flex-1 group hover:opacity-90 transition-opacity"
>
<div className="text-[0.82rem] text-ink-soft leading-5 group-hover:text-navy">
{r.case_name || r.case_number}
</div>
<div className="flex items-center gap-2 mt-0.5 flex-wrap">
{r.precedent_level && (
<Badge
variant="outline"
className={`text-[0.6rem] ${LEVEL_COLORS[r.precedent_level] ?? ""}`}
>
{LEVEL_LABELS[r.precedent_level] ?? r.precedent_level}
</Badge>
)}
{r.court && (
<span className="text-[0.68rem] text-ink-muted truncate">{r.court}</span>
)}
{r.date && (
<span className="text-[0.68rem] text-ink-muted tabular-nums" dir="ltr">
{r.date.slice(0, 10)}
</span>
)}
</div>
</a>
<UnlinkButton caseId={caseId} relatedId={r.id} />
</li>
))} ))}
</div> </ul>
)} )}
<LinkDialog <LinkDialog
@@ -222,3 +208,24 @@ export function RelatedCasesSection({ caseId, related }: SectionProps) {
</div> </div>
); );
} }
function UnlinkButton({ caseId, relatedId }: { caseId: string; relatedId: string }) {
const { mutateAsync: unlinkCase, isPending } = useUnlinkRelatedCase(caseId);
return (
<button
onClick={async () => {
try {
await unlinkCase(relatedId);
toast.success("הקישור הוסר");
} catch {
toast.error("שגיאה בהסרת הקישור");
}
}}
disabled={isPending}
className="p-0.5 rounded hover:bg-danger-bg hover:text-danger transition-colors text-ink-muted disabled:opacity-40 shrink-0"
title="הסר קישור"
>
<X className="w-3 h-3" />
</button>
);
}

View File

@@ -3,7 +3,10 @@
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { SubjectDonut } from "@/components/training/subject-donut"; import { SubjectDonut } from "@/components/training/subject-donut";
import { useStyleReport } from "@/lib/api/training"; import { useStyleReport, useCuratorStats } from "@/lib/api/training";
// Mockup 12 anatomy palette — info · gold · gold-deep · success, cycling.
const ANATOMY_COLORS = ["#4e6a8c", "#a97d3a", "#8b6428", "#4a7c59"];
function KPICard({ function KPICard({
label, label,
@@ -16,15 +19,13 @@ function KPICard({
}) { }) {
return ( return (
<Card className="bg-surface border-rule shadow-sm"> <Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-5 py-4 flex flex-col gap-0.5"> <CardContent className="px-[18px] py-4 flex flex-col">
<span className="text-[0.72rem] uppercase tracking-[0.08em] text-ink-muted"> <span className="font-display text-[1.85rem] font-bold leading-[1.1] text-navy tabular-nums">
{label}
</span>
<span className="font-display text-[2rem] font-black leading-none text-navy">
{value} {value}
</span> </span>
<span className="text-[0.81rem] text-ink-soft mt-1">{label}</span>
{caption && ( {caption && (
<span className="text-[0.78rem] text-ink-muted mt-1">{caption}</span> <span className="text-[0.72rem] text-ink-muted mt-0.5">{caption}</span>
)} )}
</CardContent> </CardContent>
</Card> </Card>
@@ -33,6 +34,7 @@ function KPICard({
export function StyleReportPanel() { export function StyleReportPanel() {
const { data, isPending, error } = useStyleReport(); const { data, isPending, error } = useStyleReport();
const curator = useCuratorStats();
if (error) { if (error) {
return ( return (
@@ -63,14 +65,13 @@ export function StyleReportPanel() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Headline */} {/* Headline banner — gold-wash, ★ aligned to start (mockup 12) */}
<Card className="bg-gold-wash border-gold/40 shadow-sm"> <div className="flex items-start gap-3 rounded-lg border border-gold bg-gold-wash px-5 py-4 shadow-sm">
<CardContent className="px-6 py-4"> <span className="text-gold-deep text-xl leading-tight shrink-0"></span>
<p className="font-display text-gold-deep text-lg font-semibold leading-snug"> <p className="text-ink-soft text-[0.95rem] leading-relaxed m-0">
{c.headline} {c.headline}
</p> </p>
</CardContent> </div>
</Card>
{/* KPIs */} {/* KPIs */}
<div className="grid gap-4 grid-cols-2 lg:grid-cols-4"> <div className="grid gap-4 grid-cols-2 lg:grid-cols-4">
@@ -121,23 +122,22 @@ export function StyleReportPanel() {
{data.anatomy.sections.length === 0 ? ( {data.anatomy.sections.length === 0 ? (
<p className="text-ink-muted text-sm">אין נתונים על מבנה</p> <p className="text-ink-muted text-sm">אין נתונים על מבנה</p>
) : ( ) : (
<ul className="space-y-2.5"> <ul className="space-y-3.5">
{data.anatomy.sections.map((s) => { {data.anatomy.sections.map((s, i) => {
const pct = Math.round(s.pct * 100); const pct = Math.round(s.pct * 100);
const fill = ANATOMY_COLORS[i % ANATOMY_COLORS.length];
return ( return (
<li key={s.type} className="space-y-1"> <li key={s.type} className="space-y-1.5">
<div className="flex items-center justify-between text-[0.78rem]"> <div className="flex items-center justify-between text-[0.81rem]">
<span className="text-ink-soft font-medium"> <span className="text-ink-soft">{s.label}</span>
{s.label} <span className="text-navy font-semibold tabular-nums">
</span>
<span className="text-ink-muted tabular-nums">
{pct}% · {s.avg_chars.toLocaleString()} תווים {pct}% · {s.avg_chars.toLocaleString()} תווים
</span> </span>
</div> </div>
<div className="h-2 rounded bg-rule-soft overflow-hidden"> <div className="h-2.5 rounded-full bg-rule-soft overflow-hidden">
<div <div
className="h-full bg-gradient-to-l from-gold to-gold-deep" className="h-full rounded-full"
style={{ width: `${pct}%` }} style={{ width: `${pct}%`, backgroundColor: fill }}
/> />
</div> </div>
</li> </li>
@@ -149,50 +149,92 @@ export function StyleReportPanel() {
</Card> </Card>
</div> </div>
{/* Signature phrases */} {/* Signature phrases + curator stat — two columns (mockup 12) */}
<Card className="bg-surface border-rule shadow-sm"> <div className="grid gap-6 lg:grid-cols-2 items-start">
<CardContent className="px-6 py-5"> <Card className="bg-surface border-rule shadow-sm">
<h3 className="text-navy text-lg mb-1">ביטויי חתימה</h3> <CardContent className="px-6 py-5">
{data.signature_phrases.headline && ( <h3 className="text-navy text-lg mb-1">ביטויי חתימה</h3>
<p className="text-[0.78rem] text-gold-deep mb-4"> {data.signature_phrases.headline && (
{data.signature_phrases.headline} <p className="text-[0.78rem] text-gold-deep mb-3">
</p> {data.signature_phrases.headline}
)} </p>
{data.signature_phrases.items.length === 0 ? ( )}
<p className="text-ink-muted text-sm">אין ביטויים שחולצו עדיין</p> {data.signature_phrases.items.length === 0 ? (
) : ( <p className="text-ink-muted text-sm">אין ביטויים שחולצו עדיין</p>
<ol className="space-y-2"> ) : (
{data.signature_phrases.items.slice(0, 12).map((p, i) => ( <ol>
<li {data.signature_phrases.items.slice(0, 12).map((p, i) => (
key={`${p.type}-${i}`} <li
className="flex items-start gap-3 rounded border border-rule bg-parchment/40 px-3 py-2" key={`${p.type}-${i}`}
> className="flex items-baseline gap-2.5 py-2.5 border-b border-rule-soft last:border-b-0"
<span className="text-[0.7rem] text-ink-muted tabular-nums shrink-0 mt-0.5">
#{i + 1}
</span>
<div className="flex-1 min-w-0">
<p className="text-ink leading-relaxed text-sm">{p.text}</p>
{p.context && (
<p className="text-[0.7rem] text-ink-muted mt-0.5">
{p.context}
</p>
)}
</div>
<span
className="
shrink-0 text-[0.72rem] rounded-full
bg-gold-wash text-gold-deep border border-gold/40
px-2 py-0.5 tabular-nums
"
> >
×{p.frequency} <span className="w-5 shrink-0 text-gold-deep font-bold tabular-nums text-sm">
</span> {i + 1}
</li> </span>
))} <div className="flex-1 min-w-0">
</ol> <p className="text-ink-soft leading-relaxed text-[0.85rem] m-0">
)} {p.text}
</CardContent> </p>
</Card> {p.context && (
<p className="text-[0.7rem] text-ink-muted mt-0.5 m-0">
{p.context}
</p>
)}
</div>
<span className="shrink-0 text-[0.72rem] font-semibold rounded-full bg-gold-wash text-gold-deep border border-rule px-2.5 py-0.5 tabular-nums whitespace-nowrap">
×{p.frequency}
</span>
</li>
))}
</ol>
)}
</CardContent>
</Card>
{/* Curator — surfaced style findings (INV-LRN1 writer gate) */}
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5 flex flex-col gap-4">
<h3 className="text-navy text-lg m-0">אוצֵר ממצאי-סגנון</h3>
<div className="flex items-center gap-4 rounded-lg border border-success bg-success-bg px-[18px] py-3.5">
<span className="text-success font-bold text-[1.75rem] leading-none tabular-nums">
{curator.data ? curator.data.findings_approved : "—"}
</span>
<div className="min-w-0">
<b className="text-navy text-sm font-semibold block">
ממצאים מאושרים (זורמים לכותב)
</b>
<span className="text-ink-muted text-[0.78rem]">
אושרו ע״י היו״ר · review_status=approved
</span>
</div>
</div>
<div className="flex gap-3.5">
<div className="flex-1 rounded-lg border border-rule bg-warn-bg px-3.5 py-3">
<div className="text-warn font-bold text-[1.35rem] tabular-nums leading-none">
{curator.data
? Math.max(
0,
curator.data.total_findings -
curator.data.findings_approved,
)
: "—"}
</div>
<div className="text-[0.78rem] text-ink-soft mt-1">לא-מאושרים</div>
</div>
<div className="flex-1 rounded-lg border border-rule bg-rule-soft px-3.5 py-3">
<div className="text-ink-muted font-bold text-[1.35rem] tabular-nums leading-none">
{curator.data ? curator.data.total_findings : "—"}
</div>
<div className="text-[0.78rem] text-ink-soft mt-1">סך ממצאים</div>
</div>
</div>
<p className="text-[0.72rem] text-ink-muted leading-relaxed m-0">
רק ממצא מאושר זורם לכותב (INV-LRN1). ממתינים ונדחים אינם משפיעים על
הטיוטות.
</p>
</CardContent>
</Card>
</div>
</div> </div>
); );
} }