Compare commits
2 Commits
d1ca1152b1
...
91bd3ac88c
| Author | SHA1 | Date | |
|---|---|---|---|
| 91bd3ac88c | |||
| bb83ddc611 |
@@ -5,7 +5,6 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Markdown } from "@/components/ui/markdown";
|
import { Markdown } from "@/components/ui/markdown";
|
||||||
import { AgentStatusWidget } from "@/components/cases/agent-status-widget";
|
|
||||||
import {
|
import {
|
||||||
useAgentActivity,
|
useAgentActivity,
|
||||||
useSendComment,
|
useSendComment,
|
||||||
@@ -17,6 +16,7 @@ import type {
|
|||||||
InteractionQuestion,
|
InteractionQuestion,
|
||||||
InteractionTask,
|
InteractionTask,
|
||||||
PaperclipComment,
|
PaperclipComment,
|
||||||
|
PaperclipAgentStatus,
|
||||||
} from "@/lib/api/agents";
|
} from "@/lib/api/agents";
|
||||||
import { formatRelative } from "@/lib/format-date";
|
import { formatRelative } from "@/lib/format-date";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -82,6 +82,104 @@ function issueStatusLabel(status: string) {
|
|||||||
return ISSUE_STATUS_LABELS[status] ?? status;
|
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 ─────────────────────────────────────────── */
|
/* ── Time formatting ─────────────────────────────────────────── */
|
||||||
|
|
||||||
function timeAgo(iso: string | null): string {
|
function timeAgo(iso: string | null): string {
|
||||||
@@ -690,7 +788,9 @@ export function AgentActivityFeed({
|
|||||||
|
|
||||||
const comments = data.comments ?? [];
|
const comments = data.comments ?? [];
|
||||||
const interactions = data.interactions ?? [];
|
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.
|
// Unified, time-sorted feed: comments + interactions interleaved.
|
||||||
type FeedItem =
|
type FeedItem =
|
||||||
@@ -703,7 +803,9 @@ export function AgentActivityFeed({
|
|||||||
at: c.created_at ? new Date(c.created_at).getTime() : 0,
|
at: c.created_at ? new Date(c.created_at).getTime() : 0,
|
||||||
comment: c,
|
comment: c,
|
||||||
})),
|
})),
|
||||||
...interactions.map<FeedItem>((i) => ({
|
...interactions
|
||||||
|
.filter((i) => i.status !== "pending")
|
||||||
|
.map<FeedItem>((i) => ({
|
||||||
kind: "interaction",
|
kind: "interaction",
|
||||||
at: i.created_at ? new Date(i.created_at).getTime() : 0,
|
at: i.created_at ? new Date(i.created_at).getTime() : 0,
|
||||||
interaction: i,
|
interaction: i,
|
||||||
@@ -719,15 +821,27 @@ export function AgentActivityFeed({
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
{/* agent roster — who's doing what right now (mockup 18i) */}
|
{/* 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">
|
<AgentRoster agents={data.agents ?? []} comments={comments} />
|
||||||
<AgentStatusWidget caseNumber={caseNumber} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* pending interactions surfaced to the top (mockup 18i — awaiting you) */}
|
{/* pending interactions surfaced to the top — the actual forms, not a
|
||||||
{pendingCount > 0 && (
|
pointer (mockup 18i — awaiting you). Excluded from the timeline. */}
|
||||||
<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.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" />
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user