Files
legal-ai/web-ui/src/app/skills/page.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

129 lines
4.8 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 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 { Skeleton } from "@/components/ui/skeleton";
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 statusBadge(s: Skill) {
if (s.not_in_db) {
return <Badge variant="outline" className="bg-warn-bg text-warn border-warn/40">לא סונכרן</Badge>;
}
if (s.db_markdown_chars > 0 && s.disk_exists) {
return <Badge variant="outline" className="bg-success-bg text-success border-success/40">מסונכרן</Badge>;
}
if (s.db_markdown_chars > 0) {
return <Badge variant="outline" className="bg-info-bg text-info border-info/40">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>
<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>
);
}
export default function SkillsPage() {
const { data, isPending, error } = useSkills();
return (
<AppShell>
<section className="space-y-6">
<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">מיומנויות Paperclip</h1>
<p className="text-ink-muted text-sm mt-1 max-w-2xl">
רשימת ה-skills המותקנים במערכת Paperclip ומצב הסנכרון שלהם בין ה-DB
לדיסק.
</p>
</header>
<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">
{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>
) : data?.length === 0 ? (
<Card className="bg-surface border-rule">
<CardContent className="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>
)}
</section>
</AppShell>
);
}