feat(case-ui): agents tab — full 18i (rich roster + surfaced pending forms)
Completes mockup 18i beyond the earlier minimal version: - AgentRoster — a 5-up grid of agents, each with a status dot, name, role label and its current activity (derived from the agent's most recent comment). Replaces the simple AgentStatusWidget list at the top of the feed (reset still lives in the banner status-rail popover). - "ממתין לתשובתך" now renders the actual pending InteractionCards (the approve/reject/answer forms) at the top, not just a count pointer — and those pending interactions are excluded from the timeline below so they aren't shown twice. All interaction logic / mutations preserved. tsc --noEmit + eslint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,6 @@ import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Markdown } from "@/components/ui/markdown";
|
||||
import { AgentStatusWidget } from "@/components/cases/agent-status-widget";
|
||||
import {
|
||||
useAgentActivity,
|
||||
useSendComment,
|
||||
@@ -17,6 +16,7 @@ import type {
|
||||
InteractionQuestion,
|
||||
InteractionTask,
|
||||
PaperclipComment,
|
||||
PaperclipAgentStatus,
|
||||
} from "@/lib/api/agents";
|
||||
import { formatRelative } from "@/lib/format-date";
|
||||
import { toast } from "sonner";
|
||||
@@ -82,6 +82,104 @@ function issueStatusLabel(status: string) {
|
||||
return ISSUE_STATUS_LABELS[status] ?? status;
|
||||
}
|
||||
|
||||
/* ── Agent roster (mockup 18i) — who's doing what right now ────── */
|
||||
|
||||
const AGENT_STATUS_LABEL: Record<string, string> = {
|
||||
active: "פעיל",
|
||||
running: "פעיל",
|
||||
idle: "ממתין",
|
||||
error: "שגיאה",
|
||||
};
|
||||
|
||||
function agentIsActive(s: string) {
|
||||
return s === "active" || s === "running";
|
||||
}
|
||||
|
||||
function agentStatusDot(s: string) {
|
||||
return agentIsActive(s)
|
||||
? "bg-success shadow-[0_0_6px_rgba(74,124,89,0.6)]"
|
||||
: s === "error"
|
||||
? "bg-danger"
|
||||
: "bg-rule";
|
||||
}
|
||||
|
||||
function agentStatusTone(s: string) {
|
||||
return agentIsActive(s)
|
||||
? "bg-success-bg text-success"
|
||||
: s === "error"
|
||||
? "bg-danger-bg text-danger"
|
||||
: "bg-rule-soft text-ink-muted";
|
||||
}
|
||||
|
||||
function AgentRoster({
|
||||
agents,
|
||||
comments,
|
||||
}: {
|
||||
agents: PaperclipAgentStatus[];
|
||||
comments: PaperclipComment[];
|
||||
}) {
|
||||
if (!agents.length) return null;
|
||||
|
||||
// "current activity" per agent = its most recent comment body (by name/role).
|
||||
const latest = new Map<string, string>();
|
||||
for (const a of agents) {
|
||||
let best: PaperclipComment | null = null;
|
||||
let bestAt = -1;
|
||||
for (const c of comments) {
|
||||
const mine =
|
||||
(c.agent_name && c.agent_name === a.name) ||
|
||||
(c.agent_role && c.agent_role === a.role);
|
||||
if (!mine) continue;
|
||||
const at = c.created_at ? new Date(c.created_at).getTime() : 0;
|
||||
if (at >= bestAt) {
|
||||
bestAt = at;
|
||||
best = c;
|
||||
}
|
||||
}
|
||||
if (best) latest.set(a.id, best.body);
|
||||
}
|
||||
|
||||
const activeCount = agents.filter((a) => agentIsActive(a.status)).length;
|
||||
|
||||
return (
|
||||
<div className="mb-3 rounded-lg border border-rule bg-parchment/40 p-3">
|
||||
<div className="flex items-center gap-2 mb-2.5">
|
||||
<Bot className="w-4 h-4 text-gold-deep shrink-0" />
|
||||
<h3 className="text-[0.82rem] font-bold text-navy">סוכני התיק</h3>
|
||||
<span className="text-[0.72rem] text-ink-muted">
|
||||
<b className="text-success font-bold">{activeCount} פעילים</b> מתוך {agents.length}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5">
|
||||
{agents.map((a) => {
|
||||
const active = agentIsActive(a.status);
|
||||
const activity = latest.get(a.id);
|
||||
return (
|
||||
<div
|
||||
key={a.id}
|
||||
className={`rounded-lg border p-2.5 ${active ? "border-success/40 bg-success-bg/40" : "border-rule bg-surface"}`}
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className={`w-2 h-2 rounded-full shrink-0 ${agentStatusDot(a.status)}`} />
|
||||
<span className="text-[0.78rem] font-bold text-navy truncate">{a.name}</span>
|
||||
<span
|
||||
className={`ms-auto text-[0.6rem] font-bold rounded-full px-1.5 py-0.5 whitespace-nowrap ${agentStatusTone(a.status)}`}
|
||||
>
|
||||
{AGENT_STATUS_LABEL[a.status] ?? a.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-[0.66rem] text-ink-muted mt-0.5">{roleLabel(a.role)}</div>
|
||||
<div className="text-[0.7rem] text-ink-soft mt-1.5 line-clamp-2 leading-tight">
|
||||
{activity || "—"}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Time formatting ─────────────────────────────────────────── */
|
||||
|
||||
function timeAgo(iso: string | null): string {
|
||||
@@ -690,7 +788,9 @@ export function AgentActivityFeed({
|
||||
|
||||
const comments = data.comments ?? [];
|
||||
const interactions = data.interactions ?? [];
|
||||
const pendingCount = interactions.filter((i) => i.status === "pending").length;
|
||||
// Pending interactions are surfaced at the top (awaiting-you, mockup 18i) and
|
||||
// NOT repeated in the timeline below.
|
||||
const pending = interactions.filter((i) => i.status === "pending");
|
||||
|
||||
// Unified, time-sorted feed: comments + interactions interleaved.
|
||||
type FeedItem =
|
||||
@@ -703,7 +803,9 @@ export function AgentActivityFeed({
|
||||
at: c.created_at ? new Date(c.created_at).getTime() : 0,
|
||||
comment: c,
|
||||
})),
|
||||
...interactions.map<FeedItem>((i) => ({
|
||||
...interactions
|
||||
.filter((i) => i.status !== "pending")
|
||||
.map<FeedItem>((i) => ({
|
||||
kind: "interaction",
|
||||
at: i.created_at ? new Date(i.created_at).getTime() : 0,
|
||||
interaction: i,
|
||||
@@ -719,15 +821,27 @@ export function AgentActivityFeed({
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* agent roster — who's doing what right now (mockup 18i) */}
|
||||
<div className="mb-3 rounded-lg border border-rule bg-parchment/40 px-3 py-2.5">
|
||||
<AgentStatusWidget caseNumber={caseNumber} />
|
||||
</div>
|
||||
<AgentRoster agents={data.agents ?? []} comments={comments} />
|
||||
|
||||
{/* pending interactions surfaced to the top (mockup 18i — awaiting you) */}
|
||||
{pendingCount > 0 && (
|
||||
<div className="mb-3 rounded-lg border border-amber-300 bg-amber-50 px-4 py-2.5 text-[0.82rem] font-semibold text-amber-800 flex items-center gap-2">
|
||||
{/* pending interactions surfaced to the top — the actual forms, not a
|
||||
pointer (mockup 18i — awaiting you). Excluded from the timeline. */}
|
||||
{pending.length > 0 && (
|
||||
<div className="mb-3 rounded-lg border border-amber-300 bg-amber-50 p-3 space-y-2">
|
||||
<div className="flex items-center gap-2 text-[0.82rem] font-bold text-amber-800">
|
||||
<Clock className="w-4 h-4" />
|
||||
ממתין לתשובתך — {pendingCount} {pendingCount === 1 ? "בקשה" : "בקשות"} בהמשך הזרם
|
||||
ממתין לתשובתך
|
||||
<span className="text-[0.7rem] font-semibold bg-amber-100 rounded-full px-2 py-0.5">
|
||||
{pending.length}
|
||||
</span>
|
||||
</div>
|
||||
{pending.map((i) => (
|
||||
<InteractionCard
|
||||
key={`pending-${i.id}`}
|
||||
interaction={i}
|
||||
caseNumber={caseNumber}
|
||||
issueMap={issueMap}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user