Files
legal-ai/web-ui/src/components/training/corpus-panel.tsx
Chaim cbe9d60901 Phase 6: polish — error boundaries, a11y, smoke test doc
Close out the read-only surface before cutover with three families of
small fixes that the previous phases left unfinished:

- Error boundaries: add src/app/error.tsx (route-segment), global-error.tsx
  (root crash fallback with its own minimal html/body — no Providers
  dependency since those may be the thing that crashed), and not-found.tsx
  for a Hebrew 404 instead of the default Next page.

- Accessibility: wire usePathname() into AppShell so the current nav item
  gets aria-current="page" and a gold underline. Add aria-label + aria-hidden
  on the icon-only buttons that Phase 5 left text-less (corpus trash,
  parties-field Plus). Nav gets an aria-label of its own.

- Metadata template: title on each route now reads "X · עוזר משפטי" via
  the layout.tsx title.template. Description localized to Jerusalem.

- README: full E2E smoke test checklist covering all 9 screens, plus a
  backend contract table so future phases know which hook wraps which
  endpoint. Documents the known Gitea→Coolify webhook issue.

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

141 lines
4.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 { Trash2 } from "lucide-react";
import { toast } from "sonner";
import {
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
} from "@/components/ui/table";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import { useCorpus, useDeleteCorpusEntry, type CorpusDecision } from "@/lib/api/training";
/*
* Corpus tab: table of all decisions currently in the style corpus, with a
* single destructive action (remove from corpus). Uses browser confirm() for
* the confirmation — a full shadcn AlertDialog would be overkill for an
* admin-only destructive action with a server-side safety net.
*/
function formatChars(n: number) {
return `${(n / 1000).toFixed(1)}K`;
}
function formatDate(iso: string) {
if (!iso) return "—";
try {
return new Date(iso).toLocaleDateString("he-IL");
} catch {
return iso;
}
}
function Row({ item }: { item: CorpusDecision }) {
const del = useDeleteCorpusEntry();
const onDelete = async () => {
if (!window.confirm(`למחוק את החלטה ${item.decision_number} מהקורפוס?`)) return;
try {
await del.mutateAsync(item.id);
toast.success("נמחק מהקורפוס");
} catch (e) {
toast.error(e instanceof Error ? e.message : "שגיאה במחיקה");
}
};
return (
<TableRow className="border-rule hover:bg-gold-wash/30">
<TableCell className="font-semibold text-navy tabular-nums">
{item.decision_number || "—"}
</TableCell>
<TableCell className="text-ink-muted tabular-nums">
{formatDate(item.decision_date)}
</TableCell>
<TableCell>
{item.subject_categories.length === 0 ? (
<span className="text-ink-light"></span>
) : (
<div className="flex flex-wrap gap-1">
{item.subject_categories.map((s) => (
<Badge
key={s}
variant="outline"
className="text-[0.7rem] bg-gold-wash text-gold-deep border-gold/40"
>
{s}
</Badge>
))}
</div>
)}
</TableCell>
<TableCell className="text-ink-soft tabular-nums">
{formatChars(item.chars)}
</TableCell>
<TableCell className="text-ink-muted tabular-nums text-[0.78rem]">
{formatDate(item.created_at)}
</TableCell>
<TableCell className="text-end">
<Button
variant="ghost"
size="sm"
onClick={onDelete}
disabled={del.isPending}
aria-label={`הסר את ${item.decision_number || "החלטה זו"} מהקורפוס`}
className="text-danger hover:text-danger hover:bg-danger-bg"
>
<Trash2 className="w-4 h-4" aria-hidden="true" />
</Button>
</TableCell>
</TableRow>
);
}
export function CorpusPanel() {
const { data, isPending, error } = useCorpus();
if (error) {
return (
<div className="rounded bg-danger-bg border border-danger/40 px-6 py-5 text-danger text-center">
{error.message}
</div>
);
}
return (
<div className="rounded-lg border border-rule bg-surface shadow-sm overflow-hidden">
<Table>
<TableHeader className="bg-rule-soft/60">
<TableRow className="border-rule">
<TableHead className="text-navy text-right">מס׳ החלטה</TableHead>
<TableHead className="text-navy text-right">תאריך</TableHead>
<TableHead className="text-navy text-right">נושאים</TableHead>
<TableHead className="text-navy text-right">תווים</TableHead>
<TableHead className="text-navy text-right">נוסף בתאריך</TableHead>
<TableHead className="text-navy" />
</TableRow>
</TableHeader>
<TableBody>
{isPending ? (
[...Array(4)].map((_, i) => (
<TableRow key={i} className="border-rule">
{[...Array(6)].map((_, j) => (
<TableCell key={j}>
<Skeleton className="h-4 w-24" />
</TableCell>
))}
</TableRow>
))
) : data?.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="text-center text-ink-muted py-12">
הקורפוס ריק
</TableCell>
</TableRow>
) : (
data?.map((item) => <Row key={item.id} item={item} />)
)}
</TableBody>
</Table>
</div>
);
}