תיקוני-מבנה ללולאת-הלמידה (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>
241 lines
9.6 KiB
TypeScript
241 lines
9.6 KiB
TypeScript
"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>
|
||
);
|
||
}
|