Files
legal-ai/web-ui/src/components/training/style-report-panel.tsx
Chaim fb1f73fa25 Phase 5: secondary screens (diagnostics, skills, training)
Port the remaining read views from the vanilla UI to Next.js:

- /diagnostics — system health snapshot (DB connected, table counts,
  active tasks, failed and stuck documents). Uses the existing
  /api/system/diagnostics payload with a 10s refetchInterval so the
  page self-updates while the user watches.
- /skills — Paperclip skill inventory with sync status (DB-only,
  disk-only, synced, not-synced) as a card grid driven by
  /api/admin/skills.
- /training — Dafna's style portrait as three tabs on one page:
  * Report: corpus KPIs + CSS conic-gradient subject donut
    (SubjectDonut ported from index.html renderHero) + horizontal
    anatomy bars + top-12 signature phrases.
  * Corpus: TanStack Table of style_corpus rows with an inline
    delete mutation (useDeleteCorpusEntry invalidates both the
    corpus list and the style report so KPIs update).
  * Compare: two-decision selector backed by /api/training/compare,
    side-by-side panels plus shared / only-A / only-B pattern
    lists.

New API modules: lib/api/system.ts, lib/api/skills.ts,
lib/api/training.ts. All three use TanStack Query with staleTime
profiles tuned per endpoint (10s for diagnostics, 30s for skills,
60s for training reports).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 17:33:33 +00:00

196 lines
6.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 } from "@/lib/api/training";
function KPICard({
label,
value,
caption,
}: {
label: string;
value: string;
caption?: string;
}) {
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">
{value}
</span>
{caption && (
<span className="text-[0.78rem] text-ink-muted mt-1">{caption}</span>
)}
</CardContent>
</Card>
);
}
export function StyleReportPanel() {
const { data, isPending, error } = useStyleReport();
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 */}
<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}
</p>
</CardContent>
</Card>
{/* 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`}
/>
<KPICard
label="דפוסי סגנון"
value={String(data.signature_phrases.items.length)}
caption={`מתוך ${data.contribution.total_patterns} שחולצו`}
/>
</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-2.5">
{data.anatomy.sections.map((s) => {
const pct = Math.round(s.pct * 100);
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">
{pct}% · {s.avg_chars.toLocaleString()} תווים
</span>
</div>
<div className="h-2 rounded bg-rule-soft overflow-hidden">
<div
className="h-full bg-gradient-to-l from-gold to-gold-deep"
style={{ width: `${pct}%` }}
/>
</div>
</li>
);
})}
</ul>
)}
</CardContent>
</Card>
</div>
{/* Signature phrases */}
<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">
{data.signature_phrases.headline}
</p>
)}
{data.signature_phrases.items.length === 0 ? (
<p className="text-ink-muted text-sm">אין ביטויים שחולצו עדיין</p>
) : (
<ol className="space-y-2">
{data.signature_phrases.items.slice(0, 12).map((p, i) => (
<li
key={`${p.type}-${i}`}
className="flex items-start gap-3 rounded border border-rule bg-parchment/40 px-3 py-2"
>
<span className="text-[0.7rem] text-ink-muted tabular-nums shrink-0 mt-0.5">
#{i + 1}
</span>
<div className="flex-1 min-w-0">
<p className="text-ink leading-relaxed text-sm">{p.text}</p>
{p.context && (
<p className="text-[0.7rem] text-ink-muted mt-0.5">
{p.context}
</p>
)}
</div>
<span
className="
shrink-0 text-[0.72rem] rounded-full
bg-gold-wash text-gold-deep border border-gold/40
px-2 py-0.5 tabular-nums
"
>
×{p.frequency}
</span>
</li>
))}
</ol>
)}
</CardContent>
</Card>
</div>
);
}