gMAS / web_ui /frontend /src /components /execution /ExecutionTimeline.tsx
Артём Боярских
feat: added dynamic topologies, new templates and conditional edges
5cdde73
import {
Play,
CheckCircle2,
XCircle,
AlertTriangle,
Zap,
Activity,
StopCircle,
GitBranch,
type LucideIcon,
} from "lucide-react";
import { ScrollArea } from "@/components/ui/scroll-area";
import type { StreamEvent, StreamEventType } from "@/types/execution";
const eventConfig: Record<string, { icon: LucideIcon; color: string }> = {
run_start: { icon: Play, color: "text-blue-500" },
run_end: { icon: CheckCircle2, color: "text-green-500" },
agent_start: { icon: Play, color: "text-blue-400" },
agent_output: { icon: CheckCircle2, color: "text-green-400" },
agent_error: { icon: XCircle, color: "text-red-500" },
budget_warning: { icon: AlertTriangle, color: "text-yellow-500" },
budget_exceeded: { icon: AlertTriangle, color: "text-red-500" },
topology_changed: { icon: GitBranch, color: "text-purple-500" },
early_stop: { icon: StopCircle, color: "text-amber-500" },
prune: { icon: Activity, color: "text-orange-500" },
parallel_start: { icon: Zap, color: "text-indigo-500" },
parallel_end: { icon: Zap, color: "text-indigo-400" },
};
function formatTime(timestamp: string): string {
try {
const d = new Date(timestamp);
return d.toLocaleTimeString([], { hour12: false } as Intl.DateTimeFormatOptions);
} catch {
return "";
}
}
function eventSummary(event: StreamEvent): string {
const ev = event as Record<string, any>;
switch (event.event_type) {
case "run_start":
return `Run started: ${event.num_agents ?? 0} agents`;
case "run_end":
return `Run ${event.success ? "completed" : "failed"} in ${(event.total_time ?? 0).toFixed(1)}s (${event.total_tokens ?? 0} tokens)`;
case "agent_start":
return `${event.agent_name || event.agent_id} started`;
case "agent_output": {
const preview = (event.content || "").slice(0, 80);
return `${event.agent_name || event.agent_id}: ${preview}${(event.content?.length ?? 0) > 80 ? "..." : ""}`;
}
case "agent_error":
return `${event.agent_id} error: ${event.error_message}`;
case "topology_changed": {
const parts: string[] = ["Topology modified"];
if (ev.added_edges) parts.push(`+${ev.added_edges} edges`);
if (ev.removed_edges) parts.push(`-${ev.removed_edges} edges`);
if (ev.skipped_agents?.length) parts.push(`skipped: ${ev.skipped_agents.join(", ")}`);
if (ev.forced_agents?.length) parts.push(`forced: ${ev.forced_agents.join(", ")}`);
return parts.join(" | ");
}
case "early_stop":
return `Early stopped: ${ev.reason || event.content || "condition met"}`;
case "prune":
return `Pruned: ${ev.agent_id || ev.agents?.join(", ") || "agents"}`;
case "parallel_start":
return `Parallel: ${event.agent_ids?.join(", ")}`;
case "error":
return `Error: ${event.error || "Unknown"}`;
case "cancelled":
return "Execution cancelled";
default:
return event.event_type;
}
}
interface ExecutionTimelineProps {
events: StreamEvent[];
}
export function ExecutionTimeline({ events }: ExecutionTimelineProps) {
const filtered = events.filter(
(e) => e.event_type !== "token" && e.event_type !== "memory_read" && e.event_type !== "memory_write"
);
return (
<ScrollArea className="h-full">
<div className="space-y-1 p-2">
{filtered.length === 0 && (
<p className="text-xs text-muted-foreground text-center py-4">
No events yet. Execute a workflow to see events here.
</p>
)}
{filtered.map((event, i) => {
const cfg = eventConfig[event.event_type] || { icon: Activity, color: "text-muted-foreground" };
const Icon = cfg.icon;
return (
<div key={i} className="flex items-start gap-2 text-xs py-1">
<Icon className={`h-3.5 w-3.5 mt-0.5 flex-shrink-0 ${cfg.color}`} />
<span className="text-muted-foreground flex-shrink-0 w-16 font-mono">
{formatTime(event.timestamp)}
</span>
<span className="text-foreground">{eventSummary(event)}</span>
</div>
);
})}
</div>
</ScrollArea>
);
}