Files
legal-ai/web-ui/src/components/training/style-report-panel.tsx
Chaim be774ab87e
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 9s
fix(learning): honest 3-channel curator card + capture curator findings (source='curator')
תיקוני-מבנה ללולאת-הלמידה (TaskMaster #157):

1. כרטיס ה-curator (/training טאב "אוצֵר") ספר decision_lessons WHERE source='curator'
   — ערך שאף מסלול-קוד לא כתב → תמיד 0. עוצב-מחדש (דרך שער-העיצוב Claude Design, אושר)
   להציג ביושר את שלושת ערוצי-ההזנה לכותב: דיסטילציה→appeal_type_rules (180, זורם),
   פאנל→decision_lessons (81, ממתין), אוצֵר→source='curator'. get_curator_stats שוכתב.

2. drift ספ↔סוכן: 07-learning.md §1.1 + INV-LRN3 קבעו שהאוצֵר רושם ממצאים כ-decision_lesson
   source='curator', אך הסוכן כתב comments בלבד — הממצאים האיכותיים אבדו. נוסף כלי-MCP
   record_curator_findings + §A.5b ב-hermes-curator.md (read-only נשמר; הצעה מגודרת-שער).

3. get_recent_decision_lessons(limit=15) חתך בשקט — נוסף WARN על מה שנחתך (חוקה §6);
   הפתרון האמיתי = סינתזת-לקחים (TaskMaster #158).

Invariants: מקיים INV-LRN1/G10 (שער-יו"ר), INV-LRN3 (לכידה מובנית), INV-IA2/IA5 (מקור-אמת
יחיד), G12 (leak-guard עובר), G2 (אין מסלול מקביל — אותם מאגרים). פער-מימוש פתוח מתועד:
§A לא רץ אוטומטית על mark-final (pipeline-wake exits) → נדחה לתכנון נפרד.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 21:20:05 +00:00

241 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { Card, CardContent } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { SubjectDonut } from "@/components/training/subject-donut";
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,
value,
caption,
}: {
label: string;
value: string;
caption?: string;
}) {
return (
<Card className="bg-surface border-rule shadow-sm">
<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.72rem] text-ink-muted mt-0.5">{caption}</span>
)}
</CardContent>
</Card>
);
}
export function StyleReportPanel() {
const { data, isPending, error } = useStyleReport();
const curator = useCuratorStats();
if (error) {
return (
<Card className="bg-danger-bg border-danger/40">
<CardContent className="px-6 py-5 text-center text-danger">
{error.message}
</CardContent>
</Card>
);
}
if (isPending || !data) {
return (
<div className="space-y-4">
<Skeleton className="h-24 w-full" />
<Skeleton className="h-40 w-full" />
</div>
);
}
const c = data.corpus;
const dateRange =
c.date_range[0] && c.date_range[1]
? `${c.date_range[0]} ${c.date_range[1]}`
: undefined;
const total = c.decision_count;
const totalSubjects = c.subject_distribution.reduce((a, b) => a + b.count, 0);
return (
<div className="space-y-6">
{/* 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>
</div>
{/* KPIs */}
<div className="grid gap-4 grid-cols-2 lg:grid-cols-4">
<KPICard label="החלטות בקורפוס" value={String(c.decision_count)} />
<KPICard
label="סך תווים"
value={`${(c.total_chars / 1000).toFixed(0)}K`}
/>
<KPICard
label="ממוצע להחלטה"
value={`${(c.avg_chars / 1000).toFixed(1)}K`}
/>
{/* LRN-4 (INV-IA5): items.length (style_patterns freq>0) and
contribution.total_patterns are independent queries — the old
"X מתוך Y" framed a false subset. Show the surfaced count alone. */}
<KPICard
label="דפוסי סגנון"
value={String(data.signature_phrases.items.length)}
caption="דפוסים חוזרים שזוהו"
/>
</div>
{/* Subjects + anatomy */}
<div className="grid gap-6 lg:grid-cols-2">
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5">
<h3 className="text-navy text-lg mb-4">פיזור נושאים</h3>
<SubjectDonut
segments={c.subject_distribution}
total={totalSubjects}
/>
{dateRange && (
<p className="text-[0.72rem] text-ink-muted mt-4">
טווח תאריכים: {dateRange}
</p>
)}
</CardContent>
</Card>
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5">
<h3 className="text-navy text-lg mb-1">אנטומיה של החלטה ממוצעת</h3>
{data.anatomy.headline && (
<p className="text-[0.78rem] text-gold-deep mb-4">
{data.anatomy.headline}
</p>
)}
{data.anatomy.sections.length === 0 ? (
<p className="text-ink-muted text-sm">אין נתונים על מבנה</p>
) : (
<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.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.5 rounded-full bg-rule-soft overflow-hidden">
<div
className="h-full rounded-full"
style={{ width: `${pct}%`, backgroundColor: fill }}
/>
</div>
</li>
);
})}
</ul>
)}
</CardContent>
</Card>
</div>
{/* 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-3">
{data.signature_phrases.headline}
</p>
)}
{data.signature_phrases.items.length === 0 ? (
<p className="text-ink-muted text-sm">אין ביטויים שחולצו עדיין</p>
) : (
<ol>
{data.signature_phrases.items.slice(0, 12).map((p, i) => (
<li
key={`${p.type}-${i}`}
className="flex items-baseline gap-2.5 py-2.5 border-b border-rule-soft last:border-b-0"
>
<span className="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-soft leading-relaxed text-[0.85rem] m-0">
{p.text}
</p>
{p.context && (
<p className="text-[0.7rem] text-ink-muted mt-0.5 m-0">
{p.context}
</p>
)}
</div>
<span className="shrink-0 text-[0.72rem] font-semibold rounded-full bg-gold-wash text-gold-deep border border-rule px-2.5 py-0.5 tabular-nums whitespace-nowrap">
×{p.frequency}
</span>
</li>
))}
</ol>
)}
</CardContent>
</Card>
{/* Curator — surfaced style findings (INV-LRN1 writer gate) */}
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5 flex flex-col gap-4">
<h3 className="text-navy text-lg m-0">אוצֵר ממצאי-סגנון</h3>
<div className="flex items-center gap-4 rounded-lg border border-success bg-success-bg px-[18px] py-3.5">
<span className="text-success font-bold text-[1.75rem] leading-none tabular-nums">
{curator.data ? curator.data.channels.curator.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.channels.curator.total -
curator.data.channels.curator.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.channels.curator.total : "—"}
</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>
);
}