feat(ui): IA redesign → production · יישום נאמן של 16 הדפים הנותרים למוקאפים
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 6s
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:
@@ -26,7 +26,8 @@ import {
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
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) {
|
||||
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 }) {
|
||||
const restore = useRestoreCase(caseNumber);
|
||||
return (
|
||||
@@ -77,7 +92,7 @@ function RestoreButton({ caseNumber }: { caseNumber: string }) {
|
||||
const columns: ColumnDef<Case>[] = [
|
||||
{
|
||||
accessorKey: "case_number",
|
||||
header: "מס׳ ערר",
|
||||
header: "מספר ערר",
|
||||
cell: ({ row }) => (
|
||||
<Link
|
||||
href={`/cases/${row.original.case_number}`}
|
||||
@@ -91,20 +106,42 @@ const columns: ColumnDef<Case>[] = [
|
||||
accessorKey: "title",
|
||||
header: "כותרת",
|
||||
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}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "appeal_subtype",
|
||||
header: "תחום",
|
||||
header: "סוג",
|
||||
cell: ({ row }) => {
|
||||
const s = row.original.appeal_subtype;
|
||||
const s = subtypeOf(row.original);
|
||||
if (!s || s === "unknown")
|
||||
return <span className="text-ink-muted">—</span>;
|
||||
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",
|
||||
header: "תאריך ארכוב",
|
||||
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)}
|
||||
</span>
|
||||
),
|
||||
@@ -130,6 +167,7 @@ export default function ArchivePage() {
|
||||
{ id: "archived_at", desc: true },
|
||||
]);
|
||||
const [globalFilter, setGlobalFilter] = useState("");
|
||||
const [typeFilter, setTypeFilter] = useState<string>("all");
|
||||
|
||||
const rows = useMemo(() => data ?? [], [data]);
|
||||
|
||||
@@ -152,59 +190,75 @@ 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 (
|
||||
<AppShell>
|
||||
<section className="space-y-8">
|
||||
<section className="space-y-6">
|
||||
<header className="space-y-1.5">
|
||||
<nav className="text-[0.78rem] text-ink-muted mb-1">
|
||||
<Link href="/" className="hover:text-gold-deep">בית</Link>
|
||||
<span aria-hidden> · </span>
|
||||
<span className="text-navy">ארכיון</span>
|
||||
</nav>
|
||||
<div className="flex items-end justify-between gap-4 flex-wrap">
|
||||
<div className="space-y-1">
|
||||
<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-base max-w-2xl leading-relaxed">
|
||||
תיקים שסגרו את הטיפול בהם. שחזור מחזיר את התיק לרשימה הראשית
|
||||
ופותח מחדש את הפרויקט המקביל ב-Paperclip.
|
||||
כל ההחלטות שהושלמו — לחיפוש, סינון ועיון. {total} תיקים סגורים.
|
||||
שחזור מחזיר את התיק לרשימה הראשית ופותח מחדש את הפרויקט המקביל ב-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>
|
||||
</header>
|
||||
|
||||
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
|
||||
|
||||
<Card className="bg-surface border-rule shadow-sm">
|
||||
<CardContent className="px-6 py-5 space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{/* filter bar — search + domain select + count aligned to end (mockup 05 .filters) */}
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<Input
|
||||
value={globalFilter}
|
||||
onChange={(e) => setGlobalFilter(e.target.value)}
|
||||
placeholder="חיפוש לפי מס׳ ערר או כותרת…"
|
||||
className="max-w-sm bg-surface"
|
||||
placeholder="חיפוש לפי מספר ערר, כותרת או צד…"
|
||||
className="flex-1 min-w-[220px] bg-surface"
|
||||
dir="rtl"
|
||||
/>
|
||||
<select
|
||||
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) */}
|
||||
<Card className="bg-surface border-rule shadow-sm overflow-hidden p-0">
|
||||
<CardContent className="p-0">
|
||||
<Table>
|
||||
<TableHeader className="bg-rule-soft/60">
|
||||
<TableHeader className="bg-parchment">
|
||||
{table.getHeaderGroups().map((hg) => (
|
||||
<TableRow key={hg.id} className="border-rule">
|
||||
{hg.headers.map((header) => (
|
||||
<TableHead
|
||||
key={header.id}
|
||||
onClick={header.column.getToggleSortingHandler()}
|
||||
className="text-navy font-semibold cursor-pointer select-none text-right"
|
||||
className="text-ink-muted font-medium text-[0.78rem] cursor-pointer select-none text-start"
|
||||
>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
@@ -221,7 +275,7 @@ export default function ArchivePage() {
|
||||
<TableBody>
|
||||
{isPending ? (
|
||||
Array.from({ length: 3 }).map((_, i) => (
|
||||
<TableRow key={i} className="border-rule">
|
||||
<TableRow key={i} className="border-rule-soft">
|
||||
{columns.map((_c, j) => (
|
||||
<TableCell key={j}>
|
||||
<Skeleton className="h-4 w-24" />
|
||||
@@ -238,7 +292,7 @@ export default function ArchivePage() {
|
||||
שגיאה בטעינת ארכיון: {error.message}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : table.getRowModel().rows.length === 0 ? (
|
||||
) : filteredRows.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
@@ -247,16 +301,16 @@ export default function ArchivePage() {
|
||||
<div className="text-gold text-2xl mb-2" aria-hidden>
|
||||
❦
|
||||
</div>
|
||||
{globalFilter
|
||||
{globalFilter || typeFilter !== "all"
|
||||
? "אין תיקים תואמים לחיפוש"
|
||||
: "אין תיקים בארכיון"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
filteredRows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
className="border-rule hover:bg-gold-wash/40 transition-colors"
|
||||
className="border-rule-soft hover:bg-gold-wash transition-colors"
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id} className="py-3">
|
||||
@@ -271,7 +325,6 @@ export default function ArchivePage() {
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { use, useRef, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { FileText } from "lucide-react";
|
||||
import { AppShell } from "@/components/app-shell";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -9,9 +10,51 @@ import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { SubsectionCard } from "@/components/compose/subsection-card";
|
||||
import { PrecedentsSection } from "@/components/compose/precedents-section";
|
||||
import { Markdown } from "@/components/ui/markdown";
|
||||
import { useCase } from "@/lib/api/cases";
|
||||
import { useCase, type CaseStatus } from "@/lib/api/cases";
|
||||
import { useResearchAnalysis } from "@/lib/api/research";
|
||||
import { useCasePrecedents } from "@/lib/api/precedents";
|
||||
import { APPEAL_SUBTYPES } from "@/lib/practice-area";
|
||||
import { DOC_TYPE_LABELS, type DocType } from "@/lib/doc-types";
|
||||
|
||||
// ── 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 }) {
|
||||
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,
|
||||
hasAnalysis,
|
||||
onUploaded,
|
||||
@@ -55,7 +99,7 @@ function AnalysisActions({
|
||||
}
|
||||
setUploadMsg({
|
||||
ok: true,
|
||||
text: `הקובץ הועלה בהצלחה — ${data.sections.threshold_claims} טענות סף, ${data.sections.issues} סוגיות`,
|
||||
text: `הקובץ הועלה — ${data.sections.threshold_claims} טענות סף, ${data.sections.issues} סוגיות`,
|
||||
});
|
||||
onUploaded();
|
||||
} catch {
|
||||
@@ -67,12 +111,10 @@ function AnalysisActions({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{uploadMsg && (
|
||||
<span className={`text-xs ${uploadMsg.ok ? "text-green-700" : "text-red-600"}`}>
|
||||
{uploadMsg.text}
|
||||
</span>
|
||||
)}
|
||||
<Card className="bg-surface border-rule shadow-sm">
|
||||
<CardContent className="px-4 py-4">
|
||||
<h3 className="text-navy text-[0.9rem] font-semibold mb-3">השלמה והעברה</h3>
|
||||
|
||||
<input
|
||||
ref={fileRef}
|
||||
type="file"
|
||||
@@ -83,16 +125,32 @@ function AnalysisActions({
|
||||
if (f) handleUpload(f);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="space-y-2">
|
||||
{hasAnalysis && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-center"
|
||||
onClick={() => {
|
||||
const a = document.createElement("a");
|
||||
a.href = `/api/cases/${caseNumber}/research/analysis/export-docx`;
|
||||
a.click();
|
||||
}}
|
||||
>
|
||||
ייצוא DOCX
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
className="w-full justify-center bg-gold text-white hover:bg-gold-deep"
|
||||
disabled={uploading}
|
||||
onClick={() => fileRef.current?.click()}
|
||||
>
|
||||
{uploading ? "מעלה..." : "העלה ניתוח מעודכן"}
|
||||
{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`;
|
||||
@@ -100,25 +158,32 @@ function AnalysisActions({
|
||||
a.click();
|
||||
}}
|
||||
>
|
||||
הורד ניתוח
|
||||
הורד ניתוח (MD)
|
||||
</Button>
|
||||
)}
|
||||
{hasAnalysis && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
const a = document.createElement("a");
|
||||
a.href = `/api/cases/${caseNumber}/research/analysis/export-docx`;
|
||||
a.click();
|
||||
}}
|
||||
>
|
||||
הורד כ-DOCX
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{uploadMsg && (
|
||||
<p className={`text-xs mt-2 ${uploadMsg.ok ? "text-success" : "text-danger"}`}>
|
||||
{uploadMsg.text}
|
||||
</p>
|
||||
)}
|
||||
<Button asChild variant="outline">
|
||||
|
||||
{/* 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>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -145,6 +210,18 @@ export default function ComposePage({
|
||||
}
|
||||
}
|
||||
const practiceArea = caseQuery.data?.practice_area ?? null;
|
||||
const subtype = subtypeLabel(caseQuery.data?.appeal_subtype);
|
||||
const parties = (() => {
|
||||
const c = caseQuery.data;
|
||||
if (!c) return null;
|
||||
const app = c.appellants?.length ? c.appellants.join(", ") : null;
|
||||
const resp = c.respondents?.length ? c.respondents.join(", ") : null;
|
||||
const out: string[] = [];
|
||||
if (app) out.push(`עוררים: ${app}`);
|
||||
if (resp) out.push(`משיבה: ${resp}`);
|
||||
return out.length ? out.join(" · ") : c.title || null;
|
||||
})();
|
||||
const documents = caseQuery.data?.documents ?? [];
|
||||
|
||||
const isNotFound =
|
||||
analysis.error instanceof Error &&
|
||||
@@ -152,34 +229,34 @@ export default function ComposePage({
|
||||
|
||||
return (
|
||||
<AppShell>
|
||||
<section className="space-y-6">
|
||||
{/* Header strip */}
|
||||
<div className="flex items-center justify-between gap-4 flex-wrap">
|
||||
<div>
|
||||
<nav className="text-[0.78rem] text-ink-muted flex items-center gap-2 mb-1">
|
||||
{/* ── Case header band (mockup 03) — parchment strip, full-bleed to the
|
||||
AppShell <main> edges (which pads px-10 py-10) ── */}
|
||||
<div className="-mx-10 -mt-10 mb-6 border-b border-rule bg-parchment px-10 py-5">
|
||||
<nav className="text-[0.78rem] text-ink-muted flex items-center gap-2 mb-2">
|
||||
<Link href="/" className="hover:text-gold-deep">בית</Link>
|
||||
<span aria-hidden>·</span>
|
||||
<Link
|
||||
href={`/cases/${caseNumber}`}
|
||||
className="hover:text-gold-deep"
|
||||
>
|
||||
<Link href={`/cases/${caseNumber}`} className="hover:text-gold-deep">
|
||||
ערר {caseNumber}
|
||||
</Link>
|
||||
<span aria-hidden>·</span>
|
||||
<span className="text-navy">עורך החלטה</span>
|
||||
</nav>
|
||||
<h1 className="text-navy mb-0">ניתוח משפטי וכתיבת עמדה</h1>
|
||||
{caseQuery.data?.title && (
|
||||
<p className="text-ink-muted text-sm mt-1 max-w-2xl">
|
||||
{caseQuery.data.title}
|
||||
</p>
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<h1 className="text-navy text-2xl font-bold mb-0">ערר {caseNumber}</h1>
|
||||
<StatusChip status={caseQuery.data?.status} />
|
||||
{subtype && (
|
||||
<span className="rounded-full text-[0.78rem] font-semibold px-3 py-0.5 border border-rule bg-gold-wash text-gold-deep">
|
||||
{subtype}
|
||||
</span>
|
||||
)}
|
||||
{/* INV-G10: source-of-truth pill — the blocks are the canonical text */}
|
||||
<span className="ms-auto rounded-lg text-[0.8rem] font-semibold px-3.5 py-1.5 border border-gold bg-gold-wash text-gold-deep">
|
||||
מקור-אמת: בלוקים
|
||||
</span>
|
||||
</div>
|
||||
<AnalysisActions caseNumber={caseNumber} hasAnalysis={!!analysis.data} onUploaded={() => analysis.refetch()} />
|
||||
{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">
|
||||
<CardContent className="px-6 py-5 space-y-3">
|
||||
@@ -209,35 +286,21 @@ export default function ComposePage({
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : analysis.data ? (
|
||||
<div className="space-y-6">
|
||||
{/* Case-level general precedents */}
|
||||
<Card className="bg-surface border-rule shadow-sm">
|
||||
<CardContent className="px-6 py-5">
|
||||
<h2 className="text-navy text-xl mb-1">פסיקה כללית לדיון</h2>
|
||||
<p className="text-[0.78rem] text-ink-muted mb-4">
|
||||
ציטוטים התומכים בעמדה באופן רוחבי — ישולבו בפתיחת בלוק י (דיון).
|
||||
</p>
|
||||
<PrecedentsSection
|
||||
caseNumber={caseNumber}
|
||||
sectionId={null}
|
||||
precedents={caseLevelPrecedents}
|
||||
practiceArea={practiceArea}
|
||||
emptyHelperText="עדיין לא צורפה פסיקה כללית לתיק"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
/* ── Two-column workspace: main editor list + 320px side rail ──────── */
|
||||
<div className="grid gap-6 lg:grid-cols-[1fr_320px] items-start">
|
||||
{/* MAIN — the block/subsection editor list */}
|
||||
<div className="space-y-6 min-w-0">
|
||||
{/* 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>
|
||||
<h2 className="text-navy text-lg font-semibold mb-0">טענות סף</h2>
|
||||
<span className="text-[0.72rem] rounded-full bg-gold-wash text-gold-deep px-2 py-0.5 border border-gold/40 tabular-nums">
|
||||
{analysis.data.threshold_claims.length}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2.5">
|
||||
{analysis.data.threshold_claims.map((tc) => (
|
||||
<SubsectionCard
|
||||
key={tc.id}
|
||||
@@ -255,12 +318,12 @@ export default function ComposePage({
|
||||
{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-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">
|
||||
{analysis.data.issues.length}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2.5">
|
||||
{analysis.data.issues.map((iss) => (
|
||||
<SubsectionCard
|
||||
key={iss.id}
|
||||
@@ -274,8 +337,8 @@ export default function ComposePage({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(!analysis.data.threshold_claims?.length &&
|
||||
!analysis.data.issues?.length) && (
|
||||
{!analysis.data.threshold_claims?.length &&
|
||||
!analysis.data.issues?.length && (
|
||||
<Card className="bg-surface border-rule">
|
||||
<CardContent className="px-6 py-10 text-center text-ink-muted">
|
||||
לא נמצאו טענות סף או סוגיות בניתוח זה.
|
||||
@@ -283,42 +346,82 @@ export default function ComposePage({
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Background prose — moved below the issues so it reads as
|
||||
supporting context after the chair has seen the main
|
||||
decision points, not as a wall of text beside them. */}
|
||||
{/* Background prose — supporting context after the decision points */}
|
||||
<Card className="bg-surface border-rule shadow-sm">
|
||||
<CardContent className="px-6 py-5 space-y-5">
|
||||
<h2 className="text-navy text-xl mb-0">רקע לניתוח</h2>
|
||||
<ProseSection
|
||||
title="צד מיוצג"
|
||||
content={analysis.data.represented_party}
|
||||
/>
|
||||
<ProseSection
|
||||
title="רקע דיוני"
|
||||
content={analysis.data.procedural_background}
|
||||
/>
|
||||
<ProseSection
|
||||
title="עובדות מוסכמות"
|
||||
content={analysis.data.agreed_facts}
|
||||
/>
|
||||
<ProseSection
|
||||
title="עובדות במחלוקת"
|
||||
content={analysis.data.disputed_facts}
|
||||
/>
|
||||
<h2 className="text-navy text-lg font-semibold mb-0">רקע לניתוח</h2>
|
||||
<ProseSection title="צד מיוצג" content={analysis.data.represented_party} />
|
||||
<ProseSection title="רקע דיוני" content={analysis.data.procedural_background} />
|
||||
<ProseSection title="עובדות מוסכמות" content={analysis.data.agreed_facts} />
|
||||
<ProseSection title="עובדות במחלוקת" content={analysis.data.disputed_facts} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{analysis.data.conclusions?.trim() && (
|
||||
<Card className="bg-gold-wash border-gold/40 shadow-sm">
|
||||
<CardContent className="px-6 py-5 space-y-3">
|
||||
<h2 className="text-gold-deep text-xl mb-0">מסקנות</h2>
|
||||
<h2 className="text-gold-deep text-lg font-semibold mb-0">מסקנות</h2>
|
||||
<Markdown content={analysis.data.conclusions.trim()} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 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}
|
||||
</section>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -45,10 +45,10 @@ export default function CaseDetailPage({
|
||||
? EXPECTED_OUTCOME_LABELS[data.expected_outcome] ?? data.expected_outcome
|
||||
: null;
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<AppShell>
|
||||
<section className="space-y-6">
|
||||
{error ? (
|
||||
<Card className="bg-danger-bg border-danger/40">
|
||||
<CardContent className="px-6 py-6 text-center space-y-3">
|
||||
<p className="text-danger font-semibold">שגיאה בטעינת התיק</p>
|
||||
@@ -58,60 +58,71 @@ export default function CaseDetailPage({
|
||||
</Button>
|
||||
</CardContent>
|
||||
</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 ? (
|
||||
<Card className="bg-surface border-rule shadow-sm">
|
||||
<CardContent className="px-6 py-5 space-y-3">
|
||||
<div className="-mx-10 -mt-10 mb-2 bg-parchment border-b border-rule px-10 pt-6 pb-4 space-y-3">
|
||||
<Skeleton className="h-4 w-40" />
|
||||
<Skeleton className="h-8 w-64" />
|
||||
<Skeleton className="h-6 w-96" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
) : (
|
||||
<CaseHeader data={data} />
|
||||
<CaseHeader data={data} actions={bandActions} tabs={tabsList} />
|
||||
)}
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-[1fr_280px]">
|
||||
<Card className="bg-surface border-rule shadow-sm">
|
||||
<CardContent className="px-6 py-5">
|
||||
<Tabs defaultValue="overview" dir="rtl">
|
||||
<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} />
|
||||
{/* two-column wrap — main tab content (1fr) + rail (340px) */}
|
||||
<div className="grid gap-6 lg:grid-cols-[1fr_340px] items-start mt-6">
|
||||
<div className="min-w-0">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<TabsContent value="overview" className="mt-5 space-y-4">
|
||||
<CardContent className="px-5 py-4 space-y-4">
|
||||
<div>
|
||||
<h3 className="text-navy text-base mb-2">תוצאה צפויה</h3>
|
||||
<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>
|
||||
<h3 className="text-navy text-base mb-2">סיכום מהיר</h3>
|
||||
<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">
|
||||
@@ -120,7 +131,7 @@ export default function CaseDetailPage({
|
||||
</dl>
|
||||
</div>
|
||||
{canStartWorkflow && (
|
||||
<div className="pt-2 border-t border-rule">
|
||||
<div className="pt-2 border-t border-rule-soft">
|
||||
<Button
|
||||
className="bg-gold-deep hover:bg-gold-deep/90 text-parchment"
|
||||
disabled={startWorkflow.isPending}
|
||||
@@ -144,44 +155,71 @@ export default function CaseDetailPage({
|
||||
</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>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-surface border-rule shadow-sm h-fit">
|
||||
<CardContent className="px-6 py-5 space-y-5">
|
||||
<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">
|
||||
<CardContent className="px-6 py-5">
|
||||
<LegalArgumentsPanel caseNumber={caseNumber} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="decision" className="mt-0">
|
||||
<Card className="bg-surface border-rule shadow-sm">
|
||||
<CardContent className="px-6 py-5">
|
||||
<DecisionBlocksPanel caseNumber={caseNumber} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="drafts" className="mt-0">
|
||||
<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} />
|
||||
<h2 className="text-navy text-base mb-4">שלב בתהליך</h2>
|
||||
<WorkflowTimeline status={data?.status} />
|
||||
<StatusChanger caseNumber={caseNumber} currentStatus={data?.status} />
|
||||
<StatusGuide />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</Tabs>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
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 { Badge } from "@/components/ui/badge";
|
||||
import { DigestListPanel } from "@/components/digests/digest-list-panel";
|
||||
import { DigestSearchPanel } from "@/components/digests/digest-search-panel";
|
||||
import { DigestUploadDialog } from "@/components/digests/digest-upload-dialog";
|
||||
import { useDigestPending } from "@/lib/api/digests";
|
||||
|
||||
/**
|
||||
@@ -38,32 +38,58 @@ export default function DigestsPage() {
|
||||
return (
|
||||
<AppShell>
|
||||
<section className="space-y-6">
|
||||
<header>
|
||||
<nav className="text-[0.78rem] text-ink-muted mb-1">
|
||||
<header className="space-y-2">
|
||||
<nav className="text-[0.78rem] text-ink-muted">
|
||||
<Link href="/" className="hover:text-gold-deep">בית</Link>
|
||||
<span aria-hidden> · </span>
|
||||
<span className="text-navy">יומונים</span>
|
||||
</nav>
|
||||
<h1 className="text-navy mb-0">יומונים — רדאר פסיקה</h1>
|
||||
<p className="text-ink-muted text-sm mt-1 max-w-3xl">
|
||||
סיכומי "כל יום" (עפר טויסטר) של פסקי דין והחלטות עדכניים.
|
||||
שכבת-גילוי בלבד: כל יומון <strong>מצביע</strong> על פסק הדין המקורי —
|
||||
הוא אינו מצוטט בהחלטה ואינו מחלץ הלכות. כשהפסק רלוונטי, מעלים אותו
|
||||
לספריית הפסיקה ומצטטים משם.
|
||||
<h1 className="text-navy mb-0">יומונים (רדאר)</h1>
|
||||
<p className="text-ink-muted text-sm max-w-3xl leading-relaxed">
|
||||
שכבת-גילוי משנית — מצביע-לא-מצוטט (X12). מאתרת פסיקה רלוונטית ומפנה
|
||||
אליה; אינה מקור-אמת לציטוט. סיכומי "כל יום" (עפר טויסטר):
|
||||
כל יומון <strong>מצביע</strong> על פסק הדין המקורי — כשהפסק רלוונטי,
|
||||
מעלים אותו לספריית הפסיקה ומצטטים משם.
|
||||
</p>
|
||||
</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">
|
||||
בחר קובץ יומון "כל יום" — 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">
|
||||
<CardContent className="px-6 py-5">
|
||||
<Tabs defaultValue="list" dir="rtl">
|
||||
<TabsList className="bg-rule-soft/60">
|
||||
<TabsTrigger value="list">
|
||||
יומונים
|
||||
<PendingBadge />
|
||||
<TabsList className="flex w-full justify-start gap-1 rounded-none border-0 border-b border-rule bg-transparent p-0 h-auto">
|
||||
{[
|
||||
{ value: "list", label: "יומונים", pill: <PendingBadge /> },
|
||||
{ value: "search", 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 value="search">חיפוש</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="list" className="mt-5">
|
||||
@@ -74,8 +100,6 @@ export default function DigestsPage() {
|
||||
<DigestSearchPanel />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
</AppShell>
|
||||
);
|
||||
|
||||
@@ -7,12 +7,11 @@ import { toast } from "sonner";
|
||||
import { AppShell } from "@/components/app-shell";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
useFeedbackList,
|
||||
useResolveFeedback,
|
||||
useCreateFeedback,
|
||||
CATEGORY_LABELS,
|
||||
CATEGORY_COLORS,
|
||||
BLOCK_LABELS,
|
||||
type ChairFeedback,
|
||||
type FeedbackCategory,
|
||||
@@ -24,6 +23,16 @@ import {
|
||||
* "טרם יושמו" וקטגוריה, וסימון כל הערה כיושמה. מוזן מ-/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 {
|
||||
if (!iso) return "";
|
||||
try {
|
||||
@@ -57,47 +66,51 @@ function FeedbackCard({ fb }: { fb: ChairFeedback }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className={`bg-surface border-rule shadow-sm ${fb.resolved ? "opacity-60" : ""}`}>
|
||||
<CardContent className="px-5 py-4 space-y-2.5">
|
||||
<div className="flex items-start gap-2 flex-wrap">
|
||||
<Badge variant="outline" className={`text-[0.7rem] ${CATEGORY_COLORS[fb.category]}`}>
|
||||
{CATEGORY_LABELS[fb.category]}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="text-[0.7rem]">
|
||||
{BLOCK_LABELS[fb.block_id] ?? fb.block_id}
|
||||
</Badge>
|
||||
<Card className={`bg-surface border-rule shadow-sm ${fb.resolved ? "opacity-[0.78]" : ""}`}>
|
||||
<CardContent className="px-[18px] py-4 space-y-2.5">
|
||||
{/* meta row — where · category chip · when (mockup 06 .meta) */}
|
||||
<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="text-[0.72rem] text-gold-deep hover:underline"
|
||||
className="hover:text-gold-deep"
|
||||
>
|
||||
תיק {fb.case_number}
|
||||
ערר {fb.case_number}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-[0.72rem] text-ink-muted">ללא תיק</span>
|
||||
"ללא תיק"
|
||||
)}
|
||||
<span className="ms-auto text-[0.72rem] text-ink-muted">
|
||||
<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]}
|
||||
</span>
|
||||
<span className="ms-auto text-[0.72rem] text-ink-muted whitespace-nowrap">
|
||||
{formatDate(fb.created_at)}
|
||||
</span>
|
||||
</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}
|
||||
</p>
|
||||
|
||||
{fb.lesson_extracted ? (
|
||||
<div className="rounded-md bg-gold-wash/40 border-s-[3px] border-gold ps-3 pe-3 py-2">
|
||||
<div className="text-[0.68rem] text-gold-deep mb-0.5">לקח שהופק</div>
|
||||
<p className="text-ink-soft text-[0.82rem] leading-relaxed m-0 whitespace-pre-wrap italic" dir="rtl">
|
||||
{fb.lesson_extracted}
|
||||
<p
|
||||
className="text-[0.82rem] text-ink-muted italic leading-relaxed m-0 whitespace-pre-wrap border-s-[3px] border-gold ps-2.5"
|
||||
dir="rtl"
|
||||
>
|
||||
לקח שחולץ: {fb.lesson_extracted}
|
||||
</p>
|
||||
</div>
|
||||
) : 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 ? (
|
||||
<span className="flex items-center gap-1 text-[0.78rem] text-emerald-700">
|
||||
<CheckCircle2 className="w-3.5 h-3.5" /> יושמה
|
||||
<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" /> יושם
|
||||
</span>
|
||||
) : (
|
||||
<Button
|
||||
@@ -118,6 +131,118 @@ function FeedbackCard({ fb }: { fb: ChairFeedback }) {
|
||||
|
||||
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() {
|
||||
const [unresolvedOnly, setUnresolvedOnly] = useState(true);
|
||||
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>
|
||||
<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>
|
||||
</div>
|
||||
<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}
|
||||
</div>
|
||||
<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" />
|
||||
|
||||
{/* 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-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
|
||||
type="button"
|
||||
onClick={() => setUnresolvedOnly(true)}
|
||||
className={`text-[0.78rem] px-3 py-1.5 rounded border transition-colors ${
|
||||
unresolvedOnly
|
||||
? "bg-navy text-parchment border-navy"
|
||||
? "bg-navy text-white border-navy"
|
||||
: "bg-surface text-ink-muted border-rule hover:bg-rule-soft"
|
||||
}`}
|
||||
>
|
||||
@@ -197,32 +347,18 @@ export default function FeedbackPage() {
|
||||
onClick={() => setUnresolvedOnly(false)}
|
||||
className={`text-[0.78rem] px-3 py-1.5 rounded border transition-colors ${
|
||||
!unresolvedOnly
|
||||
? "bg-navy text-parchment border-navy"
|
||||
? "bg-navy text-white border-navy"
|
||||
: "bg-surface text-ink-muted border-rule hover:bg-rule-soft"
|
||||
}`}
|
||||
>
|
||||
הכל
|
||||
</button>
|
||||
</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>
|
||||
|
||||
{/* two-column body — feedback list + sticky add-form rail (mockup 06 .wrap grid) */}
|
||||
<div className="grid gap-6 lg:grid-cols-[1fr_340px] items-start">
|
||||
<div>
|
||||
{error ? (
|
||||
<Card className="bg-surface border-rule">
|
||||
<CardContent className="px-6 py-5 text-ink-muted text-sm">
|
||||
@@ -230,10 +366,10 @@ export default function FeedbackPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : isPending ? (
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-3.5">
|
||||
{[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" />
|
||||
<CardContent className="px-[18px] py-4 h-28 animate-pulse" />
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
@@ -245,12 +381,16 @@ export default function FeedbackPage() {
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-3.5">
|
||||
{items.map((fb) => (
|
||||
<FeedbackCard key={fb.id} fb={fb} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<AddFeedbackForm />
|
||||
</div>
|
||||
</section>
|
||||
</AppShell>
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@ export default function GraphPage() {
|
||||
return (
|
||||
<AppShell>
|
||||
<section className="space-y-6">
|
||||
<header>
|
||||
<header className="space-y-1.5">
|
||||
<nav className="text-[0.78rem] text-ink-muted mb-1">
|
||||
<Link href="/" className="hover:text-gold-deep">
|
||||
בית
|
||||
@@ -16,16 +16,20 @@ export default function GraphPage() {
|
||||
<span aria-hidden> · </span>
|
||||
<span className="text-navy">מפת הקורפוס</span>
|
||||
</nav>
|
||||
<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-sm mt-1 max-w-3xl">
|
||||
רשת הציטוטים של ספריית הפסיקה — כל נקודה היא פסיקה או נושא, וקו מציין ציטוט או שיוך.
|
||||
גודל הנקודה משקף כמה פעמים הפסיקה צוטטה. לחצו על נקודה כדי להתמקד בשכניה.
|
||||
<p className="text-ink-muted text-sm mt-1 max-w-3xl leading-relaxed">
|
||||
גרף הציטוטים של הקורפוס — פסיקה, נושאים והלכות וקשרי-ההפניה ביניהם.
|
||||
גררו צמתים, סננו שכבות, ובחנו את מרכזי-הכובד. גודל הנקודה משקף כמה
|
||||
פעמים הפסיקה צוטטה; לחיצה על נקודה ממקדת בשכניה.
|
||||
</p>
|
||||
</header>
|
||||
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
|
||||
<Suspense
|
||||
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>
|
||||
}
|
||||
|
||||
@@ -21,8 +21,12 @@ export default function MethodologyPage() {
|
||||
</nav>
|
||||
<h1 className="text-navy mb-0">מתודולוגיה</h1>
|
||||
<p className="text-ink-muted text-sm mt-1 max-w-2xl">
|
||||
הגדרות ניסוח — יחסי אורך, כללי דיון, וצ׳קליסטים לפי סוג ערר
|
||||
העורך הקנוני היחיד לכללי-הכותב — יחסי אורך, כללי דיון, וצ׳קליסטים לפי סוג ערר
|
||||
</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>
|
||||
|
||||
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
|
||||
|
||||
@@ -3,9 +3,6 @@
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { AppShell } from "@/components/app-shell";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
useMissingPrecedents,
|
||||
@@ -17,32 +14,26 @@ import { MissingPrecedentsTable } from "@/components/missing-precedents/missing-
|
||||
* Missing-precedents page (TaskMaster #35).
|
||||
*
|
||||
* 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
|
||||
* component with a different filter. Drawer (sheet) opens on row click
|
||||
* with metadata + upload form that routes to internal_decision_upload
|
||||
* (ערר/בל"מ citations) or precedent_library_upload (court rulings).
|
||||
* precedent_library. A status filter (chips) narrows the table; each row uses
|
||||
* the same table component. Drawer (sheet) opens on row click with metadata +
|
||||
* upload form that routes to internal_decision_upload (ערר/בל"מ citations) or
|
||||
* precedent_library_upload (court rulings).
|
||||
*/
|
||||
function StatusBadge({ status, count }: { status: MissingPrecedentStatus; count: number }) {
|
||||
if (!count) return null;
|
||||
const variants: Record<MissingPrecedentStatus, string> = {
|
||||
open: "bg-gold-wash text-gold-deep border-gold/40",
|
||||
uploaded: "bg-rule-soft text-ink-muted border-rule",
|
||||
closed: "bg-emerald-50 text-emerald-800 border-emerald-300/60",
|
||||
irrelevant: "bg-rule-soft text-ink-muted border-rule",
|
||||
};
|
||||
return (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`ms-1 text-[0.65rem] ${variants[status]}`}
|
||||
>
|
||||
{count}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
type StatusFilter = MissingPrecedentStatus | "all";
|
||||
|
||||
const STATUS_CHIPS: { value: StatusFilter; label: string }[] = [
|
||||
{ value: "open", label: "פתוח" },
|
||||
{ value: "uploaded", label: "הועלה" },
|
||||
{ value: "closed", label: "נסגר" },
|
||||
{ value: "irrelevant", label: "לא-רלוונטי" },
|
||||
{ value: "all", label: "הכל" },
|
||||
];
|
||||
|
||||
export default function MissingPrecedentsPage() {
|
||||
const [caseNumber, setCaseNumber] = useState("");
|
||||
const [legalTopic, setLegalTopic] = useState("");
|
||||
const [filter, setFilter] = useState<StatusFilter>("open");
|
||||
|
||||
const counts = useMissingPrecedents({ limit: 1 });
|
||||
const byStatus = counts.data?.by_status ?? {};
|
||||
@@ -50,40 +41,39 @@ export default function MissingPrecedentsPage() {
|
||||
return (
|
||||
<AppShell>
|
||||
<section className="space-y-6">
|
||||
<header>
|
||||
<nav className="text-[0.78rem] text-ink-muted mb-1">
|
||||
<header className="space-y-3">
|
||||
<nav className="text-[0.78rem] text-ink-muted">
|
||||
<Link href="/" className="hover:text-gold-deep">בית</Link>
|
||||
<span aria-hidden> · </span>
|
||||
<span className="text-navy">פסיקה חסרה בקורפוס</span>
|
||||
</nav>
|
||||
<div className="flex items-end justify-between gap-4 flex-wrap">
|
||||
<div>
|
||||
|
||||
{/* title + inline open-count pill (mockup 09 `.open-count`) */}
|
||||
<div className="flex items-baseline gap-3.5 flex-wrap">
|
||||
<h1 className="text-navy mb-0">פסיקה חסרה בקורפוס</h1>
|
||||
<p className="text-ink-muted text-sm mt-1 max-w-3xl">
|
||||
פסיקות שצוטטו בכתבי הטענות אך אינן עדיין בקורפוס. סוכן המחקר רושם
|
||||
פערים אוטומטית; היו"ר סוגר אותם על־ידי העלאת המסמך — ניתוב
|
||||
אוטומטי בין הקורפוס הסמכותי (פסקי דין) להחלטות ועדות ערר.
|
||||
</p>
|
||||
</div>
|
||||
{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="text-2xl font-semibold text-warn leading-none tabular-nums">
|
||||
<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-lg font-bold text-warn tabular-nums leading-none">
|
||||
{byStatus.open}
|
||||
</span>
|
||||
<span className="text-[0.85rem] text-ink-soft">פתוחים</span>
|
||||
</div>
|
||||
<span className="text-[0.8rem] text-ink-soft">פתוחים</span>
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<p className="text-ink-muted text-sm max-w-3xl leading-relaxed">
|
||||
פסיקה שצוטטה בכתבי-הטענות אך אינה קיימת בקורפוס. השלמתה מאפשרת
|
||||
אימות-הלכה ועיגון-מקור (INV-AH). סוכן המחקר רושם פערים אוטומטית;
|
||||
היו"ר סוגר אותם על־ידי העלאת המסמך — ניתוב אוטומטי בין הקורפוס
|
||||
הסמכותי (פסקי דין) להחלטות ועדות ערר.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
|
||||
|
||||
<Card className="bg-surface border-rule shadow-sm">
|
||||
<CardContent className="px-6 py-5 space-y-5">
|
||||
{/* Shared filters */}
|
||||
{/* shared filters */}
|
||||
<div className="flex items-end gap-3 flex-wrap">
|
||||
<div className="flex-1 min-w-[200px]">
|
||||
<label className="text-[0.78rem] text-ink-muted">תיק (מספר ערר)</label>
|
||||
<label className="block text-[0.78rem] text-ink-muted mb-1.5">תיק (מספר ערר)</label>
|
||||
<Input
|
||||
value={caseNumber}
|
||||
onChange={(e) => setCaseNumber(e.target.value)}
|
||||
@@ -92,7 +82,7 @@ export default function MissingPrecedentsPage() {
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 min-w-[200px]">
|
||||
<label className="text-[0.78rem] text-ink-muted">נושא משפטי</label>
|
||||
<label className="block text-[0.78rem] text-ink-muted mb-1.5">נושא משפטי</label>
|
||||
<Input
|
||||
value={legalTopic}
|
||||
onChange={(e) => setLegalTopic(e.target.value)}
|
||||
@@ -102,72 +92,78 @@ export default function MissingPrecedentsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="open" dir="rtl">
|
||||
<TabsList className="bg-rule-soft/60">
|
||||
<TabsTrigger value="open">
|
||||
פתוחות
|
||||
<StatusBadge status="open" count={byStatus.open ?? 0} />
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="uploaded">
|
||||
הועלו
|
||||
<StatusBadge status="uploaded" count={byStatus.uploaded ?? 0} />
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="closed">
|
||||
נסגרו
|
||||
<StatusBadge status="closed" count={byStatus.closed ?? 0} />
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="irrelevant">
|
||||
לא רלוונטי
|
||||
<StatusBadge
|
||||
status="irrelevant"
|
||||
count={byStatus.irrelevant ?? 0}
|
||||
/>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="all">הכל</TabsTrigger>
|
||||
</TabsList>
|
||||
{/* status filter chips (mockup 09 `.filters`) — active = navy filled */}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{STATUS_CHIPS.map((c) => {
|
||||
const active = filter === c.value;
|
||||
const count =
|
||||
c.value === "all"
|
||||
? undefined
|
||||
: (byStatus[c.value as MissingPrecedentStatus] ?? 0);
|
||||
return (
|
||||
<button
|
||||
key={c.value}
|
||||
type="button"
|
||||
onClick={() => setFilter(c.value)}
|
||||
aria-pressed={active}
|
||||
className={`rounded-full border px-4 py-1.5 text-[0.82rem] transition-colors ${
|
||||
active
|
||||
? "bg-navy text-white border-navy font-semibold"
|
||||
: "bg-surface text-ink-soft border-rule font-medium hover:bg-rule-soft/50"
|
||||
}`}
|
||||
>
|
||||
{c.label}
|
||||
{count ? (
|
||||
<span className="ms-1.5 tabular-nums opacity-80">({count})</span>
|
||||
) : null}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<TabsContent value="open" className="mt-4">
|
||||
<MissingPrecedentsTable
|
||||
status="open"
|
||||
status={filter === "all" ? "" : filter}
|
||||
caseNumber={caseNumber.trim() || undefined}
|
||||
legalTopic={legalTopic.trim() || undefined}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="uploaded" className="mt-4">
|
||||
<MissingPrecedentsTable
|
||||
status="uploaded"
|
||||
caseNumber={caseNumber.trim() || undefined}
|
||||
legalTopic={legalTopic.trim() || undefined}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="closed" className="mt-4">
|
||||
<MissingPrecedentsTable
|
||||
status="closed"
|
||||
caseNumber={caseNumber.trim() || undefined}
|
||||
legalTopic={legalTopic.trim() || undefined}
|
||||
/>
|
||||
</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>
|
||||
{/* lifecycle note (mockup 09 `.lifecycle`) */}
|
||||
<div className="rounded-lg border border-rule bg-parchment px-5 py-3.5 text-[0.82rem] text-ink-muted leading-7">
|
||||
<b className="text-ink-soft">מחזור-חיים:</b>{" "}
|
||||
<LifecycleChip tone="open">פתוח</LifecycleChip> →{" "}
|
||||
<LifecycleChip tone="up">הועלה</LifecycleChip> →{" "}
|
||||
<LifecycleChip tone="closed">נסגר</LifecycleChip>. פריט נפתח אוטומטית
|
||||
בעת חילוץ ציטוט שאין לו תקדים בקורפוס; בהעלאת פסק-הדין הוא מקושר לרשומת
|
||||
הפסיקה דרך{" "}
|
||||
<code className="rounded border border-rule bg-surface px-1.5 py-0.5 text-[0.75rem] text-gold-deep" dir="ltr">
|
||||
linked_case_law_id
|
||||
</code>{" "}
|
||||
ונסגר. פריט שאינו רלוונטי מסומן{" "}
|
||||
<LifecycleChip tone="na">לא-רלוונטי</LifecycleChip> מבלי שתידרש העלאה.
|
||||
</div>
|
||||
</section>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,12 @@ function mb(bytes: number): string {
|
||||
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 {
|
||||
if (!ms) return "—";
|
||||
const secs = Math.floor((Date.now() - ms) / 1000);
|
||||
@@ -189,26 +195,49 @@ function ServicesPanel({ data }: { data: OperationsSnapshot }) {
|
||||
return (
|
||||
<Card className="bg-surface border-rule shadow-sm">
|
||||
<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">
|
||||
כמו "שירותים" ב-Windows. דמון = שירות רץ-תמיד (הפעל-מחדש/עצור/הפעל).
|
||||
תזמון (cron) = רץ לפי לוח-זמנים ("הרץ עכשיו" להרצה מיידית, ומתג
|
||||
הפעלה/כיבוי של התזמון).
|
||||
ניהול תהליכי-רקע (pm2) — כמו "שירותים" ב-Windows. דמון = שירות
|
||||
רץ-תמיד (הפעל-מחדש/עצור/הפעל). תזמון (cron) = רץ לפי לוח-זמנים
|
||||
("הרץ עכשיו" להרצה מיידית, ומתג הפעלה/כיבוי של התזמון).
|
||||
</p>
|
||||
{data.services_error ? (
|
||||
<p className="text-sm text-destructive">{data.services_error}</p>
|
||||
) : data.services.length === 0 ? (
|
||||
<p className="text-sm text-ink-muted">אין שירותים.</p>
|
||||
) : (
|
||||
<div className="grid gap-2">
|
||||
/* mockup 02: services as a table — שירות · סטטוס · זמן-ריצה · זיכרון · controls */
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="border-b border-rule-soft text-ink-muted">
|
||||
<th className="text-start font-medium text-xs py-2 pe-3">שירות</th>
|
||||
<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">
|
||||
זמן-ריצה
|
||||
</th>
|
||||
<th className="text-start font-medium text-xs py-2 px-3 whitespace-nowrap">
|
||||
זיכרון / ↻
|
||||
</th>
|
||||
<th className="py-2 ps-3" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.services.map((s: OpsService) => {
|
||||
const isCron = !!s.cron;
|
||||
return (
|
||||
<div
|
||||
<tr
|
||||
key={s.name}
|
||||
className="flex items-center justify-between gap-3 rounded-md border border-rule-soft bg-rule-soft/30 px-3 py-2"
|
||||
className="border-b border-rule-soft last:border-0 align-top"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<td className="py-2.5 pe-3">
|
||||
<div className="text-navy font-semibold text-[0.82rem]">
|
||||
{SERVICE_LABELS[s.name] ?? s.name}
|
||||
</div>
|
||||
<div className="text-[0.66rem] text-ink-muted font-mono" dir="ltr">
|
||||
{s.name}
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-2.5 px-3">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{isCron ? (
|
||||
<Badge
|
||||
@@ -221,23 +250,23 @@ function ServicesPanel({ data }: { data: OperationsSnapshot }) {
|
||||
<StatusBadge value={s.status} />
|
||||
)}
|
||||
{s.cron ? (
|
||||
<span className="text-[0.7rem] text-ink-muted font-mono" dir="ltr">
|
||||
<span
|
||||
className="text-[0.66rem] text-ink-muted font-mono"
|
||||
dir="ltr"
|
||||
>
|
||||
{s.cron}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="text-[0.8rem] text-navy truncate mt-0.5">
|
||||
{SERVICE_LABELS[s.name] ?? s.name}
|
||||
</div>
|
||||
<div className="text-[0.66rem] text-ink-muted flex items-center gap-2 flex-wrap">
|
||||
<span className="font-mono" dir="ltr">
|
||||
{s.name}
|
||||
</span>
|
||||
<span>{mb(s.memory_bytes)}</span>
|
||||
<span>↻{s.restarts}</span>
|
||||
<span>{isCron ? `ריצה אחרונה ${ago(s.uptime_ms)}` : ago(s.uptime_ms)}</span>
|
||||
</div>
|
||||
</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}
|
||||
@@ -245,8 +274,12 @@ function ServicesPanel({ data }: { data: OperationsSnapshot }) {
|
||||
onToggle={(disabled) => toggle.mutate({ name: s.name, disabled })}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
@@ -336,6 +369,7 @@ function PipelineCard({
|
||||
children,
|
||||
href,
|
||||
hrefLabel,
|
||||
gate = false,
|
||||
}: {
|
||||
title: string;
|
||||
desc: string;
|
||||
@@ -344,9 +378,18 @@ function PipelineCard({
|
||||
// (/approvals), never duplicated here. /operations only monitors.
|
||||
href?: 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 (
|
||||
<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">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
@@ -356,7 +399,7 @@ function PipelineCard({
|
||||
{href && (
|
||||
<Link
|
||||
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 ?? "לטיפול ←"}
|
||||
</Link>
|
||||
@@ -456,10 +499,9 @@ function LiveAgentsPanel() {
|
||||
return (
|
||||
<Card className="bg-surface border-rule shadow-sm">
|
||||
<CardContent className="px-6 py-5">
|
||||
<div className="flex items-center justify-between gap-3 mb-1 flex-wrap">
|
||||
<h2 className="text-navy text-lg mb-0">סוכנים פעילים</h2>
|
||||
{/* mockup 02: status pills row (running / queued) + company hint */}
|
||||
{data ? (
|
||||
<div className="flex items-center gap-2 text-[0.72rem]">
|
||||
<div className="flex items-center gap-2 text-[0.72rem] mb-3 flex-wrap">
|
||||
{/* ADM-6 (INV-IA5): counts sum only the companies that loaded.
|
||||
When a company errored, mark the totals as a floor ("+") so
|
||||
the operator isn't shown a shrunken depth as if complete. */}
|
||||
@@ -477,9 +519,9 @@ function LiveAgentsPanel() {
|
||||
⚠ חלקי
|
||||
</span>
|
||||
) : null}
|
||||
<span className="text-ink-muted ms-auto">חברות: CMP · CMPA</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<p className="text-ink-muted text-xs mb-4">
|
||||
מי מבין סוכני-הוועדה עובד כרגע ומה הפלט שלו — כולל עבודה שלא קשורה לתיק (כמו
|
||||
ריקון תור הלכות ע״י ה-CEO). עצירה היא מבוקרת דרך הפלטפורמה (לא kill).
|
||||
@@ -597,10 +639,14 @@ export default function OperationsPage() {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<SectionHeader>סוכנים פעילים</SectionHeader>
|
||||
<LiveAgentsPanel />
|
||||
|
||||
<SectionHeader>שירותים</SectionHeader>
|
||||
<ServicesPanel data={data} />
|
||||
|
||||
<SectionHeader>צינורות-עבודה</SectionHeader>
|
||||
{/* Automatic pipelines — uniform stat cards */}
|
||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
<PipelineCard
|
||||
title="אחזור פסיקה (X13)"
|
||||
@@ -633,10 +679,14 @@ export default function OperationsPage() {
|
||||
לפסיקה
|
||||
</p>
|
||||
</PipelineCard>
|
||||
</div>
|
||||
|
||||
{/* mockup 02: human-gate pointer cards — gold-wash, "לתיבת-האישורים ←" */}
|
||||
<div className="grid gap-4 md:grid-cols-2 mt-4">
|
||||
<PipelineCard
|
||||
gate
|
||||
title="אישור הלכות (שער יו״ר)"
|
||||
desc="הלכות שחולצו, ממתינות להכרעת דפנה — שער-אנושי, לא תהליך"
|
||||
desc="שער-אנושי, לא תהליך — הפעולה ב-/approvals"
|
||||
href="/approvals"
|
||||
hrefLabel="לתיבת-האישורים ←"
|
||||
>
|
||||
@@ -644,6 +694,7 @@ export default function OperationsPage() {
|
||||
</PipelineCard>
|
||||
|
||||
<PipelineCard
|
||||
gate
|
||||
title="פסיקה חסרה"
|
||||
desc="פערים בקורפוס — נסגרים אוטומטית כשנקלטים"
|
||||
href="/approvals"
|
||||
@@ -653,6 +704,7 @@ export default function OperationsPage() {
|
||||
</PipelineCard>
|
||||
</div>
|
||||
|
||||
<SectionHeader>אחזורים אחרונים</SectionHeader>
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<Card className="bg-surface border-rule shadow-sm">
|
||||
<CardContent className="px-5 py-4">
|
||||
@@ -712,9 +764,9 @@ export default function OperationsPage() {
|
||||
|
||||
{/* INV-IA4: the former /diagnostics surface, folded in here — one
|
||||
monitoring intent, one surface. /diagnostics now redirects here. */}
|
||||
<div className="space-y-3 pt-2">
|
||||
<h2 className="text-navy text-lg mb-0">בריאות-מערכת</h2>
|
||||
<p className="text-ink-muted text-sm">
|
||||
<div className="pt-2">
|
||||
<SectionHeader>בריאות-מערכת</SectionHeader>
|
||||
<p className="text-ink-muted text-sm mb-3">
|
||||
מצב ה-DB, תורי-אישור, ומסמכים שנכשלו/תקועים. (אוחד מ״אבחון״.)
|
||||
</p>
|
||||
<SystemHealthSection />
|
||||
|
||||
@@ -9,8 +9,30 @@ import { AppealTypeBars, subtypeOf } from "@/components/cases/appeal-type-bars";
|
||||
import { CasesTable } from "@/components/cases/cases-table";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useCases, type Case } from "@/lib/api/cases";
|
||||
import { usePendingApprovals } from "@/lib/api/chair";
|
||||
import { useCases, type Case, type CaseStatus } from "@/lib/api/cases";
|
||||
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() {
|
||||
const { data, isPending, error } = useCases(true);
|
||||
@@ -30,9 +52,19 @@ export default function HomePage() {
|
||||
return { permits, levies };
|
||||
}, [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 (
|
||||
<AppShell>
|
||||
<section className="space-y-8">
|
||||
<section className="space-y-7">
|
||||
<header className="flex items-end justify-between gap-6 flex-wrap">
|
||||
<div className="space-y-1.5">
|
||||
<div className="text-[0.75rem] uppercase tracking-[0.12em] text-gold-deep">
|
||||
@@ -40,21 +72,53 @@ export default function HomePage() {
|
||||
</div>
|
||||
<h1 className="text-navy">עוזר משפטי</h1>
|
||||
<p className="text-ink-muted text-base max-w-2xl leading-relaxed">
|
||||
לוח בקרה לניהול תיקי ערר, ניתוח סגנון, וכתיבת החלטות לפי ארכיטקטורת
|
||||
12 הבלוקים.
|
||||
מבט-על על הוועדה — תיקים, אישורים ופעילות אחרונה במקום אחד.
|
||||
</p>
|
||||
</div>
|
||||
<Button asChild className="bg-navy hover:bg-navy-soft text-parchment">
|
||||
<div className="flex gap-2.5">
|
||||
<Button asChild className="bg-gold text-white hover:bg-gold-deep border-transparent">
|
||||
<Link href="/cases/new">+ תיק חדש</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" className="border-rule text-navy">
|
||||
<Link href="/precedents">חיפוש בקורפוס</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<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} />
|
||||
|
||||
<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">
|
||||
{/* תיקים לפי סטטוס — 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">
|
||||
<CardContent className="px-6 py-5">
|
||||
<div className="flex items-center justify-between gap-3 mb-4 flex-wrap">
|
||||
@@ -103,38 +167,39 @@ export default function HomePage() {
|
||||
</div>
|
||||
|
||||
<aside className="space-y-6 lg:sticky lg:top-6 lg:self-start">
|
||||
{approvals && approvals.total_pending > 0 ? (
|
||||
<Card className="bg-gold-wash border-gold/40 shadow-sm">
|
||||
{/* מה ממתין להכרעתך — gold gate card with dot+label+count rows (mockup 04 .gatecard) */}
|
||||
<Card className="bg-gold-wash border-gold/50 shadow-sm">
|
||||
<CardContent className="px-6 py-5">
|
||||
<div className="flex items-center justify-between gap-3 mb-3">
|
||||
<h2 className="text-navy text-lg mb-0">מה ממתין להכרעתך</h2>
|
||||
<span className="text-2xl font-semibold text-gold-deep leading-none tabular-nums">
|
||||
{approvals.total_pending}
|
||||
</span>
|
||||
</div>
|
||||
<ul className="space-y-1.5 mb-4">
|
||||
{approvals.categories
|
||||
.filter((c) => c.count > 0)
|
||||
.map((c) => (
|
||||
<h2 className="text-navy text-lg mb-3">מה ממתין להכרעתך</h2>
|
||||
{approvals && approvals.categories.length > 0 ? (
|
||||
<ul className="mb-1">
|
||||
{approvals.categories.map((c) => (
|
||||
<li
|
||||
key={c.key}
|
||||
className="flex items-center justify-between gap-2 text-[0.85rem] text-ink-soft"
|
||||
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"
|
||||
>
|
||||
<span>{c.label}</span>
|
||||
<span className="text-navy font-semibold tabular-nums">{c.count}</span>
|
||||
<span
|
||||
className={`h-2.5 w-2.5 shrink-0 rounded-full ${SEVERITY_DOT[c.severity]}`}
|
||||
aria-hidden
|
||||
/>
|
||||
<span className="grow min-w-0">{c.label}</span>
|
||||
<span className="font-bold text-navy tabular-nums">{c.count}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Button
|
||||
asChild
|
||||
size="sm"
|
||||
className="bg-gold text-white hover:bg-gold-deep border-transparent w-full"
|
||||
) : (
|
||||
<p className="text-[0.85rem] text-ink-muted mb-2">
|
||||
אין פריטים הממתינים להכרעתך.
|
||||
</p>
|
||||
)}
|
||||
<Link
|
||||
href="/approvals"
|
||||
className="inline-block mt-3 text-[0.85rem] font-semibold text-gold-deep hover:text-navy"
|
||||
>
|
||||
<Link href="/approvals">למרכז האישורים ←</Link>
|
||||
</Button>
|
||||
למרכז האישורים ←
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
<Card className="bg-surface border-rule shadow-sm">
|
||||
<CardContent className="px-6 py-5">
|
||||
|
||||
@@ -5,7 +5,6 @@ import Link from "next/link";
|
||||
import { Pencil, Check, X, Share2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { AppShell } from "@/components/app-shell";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
@@ -34,6 +33,16 @@ const SOURCE_TYPE_LABELS: Record<string, string> = {
|
||||
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.
|
||||
* The `use()` hook unwraps them inside a client component. */
|
||||
export default function PrecedentDetailPage({
|
||||
@@ -48,61 +57,77 @@ export default function PrecedentDetailPage({
|
||||
const [editingCitation, setEditingCitation] = useState(false);
|
||||
const [citationDraft, setCitationDraft] = useState("");
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<AppShell>
|
||||
<section className="space-y-6" dir="rtl">
|
||||
<header>
|
||||
<nav className="text-[0.78rem] text-ink-muted mb-1">
|
||||
<Link href="/" className="hover:text-gold-deep">בית</Link>
|
||||
<span aria-hidden> · </span>
|
||||
<Link href="/precedents" className="hover:text-gold-deep">ספריית פסיקה</Link>
|
||||
<span aria-hidden> · </span>
|
||||
<span className="text-navy">פרטי פסיקה</span>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
{error ? (
|
||||
<Card className="bg-danger-bg border-danger/40">
|
||||
<CardContent className="px-6 py-6 text-center space-y-3">
|
||||
<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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : isPending || !data ? (
|
||||
<div className="space-y-3">
|
||||
{[...Array(5)].map((_, i) => <Skeleton key={i} className="h-16 w-full" />)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Card className="bg-surface border-rule shadow-sm">
|
||||
<CardContent className="px-6 py-5 space-y-4">
|
||||
</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 (
|
||||
<AppShell>
|
||||
<div dir="rtl">
|
||||
{/* ── parchment header band (mockup 08 `.band`) — breaks out to the
|
||||
AppShell <main> edges (px-10 py-10) for a full-width band. ──── */}
|
||||
<div className="-mx-10 -mt-10 mb-6 border-b border-rule bg-parchment px-10 py-6">
|
||||
<nav className="text-[0.78rem] text-ink-muted mb-2">
|
||||
<Link href="/precedents" className="text-gold-deep hover:underline">פסיקה</Link>
|
||||
<span aria-hidden> ← </span>
|
||||
<Link href="/precedents" className="text-gold-deep hover:underline">ספרייה</Link>
|
||||
<span aria-hidden> ← </span>
|
||||
<span>תקדים</span>
|
||||
</nav>
|
||||
|
||||
<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">
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-navy text-2xl font-bold leading-snug mb-1">
|
||||
{data.case_name || "—"}
|
||||
</h1>
|
||||
<div className="text-ink-muted text-sm font-mono" dir="ltr">
|
||||
<div className="text-ink-soft text-sm font-mono" dir="ltr">
|
||||
{data.case_number}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<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 variant="outline" size="sm" onClick={() => setEditing(true)}>
|
||||
<Button variant="outline" size="sm" className="border-rule" onClick={() => setEditing(true)}>
|
||||
<Pencil className="w-3.5 h-3.5 me-1" /> ערוך פרטים
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Citation per Israeli unified citation rules. The LLM
|
||||
extractor composes this from the document; the chair
|
||||
can override below. */}
|
||||
{/* citation (unified Israeli citation rules) — chair-editable */}
|
||||
<div className="mt-4 max-w-3xl">
|
||||
<CitationBlock
|
||||
precedent={data as Precedent}
|
||||
editing={editingCitation}
|
||||
@@ -122,113 +147,134 @@ export default function PrecedentDetailPage({
|
||||
toast.success("מראה מקום עודכן");
|
||||
setEditingCitation(false);
|
||||
} catch (e) {
|
||||
toast.error(
|
||||
e instanceof Error ? e.message : "שמירה נכשלה",
|
||||
);
|
||||
toast.error(e instanceof Error ? e.message : "שמירה נכשלה");
|
||||
}
|
||||
}}
|
||||
saving={update.isPending}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{/* meta-band — label/value pairs + chips (mockup 08 `.metaband`) */}
|
||||
<div className="mt-4 flex flex-wrap items-center gap-x-6 gap-y-3">
|
||||
{data.court ? <MetaItem label="בית-משפט">{data.court}</MetaItem> : null}
|
||||
{date ? (
|
||||
<MetaItem label="תאריך">
|
||||
<span className="tabular-nums" dir="ltr">{date}</span>
|
||||
</MetaItem>
|
||||
) : null}
|
||||
{data.practice_area ? (
|
||||
<Badge variant="outline" className="text-[0.7rem] bg-info-bg text-info border-transparent">
|
||||
<MetaItem label="תחום">
|
||||
<Badge variant="outline" className="text-[0.72rem] bg-info-bg text-info border-transparent rounded-full px-3">
|
||||
{PRACTICE_AREA_LABELS[data.practice_area] ?? data.practice_area}
|
||||
</Badge>
|
||||
</MetaItem>
|
||||
) : null}
|
||||
{data.source_type ? (
|
||||
<Badge variant="outline" className="text-[0.7rem]">
|
||||
<MetaItem label="סוג-מקור">
|
||||
<Badge variant="outline" className="text-[0.72rem] rounded-full px-3">
|
||||
{SOURCE_TYPE_LABELS[data.source_type] ?? data.source_type}
|
||||
</Badge>
|
||||
</MetaItem>
|
||||
) : null}
|
||||
{data.precedent_level ? (
|
||||
<Badge variant="outline" className="text-[0.7rem] bg-gold-wash text-gold-deep border-rule">
|
||||
<MetaItem label="רמת-תקדים">
|
||||
<Badge variant="outline" className="text-[0.72rem] bg-gold-wash text-gold-deep border-rule rounded-full px-3">
|
||||
{data.precedent_level}
|
||||
</Badge>
|
||||
</MetaItem>
|
||||
) : null}
|
||||
{data.is_binding ? (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[0.7rem] bg-success-bg text-success border-transparent"
|
||||
>
|
||||
הלכה מחייבת
|
||||
<MetaItem label="סיווג-מחייבות">
|
||||
<Badge variant="outline" className="text-[0.72rem] bg-success-bg text-success border-transparent rounded-full px-3">
|
||||
מחייב
|
||||
</Badge>
|
||||
) : null}
|
||||
{data.court ? (
|
||||
<span className="text-[0.78rem] text-ink-muted">{data.court}</span>
|
||||
) : null}
|
||||
{data.date ? (
|
||||
<span className="text-[0.78rem] text-ink-muted tabular-nums" dir="ltr">
|
||||
{data.date.slice(0, 10)}
|
||||
</span>
|
||||
</MetaItem>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{data.headnote ? (
|
||||
<div>
|
||||
<h3 className="text-navy text-sm font-semibold m-0 mb-1">Headnote</h3>
|
||||
<p className="text-ink-soft text-sm leading-relaxed m-0">
|
||||
{data.headnote}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{data.summary ? (
|
||||
<div>
|
||||
<h3 className="text-navy text-sm font-semibold m-0 mb-1">תקציר</h3>
|
||||
<p className="text-ink-soft text-sm leading-relaxed m-0 whitespace-pre-line">
|
||||
{data.summary}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{(data as { key_quote?: string }).key_quote ? (
|
||||
<div>
|
||||
<h3 className="text-navy text-sm font-semibold m-0 mb-1">ציטוט מרכזי</h3>
|
||||
<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">
|
||||
{(data as { key_quote?: string }).key_quote}
|
||||
</blockquote>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{data.subject_tags?.length ? (
|
||||
<div className="flex items-center gap-1 flex-wrap pt-1">
|
||||
<div className="mt-3 flex items-center gap-1 flex-wrap">
|
||||
{data.subject_tags.map((t) => (
|
||||
<Badge key={t} variant="outline" className="text-[0.65rem]">
|
||||
<Badge key={t} variant="outline" className="text-[0.65rem] bg-surface">
|
||||
{t}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card className="bg-surface border-rule shadow-sm">
|
||||
<CardContent className="px-6 py-5">
|
||||
<RelatedCasesSection
|
||||
caseId={id}
|
||||
related={data.related_cases ?? []}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/* ── two-column body (mockup 08 `.wrap` grid) ────────────────── */}
|
||||
<div className="grid gap-6 lg:grid-cols-[1fr_320px]">
|
||||
{/* main column */}
|
||||
<div className="space-y-4">
|
||||
{data.summary ? (
|
||||
<DetailCard title="תקציר">
|
||||
<p className="text-ink-soft text-sm leading-8 m-0 whitespace-pre-line">
|
||||
{data.summary}
|
||||
</p>
|
||||
</DetailCard>
|
||||
) : null}
|
||||
|
||||
<Card className="bg-surface border-rule shadow-sm">
|
||||
<CardContent className="px-6 py-5">
|
||||
{data.headnote ? (
|
||||
<DetailCard title="כותרת-הלכה (headnote)" prov="opus">
|
||||
<p className="text-ink-soft text-sm leading-8 m-0">{data.headnote}</p>
|
||||
</DetailCard>
|
||||
) : null}
|
||||
|
||||
{(data as { key_quote?: string }).key_quote ? (
|
||||
<DetailCard title="ציטוט-מפתח" prov="opus">
|
||||
<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">
|
||||
{(data as { key_quote?: string }).key_quote}
|
||||
</blockquote>
|
||||
</DetailCard>
|
||||
) : null}
|
||||
|
||||
<div className="rounded-lg border border-rule bg-surface shadow-sm px-5 py-4">
|
||||
<ExtractedHalachotSection halachot={data.halachot ?? []} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* side rail — citations + corroboration */}
|
||||
<div className="space-y-4">
|
||||
<RelatedCasesSection caseId={id} related={data.related_cases ?? []} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PrecedentEditSheet
|
||||
caseLawId={editing ? id : null}
|
||||
onOpenChange={(open) => setEditing(open)}
|
||||
/>
|
||||
</section>
|
||||
</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({
|
||||
precedent,
|
||||
editing,
|
||||
@@ -252,7 +298,7 @@ function CitationBlock({
|
||||
|
||||
if (editing) {
|
||||
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">
|
||||
<span className="text-[0.78rem] font-semibold text-navy">
|
||||
עריכת מראה מקום
|
||||
@@ -266,7 +312,7 @@ function CitationBlock({
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
rows={3}
|
||||
dir="rtl"
|
||||
className="font-mono text-sm"
|
||||
className="font-mono text-sm bg-surface"
|
||||
placeholder='ערר (ועדות ערר ...) 1234/24 **עורר נ' הוועדה המקומית** (נבו 1.2.2025)'
|
||||
disabled={saving}
|
||||
/>
|
||||
@@ -280,12 +326,7 @@ function CitationBlock({
|
||||
<Check className="w-3.5 h-3.5 me-1" />
|
||||
שמור
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={onCancel}
|
||||
disabled={saving}
|
||||
>
|
||||
<Button size="sm" variant="outline" onClick={onCancel} disabled={saving}>
|
||||
<X className="w-3.5 h-3.5 me-1" />
|
||||
ביטול
|
||||
</Button>
|
||||
@@ -296,7 +337,7 @@ function CitationBlock({
|
||||
|
||||
if (!citation) {
|
||||
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>
|
||||
@@ -309,7 +350,7 @@ function CitationBlock({
|
||||
}
|
||||
|
||||
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">
|
||||
<span className="text-[0.7rem] uppercase tracking-wide text-ink-muted">
|
||||
מראה מקום
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
"use client";
|
||||
|
||||
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 { Badge } from "@/components/ui/badge";
|
||||
import { AppShell } from "@/components/app-shell";
|
||||
import { LibraryListPanel } from "@/components/precedents/library-list-panel";
|
||||
import { LibrarySearchPanel } from "@/components/precedents/library-search-panel";
|
||||
import { HalachaReviewPanel } from "@/components/precedents/halacha-review-panel";
|
||||
import { LibraryStatsPanel } from "@/components/precedents/library-stats-panel";
|
||||
import { useHalachotPending } from "@/lib/api/precedent-library";
|
||||
import { useMissingPrecedents } from "@/lib/api/missing-precedents";
|
||||
|
||||
/**
|
||||
* 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).
|
||||
*/
|
||||
|
||||
function PendingBadge() {
|
||||
const { data } = useHalachotPending();
|
||||
const n = data?.count ?? 0;
|
||||
/** Colored count pill riding on a tab trigger (mockup 07: warn for review
|
||||
* queue, info for incoming). Returns null when the queue is empty. */
|
||||
function CountPill({ n, tone }: { n: number; tone: "warn" | "info" }) {
|
||||
if (!n) return null;
|
||||
const cls =
|
||||
tone === "warn"
|
||||
? "bg-warn text-white"
|
||||
: "bg-info text-white";
|
||||
return (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="ms-1 bg-gold-wash text-gold-deep border-gold/40 text-[0.65rem]"
|
||||
<span
|
||||
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}`}
|
||||
>
|
||||
{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() {
|
||||
return (
|
||||
<AppShell>
|
||||
<Tabs defaultValue="library" dir="rtl">
|
||||
<section className="space-y-6">
|
||||
<header>
|
||||
<nav className="text-[0.78rem] text-ink-muted mb-1">
|
||||
<header className="space-y-3">
|
||||
<nav className="text-[0.78rem] text-ink-muted">
|
||||
<Link href="/" className="hover:text-gold-deep">בית</Link>
|
||||
<span aria-hidden> · </span>
|
||||
<span className="text-navy">ספריית פסיקה</span>
|
||||
</nav>
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-navy mb-0">ספריית הפסיקה הסמכותית</h1>
|
||||
<p className="text-ink-muted text-sm mt-1 max-w-3xl">
|
||||
פסיקה חיצונית — פסקי דין של ערכאות עליונות והחלטות של ועדות ערר אחרות.
|
||||
כל קובץ עובר חילוץ הלכות אוטומטי, וההלכות ממתינות לאישור היו"ר לפני
|
||||
שהן זמינות לסוכני הכתיבה (legal-writer וכו').
|
||||
<p className="text-ink-muted text-sm mt-1 max-w-3xl leading-relaxed">
|
||||
קורפוס הפסיקה והלכות המערכת — חיפוש סמנטי, תור-אישור והשלמת
|
||||
פסיקה חסרה. כל קובץ עובר חילוץ הלכות אוטומטי, וההלכות ממתינות
|
||||
לאישור היו"ר לפני שהן זמינות לסוכני הכתיבה.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* tabs as a dedicated row under the header — underline-style
|
||||
triggers with colored count pills (mockup 07). */}
|
||||
<TabsList className="flex w-full justify-start gap-1 rounded-none border-0 border-b border-rule bg-transparent p-0 h-auto">
|
||||
{[
|
||||
{ value: "library", label: "ספרייה", pill: null },
|
||||
{ value: "search", label: "חיפוש בקורפוס", pill: null },
|
||||
{ value: "review", label: "תור הלכות", pill: <PendingPill /> },
|
||||
{
|
||||
value: "incoming",
|
||||
label: "פסיקה נכנסת",
|
||||
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>
|
||||
))}
|
||||
</TabsList>
|
||||
</header>
|
||||
|
||||
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
|
||||
|
||||
<Card className="bg-surface border-rule shadow-sm">
|
||||
<CardContent className="px-6 py-5">
|
||||
<Tabs defaultValue="library" dir="rtl">
|
||||
<TabsList className="bg-rule-soft/60">
|
||||
<TabsTrigger value="library">ספרייה</TabsTrigger>
|
||||
<TabsTrigger value="search">חיפוש סמנטי</TabsTrigger>
|
||||
<TabsTrigger value="review">
|
||||
ממתין לאישור
|
||||
<PendingBadge />
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="stats">סטטיסטיקה</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="library" className="mt-5">
|
||||
<TabsContent value="library" className="mt-0">
|
||||
<LibraryListPanel />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="search" className="mt-5">
|
||||
<TabsContent value="search" className="mt-0">
|
||||
<LibrarySearchPanel />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="review" className="mt-5">
|
||||
<TabsContent value="review" className="mt-0">
|
||||
<HalachaReviewPanel />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="stats" className="mt-5">
|
||||
{/* "פסיקה נכנסת" — the incoming/missing-precedent queue. Kept as a
|
||||
tab per the mockup; full management lives on /missing-precedents. */}
|
||||
<TabsContent value="incoming" className="mt-0">
|
||||
<IncomingTab />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="stats" className="mt-0">
|
||||
<LibraryStatsPanel />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
</Tabs>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,27 +1,133 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import Link from "next/link";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { AppShell } from "@/components/app-shell";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Markdown } from "@/components/ui/markdown";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
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
|
||||
* tables), served by GET /api/scripts/catalog. SCRIPTS.md is the single
|
||||
* source of truth — CLAUDE.md mandates updating it on every script change —
|
||||
* so we render it directly rather than re-describing the scripts here.
|
||||
* The single source of truth is still `scripts/SCRIPTS.md` (CLAUDE.md mandates
|
||||
* updating it on every script change), served verbatim by
|
||||
* GET /api/scripts/catalog. We parse its markdown tables into structured rows
|
||||
* 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() {
|
||||
const { data, isLoading, isError, error } = useQuery({
|
||||
queryKey: ["scripts-catalog"],
|
||||
queryFn: ({ signal }) => fetchScriptsCatalog(signal),
|
||||
});
|
||||
|
||||
const rows = useMemo(
|
||||
() => (data?.content ? parseScripts(data.content) : []),
|
||||
[data],
|
||||
);
|
||||
|
||||
const lastModified =
|
||||
data?.last_modified != null
|
||||
? new Date(data.last_modified * 1000).toLocaleDateString("he-IL", {
|
||||
@@ -31,63 +137,113 @@ export default function ScriptsPage() {
|
||||
})
|
||||
: null;
|
||||
|
||||
const giteaBase = data?.gitea_url ?? null;
|
||||
|
||||
return (
|
||||
<AppShell>
|
||||
<section className="space-y-6">
|
||||
<div className="flex items-end justify-between gap-4">
|
||||
<div>
|
||||
<header>
|
||||
<nav className="text-[0.78rem] text-ink-muted mb-1">
|
||||
<Link href="/" className="hover:text-gold-deep">בית</Link>
|
||||
<span aria-hidden> · </span>
|
||||
<span className="text-navy">סקריפטים</span>
|
||||
</nav>
|
||||
<h1 className="text-navy mb-0">סקריפטים</h1>
|
||||
<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]">
|
||||
scripts/
|
||||
</code>{" "}
|
||||
— שם, סוג, תפקיד ותזמון. מקור-האמת הוא{" "}
|
||||
<p className="text-sm text-ink-muted mt-1 max-w-2xl">
|
||||
סקריפטי-תחזוקה ותפעול. מקור-האמת הוא{" "}
|
||||
<code className="rounded bg-rule-soft px-1 py-0.5 font-mono text-[0.78rem]">
|
||||
scripts/SCRIPTS.md
|
||||
</code>
|
||||
; עריכה דרך git, לא מכאן.
|
||||
</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>
|
||||
</header>
|
||||
|
||||
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
|
||||
|
||||
<Card className="bg-surface border-rule shadow-sm">
|
||||
<CardContent className="px-6 py-5">
|
||||
{isLoading ? (
|
||||
<p className="text-sm text-ink-muted">טוען קטלוג…</p>
|
||||
<Card className="bg-surface border-rule px-6 py-5 text-sm text-ink-muted">
|
||||
טוען קטלוג…
|
||||
</Card>
|
||||
) : 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 ?? "לא ידוע"}
|
||||
</p>
|
||||
) : data ? (
|
||||
<>
|
||||
<Markdown content={data.content} />
|
||||
</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="mt-6 pt-3 border-t border-rule text-xs text-ink-muted">
|
||||
<p className="px-5 py-3 border-t border-rule text-xs text-ink-muted">
|
||||
עודכן לאחרונה: {lastModified}
|
||||
</p>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</section>
|
||||
</AppShell>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { AlertTriangle, CheckCircle2, HelpCircle } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
type Props = {
|
||||
drift: boolean;
|
||||
@@ -9,31 +8,31 @@ type Props = {
|
||||
coolifyAvailable?: boolean;
|
||||
};
|
||||
|
||||
// Filled pill chips matching IA-redesign mockup 15 (.c-synced / .c-drift).
|
||||
export function DriftBadge({ drift, coolifyAvailable = true }: Props) {
|
||||
if (!coolifyAvailable) {
|
||||
return (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-ink-muted border-rule gap-1"
|
||||
<span
|
||||
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"
|
||||
title="Coolify לא זמין — מצב ה-drift לא ידוע"
|
||||
>
|
||||
<HelpCircle className="w-3 h-3" />
|
||||
Unknown
|
||||
</Badge>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (drift) {
|
||||
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" />
|
||||
Drift
|
||||
</Badge>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
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" />
|
||||
Synced
|
||||
</Badge>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { useState } from "react";
|
||||
import { ExternalLink, Save, Lock } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import type { McpEnvVar } from "@/lib/api/settings";
|
||||
import { useUpdateMcpEnv } from "@/lib/api/settings";
|
||||
import { toast } from "sonner";
|
||||
@@ -44,36 +43,38 @@ export function EnvVarRow({
|
||||
`https://coolify.nautilus.marcusgroup.org/project/applications/${coolifyAppUuid}/environment-variables`;
|
||||
|
||||
return (
|
||||
<div className="rounded-md border border-rule p-4 bg-rule-soft/20 hover:bg-rule-soft/40 transition-colors">
|
||||
<div className="flex items-start justify-between gap-3 mb-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="px-5 py-4 border-b border-rule-soft last:border-b-0">
|
||||
{/* envtop — key + type chip + secret chip + drift/synced chip */}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<code className="font-mono text-sm font-medium text-navy" dir="ltr">
|
||||
<code className="font-mono text-[0.84rem] font-semibold text-navy" dir="ltr">
|
||||
{spec.key}
|
||||
</code>
|
||||
<Badge variant="outline" className="text-[0.7rem]">
|
||||
<span className="inline-flex items-center rounded-full bg-info-bg text-info text-[0.72rem] font-semibold px-2.5 py-0.5">
|
||||
{spec.type}
|
||||
</Badge>
|
||||
</span>
|
||||
{spec.is_secret && (
|
||||
<Badge variant="outline" className="text-[0.7rem] text-warn border-warn/40 gap-1">
|
||||
<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">
|
||||
<Lock className="w-3 h-3" />
|
||||
secret
|
||||
</Badge>
|
||||
</span>
|
||||
)}
|
||||
<DriftBadge drift={spec.drift} coolifyAvailable={coolifyAvailable} />
|
||||
{spec.has_duplicates && (
|
||||
<Badge variant="outline" className="text-[0.7rem] 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">
|
||||
duplicates
|
||||
</Badge>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-ink-muted mt-1">{spec.description}</p>
|
||||
</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">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[0.72rem] text-ink-muted w-20">Coolify:</span>
|
||||
{/* vals — Coolify value | Container value (+pending) | save (mockup .vals 3-col) */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-[1fr_1fr_auto] gap-3.5 items-end mt-3.5">
|
||||
<div>
|
||||
<span className="block text-[0.72rem] text-ink-muted font-semibold mb-1">
|
||||
Coolify:
|
||||
</span>
|
||||
{spec.is_editable ? (
|
||||
<EnvVarEditor
|
||||
spec={spec}
|
||||
@@ -82,53 +83,61 @@ export function EnvVarRow({
|
||||
disabled={update.isPending}
|
||||
/>
|
||||
) : (
|
||||
<span className="font-mono text-ink" dir="ltr">
|
||||
{spec.coolify_value ?? <em className="text-ink-muted">— לא מוגדר —</em>}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<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
|
||||
</Badge>
|
||||
)}
|
||||
</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"
|
||||
className="font-mono text-[0.81rem] text-gold-deep hover:underline flex items-center gap-1"
|
||||
dir="ltr"
|
||||
>
|
||||
{spec.coolify_value ?? "— לא מוגדר —"}
|
||||
<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>
|
||||
)}
|
||||
{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>
|
||||
);
|
||||
|
||||
@@ -72,37 +72,39 @@ export function EnvironmentTab() {
|
||||
const duplicatesCount = data.vars.filter((v) => v.has_duplicates).length;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Card className="bg-surface border-rule">
|
||||
<div className="space-y-5">
|
||||
{/* 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">
|
||||
<div className="flex items-center gap-3 flex-wrap text-sm">
|
||||
<Badge variant="outline">
|
||||
Coolify app: <code dir="ltr" className="ms-1">{data.coolify_app_uuid.slice(0, 8)}…</code>
|
||||
</Badge>
|
||||
{driftCount > 0 && (
|
||||
<Badge variant="outline" className="text-warn border-warn/40">
|
||||
{driftCount} drift
|
||||
</Badge>
|
||||
<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
|
||||
</span>
|
||||
)}
|
||||
{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
|
||||
</Badge>
|
||||
</span>
|
||||
)}
|
||||
{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(", ")}
|
||||
</Badge>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleRedeploy}
|
||||
disabled={redeploy.isPending}
|
||||
variant={pendingRedeploy ? "default" : "outline"}
|
||||
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" />
|
||||
{redeploy.isPending ? "Redeploying..." : "Redeploy now"}
|
||||
{redeploy.isPending ? "פורס מחדש..." : "פרוס מחדש"}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -110,16 +112,31 @@ export function EnvironmentTab() {
|
||||
{CATEGORY_ORDER.map((cat) => {
|
||||
const vars = grouped.get(cat);
|
||||
if (!vars || vars.length === 0) return null;
|
||||
const catDrift = vars.filter((v) => v.drift).length;
|
||||
return (
|
||||
<Card key={cat} className="bg-surface border-rule">
|
||||
<CardContent className="px-6 py-5">
|
||||
<h2 className="text-navy text-lg mb-4 flex items-center gap-2">
|
||||
<div
|
||||
key={cat}
|
||||
className="rounded-lg border border-rule bg-surface shadow-sm overflow-hidden"
|
||||
>
|
||||
{/* panel header (.ph) — parchment band: title + count + drift chip */}
|
||||
<div className="flex items-center justify-between gap-3 px-5 py-4 border-b border-rule bg-parchment">
|
||||
<div>
|
||||
<h2 className="text-navy text-base font-semibold mb-0 flex items-center gap-2">
|
||||
{CATEGORY_LABELS[cat]}
|
||||
<Badge variant="outline" className="text-[0.7rem] tabular-nums">
|
||||
{vars.length}
|
||||
</Badge>
|
||||
<span className="text-[0.72rem] text-ink-muted font-medium tabular-nums">
|
||||
({vars.length})
|
||||
</span>
|
||||
</h2>
|
||||
<div className="space-y-3">
|
||||
<p className="text-[0.78rem] text-ink-muted mt-0.5">
|
||||
מקור-האמת הוא Coolify. שינוי נכנס לתוקף רק לאחר redeploy של הקונטיינר.
|
||||
</p>
|
||||
</div>
|
||||
{coolifyAvailable && catDrift > 0 && (
|
||||
<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}
|
||||
@@ -130,8 +147,6 @@ export function EnvironmentTab() {
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -11,11 +11,34 @@ import { RegistrationsTab } from "./_components/registrations-tab";
|
||||
import { BlocksTab } from "./_components/blocks-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() {
|
||||
return (
|
||||
<AppShell>
|
||||
<section className="space-y-6">
|
||||
<header>
|
||||
<header className="flex items-end justify-between gap-4 flex-wrap">
|
||||
<div>
|
||||
<nav className="text-[0.78rem] text-ink-muted mb-1">
|
||||
<Link href="/" className="hover:text-gold-deep">
|
||||
בית
|
||||
@@ -25,46 +48,55 @@ export default function SettingsPage() {
|
||||
</nav>
|
||||
<h1 className="text-navy mb-0">הגדרות</h1>
|
||||
<p className="text-ink-muted text-sm mt-1 max-w-2xl">
|
||||
תצורת המערכת, MCP server, ו-Paperclip integration.
|
||||
תצורת הפלטפורמה — סוכנים, סביבה, כלים ובלוקים.
|
||||
</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>
|
||||
|
||||
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
|
||||
|
||||
<Tabs dir="rtl" defaultValue="paperclip" className="space-y-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="paperclip">
|
||||
<Building2 className="w-4 h-4" data-icon="inline-start" />
|
||||
Paperclip
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="agents">
|
||||
<Bot className="w-4 h-4" data-icon="inline-start" />
|
||||
סוכנים
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="environment">
|
||||
<Server className="w-4 h-4" data-icon="inline-start" />
|
||||
סביבה
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="tools">
|
||||
<Wrench className="w-4 h-4" data-icon="inline-start" />
|
||||
כלים
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="blocks">
|
||||
<Layers className="w-4 h-4" data-icon="inline-start" />
|
||||
בלוקים
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="registrations">
|
||||
<Plug className="w-4 h-4" data-icon="inline-start" />
|
||||
רישומים
|
||||
<Tabs
|
||||
dir="rtl"
|
||||
defaultValue="environment"
|
||||
orientation="vertical"
|
||||
className="flex-row gap-6 items-start"
|
||||
>
|
||||
{/* vertical sidenav (mockup .sidenav) */}
|
||||
<TabsList
|
||||
variant="line"
|
||||
className="w-[230px] shrink-0 items-stretch gap-0 rounded-lg border border-rule bg-surface shadow-sm overflow-hidden p-0"
|
||||
>
|
||||
{SECTIONS.map((s) => {
|
||||
const Icon = s.icon;
|
||||
return (
|
||||
<TabsTrigger
|
||||
key={s.value}
|
||||
value={s.value}
|
||||
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"
|
||||
>
|
||||
<Icon className="w-4 h-4 shrink-0" />
|
||||
{s.label}
|
||||
</TabsTrigger>
|
||||
);
|
||||
})}
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="paperclip"><PaperclipTab /></TabsContent>
|
||||
<TabsContent value="agents"><AgentsTab /></TabsContent>
|
||||
<TabsContent value="environment"><EnvironmentTab /></TabsContent>
|
||||
<TabsContent value="tools"><ToolsTab /></TabsContent>
|
||||
<TabsContent value="blocks"><BlocksTab /></TabsContent>
|
||||
<TabsContent value="registrations"><RegistrationsTab /></TabsContent>
|
||||
<div className="flex-1 min-w-0">
|
||||
<TabsContent value="paperclip" className="mt-0"><PaperclipTab /></TabsContent>
|
||||
<TabsContent value="agents" className="mt-0"><AgentsTab /></TabsContent>
|
||||
<TabsContent value="environment" className="mt-0"><EnvironmentTab /></TabsContent>
|
||||
<TabsContent value="tools" className="mt-0"><ToolsTab /></TabsContent>
|
||||
<TabsContent value="blocks" className="mt-0"><BlocksTab /></TabsContent>
|
||||
<TabsContent value="registrations" className="mt-0"><RegistrationsTab /></TabsContent>
|
||||
</div>
|
||||
</Tabs>
|
||||
</section>
|
||||
</AppShell>
|
||||
|
||||
@@ -1,95 +1,64 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { Plug, HardDrive, Database, FileText } from "lucide-react";
|
||||
import { AppShell } from "@/components/app-shell";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card } from "@/components/ui/card";
|
||||
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";
|
||||
|
||||
function formatSize(bytes: number | null) {
|
||||
if (bytes == null) return "—";
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
function formatChars(skill: Skill): string {
|
||||
// Mockup column = "גודל (תווים)" — the DB markdown char count, grouped.
|
||||
const n = skill.db_markdown_chars ?? 0;
|
||||
return n.toLocaleString("en-US");
|
||||
}
|
||||
|
||||
function StatusDot({ tone }: { tone: string }) {
|
||||
return <span className={`h-1.5 w-1.5 rounded-full ${tone}`} aria-hidden />;
|
||||
function formatUpdated(iso: string | null): string {
|
||||
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) {
|
||||
return (
|
||||
<Badge variant="outline" className="gap-1.5 bg-warn-bg text-warn border-warn/40">
|
||||
<StatusDot tone="bg-warn" />לא סונכרן
|
||||
</Badge>
|
||||
);
|
||||
/**
|
||||
* Sync chip — colored dot + label, faithful to mockup 14:
|
||||
* - מסונכרן (success) when present in DB + on disk
|
||||
* - DB בלבד (info) when in DB but no disk copy
|
||||
* - לא מסונכרן (warn) when missing from the DB
|
||||
*/
|
||||
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 (
|
||||
<Card className="bg-surface border-rule shadow-sm hover:shadow-md transition-shadow">
|
||||
<CardContent className="px-5 py-4">
|
||||
<div className="flex items-start justify-between gap-3 mb-2">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<Plug className="w-4 h-4 text-gold-deep shrink-0" />
|
||||
<div className="min-w-0">
|
||||
<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
|
||||
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 />
|
||||
{label}
|
||||
</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 className="text-navy">מיומנויות</span>
|
||||
</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">
|
||||
רשימת ה-skills המותקנים במערכת Paperclip ומצב הסנכרון שלהם בין ה-DB
|
||||
סקילים מותקנים בפלטפורמה — שם, גודל, מועד עדכון ומצב הסנכרון בין ה-DB
|
||||
לדיסק.
|
||||
</p>
|
||||
</header>
|
||||
@@ -115,28 +84,69 @@ export default function SkillsPage() {
|
||||
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
|
||||
|
||||
{error ? (
|
||||
<Card className="bg-danger-bg border-danger/40">
|
||||
<CardContent className="px-6 py-6 text-center text-danger">
|
||||
<Card className="bg-danger-bg border-danger/40 px-6 py-6 text-center text-danger">
|
||||
{error.message}
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : isPending ? (
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{[...Array(6)].map((_, i) => (
|
||||
<Skeleton key={i} className="h-32 w-full rounded-lg" />
|
||||
))}
|
||||
</div>
|
||||
<Skeleton className="h-80 w-full rounded-lg" />
|
||||
) : data?.length === 0 ? (
|
||||
<Card className="bg-surface border-rule">
|
||||
<CardContent className="px-6 py-12 text-center text-ink-muted">
|
||||
<Card className="bg-surface border-rule px-6 py-12 text-center text-ink-muted">
|
||||
<div className="text-gold text-3xl mb-2" aria-hidden>❦</div>
|
||||
אין skills מותקנים
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{data?.map((s) => <SkillCard key={s.slug} skill={s} />)}
|
||||
</div>
|
||||
<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-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>
|
||||
</AppShell>
|
||||
|
||||
@@ -22,16 +22,19 @@ export default function TrainingPage() {
|
||||
<AppShell>
|
||||
<section className="space-y-6">
|
||||
<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">
|
||||
<Link href="/" className="hover:text-gold-deep">בית</Link>
|
||||
<span aria-hidden> · </span>
|
||||
<span className="text-navy">אימון סגנון</span>
|
||||
</nav>
|
||||
<h1 className="text-navy mb-0">הפורטרט הסגנוני של דפנה</h1>
|
||||
<p className="text-ink-muted text-sm mt-1 max-w-2xl">
|
||||
לוח בקרה של קורפוס האימון — סטטיסטיקות, אנטומיית החלטה ממוצעת,
|
||||
ביטויי חתימה, וכלי השוואה בין שתי החלטות.
|
||||
<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-sm mt-1 max-w-2xl leading-relaxed">
|
||||
פורטרט הקול שנלמד מהקורפוס, מוזן read-only לכותב — סטטיסטיקות,
|
||||
אנטומיית החלטה ממוצעת, ביטויי חתימה, וכלי השוואה בין החלטות.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ReactNode } from "react";
|
||||
import Link from "next/link";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { StatusBadge } from "@/components/cases/status-badge";
|
||||
import { SyncIndicator } from "@/components/cases/sync-indicator";
|
||||
@@ -25,10 +25,36 @@ 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 (
|
||||
<Card className="bg-surface border-rule shadow-sm">
|
||||
<CardContent className="px-6 py-5">
|
||||
<div className="-mx-10 -mt-10 mb-2 bg-parchment border-b border-rule px-10 pt-6">
|
||||
<nav className="text-[0.78rem] text-ink-muted mb-3 flex items-center gap-2">
|
||||
<Link href="/" className="hover:text-gold-deep">בית</Link>
|
||||
<span aria-hidden>·</span>
|
||||
@@ -38,16 +64,17 @@ export function CaseHeader({ data }: { data?: CaseDetail }) {
|
||||
</nav>
|
||||
|
||||
<div className="flex items-start justify-between gap-6 flex-wrap">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<span className="font-display text-[2rem] font-black text-navy leading-none tabular-nums">
|
||||
<div className="min-w-0">
|
||||
{/* title row — H1 + status/type/blam chips inline (mockup .band h1) */}
|
||||
<h1 className="text-navy text-[1.7rem] font-bold leading-tight flex items-center gap-3 flex-wrap mb-0">
|
||||
<span className="tabular-nums">
|
||||
{data?.proceeding_type ?? "ערר"} {data?.case_number ?? "—"}
|
||||
</span>
|
||||
{data?.status && <StatusBadge status={data.status} />}
|
||||
{data?.archived_at && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
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"
|
||||
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>
|
||||
@@ -55,7 +82,7 @@ export function CaseHeader({ data }: { data?: CaseDetail }) {
|
||||
{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"
|
||||
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" && (
|
||||
@@ -63,15 +90,34 @@ export function CaseHeader({ data }: { data?: CaseDetail }) {
|
||||
)}
|
||||
</Badge>
|
||||
)}
|
||||
{(data?.proceeding_type === 'בל"מ' || isBlamSubtype(data?.appeal_subtype)) && (
|
||||
{isBlam && (
|
||||
<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"
|
||||
className="rounded-full px-3 py-0.5 text-[0.75rem] font-bold bg-warn/10 text-warn-deep border-warn/40"
|
||||
title="בקשה להארכת מועד להגשת ערר"
|
||||
>
|
||||
בל"מ
|
||||
</Badge>
|
||||
)}
|
||||
</h1>
|
||||
|
||||
{/* case title / subject under the heading */}
|
||||
{data?.title && (
|
||||
<p className="text-navy/90 text-base font-semibold mt-2 max-w-3xl leading-snug">
|
||||
{data.title}
|
||||
</p>
|
||||
)}
|
||||
{/* parties line (mockup .parties) */}
|
||||
{parties ? (
|
||||
<p className="text-ink-soft text-sm mt-1.5">{parties}</p>
|
||||
) : data?.subject ? (
|
||||
<p className="text-ink-soft text-sm mt-1.5 max-w-3xl leading-relaxed">
|
||||
{data.subject}
|
||||
</p>
|
||||
) : 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}
|
||||
@@ -79,18 +125,12 @@ export function CaseHeader({ data }: { data?: CaseDetail }) {
|
||||
/>
|
||||
)}
|
||||
<CreateRepoButton data={data} />
|
||||
{actions}
|
||||
</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>
|
||||
|
||||
<dl className="grid grid-cols-2 gap-x-6 gap-y-1 text-sm">
|
||||
{/* 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>
|
||||
@@ -105,7 +145,9 @@ export function CaseHeader({ data }: { data?: CaseDetail }) {
|
||||
<dd><SyncIndicator caseNumber={data?.case_number} /></dd>
|
||||
</dl>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* tab strip anchored to band bottom (mockup .tabs) */}
|
||||
{tabs ? <div className="mt-5">{tabs}</div> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState, type ReactNode } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import {
|
||||
@@ -295,6 +295,25 @@ function DocumentRow({
|
||||
|
||||
/* ── 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({
|
||||
data,
|
||||
}: {
|
||||
@@ -305,10 +324,12 @@ export function DocumentsPanel({
|
||||
|
||||
if (docs.length === 0) {
|
||||
return (
|
||||
<DocumentsShell count={docs.length}>
|
||||
<div className="text-center py-12 text-ink-muted">
|
||||
<div className="text-gold text-2xl mb-2" aria-hidden="true">❦</div>
|
||||
<p className="text-sm">אין מסמכים בתיק זה</p>
|
||||
</div>
|
||||
</DocumentsShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -328,6 +349,7 @@ export function DocumentsPanel({
|
||||
const pct = docs.length > 0 ? Math.round((done / docs.length) * 100) : 0;
|
||||
|
||||
return (
|
||||
<DocumentsShell count={docs.length}>
|
||||
<div className="space-y-3">
|
||||
{hasIncomplete && (
|
||||
<div className="rounded-lg border border-rule bg-parchment/40 px-4 py-3 space-y-2" dir="rtl">
|
||||
@@ -372,5 +394,6 @@ export function DocumentsPanel({
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentsShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,11 +38,7 @@ export function WorkflowTimeline({ status }: { status?: CaseStatus }) {
|
||||
const currentIdx = phaseIndexOf(status);
|
||||
|
||||
return (
|
||||
<ol className="relative space-y-4">
|
||||
<div
|
||||
className="absolute top-2 bottom-2 right-[11px] w-px bg-rule"
|
||||
aria-hidden
|
||||
/>
|
||||
<ol className="relative">
|
||||
{PHASES.map((phase, i) => {
|
||||
const state =
|
||||
currentIdx === -1 ? "pending"
|
||||
@@ -51,9 +47,9 @@ export function WorkflowTimeline({ status }: { status?: CaseStatus }) {
|
||||
: "pending";
|
||||
|
||||
const dotTone =
|
||||
state === "done" ? "bg-success border-success"
|
||||
: state === "current" ? "bg-gold border-gold shadow-[0_0_0_4px_color-mix(in_oklab,var(--color-gold)_20%,transparent)]"
|
||||
: "bg-surface border-rule";
|
||||
state === "done" ? "bg-success [box-shadow:0_0_0_1px_var(--color-success)]"
|
||||
: state === "current" ? "bg-gold [box-shadow:0_0_0_1px_var(--color-gold)]"
|
||||
: "bg-rule";
|
||||
|
||||
const labelTone =
|
||||
state === "done" ? "text-ink-soft"
|
||||
@@ -66,34 +62,55 @@ export function WorkflowTimeline({ status }: { status?: CaseStatus }) {
|
||||
: "text-ink-muted/50";
|
||||
|
||||
const PhaseIcon = phase.icon;
|
||||
const StatusIcon = status ? STATUS_ICONS[status] : null;
|
||||
const isLast = i === PHASES.length - 1;
|
||||
|
||||
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 right-[5px] top-1 inline-block w-3 h-3 rounded-full border-2 ${dotTone}`}
|
||||
className="absolute top-[26px] w-px h-[20px] bg-rule"
|
||||
style={{ insetInlineStart: "5px" }}
|
||||
aria-hidden
|
||||
/>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className={`text-sm flex items-center gap-1.5 ${labelTone}`}>
|
||||
)}
|
||||
<span
|
||||
className={`inline-block w-[11px] h-[11px] rounded-full border-2 border-surface shrink-0 ${dotTone}`}
|
||||
aria-hidden
|
||||
/>
|
||||
<div className="flex items-center gap-2 grow min-w-0">
|
||||
<span className={`text-[0.84rem] flex items-center gap-1.5 ${labelTone}`}>
|
||||
<PhaseIcon className={`w-3.5 h-3.5 shrink-0 ${iconTone}`} />
|
||||
{phase.label}
|
||||
</span>
|
||||
{state === "current" && status && (
|
||||
<span className="text-[0.72rem] text-gold-deep flex items-center gap-1">
|
||||
{StatusIcon && <StatusIcon className="w-3 h-3 shrink-0" />}
|
||||
{STATUS_LABELS[status]}
|
||||
<span className="ms-auto text-[0.72rem] text-ink-muted tabular-nums shrink-0">
|
||||
{state === "current" ? "כעת" : state === "done" ? "✓" : "—"}
|
||||
</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>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,26 +33,24 @@ export function DigestCard({
|
||||
actions?: ReactNode;
|
||||
}) {
|
||||
const linked = Boolean(digest.linked_case_law_id);
|
||||
const pending = digest.extraction_status !== "completed";
|
||||
return (
|
||||
<div className="rounded-lg border border-rule bg-surface p-4 space-y-2">
|
||||
<div className="flex items-center gap-2 text-[0.78rem] text-ink-muted flex-wrap">
|
||||
{digest.concept_tag && (
|
||||
<Badge className="bg-gold text-navy border-0">{digest.concept_tag}</Badge>
|
||||
)}
|
||||
<div className="flex flex-col rounded-lg border border-rule bg-surface shadow-sm p-4">
|
||||
{/* top row — yomon-number + date at start, concept tag pushed to end */}
|
||||
<div className="flex items-center gap-2.5 mb-2.5 flex-wrap">
|
||||
{digest.yomon_number && (
|
||||
<span className="font-mono" dir="ltr">
|
||||
<span className="text-navy font-bold text-[0.95rem]">
|
||||
יומון {digest.yomon_number}
|
||||
</span>
|
||||
)}
|
||||
{digest.digest_date && (
|
||||
<span className="text-ink-muted text-[0.78rem]">· {formatDate(digest.digest_date)}</span>
|
||||
)}
|
||||
{digest.publication && digest.publication !== "כל יום" && (
|
||||
<Badge variant="outline" className="bg-rule-soft text-ink-muted text-[0.65rem]">
|
||||
{digest.publication}
|
||||
</Badge>
|
||||
)}
|
||||
{digest.digest_date && <span>· {formatDate(digest.digest_date)}</span>}
|
||||
{digest.practice_area && (
|
||||
<span>· {practiceAreaLabel(digest.practice_area)}</span>
|
||||
)}
|
||||
{digest.digest_kind === "announcement" && (
|
||||
<Badge variant="outline" className="bg-sky-50 text-sky-700 border-sky-300 text-[0.65rem]">
|
||||
עדכון
|
||||
@@ -63,47 +61,45 @@ export function DigestCard({
|
||||
מאמר
|
||||
</Badge>
|
||||
)}
|
||||
{digest.extraction_status !== "completed" && (
|
||||
<Badge variant="outline" className="bg-rule-soft text-ink-muted text-[0.65rem]">
|
||||
{digest.extraction_status === "pending" ? "ממתין לעיבוד" : digest.extraction_status}
|
||||
</Badge>
|
||||
{digest.concept_tag && (
|
||||
<span className="ms-auto rounded-full bg-info-bg text-info text-[0.72rem] font-semibold px-2.5 py-0.5">
|
||||
{digest.concept_tag}
|
||||
</span>
|
||||
)}
|
||||
{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>
|
||||
|
||||
{/* holding — the digest headline */}
|
||||
{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}
|
||||
</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 && (
|
||||
<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}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-start gap-2 flex-wrap pt-1 border-t border-rule-soft mt-1">
|
||||
<span className="text-[0.72rem] text-ink-muted mt-1">פסק מקורי:</span>
|
||||
<span className="text-[0.82rem] text-ink font-mono flex-1 min-w-[200px]" dir="rtl">
|
||||
{digest.underlying_citation || "—"}
|
||||
{digest.practice_area && (
|
||||
<span className="text-ink-muted text-[0.72rem] mt-2">
|
||||
{practiceAreaLabel(digest.practice_area)}
|
||||
</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 && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
{digest.subject_tags.map((t) => (
|
||||
<Badge key={t} variant="outline" className="text-[0.65rem] bg-surface">
|
||||
{t}
|
||||
@@ -112,7 +108,31 @@ export function DigestCard({
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
import type { PracticeArea } from "@/lib/api/precedent-library";
|
||||
import { PRACTICE_AREAS } from "@/components/precedents/practice-area";
|
||||
import { DigestCard } from "./digest-card";
|
||||
import { DigestUploadDialog } from "./digest-upload-dialog";
|
||||
|
||||
type LinkedFilter = "all" | "linked" | "unlinked";
|
||||
|
||||
@@ -96,9 +95,6 @@ export function DigestListPanel() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="ms-auto">
|
||||
<DigestUploadDialog />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
@@ -117,6 +113,8 @@ export function DigestListPanel() {
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<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) => (
|
||||
<DigestCard
|
||||
key={d.id}
|
||||
@@ -149,6 +147,7 @@ export function DigestListPanel() {
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -24,7 +24,7 @@ import { PRACTICE_AREAS } from "@/components/precedents/practice-area";
|
||||
* via the MCP drainer ``digest_process_pending`` — so the toast tells the
|
||||
* user the digest is queued, not yet searchable.
|
||||
*/
|
||||
export function DigestUploadDialog() {
|
||||
export function DigestUploadDialog({ trigger }: { trigger?: React.ReactNode } = {}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [yomonNumber, setYomonNumber] = useState("");
|
||||
@@ -71,10 +71,12 @@ export function DigestUploadDialog() {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="bg-navy text-parchment hover:bg-navy-soft">
|
||||
{trigger ?? (
|
||||
<Button className="bg-gold text-white hover:bg-gold-deep border-transparent">
|
||||
<Upload className="w-4 h-4 me-1" />
|
||||
העלאת יומון
|
||||
</Button>
|
||||
)}
|
||||
</DialogTrigger>
|
||||
<DialogContent dir="rtl">
|
||||
<DialogHeader>
|
||||
@@ -140,7 +142,7 @@ export function DigestUploadDialog() {
|
||||
<Button
|
||||
type="submit"
|
||||
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 ? "מעלה…" : "העלה"}
|
||||
</Button>
|
||||
|
||||
@@ -88,8 +88,11 @@ export function GraphFilterPanel({
|
||||
facets?: GraphFacets;
|
||||
}) {
|
||||
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">
|
||||
<div className="text-[0.8rem] font-semibold text-navy border-b border-rule-soft pb-2">
|
||||
פילטרים וסינון
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="graph-search" className="text-xs text-ink-muted">
|
||||
חיפוש פסיקה
|
||||
@@ -250,33 +253,51 @@ export function GraphFilterPanel({
|
||||
<Separator />
|
||||
|
||||
<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
|
||||
label="נקודות-נושא"
|
||||
swatch="#a97d3a"
|
||||
checked={controls.showTopics}
|
||||
onCheckedChange={(v) => onChange({ showTopics: v })}
|
||||
/>
|
||||
<ToggleRow
|
||||
label="נקודות-תחום"
|
||||
swatch="#4a7c59"
|
||||
checked={controls.showPracticeAreas}
|
||||
onCheckedChange={(v) => onChange({ showPracticeAreas: v })}
|
||||
/>
|
||||
<ToggleRow
|
||||
label="חוסרי מחקר (פסיקה חסרה)"
|
||||
swatch="#a54242"
|
||||
checked={controls.showGaps}
|
||||
onCheckedChange={(v) => onChange({ showGaps: v })}
|
||||
/>
|
||||
<ToggleRow
|
||||
label="יומונים (כל יום)"
|
||||
swatch="#b8894a"
|
||||
checked={controls.showDigests}
|
||||
onCheckedChange={(v) => onChange({ showDigests: v })}
|
||||
/>
|
||||
<ToggleRow
|
||||
label="הלכות"
|
||||
</div>
|
||||
|
||||
{/* Stage-2 gate — halacha layer is dense, gated by default (mockup 11) */}
|
||||
<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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
@@ -350,18 +371,28 @@ function ToggleRow({
|
||||
checked,
|
||||
onCheckedChange,
|
||||
disabled,
|
||||
swatch,
|
||||
}: {
|
||||
label: string;
|
||||
checked: boolean;
|
||||
onCheckedChange: (v: boolean) => void;
|
||||
disabled?: boolean;
|
||||
swatch?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className={`text-sm ${disabled ? "text-ink-muted/50" : "text-ink"}`}>
|
||||
<div className={`flex items-center gap-2.5 ${disabled ? "opacity-55" : ""}`}>
|
||||
{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}
|
||||
</span>
|
||||
<Switch
|
||||
className="ms-auto"
|
||||
checked={checked}
|
||||
onCheckedChange={onCheckedChange}
|
||||
disabled={disabled}
|
||||
|
||||
@@ -226,7 +226,7 @@ export function GraphView() {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<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} קשרים` : "—"}
|
||||
</span>
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -248,12 +248,12 @@ export function GraphView() {
|
||||
</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} />
|
||||
|
||||
<div
|
||||
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 ? (
|
||||
<div className="grid h-full place-items-center p-6 text-center">
|
||||
@@ -311,6 +311,12 @@ export function GraphView() {
|
||||
)}
|
||||
|
||||
<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>
|
||||
|
||||
{selectedNode ? (
|
||||
@@ -340,9 +346,16 @@ function RankingPanel({
|
||||
.sort((a, b) => (b.betweenness ?? 0) - (a.betweenness ?? 0))
|
||||
.slice(0, 12);
|
||||
|
||||
const communities = new Set(
|
||||
nodes.map((n) => n.community).filter((c) => c != null),
|
||||
).size;
|
||||
|
||||
return (
|
||||
<Card className="bg-surface border-rule shadow-sm w-72 shrink-0 overflow-y-auto">
|
||||
<CardContent className="p-4">
|
||||
<Card className="bg-surface border-rule shadow-sm w-[300px] shrink-0 max-h-full overflow-y-auto">
|
||||
<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">
|
||||
<TabsList className="w-full">
|
||||
<TabsTrigger value="pagerank" className="flex-1">
|
||||
@@ -359,6 +372,12 @@ function RankingPanel({
|
||||
<RankList items={byBetweenness} metric="betweenness" onPick={onPick} />
|
||||
</TabsContent>
|
||||
</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>
|
||||
</Card>
|
||||
);
|
||||
@@ -377,18 +396,19 @@ function RankList({
|
||||
return <p className="text-ink-muted text-xs mt-3">אין נתונים.</p>;
|
||||
}
|
||||
return (
|
||||
<ol className="mt-2 space-y-1">
|
||||
<ol className="mt-2">
|
||||
{items.map((n, i) => (
|
||||
<li key={n.id}>
|
||||
<li key={n.id} className="border-b border-rule-soft last:border-b-0">
|
||||
<button
|
||||
type="button"
|
||||
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="text-ink-muted text-xs">{i + 1}.</span> {n.label}
|
||||
<span className="w-4 shrink-0 text-ink-muted text-xs tabular-nums">
|
||||
{i + 1}
|
||||
</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)}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
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 { Badge } from "@/components/ui/badge";
|
||||
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 (
|
||||
<div className="space-y-4">
|
||||
{/* Tab selector */}
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{items.map((item) => (
|
||||
<Button
|
||||
<div className="space-y-[18px]">
|
||||
{/* Type buttons — gold active (mockup 13) */}
|
||||
<div className="flex gap-2.5 flex-wrap">
|
||||
{items.map((item) => {
|
||||
const isActive = active === item.key;
|
||||
return (
|
||||
<button
|
||||
key={item.key}
|
||||
size="sm"
|
||||
variant={active === item.key ? "default" : "outline"}
|
||||
onClick={() => { setActive(item.key); setPreview(false); }}
|
||||
className="text-xs"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setActive(item.key);
|
||||
setPreview(false);
|
||||
}}
|
||||
className={
|
||||
isActive
|
||||
? "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"
|
||||
}
|
||||
>
|
||||
{item.label}
|
||||
{item.isOverride && (
|
||||
<Badge variant="secondary" className="text-[9px] mr-1.5 px-1">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className={`text-[9px] ms-1.5 px-1 ${isActive ? "bg-white/20 text-white" : ""}`}
|
||||
>
|
||||
מותאם
|
||||
</Badge>
|
||||
)}
|
||||
</Button>
|
||||
))}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Editor / Preview */}
|
||||
{current && (
|
||||
<Card className="border-rule">
|
||||
<CardContent className="px-5 py-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-sm font-semibold text-navy">{current.label}</h3>
|
||||
{CHECKLIST_APPLIES[current.key] && (
|
||||
<p className="text-[0.72rem] text-ink-muted mt-0.5">
|
||||
חל על: {CHECKLIST_APPLIES[current.key]}
|
||||
</p>
|
||||
)}
|
||||
{/* "חל על:" 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 && (
|
||||
<Card className="border-rule shadow-sm overflow-hidden p-0 gap-0">
|
||||
<div className="flex items-center gap-2.5 px-[18px] py-3.5 border-b border-rule-soft">
|
||||
<h2 className="text-[0.95rem] font-semibold text-navy m-0">
|
||||
צ׳קליסט תוכן — {current.label}
|
||||
</h2>
|
||||
<span
|
||||
className={`ms-auto rounded-full text-xs font-semibold px-2.5 py-0.5 ${
|
||||
current.isOverride
|
||||
? "bg-gold-wash text-gold-deep border border-rule"
|
||||
: "bg-info-bg text-info"
|
||||
}`}
|
||||
>
|
||||
{current.isOverride ? "מותאם" : "ידני"}
|
||||
</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => setPreview(!preview)}
|
||||
className="text-xs shrink-0"
|
||||
className="text-xs shrink-0 h-7"
|
||||
>
|
||||
{preview ? <EyeOff className="w-3.5 h-3.5 ml-1" /> : <Eye className="w-3.5 h-3.5 ml-1" />}
|
||||
{preview ? (
|
||||
<EyeOff className="w-3.5 h-3.5 ms-1" />
|
||||
) : (
|
||||
<Eye className="w-3.5 h-3.5 ms-1" />
|
||||
)}
|
||||
{preview ? "עריכה" : "תצוגה מקדימה"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{preview ? (
|
||||
<div className="border border-rule rounded-md p-4 bg-sand-soft/30 max-h-[500px] overflow-y-auto">
|
||||
<div className="p-[18px] bg-parchment max-h-[500px] overflow-y-auto border-b border-rule-soft">
|
||||
<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"
|
||||
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">
|
||||
<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" />}
|
||||
<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 size="sm" variant="outline" disabled={reset.isPending} onClick={handleReset}>
|
||||
<RotateCcw className="w-3 h-3 ml-1" />
|
||||
איפוס לברירת מחדל
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={reset.isPending}
|
||||
onClick={handleReset}
|
||||
className="border-rule text-navy"
|
||||
>
|
||||
<RotateCcw className="w-3.5 h-3.5 ms-1" />
|
||||
אפס לברירת-מחדל
|
||||
</Button>
|
||||
)}
|
||||
<Badge
|
||||
variant={current.isOverride ? "default" : "secondary"}
|
||||
className="text-[10px] mr-auto"
|
||||
>
|
||||
{current.isOverride ? "מותאם" : "ברירת מחדל"}
|
||||
</Badge>
|
||||
<span className="ms-auto text-[0.78rem] text-ink-muted tabular-nums">
|
||||
{itemCount} פריטים
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
useDeleteMissingPrecedent,
|
||||
CITED_BY_PARTY_LABELS,
|
||||
STATUS_LABELS,
|
||||
type CitedByParty,
|
||||
type MissingPrecedent,
|
||||
type MissingPrecedentStatus,
|
||||
} 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 }) {
|
||||
const variants: Record<MissingPrecedentStatus, string> = {
|
||||
open: "bg-gold-wash text-gold-deep border-gold/40",
|
||||
uploaded: "bg-rule-soft text-ink-muted border-rule",
|
||||
closed: "bg-emerald-50 text-emerald-800 border-emerald-300/60",
|
||||
irrelevant: "bg-rule-soft text-ink-muted border-rule line-through",
|
||||
open: "bg-warn-bg text-warn border-transparent",
|
||||
uploaded: "bg-info-bg text-info border-transparent",
|
||||
closed: "bg-success-bg text-success border-transparent",
|
||||
irrelevant: "bg-rule-soft text-ink-muted border-transparent",
|
||||
};
|
||||
return (
|
||||
<Badge variant="outline" className={variants[status]}>
|
||||
<Badge variant="outline" className={`rounded-full whitespace-nowrap ${variants[status]}`}>
|
||||
{STATUS_LABELS[status]}
|
||||
</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 }) {
|
||||
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">
|
||||
<Table>
|
||||
<TableHeader className="bg-rule-soft/60">
|
||||
<TableRow className="border-rule">
|
||||
<TableHead className="text-navy text-right">פסיקה</TableHead>
|
||||
<TableHead className="text-navy text-right">נושא</TableHead>
|
||||
<TableHead className="text-navy text-right">תיק</TableHead>
|
||||
<TableHead className="text-navy text-right">צד מצטט</TableHead>
|
||||
<TableHead className="text-navy text-right">סטטוס</TableHead>
|
||||
<TableHead className="text-navy text-right">נוצר</TableHead>
|
||||
<TableHeader className="bg-parchment">
|
||||
<TableRow className="border-rule hover:bg-transparent">
|
||||
<TableHead className="text-ink-muted text-right font-medium text-xs">פסיקה</TableHead>
|
||||
<TableHead className="text-ink-muted text-right font-medium text-xs">נושא</TableHead>
|
||||
<TableHead className="text-ink-muted text-right font-medium text-xs">תיק</TableHead>
|
||||
<TableHead className="text-ink-muted text-right font-medium text-xs">צוטט ע״י</TableHead>
|
||||
<TableHead className="text-ink-muted text-right font-medium text-xs">סטטוס</TableHead>
|
||||
<TableHead className="text-ink-muted text-right font-medium text-xs">נוצר</TableHead>
|
||||
<TableHead className="text-navy" />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -128,7 +148,7 @@ export function MissingPrecedentsTable({ status, caseNumber, legalTopic }: Props
|
||||
onClick={() => setOpenId(mp.id)}
|
||||
>
|
||||
<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(" ")}
|
||||
</div>
|
||||
<div className="text-[0.72rem] text-ink-muted truncate" dir="rtl">
|
||||
@@ -153,11 +173,9 @@ export function MissingPrecedentsTable({ status, caseNumber, legalTopic }: Props
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-ink">
|
||||
{mp.cited_by_party
|
||||
? CITED_BY_PARTY_LABELS[mp.cited_by_party]
|
||||
: "—"}
|
||||
<SourceChip party={mp.cited_by_party} />
|
||||
{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}
|
||||
</div>
|
||||
) : null}
|
||||
@@ -165,7 +183,7 @@ export function MissingPrecedentsTable({ status, caseNumber, legalTopic }: Props
|
||||
<TableCell>
|
||||
<StatusBadge status={mp.status} />
|
||||
{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}
|
||||
</div>
|
||||
) : null}
|
||||
@@ -174,7 +192,27 @@ export function MissingPrecedentsTable({ status, caseNumber, legalTopic }: Props
|
||||
{formatDate(mp.created_at)}
|
||||
</TableCell>
|
||||
<TableCell className="text-end">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
{mp.status === "open" ? (
|
||||
/* gold "העלה והשלם" CTA (mockup 09 `.btn`) */
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setOpenId(mp.id);
|
||||
}}
|
||||
className="h-7 bg-gold text-white hover:bg-gold-deep border-transparent text-[0.78rem] font-semibold"
|
||||
>
|
||||
<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"
|
||||
@@ -182,14 +220,11 @@ export function MissingPrecedentsTable({ status, caseNumber, legalTopic }: Props
|
||||
e.stopPropagation();
|
||||
setOpenId(mp.id);
|
||||
}}
|
||||
title={mp.status === "open" ? "העלאה" : "פרטים"}
|
||||
title="פרטים"
|
||||
>
|
||||
{mp.status === "open" ? (
|
||||
<Upload className="w-4 h-4" />
|
||||
) : (
|
||||
<Pencil className="w-4 h-4" />
|
||||
)}
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
|
||||
@@ -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" }> }) {
|
||||
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">
|
||||
<Badge className="bg-gold text-navy border-0">הלכה</Badge>
|
||||
<span className="font-mono" dir="ltr">{hit.case_number}</span>
|
||||
{hit.court && <span>· {hit.court}</span>}
|
||||
{hit.decision_date && <span>· {formatDate(hit.decision_date)}</span>}
|
||||
{hit.precedent_level && <span>· {hit.precedent_level}</span>}
|
||||
{/* PRE-3/PRE-5 (INV-IA5): the derived authority (binding/persuasive)
|
||||
rides on the wire but was dropped here — render it as in the review
|
||||
tab so search shows the same provenance everywhere. */}
|
||||
<Badge className="rounded bg-gold text-white border-0 text-[0.68rem] font-bold tracking-wide">
|
||||
הלכה
|
||||
</Badge>
|
||||
<span className="text-navy font-semibold" dir="ltr">{hit.case_number}</span>
|
||||
{(hit.court || hit.decision_date || hit.precedent_level) && (
|
||||
<span className="text-ink-muted">
|
||||
{hit.court ? `· ${hit.court}` : ""}
|
||||
{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} />
|
||||
<span className="ms-auto tabular-nums">דירוג {hit.score.toFixed(2)}</span>
|
||||
<ScoreChip score={hit.score} onWash />
|
||||
</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}
|
||||
</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"
|
||||
>
|
||||
“{hit.supporting_quote}”
|
||||
{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>
|
||||
{hit.subject_tags?.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{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}
|
||||
</Badge>
|
||||
))}
|
||||
@@ -61,16 +85,24 @@ function HalachaCard({ hit }: { hit: Extract<SearchHit, { type: "halacha" }> })
|
||||
|
||||
function PassageCard({ hit }: { hit: Extract<SearchHit, { type: "passage" }> }) {
|
||||
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">
|
||||
<Badge variant="outline" className="bg-info-bg text-info border-transparent">קטע</Badge>
|
||||
<span className="font-mono" dir="ltr">{hit.case_number}</span>
|
||||
{hit.court && <span>· {hit.court}</span>}
|
||||
{hit.decision_date && <span>· {formatDate(hit.decision_date)}</span>}
|
||||
<span className="text-[0.7rem]">· {hit.section_type}</span>
|
||||
<span className="ms-auto tabular-nums">דירוג {hit.score.toFixed(2)}</span>
|
||||
<Badge variant="outline" className="rounded bg-info-bg text-info border-transparent text-[0.68rem] font-bold tracking-wide">
|
||||
קטע
|
||||
</Badge>
|
||||
<span className="text-navy font-semibold" dir="ltr">{hit.case_number}</span>
|
||||
{(hit.court || hit.decision_date) && (
|
||||
<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>
|
||||
<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.length > 600 && <span>…</span>}
|
||||
</p>
|
||||
@@ -98,18 +130,26 @@ export function LibrarySearchPanel() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<form onSubmit={onSubmit} className="flex items-end gap-3 flex-wrap">
|
||||
<div className="flex-1 min-w-[300px]">
|
||||
<label className="text-[0.78rem] text-ink-muted">שאילתת חיפוש</label>
|
||||
<div className="space-y-6">
|
||||
{/* search panel — boxed surface with query row + two-up filter row +
|
||||
controls strip (checkbox start, gold CTA end) per mockup 07. */}
|
||||
<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)}
|
||||
placeholder="השבחה אובייקטיבית" dir="rtl" />
|
||||
</div>
|
||||
<div className="min-w-[180px]">
|
||||
<label className="text-[0.78rem] text-ink-muted">תחום</label>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3.5">
|
||||
<div>
|
||||
<label className="block text-[0.78rem] text-ink-muted font-medium mb-1.5">תחום</label>
|
||||
<Select value={practiceArea || "_all"}
|
||||
onValueChange={(v) => setPracticeArea(v === "_all" ? "" : v as PracticeArea)}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectTrigger className="w-full"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="_all">הכל</SelectItem>
|
||||
{PRACTICE_AREAS.map((a) => (
|
||||
@@ -118,11 +158,11 @@ export function LibrarySearchPanel() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="min-w-[170px]">
|
||||
<label className="text-[0.78rem] text-ink-muted">רמת תקדים</label>
|
||||
<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><SelectValue /></SelectTrigger>
|
||||
<SelectTrigger className="w-full"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="_all">הכל</SelectItem>
|
||||
{PRECEDENT_LEVELS.map((l) => (
|
||||
@@ -131,18 +171,20 @@ export function LibrarySearchPanel() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Button type="submit" className="bg-navy text-parchment hover:bg-navy-soft">
|
||||
</div>
|
||||
<div className="flex items-center gap-4 flex-wrap pt-1">
|
||||
<label className="flex items-center gap-2 cursor-pointer text-[0.85rem] text-ink-soft">
|
||||
<input type="checkbox" className="w-[15px] h-[15px] accent-gold" checked={includeHalachot}
|
||||
onChange={(e) => setIncludeHalachot(e.target.checked)} />
|
||||
כלול הלכות
|
||||
</label>
|
||||
<Button type="submit" className="ms-auto bg-gold text-white hover:bg-gold-deep border-transparent">
|
||||
<Search className="w-4 h-4 me-1" />
|
||||
חפש
|
||||
</Button>
|
||||
</div>
|
||||
</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() ? (
|
||||
<div className="text-center text-ink-muted py-12">
|
||||
הקלד שאילתא כדי לחפש בקורפוס. החיפוש סמנטי — לא טקסטואלי.
|
||||
@@ -160,11 +202,13 @@ export function LibrarySearchPanel() {
|
||||
לא נמצאו תוצאות. נסה ניסוח אחר או הסר פילטרים.
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<p className="text-[0.78rem] 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">
|
||||
<div className="space-y-3.5">
|
||||
<p className="text-[0.82rem] text-ink-muted flex items-center gap-2">
|
||||
<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 aria-hidden>·</span>
|
||||
<span className="tabular-nums">{data.count} תוצאות</span>
|
||||
</p>
|
||||
{data.items.map((hit, i) =>
|
||||
|
||||
@@ -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 ─────────────────────────────────────────
|
||||
|
||||
type SectionProps = {
|
||||
@@ -189,28 +135,68 @@ type SectionProps = {
|
||||
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) {
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-navy text-sm font-semibold">
|
||||
החלטות קשורות{related.length > 0 ? ` (${related.length})` : ""}
|
||||
<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 gap-2">
|
||||
<h3 className="text-navy text-[0.92rem] font-semibold m-0">
|
||||
ציטוטים מקושרים{related.length > 0 ? ` (${related.length})` : ""}
|
||||
</h3>
|
||||
<Button variant="outline" size="sm" onClick={() => setDialogOpen(true)}>
|
||||
<Link2 className="w-3.5 h-3.5 me-1" /> קשר החלטה
|
||||
<Button
|
||||
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>
|
||||
</div>
|
||||
|
||||
{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) => (
|
||||
<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>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
<LinkDialog
|
||||
@@ -222,3 +208,24 @@ export function RelatedCasesSection({ caseId, related }: SectionProps) {
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
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({
|
||||
label,
|
||||
@@ -16,15 +19,13 @@ function KPICard({
|
||||
}) {
|
||||
return (
|
||||
<Card className="bg-surface border-rule shadow-sm">
|
||||
<CardContent className="px-5 py-4 flex flex-col gap-0.5">
|
||||
<span className="text-[0.72rem] uppercase tracking-[0.08em] text-ink-muted">
|
||||
{label}
|
||||
</span>
|
||||
<span className="font-display text-[2rem] font-black leading-none text-navy">
|
||||
<CardContent className="px-[18px] py-4 flex flex-col">
|
||||
<span className="font-display text-[1.85rem] font-bold leading-[1.1] text-navy tabular-nums">
|
||||
{value}
|
||||
</span>
|
||||
<span className="text-[0.81rem] text-ink-soft mt-1">{label}</span>
|
||||
{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>
|
||||
</Card>
|
||||
@@ -33,6 +34,7 @@ function KPICard({
|
||||
|
||||
export function StyleReportPanel() {
|
||||
const { data, isPending, error } = useStyleReport();
|
||||
const curator = useCuratorStats();
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
@@ -63,14 +65,13 @@ export function StyleReportPanel() {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Headline */}
|
||||
<Card className="bg-gold-wash border-gold/40 shadow-sm">
|
||||
<CardContent className="px-6 py-4">
|
||||
<p className="font-display text-gold-deep text-lg font-semibold leading-snug">
|
||||
★ {c.headline}
|
||||
{/* Headline banner — gold-wash, ★ aligned to start (mockup 12) */}
|
||||
<div className="flex items-start gap-3 rounded-lg border border-gold bg-gold-wash px-5 py-4 shadow-sm">
|
||||
<span className="text-gold-deep text-xl leading-tight shrink-0">★</span>
|
||||
<p className="text-ink-soft text-[0.95rem] leading-relaxed m-0">
|
||||
{c.headline}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* KPIs */}
|
||||
<div className="grid gap-4 grid-cols-2 lg:grid-cols-4">
|
||||
@@ -121,23 +122,22 @@ export function StyleReportPanel() {
|
||||
{data.anatomy.sections.length === 0 ? (
|
||||
<p className="text-ink-muted text-sm">אין נתונים על מבנה</p>
|
||||
) : (
|
||||
<ul className="space-y-2.5">
|
||||
{data.anatomy.sections.map((s) => {
|
||||
<ul className="space-y-3.5">
|
||||
{data.anatomy.sections.map((s, i) => {
|
||||
const pct = Math.round(s.pct * 100);
|
||||
const fill = ANATOMY_COLORS[i % ANATOMY_COLORS.length];
|
||||
return (
|
||||
<li key={s.type} className="space-y-1">
|
||||
<div className="flex items-center justify-between text-[0.78rem]">
|
||||
<span className="text-ink-soft font-medium">
|
||||
{s.label}
|
||||
</span>
|
||||
<span className="text-ink-muted tabular-nums">
|
||||
<li key={s.type} className="space-y-1.5">
|
||||
<div className="flex items-center justify-between text-[0.81rem]">
|
||||
<span className="text-ink-soft">{s.label}</span>
|
||||
<span className="text-navy font-semibold tabular-nums">
|
||||
{pct}% · {s.avg_chars.toLocaleString()} תווים
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2 rounded bg-rule-soft overflow-hidden">
|
||||
<div className="h-2.5 rounded-full bg-rule-soft overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-l from-gold to-gold-deep"
|
||||
style={{ width: `${pct}%` }}
|
||||
className="h-full rounded-full"
|
||||
style={{ width: `${pct}%`, backgroundColor: fill }}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
@@ -149,42 +149,39 @@ export function StyleReportPanel() {
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Signature phrases */}
|
||||
{/* Signature phrases + curator stat — two columns (mockup 12) */}
|
||||
<div className="grid gap-6 lg:grid-cols-2 items-start">
|
||||
<Card className="bg-surface border-rule shadow-sm">
|
||||
<CardContent className="px-6 py-5">
|
||||
<h3 className="text-navy text-lg mb-1">ביטויי חתימה</h3>
|
||||
{data.signature_phrases.headline && (
|
||||
<p className="text-[0.78rem] text-gold-deep mb-4">
|
||||
<p className="text-[0.78rem] text-gold-deep mb-3">
|
||||
{data.signature_phrases.headline}
|
||||
</p>
|
||||
)}
|
||||
{data.signature_phrases.items.length === 0 ? (
|
||||
<p className="text-ink-muted text-sm">אין ביטויים שחולצו עדיין</p>
|
||||
) : (
|
||||
<ol className="space-y-2">
|
||||
<ol>
|
||||
{data.signature_phrases.items.slice(0, 12).map((p, i) => (
|
||||
<li
|
||||
key={`${p.type}-${i}`}
|
||||
className="flex items-start gap-3 rounded border border-rule bg-parchment/40 px-3 py-2"
|
||||
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 className="w-5 shrink-0 text-gold-deep font-bold tabular-nums text-sm">
|
||||
{i + 1}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-ink leading-relaxed text-sm">{p.text}</p>
|
||||
<p className="text-ink-soft leading-relaxed text-[0.85rem] m-0">
|
||||
{p.text}
|
||||
</p>
|
||||
{p.context && (
|
||||
<p className="text-[0.7rem] text-ink-muted mt-0.5">
|
||||
<p className="text-[0.7rem] text-ink-muted mt-0.5 m-0">
|
||||
{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
|
||||
"
|
||||
>
|
||||
<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>
|
||||
@@ -193,6 +190,51 @@ export function StyleReportPanel() {
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user