Files
legal-ai/web-ui/src/components/cases/legal-arguments-panel.tsx
Chaim 1f1a025509 fix(lint): תיקון 10 שגיאות ESLint + ניקוי directives מיותרים
10 שגיאות (כולן קיימות-מראש, לא מהפיצ'רים האחרונים):
- react/no-unescaped-entities (3): legal-arguments-panel, precedent-edit-sheet
  — escaping של מרכאות ב-JSX (“/")
- react-hooks/set-state-in-effect (6): documents-panel, chair-editor,
  content-checklists, discussion-rules, golden-ratios, documents.ts
  — disable-comment לדפוסי sync/reset לגיטימיים (false-positive ידוע)
- React Compiler reassign (1): subject-donut — refactor לחישוב prefix-sums
  ללא mutable accumulator

ניקוי: הסרת 5 eslint-disable directives מיותרים (halacha-review-panel,
precedent-upload-sheet). תוצאה: 0 errors (היה 10), 24→ warnings (היה 29).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 13:31:31 +00:00

223 lines
7.5 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 { useMemo } from "react";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import {
PARTY_LABELS_HE,
PRIORITY_LABELS_HE,
PRIORITY_ORDER,
useAggregateArguments,
useLegalArguments,
type LegalArgument,
type LegalArgumentParty,
type LegalArgumentPriority,
} from "@/lib/api/legal-arguments";
import { toast } from "sonner";
import { Loader2, RefreshCw, Sparkles } from "lucide-react";
const PRIORITY_BADGE_TONE: Record<LegalArgumentPriority, string> = {
threshold: "bg-danger-bg/60 text-danger-strong border-danger/40",
substantive: "bg-gold-soft/50 text-navy border-gold/40",
procedural: "bg-rule-soft text-ink border-rule",
relief: "bg-emerald-50 text-emerald-900 border-emerald-200",
};
function groupByPriority(
args: LegalArgument[],
): Record<LegalArgumentPriority, LegalArgument[]> {
const out: Record<LegalArgumentPriority, LegalArgument[]> = {
threshold: [],
substantive: [],
procedural: [],
relief: [],
};
for (const a of args) {
(out[a.priority] ?? out.substantive).push(a);
}
for (const key of PRIORITY_ORDER) {
out[key].sort((x, y) => x.argument_index - y.argument_index);
}
return out;
}
type PartySectionProps = {
party: LegalArgumentParty;
args: LegalArgument[];
};
function PartySection({ party, args }: PartySectionProps) {
const grouped = useMemo(() => groupByPriority(args), [args]);
return (
<div className="space-y-3">
<div className="flex items-baseline justify-between border-b border-rule pb-2">
<h3 className="text-navy text-base font-semibold">
{PARTY_LABELS_HE[party] ?? party}
</h3>
<span className="text-ink-muted text-xs">
{args.length} טיעונים
</span>
</div>
{PRIORITY_ORDER.map((priority) => {
const list = grouped[priority];
if (!list?.length) return null;
return (
<div key={priority} className="space-y-1">
<div className="flex items-center gap-2">
<Badge
variant="outline"
className={`${PRIORITY_BADGE_TONE[priority]} text-xs`}
>
{PRIORITY_LABELS_HE[priority]}
</Badge>
<span className="text-ink-muted text-xs">
{list.length} טיעונים
</span>
</div>
<Accordion type="multiple" className="rounded-md border border-rule bg-surface">
{list.map((arg) => (
<AccordionItem key={arg.id} value={arg.id} className="px-3">
<AccordionTrigger className="text-start">
<div className="flex flex-1 flex-col items-start gap-1">
<span className="text-navy text-sm font-medium leading-tight">
{arg.argument_index}. {arg.argument_title}
</span>
{arg.legal_topic && (
<span className="text-ink-muted text-xs">
{arg.legal_topic}
</span>
)}
</div>
</AccordionTrigger>
<AccordionContent>
<div className="space-y-2 px-1">
<p className="text-ink leading-relaxed whitespace-pre-line">
{arg.argument_body}
</p>
{arg.supporting_claims.length > 0 && (
<p className="text-ink-muted text-xs">
מסתמך על {arg.supporting_claims.length} פרופוזיציות
גולמיות.
</p>
)}
</div>
</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
);
})}
</div>
);
}
type LegalArgumentsPanelProps = {
caseNumber: string;
};
export function LegalArgumentsPanel({ caseNumber }: LegalArgumentsPanelProps) {
const { data, isPending, isError, error } = useLegalArguments(caseNumber);
const aggregate = useAggregateArguments(caseNumber);
const parties = useMemo<LegalArgumentParty[]>(() => {
if (!data?.by_party) return [];
const order: LegalArgumentParty[] = [
"appellant",
"respondent",
"committee",
"permit_applicant",
"unknown",
];
return order.filter((p) => (data.by_party[p]?.length ?? 0) > 0);
}, [data]);
const handleAggregate = (force: boolean) => {
aggregate.mutate(force, {
onSuccess: () => {
toast.success(
force
? "הופעלה חזרה חישוב טיעונים (force). יסתיים תוך דקה."
: "הופעל חישוב טיעונים. רענן בעוד דקה.",
);
},
onError: (e) => toast.error(`שגיאה: ${(e as Error).message}`),
});
};
return (
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5 space-y-4">
<div className="flex items-center justify-between flex-wrap gap-3">
<div>
<h2 className="text-navy text-base font-semibold">
טיעונים משפטיים
</h2>
<p className="text-ink-muted text-xs mt-0.5">
טיעונים מאוגדים מתוך הפרופוזיציות הגולמיות, מקובצים לפי צד וקדימות.
</p>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
disabled={aggregate.isPending}
onClick={() => handleAggregate(false)}
>
{aggregate.isPending ? (
<Loader2 className="w-3.5 h-3.5 animate-spin me-1.5" />
) : (
<Sparkles className="w-3.5 h-3.5 me-1.5" />
)}
חשב טיעונים
</Button>
<Button
variant="ghost"
size="sm"
disabled={aggregate.isPending || !data?.total}
onClick={() => handleAggregate(true)}
title="חישוב מחדש (מוחק טיעונים קיימים)"
>
<RefreshCw className="w-3.5 h-3.5" />
</Button>
</div>
</div>
{isPending ? (
<div className="space-y-2">
<Skeleton className="h-6 w-48" />
<Skeleton className="h-20 w-full" />
<Skeleton className="h-20 w-full" />
</div>
) : isError ? (
<p className="text-danger text-sm">
שגיאה בטעינת טיעונים: {(error as Error).message}
</p>
) : !data?.total ? (
<p className="text-ink-muted text-sm">
אין טיעונים מאוגדים עדיין. לחץ &ldquo;חשב טיעונים&rdquo; כדי להריץ את ה-aggregator.
</p>
) : (
<div className="space-y-6">
{parties.map((party) => (
<PartySection
key={party}
party={party}
args={data.by_party[party] ?? []}
/>
))}
</div>
)}
</CardContent>
</Card>
);
}