Merge pull request 'feat(arguments): inline status banners + fix double-card nesting' (#328) from worktree-arguments-banners into main
This commit was merged in pull request #328.
This commit is contained in:
@@ -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 ? (
|
||||||
אין טיעונים מאוגדים עדיין. לחץ “חשב טיעונים” כדי להריץ את ה-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
|
אין טיעונים מאוגדים עדיין. לחץ “חשב טיעונים” כדי לשלוח את
|
||||||
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user