Merge pull request 'feat(arguments): inline status banners + fix double-card nesting' (#328) from worktree-arguments-banners into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 43s
G12 Leak-Guard / leak-guard (push) Successful in 4s
Lint — undefined names / undefined-names (push) Successful in 11s

This commit was merged in pull request #328.
This commit is contained in:
2026-06-20 18:36:21 +00:00

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { useMemo } from "react"; import { useMemo, type ReactNode } from "react";
import { import {
Accordion, Accordion,
AccordionContent, AccordionContent,
@@ -9,7 +9,6 @@ import {
} from "@/components/ui/accordion"; } from "@/components/ui/accordion";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { import {
Popover, Popover,
PopoverContent, PopoverContent,
@@ -22,6 +21,7 @@ import {
PRIORITY_ORDER, PRIORITY_ORDER,
useAggregateArguments, useAggregateArguments,
useLegalArguments, useLegalArguments,
type AggregateArgumentsResult,
type LegalArgument, type LegalArgument,
type LegalArgumentParty, type LegalArgumentParty,
type LegalArgumentPriority, type LegalArgumentPriority,
@@ -189,97 +189,167 @@ export function LegalArgumentsPanel({ caseNumber }: LegalArgumentsPanelProps) {
}, [data]); }, [data]);
const handleAggregate = (force: boolean) => { 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, { 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}`), onError: (e) => toast.error(`שגיאה: ${(e as Error).message}`),
}); });
}; };
return ( return (
<Card className="bg-surface border-rule shadow-sm"> <div className="space-y-4">
<CardContent className="px-6 py-5 space-y-4"> <div className="flex items-center justify-between flex-wrap gap-3">
<div className="flex items-center justify-between flex-wrap gap-3"> <div>
<div> <h2 className="text-navy text-base font-semibold">טיעונים משפטיים</h2>
<h2 className="text-navy text-base font-semibold"> <p className="text-ink-muted text-xs mt-0.5">
טיעונים משפטיים טיעונים מאוגדים מתוך הטענות הגולמיות, מקובצים לפי צד וקדימות. החישוב
</h2> רץ אצל המנתח המשפטי ומדווח בחזרה.
<p className="text-ink-muted text-xs mt-0.5"> </p>
טיעונים מאוגדים מתוך הפרופוזיציות הגולמיות, מקובצים לפי צד וקדימות.
</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> </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 ? ( <AggregateStatusBanner
<div className="space-y-2"> result={aggregate.data}
<Skeleton className="h-6 w-48" /> force={aggregate.variables ?? false}
<Skeleton className="h-20 w-full" /> />
<Skeleton className="h-20 w-full" />
</div> {isPending ? (
) : isError ? ( <div className="space-y-2">
<p className="text-danger text-sm"> <Skeleton className="h-6 w-48" />
שגיאה בטעינת טיעונים: {(error as Error).message} <Skeleton className="h-20 w-full" />
</p> <Skeleton className="h-20 w-full" />
) : !data?.total ? ( </div>
<p className="text-ink-muted text-sm"> ) : isError ? (
אין טיעונים מאוגדים עדיין. לחץ &ldquo;חשב טיעונים&rdquo; כדי להריץ את ה-aggregator. <p className="text-danger text-sm">
</p> שגיאה בטעינת טיעונים: {(error as Error).message}
) : ( </p>
<div className="space-y-6"> ) : !data?.total ? (
{parties.map((party) => ( <p className="text-ink-muted text-sm">
<PartySection אין טיעונים מאוגדים עדיין. לחץ &ldquo;חשב טיעונים&rdquo; כדי לשלוח את
key={party} החישוב למנתח המשפטי.
party={party} </p>
args={data.by_party[party] ?? []} ) : (
/> <div className="space-y-6">
))} {parties.map((party) => (
</div> <PartySection
)} key={party}
</CardContent> party={party}
</Card> 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>
); );
} }