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>
This commit is contained in:
72
web-ui/src/components/training/subject-donut.tsx
Normal file
72
web-ui/src/components/training/subject-donut.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
"use client";
|
||||
|
||||
/*
|
||||
* Corpus subject-distribution donut.
|
||||
*
|
||||
* Pure CSS conic-gradient — same recipe as the cases StatusDonut, but
|
||||
* uses a palette-of-gold instead of a status-tone palette. Ported from
|
||||
* legal-ai/web/static/index.html `renderHero`.
|
||||
*/
|
||||
|
||||
const DONUT_COLORS = [
|
||||
"var(--color-navy)",
|
||||
"var(--color-gold)",
|
||||
"var(--color-info)",
|
||||
"var(--color-warn)",
|
||||
"var(--color-success)",
|
||||
"var(--color-ink-muted)",
|
||||
"var(--color-gold-deep)",
|
||||
];
|
||||
|
||||
export function SubjectDonut({
|
||||
segments,
|
||||
total,
|
||||
}: {
|
||||
segments: Array<{ label: string; count: number }>;
|
||||
total: number;
|
||||
}) {
|
||||
let pct = 0;
|
||||
const parts = segments.map((s, i) => {
|
||||
const start = total === 0 ? 0 : (pct / total) * 360;
|
||||
pct += s.count;
|
||||
const end = total === 0 ? 360 : (pct / total) * 360;
|
||||
return { ...s, start, end, color: DONUT_COLORS[i % DONUT_COLORS.length] };
|
||||
});
|
||||
|
||||
const background =
|
||||
total === 0
|
||||
? "conic-gradient(var(--color-rule-soft) 0deg 360deg)"
|
||||
: `conic-gradient(${parts
|
||||
.map((p) => `${p.color} ${p.start}deg ${p.end}deg`)
|
||||
.join(", ")})`;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-6">
|
||||
<div
|
||||
className="relative w-[140px] h-[140px] rounded-full shadow-sm shrink-0"
|
||||
style={{ background }}
|
||||
aria-label="פיזור נושאים בקורפוס"
|
||||
>
|
||||
<div className="absolute inset-[18px] bg-surface rounded-full flex flex-col items-center justify-center">
|
||||
<span className="font-display text-2xl font-black text-navy leading-none">
|
||||
{total}
|
||||
</span>
|
||||
<span className="text-[0.7rem] text-ink-muted mt-1">החלטות</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul className="flex flex-col gap-1.5 text-sm min-w-0">
|
||||
{parts.map((p) => (
|
||||
<li key={p.label} className="flex items-center gap-2">
|
||||
<span
|
||||
className="inline-block w-2.5 h-2.5 rounded-full shrink-0"
|
||||
style={{ background: p.color }}
|
||||
/>
|
||||
<span className="text-ink-soft truncate">{p.label}</span>
|
||||
<span className="text-ink-muted tabular-nums ms-1">{p.count}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user