feat(arguments): inline status banners + fix double-card nesting
Implements the Claude-Design-approved inline status banners for the "חשב טיעונים" panel (mockup 25-legal-arguments-panel): the four endpoint states (queued / exists / no_claims / skipped) now render as tone-coded banners below the header instead of a transient toast. Toast is kept only for hard transport errors. Also fixes a pre-existing double-card bug found while reviewing the page: the page's "arguments" tab already wraps the panel in <Card><CardContent> (page.tsx:151-155), yet LegalArgumentsPanel rendered its OWN <Card> too — unlike its sibling tab panels (DecisionBlocksPanel/DraftsPanel/ AgentActivityFeed) which render a plain <div>. The panel now matches that convention (single card, no double border/padding), consistent with the approved single-card mockup. Design gate: card 25 approved by חיים. tsc + eslint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { useMemo, type ReactNode } from "react";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
} from "@/components/ui/accordion";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
@@ -22,6 +21,7 @@ import {
|
||||
PRIORITY_ORDER,
|
||||
useAggregateArguments,
|
||||
useLegalArguments,
|
||||
type AggregateArgumentsResult,
|
||||
type LegalArgument,
|
||||
type LegalArgumentParty,
|
||||
type LegalArgumentPriority,
|
||||
@@ -189,97 +189,167 @@ export function LegalArgumentsPanel({ caseNumber }: LegalArgumentsPanelProps) {
|
||||
}, [data]);
|
||||
|
||||
const handleAggregate = (force: boolean) => {
|
||||
// Status feedback is rendered inline as a banner from `aggregate.data`;
|
||||
// only hard transport/HTTP errors fall through to a toast.
|
||||
aggregate.mutate(force, {
|
||||
onSuccess: (result) => {
|
||||
switch (result.status) {
|
||||
case "queued":
|
||||
toast.success(
|
||||
force
|
||||
? "נשלח לאנליטיקאי לחישוב מחדש. התוצאה תופיע תוך כמה דקות — רענן את הדף."
|
||||
: "נשלח לאנליטיקאי. החישוב רץ ברקע; התוצאה תופיע תוך כמה דקות — רענן את הדף.",
|
||||
);
|
||||
break;
|
||||
case "no_claims":
|
||||
case "exists":
|
||||
toast.info(result.message);
|
||||
break;
|
||||
case "skipped":
|
||||
toast.warning(
|
||||
`לא ניתן להפעיל אוטומטית (${result.reason}). הרץ ידנית מ-Claude Code: mcp__legal-ai__aggregate_claims_to_arguments`,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
toast.success("הבקשה נשלחה.");
|
||||
}
|
||||
},
|
||||
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 className="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">
|
||||
אין טיעונים מאוגדים עדיין. לחץ “חשב טיעונים” כדי להריץ את ה-aggregator.
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{parties.map((party) => (
|
||||
<PartySection
|
||||
key={party}
|
||||
party={party}
|
||||
args={data.by_party[party] ?? []}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<AggregateStatusBanner
|
||||
result={aggregate.data}
|
||||
force={aggregate.variables ?? false}
|
||||
/>
|
||||
|
||||
{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">
|
||||
אין טיעונים מאוגדים עדיין. לחץ “חשב טיעונים” כדי לשלוח את
|
||||
החישוב למנתח המשפטי.
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{parties.map((party) => (
|
||||
<PartySection
|
||||
key={party}
|
||||
party={party}
|
||||
args={data.by_party[party] ?? []}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const BANNER_TONE = {
|
||||
info: "border-info/30 bg-info-bg",
|
||||
gold: "border-gold/40 bg-gold-wash",
|
||||
muted: "border-rule bg-rule-soft",
|
||||
warn: "border-warn/40 bg-warn-bg",
|
||||
} as const;
|
||||
|
||||
const DOT_TONE = {
|
||||
info: "bg-info",
|
||||
gold: "bg-gold-deep",
|
||||
muted: "bg-ink-light",
|
||||
warn: "bg-warn",
|
||||
} as const;
|
||||
|
||||
const TITLE_TONE = {
|
||||
info: "text-info",
|
||||
gold: "text-gold-deep",
|
||||
muted: "text-ink-soft",
|
||||
warn: "text-warn",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Inline status feedback for the aggregation trigger — mirrors the approved
|
||||
* Claude Design mockup (25-legal-arguments-panel). Shown only after a click;
|
||||
* the four states map to the endpoint's discriminated-union result.
|
||||
*/
|
||||
function AggregateStatusBanner({
|
||||
result,
|
||||
force,
|
||||
}: {
|
||||
result: AggregateArgumentsResult | undefined;
|
||||
force: boolean;
|
||||
}) {
|
||||
if (!result) return null;
|
||||
|
||||
let tone: keyof typeof BANNER_TONE;
|
||||
let title: string;
|
||||
let body: ReactNode;
|
||||
|
||||
switch (result.status) {
|
||||
case "queued":
|
||||
tone = "info";
|
||||
title = "נשלח לאנליטיקאי.";
|
||||
body = `${force ? "החישוב-מחדש" : "החישוב"} רץ ברקע אצל המנתח המשפטי; התוצאה תופיע תוך כמה דקות — רענן את הדף.`;
|
||||
break;
|
||||
case "exists":
|
||||
tone = "gold";
|
||||
title = "כבר חושב.";
|
||||
body = result.message;
|
||||
break;
|
||||
case "no_claims":
|
||||
tone = "muted";
|
||||
title = "אין טענות גולמיות.";
|
||||
body = result.message;
|
||||
break;
|
||||
case "skipped":
|
||||
tone = "warn";
|
||||
title = "לא ניתן להפעיל אוטומטית";
|
||||
body = (
|
||||
<>
|
||||
{` (${result.reason}). הרץ ידנית מ-Claude Code: `}
|
||||
<code className="font-mono text-[0.72rem] bg-navy/5 rounded px-1.5 py-0.5 select-all">
|
||||
mcp__legal-ai__aggregate_claims_to_arguments
|
||||
</code>
|
||||
</>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex items-start gap-2.5 rounded-md border px-3.5 py-2.5 text-[0.8rem] leading-relaxed text-ink-soft ${BANNER_TONE[tone]}`}
|
||||
>
|
||||
<span
|
||||
className={`mt-1.5 size-2 flex-none rounded-full ${DOT_TONE[tone]}`}
|
||||
aria-hidden
|
||||
/>
|
||||
<p>
|
||||
<strong className={`font-semibold ${TITLE_TONE[tone]}`}>{title}</strong>{" "}
|
||||
{body}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user