+
{selected.name}
- {selected.vendor} · {selected.role}
+ {selected.vendor} · {selected.kind.toUpperCase()} ·{" "}
+ {selected.role}
MODEL / ADAPTER
{selected.model}
Configured for demonstration only
+
DEMO TASKS
@@ -698,11 +636,11 @@ export function AgentsPage() {
return (
setSelectedId(other.id)}
>
-
+
{other.name}
{edge.label}
@@ -713,10 +651,30 @@ export function AgentsPage() {
})
) : (
- No demo connections are shown for this adapter.
+ No demo connections are shown for this entity.
)}
+
+
+ GRAPH ENTITIES
+ {String(agents.length).padStart(2, "0")}
+
+ {agents.map((agent) => (
+ setSelectedId(agent.id)}
+ >
+
+
+ {agent.name}
+ {agent.kind}
+
+
+ ))}
+
- Sample graph data only {" "}
+ Sample graph data only
setLive((value) => !value)}
@@ -743,7 +701,973 @@ export function AgentsPage() {
Motion {live ? "on" : "off"}
Decorative connection motion can be switched off.
+
+ Reset layout
+
);
}
+export function AgentsPage() {
+ return (
+
+
+
+ );
+}
+
+// ORIGINAL (preserved, not deleted — disabled by comment): import {
+// ORIGINAL (preserved, not deleted — disabled by comment): Activity,
+// ORIGINAL (preserved, not deleted — disabled by comment): ArrowDown,
+// ORIGINAL (preserved, not deleted — disabled by comment): ArrowLeft,
+// ORIGINAL (preserved, not deleted — disabled by comment): ArrowRight,
+// ORIGINAL (preserved, not deleted — disabled by comment): ArrowUp,
+// ORIGINAL (preserved, not deleted — disabled by comment): Bot,
+// ORIGINAL (preserved, not deleted — disabled by comment): BrainCircuit,
+// ORIGINAL (preserved, not deleted — disabled by comment): ChevronDown,
+// ORIGINAL (preserved, not deleted — disabled by comment): CircleHelp,
+// ORIGINAL (preserved, not deleted — disabled by comment): Command,
+// ORIGINAL (preserved, not deleted — disabled by comment): Cpu,
+// ORIGINAL (preserved, not deleted — disabled by comment): GitBranch,
+// ORIGINAL (preserved, not deleted — disabled by comment): Minus,
+// ORIGINAL (preserved, not deleted — disabled by comment): Plus,
+// ORIGINAL (preserved, not deleted — disabled by comment): RotateCcw,
+// ORIGINAL (preserved, not deleted — disabled by comment): Sparkles,
+// ORIGINAL (preserved, not deleted — disabled by comment): Waypoints,
+// ORIGINAL (preserved, not deleted — disabled by comment): } from "lucide-react";
+// ORIGINAL (preserved, not deleted — disabled by comment): import {
+// ORIGINAL (preserved, not deleted — disabled by comment): // ORIGINAL (preserved, not deleted — disabled by comment): type KeyboardEvent as ReactKeyboardEvent,
+// ORIGINAL (preserved, not deleted — disabled by comment): type PointerEvent as ReactPointerEvent,
+// ORIGINAL (preserved, not deleted — disabled by comment): type WheelEvent as ReactWheelEvent,
+// ORIGINAL (preserved, not deleted — disabled by comment): useCallback,
+// ORIGINAL (preserved, not deleted — disabled by comment): useEffect,
+// ORIGINAL (preserved, not deleted — disabled by comment): useRef,
+// ORIGINAL (preserved, not deleted — disabled by comment): useState,
+// ORIGINAL (preserved, not deleted — disabled by comment): } from "react";
+// ORIGINAL (preserved, not deleted — disabled by comment): import { PageTitle } from "../components/ui";
+// ORIGINAL (preserved, not deleted — disabled by comment): import {
+// ORIGINAL (preserved, not deleted — disabled by comment): type AgentGraphLayout,
+// ORIGINAL (preserved, not deleted — disabled by comment): readAgentGraphLayout,
+// ORIGINAL (preserved, not deleted — disabled by comment): writeAgentGraphLayout,
+// ORIGINAL (preserved, not deleted — disabled by comment): } from "../data/agentLayout";
+// ORIGINAL (preserved, not deleted — disabled by comment): import "./Agents.css";
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): type AgentStatus = "active" | "idle" | "attention" | "paused";
+// ORIGINAL (preserved, not deleted — disabled by comment): type GraphMode = "network" | "radial";
+// ORIGINAL (preserved, not deleted — disabled by comment): type Point = { x: number; y: number };
+// ORIGINAL (preserved, not deleted — disabled by comment): type AgentNode = {
+// ORIGINAL (preserved, not deleted — disabled by comment): id: string;
+// ORIGINAL (preserved, not deleted — disabled by comment): name: string;
+// ORIGINAL (preserved, not deleted — disabled by comment): vendor: string;
+// ORIGINAL (preserved, not deleted — disabled by comment): model: string;
+// ORIGINAL (preserved, not deleted — disabled by comment): role: string;
+// ORIGINAL (preserved, not deleted — disabled by comment): status: AgentStatus;
+// ORIGINAL (preserved, not deleted — disabled by comment): tasks: number;
+// ORIGINAL (preserved, not deleted — disabled by comment): latency: string;
+// ORIGINAL (preserved, not deleted — disabled by comment): color: string;
+// ORIGINAL (preserved, not deleted — disabled by comment): icon: "codex" | "claude" | "hermes" | "openclaw" | "custom";
+// ORIGINAL (preserved, not deleted — disabled by comment): kind: "agent" | "model" | "tool" | "memory" | "task" | "service";
+// ORIGINAL (preserved, not deleted — disabled by comment): };
+// ORIGINAL (preserved, not deleted — disabled by comment): type AgentEdge = {
+// ORIGINAL (preserved, not deleted — disabled by comment): source: string;
+// ORIGINAL (preserved, not deleted — disabled by comment): target: string;
+// ORIGINAL (preserved, not deleted — disabled by comment): label: string;
+// ORIGINAL (preserved, not deleted — disabled by comment): strength: number;
+// ORIGINAL (preserved, not deleted — disabled by comment): };
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): const VIEW = { width: 900, height: 570 };
+// ORIGINAL (preserved, not deleted — disabled by comment): const INITIAL_AGENTS: AgentNode[] = [
+// ORIGINAL (preserved, not deleted — disabled by comment): {
+// ORIGINAL (preserved, not deleted — disabled by comment): id: "codex",
+// ORIGINAL (preserved, not deleted — disabled by comment): name: "Codex",
+// ORIGINAL (preserved, not deleted — disabled by comment): vendor: "OpenAI",
+// ORIGINAL (preserved, not deleted — disabled by comment): model: "Codex · demo adapter",
+// ORIGINAL (preserved, not deleted — disabled by comment): role: "Workflow coordinator",
+// ORIGINAL (preserved, not deleted — disabled by comment): status: "active",
+// ORIGINAL (preserved, not deleted — disabled by comment): tasks: 4,
+// ORIGINAL (preserved, not deleted — disabled by comment): latency: "42 ms",
+// ORIGINAL (preserved, not deleted — disabled by comment): color: "mint",
+// ORIGINAL (preserved, not deleted — disabled by comment): icon: "codex",
+// ORIGINAL (preserved, not deleted — disabled by comment): kind: "agent",
+// ORIGINAL (preserved, not deleted — disabled by comment): },
+// ORIGINAL (preserved, not deleted — disabled by comment): {
+// ORIGINAL (preserved, not deleted — disabled by comment): id: "claude",
+// ORIGINAL (preserved, not deleted — disabled by comment): name: "Claude Code",
+// ORIGINAL (preserved, not deleted — disabled by comment): vendor: "Anthropic",
+// ORIGINAL (preserved, not deleted — disabled by comment): model: "Claude · demo adapter",
+// ORIGINAL (preserved, not deleted — disabled by comment): role: "Code analysis",
+// ORIGINAL (preserved, not deleted — disabled by comment): status: "active",
+// ORIGINAL (preserved, not deleted — disabled by comment): tasks: 2,
+// ORIGINAL (preserved, not deleted — disabled by comment): latency: "58 ms",
+// ORIGINAL (preserved, not deleted — disabled by comment): color: "peach",
+// ORIGINAL (preserved, not deleted — disabled by comment): icon: "claude",
+// ORIGINAL (preserved, not deleted — disabled by comment): kind: "agent",
+// ORIGINAL (preserved, not deleted — disabled by comment): },
+// ORIGINAL (preserved, not deleted — disabled by comment): {
+// ORIGINAL (preserved, not deleted — disabled by comment): id: "hermes",
+// ORIGINAL (preserved, not deleted — disabled by comment): name: "Hermes Agent",
+// ORIGINAL (preserved, not deleted — disabled by comment): vendor: "Open source",
+// ORIGINAL (preserved, not deleted — disabled by comment): model: "Hermes · demo adapter",
+// ORIGINAL (preserved, not deleted — disabled by comment): role: "Task automation",
+// ORIGINAL (preserved, not deleted — disabled by comment): status: "idle",
+// ORIGINAL (preserved, not deleted — disabled by comment): tasks: 0,
+// ORIGINAL (preserved, not deleted — disabled by comment): latency: "—",
+// ORIGINAL (preserved, not deleted — disabled by comment): color: "violet",
+// ORIGINAL (preserved, not deleted — disabled by comment): icon: "hermes",
+// ORIGINAL (preserved, not deleted — disabled by comment): kind: "agent",
+// ORIGINAL (preserved, not deleted — disabled by comment): },
+// ORIGINAL (preserved, not deleted — disabled by comment): {
+// ORIGINAL (preserved, not deleted — disabled by comment): id: "openclaw",
+// ORIGINAL (preserved, not deleted — disabled by comment): name: "OpenClaw",
+// ORIGINAL (preserved, not deleted — disabled by comment): vendor: "Community",
+// ORIGINAL (preserved, not deleted — disabled by comment): model: "OpenClaw · demo adapter",
+// ORIGINAL (preserved, not deleted — disabled by comment): role: "Tool routing",
+// ORIGINAL (preserved, not deleted — disabled by comment): status: "attention",
+// ORIGINAL (preserved, not deleted — disabled by comment): tasks: 1,
+// ORIGINAL (preserved, not deleted — disabled by comment): latency: "91 ms",
+// ORIGINAL (preserved, not deleted — disabled by comment): color: "blue",
+// ORIGINAL (preserved, not deleted — disabled by comment): icon: "openclaw",
+// ORIGINAL (preserved, not deleted — disabled by comment): kind: "agent",
+// ORIGINAL (preserved, not deleted — disabled by comment): },
+// ORIGINAL (preserved, not deleted — disabled by comment): {
+// ORIGINAL (preserved, not deleted — disabled by comment): id: "custom",
+// ORIGINAL (preserved, not deleted — disabled by comment): name: "Custom adapter",
+// ORIGINAL (preserved, not deleted — disabled by comment): vendor: "Local example",
+// ORIGINAL (preserved, not deleted — disabled by comment): model: "Compatible endpoint · demo",
+// ORIGINAL (preserved, not deleted — disabled by comment): role: "Custom integration",
+// ORIGINAL (preserved, not deleted — disabled by comment): status: "paused",
+// ORIGINAL (preserved, not deleted — disabled by comment): tasks: 0,
+// ORIGINAL (preserved, not deleted — disabled by comment): latency: "—",
+// ORIGINAL (preserved, not deleted — disabled by comment): color: "gold",
+// ORIGINAL (preserved, not deleted — disabled by comment): icon: "custom",
+// ORIGINAL (preserved, not deleted — disabled by comment): kind: "agent",
+// ORIGINAL (preserved, not deleted — disabled by comment): },
+// ORIGINAL (preserved, not deleted — disabled by comment): {
+// ORIGINAL (preserved, not deleted — disabled by comment): id: "model",
+// ORIGINAL (preserved, not deleted — disabled by comment): name: "Reasoning model",
+// ORIGINAL (preserved, not deleted — disabled by comment): vendor: "MODEL",
+// ORIGINAL (preserved, not deleted — disabled by comment): model: "Local demo model",
+// ORIGINAL (preserved, not deleted — disabled by comment): role: "Language model",
+// ORIGINAL (preserved, not deleted — disabled by comment): status: "active",
+// ORIGINAL (preserved, not deleted — disabled by comment): tasks: 3,
+// ORIGINAL (preserved, not deleted — disabled by comment): latency: "36 ms",
+// ORIGINAL (preserved, not deleted — disabled by comment): color: "violet",
+// ORIGINAL (preserved, not deleted — disabled by comment): icon: "custom",
+// ORIGINAL (preserved, not deleted — disabled by comment): kind: "model",
+// ORIGINAL (preserved, not deleted — disabled by comment): },
+// ORIGINAL (preserved, not deleted — disabled by comment): {
+// ORIGINAL (preserved, not deleted — disabled by comment): id: "tools",
+// ORIGINAL (preserved, not deleted — disabled by comment): name: "Tool registry",
+// ORIGINAL (preserved, not deleted — disabled by comment): vendor: "TOOLS",
+// ORIGINAL (preserved, not deleted — disabled by comment): model: "Local demo tools",
+// ORIGINAL (preserved, not deleted — disabled by comment): role: "Tool collection",
+// ORIGINAL (preserved, not deleted — disabled by comment): status: "idle",
+// ORIGINAL (preserved, not deleted — disabled by comment): tasks: 0,
+// ORIGINAL (preserved, not deleted — disabled by comment): latency: "—",
+// ORIGINAL (preserved, not deleted — disabled by comment): color: "blue",
+// ORIGINAL (preserved, not deleted — disabled by comment): icon: "openclaw",
+// ORIGINAL (preserved, not deleted — disabled by comment): kind: "tool",
+// ORIGINAL (preserved, not deleted — disabled by comment): },
+// ORIGINAL (preserved, not deleted — disabled by comment): {
+// ORIGINAL (preserved, not deleted — disabled by comment): id: "memory",
+// ORIGINAL (preserved, not deleted — disabled by comment): name: "Shared context",
+// ORIGINAL (preserved, not deleted — disabled by comment): vendor: "MEMORY",
+// ORIGINAL (preserved, not deleted — disabled by comment): model: "Session memory · demo",
+// ORIGINAL (preserved, not deleted — disabled by comment): role: "Context store",
+// ORIGINAL (preserved, not deleted — disabled by comment): status: "active",
+// ORIGINAL (preserved, not deleted — disabled by comment): tasks: 2,
+// ORIGINAL (preserved, not deleted — disabled by comment): latency: "12 ms",
+// ORIGINAL (preserved, not deleted — disabled by comment): color: "gold",
+// ORIGINAL (preserved, not deleted — disabled by comment): icon: "hermes",
+// ORIGINAL (preserved, not deleted — disabled by comment): kind: "memory",
+// ORIGINAL (preserved, not deleted — disabled by comment): },
+// ORIGINAL (preserved, not deleted — disabled by comment): {
+// ORIGINAL (preserved, not deleted — disabled by comment): id: "task",
+// ORIGINAL (preserved, not deleted — disabled by comment): name: "Build review",
+// ORIGINAL (preserved, not deleted — disabled by comment): vendor: "TASK",
+// ORIGINAL (preserved, not deleted — disabled by comment): model: "Sample job · demo",
+// ORIGINAL (preserved, not deleted — disabled by comment): role: "Queued task",
+// ORIGINAL (preserved, not deleted — disabled by comment): status: "idle",
+// ORIGINAL (preserved, not deleted — disabled by comment): tasks: 1,
+// ORIGINAL (preserved, not deleted — disabled by comment): latency: "—",
+// ORIGINAL (preserved, not deleted — disabled by comment): color: "peach",
+// ORIGINAL (preserved, not deleted — disabled by comment): icon: "claude",
+// ORIGINAL (preserved, not deleted — disabled by comment): kind: "task",
+// ORIGINAL (preserved, not deleted — disabled by comment): },
+// ORIGINAL (preserved, not deleted — disabled by comment): {
+// ORIGINAL (preserved, not deleted — disabled by comment): id: "service",
+// ORIGINAL (preserved, not deleted — disabled by comment): name: "Git provider",
+// ORIGINAL (preserved, not deleted — disabled by comment): vendor: "SERVICE",
+// ORIGINAL (preserved, not deleted — disabled by comment): model: "External service · illustrative",
+// ORIGINAL (preserved, not deleted — disabled by comment): role: "Integration endpoint",
+// ORIGINAL (preserved, not deleted — disabled by comment): status: "paused",
+// ORIGINAL (preserved, not deleted — disabled by comment): tasks: 0,
+// ORIGINAL (preserved, not deleted — disabled by comment): latency: "—",
+// ORIGINAL (preserved, not deleted — disabled by comment): color: "blue",
+// ORIGINAL (preserved, not deleted — disabled by comment): icon: "custom",
+// ORIGINAL (preserved, not deleted — disabled by comment): kind: "service",
+// ORIGINAL (preserved, not deleted — disabled by comment): },
+// ORIGINAL (preserved, not deleted — disabled by comment): ];
+// ORIGINAL (preserved, not deleted — disabled by comment): const INITIAL_EDGES: AgentEdge[] = [
+// ORIGINAL (preserved, not deleted — disabled by comment): {
+// ORIGINAL (preserved, not deleted — disabled by comment): source: "codex",
+// ORIGINAL (preserved, not deleted — disabled by comment): target: "claude",
+// ORIGINAL (preserved, not deleted — disabled by comment): label: "delegates review",
+// ORIGINAL (preserved, not deleted — disabled by comment): strength: 0.82,
+// ORIGINAL (preserved, not deleted — disabled by comment): },
+// ORIGINAL (preserved, not deleted — disabled by comment): { source: "codex", target: "hermes", label: "routes tasks", strength: 0.62 },
+// ORIGINAL (preserved, not deleted — disabled by comment): {
+// ORIGINAL (preserved, not deleted — disabled by comment): source: "codex",
+// ORIGINAL (preserved, not deleted — disabled by comment): target: "openclaw",
+// ORIGINAL (preserved, not deleted — disabled by comment): label: "shares context",
+// ORIGINAL (preserved, not deleted — disabled by comment): strength: 0.46,
+// ORIGINAL (preserved, not deleted — disabled by comment): },
+// ORIGINAL (preserved, not deleted — disabled by comment): { source: "claude", target: "custom", label: "handoff", strength: 0.34 },
+// ORIGINAL (preserved, not deleted — disabled by comment): { source: "hermes", target: "custom", label: "tool access", strength: 0.3 },
+// ORIGINAL (preserved, not deleted — disabled by comment): {
+// ORIGINAL (preserved, not deleted — disabled by comment): source: "openclaw",
+// ORIGINAL (preserved, not deleted — disabled by comment): target: "hermes",
+// ORIGINAL (preserved, not deleted — disabled by comment): label: "event bridge",
+// ORIGINAL (preserved, not deleted — disabled by comment): strength: 0.56,
+// ORIGINAL (preserved, not deleted — disabled by comment): },
+// ORIGINAL (preserved, not deleted — disabled by comment): { source: "codex", target: "model", label: "uses model", strength: 0.66 },
+// ORIGINAL (preserved, not deleted — disabled by comment): { source: "claude", target: "tools", label: "uses tools", strength: 0.42 },
+// ORIGINAL (preserved, not deleted — disabled by comment): {
+// ORIGINAL (preserved, not deleted — disabled by comment): source: "hermes",
+// ORIGINAL (preserved, not deleted — disabled by comment): target: "memory",
+// ORIGINAL (preserved, not deleted — disabled by comment): label: "reads context",
+// ORIGINAL (preserved, not deleted — disabled by comment): strength: 0.52,
+// ORIGINAL (preserved, not deleted — disabled by comment): },
+// ORIGINAL (preserved, not deleted — disabled by comment): { source: "codex", target: "task", label: "runs task", strength: 0.72 },
+// ORIGINAL (preserved, not deleted — disabled by comment): {
+// ORIGINAL (preserved, not deleted — disabled by comment): source: "task",
+// ORIGINAL (preserved, not deleted — disabled by comment): target: "service",
+// ORIGINAL (preserved, not deleted — disabled by comment): label: "requests change",
+// ORIGINAL (preserved, not deleted — disabled by comment): strength: 0.38,
+// ORIGINAL (preserved, not deleted — disabled by comment): },
+// ORIGINAL (preserved, not deleted — disabled by comment): ];
+// ORIGINAL (preserved, not deleted — disabled by comment): // ORIGINAL (preserved, not deleted — disabled by comment): const edges = INITIAL_EDGES;
+// ORIGINAL (preserved, not deleted — disabled by comment): const NETWORK_POSITIONS: Record
= {
+// ORIGINAL (preserved, not deleted — disabled by comment): codex: { x: 250, y: 280 },
+// ORIGINAL (preserved, not deleted — disabled by comment): claude: { x: 570, y: 145 },
+// ORIGINAL (preserved, not deleted — disabled by comment): hermes: { x: 675, y: 310 },
+// ORIGINAL (preserved, not deleted — disabled by comment): openclaw: { x: 515, y: 445 },
+// ORIGINAL (preserved, not deleted — disabled by comment): custom: { x: 225, y: 460 },
+// ORIGINAL (preserved, not deleted — disabled by comment): model: { x: 110, y: 155 },
+// ORIGINAL (preserved, not deleted — disabled by comment): tools: { x: 775, y: 165 },
+// ORIGINAL (preserved, not deleted — disabled by comment): memory: { x: 800, y: 480 },
+// ORIGINAL (preserved, not deleted — disabled by comment): task: { x: 345, y: 510 },
+// ORIGINAL (preserved, not deleted — disabled by comment): service: { x: 110, y: 390 },
+// ORIGINAL (preserved, not deleted — disabled by comment): };
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): function positionsFor(mode: GraphMode): Record {
+// ORIGINAL (preserved, not deleted — disabled by comment): if (mode === "network") return NETWORK_POSITIONS;
+// ORIGINAL (preserved, not deleted — disabled by comment): const center = { x: VIEW.width / 2, y: VIEW.height / 2 };
+// ORIGINAL (preserved, not deleted — disabled by comment): const radiusX = 255;
+// ORIGINAL (preserved, not deleted — disabled by comment): const radiusY = 190;
+// ORIGINAL (preserved, not deleted — disabled by comment): return Object.fromEntries(
+// ORIGINAL (preserved, not deleted — disabled by comment): INITIAL_AGENTS.map((agent) => {
+// ORIGINAL (preserved, not deleted — disabled by comment): if (agent.id === "codex") return [agent.id, center];
+// ORIGINAL (preserved, not deleted — disabled by comment): const peers = INITIAL_AGENTS.filter((item) => item.id !== "codex");
+// ORIGINAL (preserved, not deleted — disabled by comment): const spoke = peers.findIndex((item) => item.id === agent.id);
+// ORIGINAL (preserved, not deleted — disabled by comment): const angle = -Math.PI / 2 + (spoke * (Math.PI * 2)) / peers.length;
+// ORIGINAL (preserved, not deleted — disabled by comment): return [
+// ORIGINAL (preserved, not deleted — disabled by comment): agent.id,
+// ORIGINAL (preserved, not deleted — disabled by comment): {
+// ORIGINAL (preserved, not deleted — disabled by comment): x: center.x + Math.cos(angle) * radiusX,
+// ORIGINAL (preserved, not deleted — disabled by comment): y: center.y + Math.sin(angle) * radiusY,
+// ORIGINAL (preserved, not deleted — disabled by comment): },
+// ORIGINAL (preserved, not deleted — disabled by comment): ];
+// ORIGINAL (preserved, not deleted — disabled by comment): }),
+// ORIGINAL (preserved, not deleted — disabled by comment): );
+// ORIGINAL (preserved, not deleted — disabled by comment): }
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): function initialLayout(): AgentGraphLayout {
+// ORIGINAL (preserved, not deleted — disabled by comment): return {
+// ORIGINAL (preserved, not deleted — disabled by comment): positions: { network: NETWORK_POSITIONS, radial: positionsFor("radial") },
+// ORIGINAL (preserved, not deleted — disabled by comment): cameras: {
+// ORIGINAL (preserved, not deleted — disabled by comment): network: { x: 0, y: 0, zoom: 1 },
+// ORIGINAL (preserved, not deleted — disabled by comment): radial: { x: 0, y: 0, zoom: 1 },
+// ORIGINAL (preserved, not deleted — disabled by comment): },
+// ORIGINAL (preserved, not deleted — disabled by comment): };
+// ORIGINAL (preserved, not deleted — disabled by comment): }
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): function AgentGlyph({ icon }: { icon: AgentNode["icon"] }) {
+// ORIGINAL (preserved, not deleted — disabled by comment): if (icon === "codex") return ;
+// ORIGINAL (preserved, not deleted — disabled by comment): if (icon === "claude") return ;
+// ORIGINAL (preserved, not deleted — disabled by comment): if (icon === "hermes") return ;
+// ORIGINAL (preserved, not deleted — disabled by comment): if (icon === "openclaw") return ;
+// ORIGINAL (preserved, not deleted — disabled by comment): return ;
+// ORIGINAL (preserved, not deleted — disabled by comment): }
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): function edgePath(a: Point, b: Point) {
+// ORIGINAL (preserved, not deleted — disabled by comment): const middleX = (a.x + b.x) / 2;
+// ORIGINAL (preserved, not deleted — disabled by comment): const middleY = (a.y + b.y) / 2;
+// ORIGINAL (preserved, not deleted — disabled by comment): const curve = (b.x - a.x) * 0.08;
+// ORIGINAL (preserved, not deleted — disabled by comment): return `M ${a.x} ${a.y} Q ${middleX - curve} ${middleY + curve} ${b.x} ${b.y}`;
+// ORIGINAL (preserved, not deleted — disabled by comment): }
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): export function AgentsPage() {
+// ORIGINAL (preserved, not deleted — disabled by comment): const [agents, setAgents] = useState(INITIAL_AGENTS);
+// ORIGINAL (preserved, not deleted — disabled by comment): const [layout, setLayout] = useState(initialLayout);
+// ORIGINAL (preserved, not deleted — disabled by comment): const layoutEdited = useRef(false);
+// ORIGINAL (preserved, not deleted — disabled by comment): const [layoutLoaded, setLayoutLoaded] = useState(false);
+// ORIGINAL (preserved, not deleted — disabled by comment): const [mode, setMode] = useState("network");
+// ORIGINAL (preserved, not deleted — disabled by comment): const positions = layout.positions[mode];
+// ORIGINAL (preserved, not deleted — disabled by comment): const [selectedId, setSelectedId] = useState("codex");
+// ORIGINAL (preserved, not deleted — disabled by comment): const [zoom, setZoom] = useState(1);
+// ORIGINAL (preserved, not deleted — disabled by comment): const [pan, setPan] = useState({ x: 0, y: 0 });
+// ORIGINAL (preserved, not deleted — disabled by comment): const [live, setLive] = useState(true);
+// ORIGINAL (preserved, not deleted — disabled by comment): const svgRef = useRef(null);
+// ORIGINAL (preserved, not deleted — disabled by comment): const gesture = useRef<
+// ORIGINAL (preserved, not deleted — disabled by comment): | { type: "node"; id: string; pointer: Point }
+// ORIGINAL (preserved, not deleted — disabled by comment): | { type: "pan"; pointer: Point; pan: Point }
+// ORIGINAL (preserved, not deleted — disabled by comment): | null
+// ORIGINAL (preserved, not deleted — disabled by comment): >(null);
+// ORIGINAL (preserved, not deleted — disabled by comment): const selected = agents.find((agent) => agent.id === selectedId) ?? agents[0];
+// ORIGINAL (preserved, not deleted — disabled by comment): const selectedEdges = INITIAL_EDGES.filter(
+// ORIGINAL (preserved, not deleted — disabled by comment): (edge) => edge.source === selected.id || edge.target === selected.id,
+// ORIGINAL (preserved, not deleted — disabled by comment): );
+// ORIGINAL (preserved, not deleted — disabled by comment): useEffect(() => {
+// ORIGINAL (preserved, not deleted — disabled by comment): let active = true;
+// ORIGINAL (preserved, not deleted — disabled by comment): void readAgentGraphLayout(initialLayout()).then((saved) => {
+// ORIGINAL (preserved, not deleted — disabled by comment): if (!active) return;
+// ORIGINAL (preserved, not deleted — disabled by comment): if (!layoutEdited.current) setLayout(saved);
+// ORIGINAL (preserved, not deleted — disabled by comment): setLayoutLoaded(true);
+// ORIGINAL (preserved, not deleted — disabled by comment): });
+// ORIGINAL (preserved, not deleted — disabled by comment): return () => {
+// ORIGINAL (preserved, not deleted — disabled by comment): active = false;
+// ORIGINAL (preserved, not deleted — disabled by comment): };
+// ORIGINAL (preserved, not deleted — disabled by comment): }, []);
+// ORIGINAL (preserved, not deleted — disabled by comment): useEffect(() => {
+// ORIGINAL (preserved, not deleted — disabled by comment): if (!layoutLoaded) return;
+// ORIGINAL (preserved, not deleted — disabled by comment): const timeout = window.setTimeout(
+// ORIGINAL (preserved, not deleted — disabled by comment): () => void writeAgentGraphLayout(layout),
+// ORIGINAL (preserved, not deleted — disabled by comment): 250,
+// ORIGINAL (preserved, not deleted — disabled by comment): );
+// ORIGINAL (preserved, not deleted — disabled by comment): return () => window.clearTimeout(timeout);
+// ORIGINAL (preserved, not deleted — disabled by comment): }, [layout, layoutLoaded]);
+// ORIGINAL (preserved, not deleted — disabled by comment): useEffect(() => {
+// ORIGINAL (preserved, not deleted — disabled by comment): setZoom(layout.cameras[mode].zoom);
+// ORIGINAL (preserved, not deleted — disabled by comment): setPan({ x: layout.cameras[mode].x, y: layout.cameras[mode].y });
+// ORIGINAL (preserved, not deleted — disabled by comment): }, [layout.cameras, mode]);
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): const updateCamera = (next: Partial & { zoom?: number }) => {
+// ORIGINAL (preserved, not deleted — disabled by comment): layoutEdited.current = true;
+// ORIGINAL (preserved, not deleted — disabled by comment): setLayout((value) => ({
+// ORIGINAL (preserved, not deleted — disabled by comment): ...value,
+// ORIGINAL (preserved, not deleted — disabled by comment): cameras: {
+// ORIGINAL (preserved, not deleted — disabled by comment): ...value.cameras,
+// ORIGINAL (preserved, not deleted — disabled by comment): [mode]: { ...value.cameras[mode], ...next },
+// ORIGINAL (preserved, not deleted — disabled by comment): },
+// ORIGINAL (preserved, not deleted — disabled by comment): }));
+// ORIGINAL (preserved, not deleted — disabled by comment): };
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): const pointerPoint = useCallback(
+// ORIGINAL (preserved, not deleted — disabled by comment): (
+// ORIGINAL (preserved, not deleted — disabled by comment): event: ReactPointerEvent | ReactWheelEvent,
+// ORIGINAL (preserved, not deleted — disabled by comment): ) => {
+// ORIGINAL (preserved, not deleted — disabled by comment): const svg = svgRef.current;
+// ORIGINAL (preserved, not deleted — disabled by comment): if (!svg) return { x: 0, y: 0 };
+// ORIGINAL (preserved, not deleted — disabled by comment): const transform = svg.getScreenCTM()?.inverse();
+// ORIGINAL (preserved, not deleted — disabled by comment): if (!transform) return { x: 0, y: 0 };
+// ORIGINAL (preserved, not deleted — disabled by comment): const point = svg.createSVGPoint();
+// ORIGINAL (preserved, not deleted — disabled by comment): point.x = event.clientX;
+// ORIGINAL (preserved, not deleted — disabled by comment): point.y = event.clientY;
+// ORIGINAL (preserved, not deleted — disabled by comment): const local = point.matrixTransform(transform);
+// ORIGINAL (preserved, not deleted — disabled by comment): return { x: local.x, y: local.y };
+// ORIGINAL (preserved, not deleted — disabled by comment): },
+// ORIGINAL (preserved, not deleted — disabled by comment): [],
+// ORIGINAL (preserved, not deleted — disabled by comment): );
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): const onPointerDown = (event: ReactPointerEvent) => {
+// ORIGINAL (preserved, not deleted — disabled by comment): if (event.button !== 0) return;
+// ORIGINAL (preserved, not deleted — disabled by comment): const target = (event.target as Element).closest(
+// ORIGINAL (preserved, not deleted — disabled by comment): "[data-agent-id]",
+// ORIGINAL (preserved, not deleted — disabled by comment): );
+// ORIGINAL (preserved, not deleted — disabled by comment): const pointer = pointerPoint(event);
+// ORIGINAL (preserved, not deleted — disabled by comment): if (target?.dataset.agentId) {
+// ORIGINAL (preserved, not deleted — disabled by comment): const id = target.dataset.agentId;
+// ORIGINAL (preserved, not deleted — disabled by comment): setSelectedId(id);
+// ORIGINAL (preserved, not deleted — disabled by comment): gesture.current = { type: "node", id, pointer };
+// ORIGINAL (preserved, not deleted — disabled by comment): } else {
+// ORIGINAL (preserved, not deleted — disabled by comment): gesture.current = { type: "pan", pointer, pan };
+// ORIGINAL (preserved, not deleted — disabled by comment): }
+// ORIGINAL (preserved, not deleted — disabled by comment): event.currentTarget.setPointerCapture(event.pointerId);
+// ORIGINAL (preserved, not deleted — disabled by comment): };
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): const onPointerMove = (event: ReactPointerEvent) => {
+// ORIGINAL (preserved, not deleted — disabled by comment): const active = gesture.current;
+// ORIGINAL (preserved, not deleted — disabled by comment): if (!active) return;
+// ORIGINAL (preserved, not deleted — disabled by comment): const current = pointerPoint(event);
+// ORIGINAL (preserved, not deleted — disabled by comment): const delta = {
+// ORIGINAL (preserved, not deleted — disabled by comment): x: current.x - active.pointer.x,
+// ORIGINAL (preserved, not deleted — disabled by comment): y: current.y - active.pointer.y,
+// ORIGINAL (preserved, not deleted — disabled by comment): };
+// ORIGINAL (preserved, not deleted — disabled by comment): if (active.type === "node") {
+// ORIGINAL (preserved, not deleted — disabled by comment): layoutEdited.current = true;
+// ORIGINAL (preserved, not deleted — disabled by comment): setLayout((previous) => {
+// ORIGINAL (preserved, not deleted — disabled by comment): const original = previous.positions[mode][active.id];
+// ORIGINAL (preserved, not deleted — disabled by comment): return {
+// ORIGINAL (preserved, not deleted — disabled by comment): ...previous,
+// ORIGINAL (preserved, not deleted — disabled by comment): positions: {
+// ORIGINAL (preserved, not deleted — disabled by comment): ...previous.positions,
+// ORIGINAL (preserved, not deleted — disabled by comment): [mode]: {
+// ORIGINAL (preserved, not deleted — disabled by comment): ...previous.positions[mode],
+// ORIGINAL (preserved, not deleted — disabled by comment): [active.id]: {
+// ORIGINAL (preserved, not deleted — disabled by comment): x: original.x + delta.x / zoom,
+// ORIGINAL (preserved, not deleted — disabled by comment): y: original.y + delta.y / zoom,
+// ORIGINAL (preserved, not deleted — disabled by comment): },
+// ORIGINAL (preserved, not deleted — disabled by comment): },
+// ORIGINAL (preserved, not deleted — disabled by comment): },
+// ORIGINAL (preserved, not deleted — disabled by comment): };
+// ORIGINAL (preserved, not deleted — disabled by comment): });
+// ORIGINAL (preserved, not deleted — disabled by comment): active.pointer = current;
+// ORIGINAL (preserved, not deleted — disabled by comment): } else {
+// ORIGINAL (preserved, not deleted — disabled by comment): const next = { x: active.pan.x + delta.x, y: active.pan.y + delta.y };
+// ORIGINAL (preserved, not deleted — disabled by comment): setPan(next);
+// ORIGINAL (preserved, not deleted — disabled by comment): updateCamera(next);
+// ORIGINAL (preserved, not deleted — disabled by comment): }
+// ORIGINAL (preserved, not deleted — disabled by comment): };
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): const onWheel = (event: ReactWheelEvent) => {
+// ORIGINAL (preserved, not deleted — disabled by comment): event.preventDefault();
+// ORIGINAL (preserved, not deleted — disabled by comment): setZoom((current) => {
+// ORIGINAL (preserved, not deleted — disabled by comment): const next = Math.max(
+// ORIGINAL (preserved, not deleted — disabled by comment): 0.65,
+// ORIGINAL (preserved, not deleted — disabled by comment): Math.min(1.8, current * (event.deltaY < 0 ? 1.08 : 0.92)),
+// ORIGINAL (preserved, not deleted — disabled by comment): );
+// ORIGINAL (preserved, not deleted — disabled by comment): updateCamera({ zoom: next });
+// ORIGINAL (preserved, not deleted — disabled by comment): return next;
+// ORIGINAL (preserved, not deleted — disabled by comment): });
+// ORIGINAL (preserved, not deleted — disabled by comment): };
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): /* ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): const onGraphKeyDown = (event: ReactKeyboardEvent) => {
+// ORIGINAL (preserved, not deleted — disabled by comment): const step = event.shiftKey ? 40 : 18;
+// ORIGINAL (preserved, not deleted — disabled by comment): if (event.key === "ArrowLeft")
+// ORIGINAL (preserved, not deleted — disabled by comment): setPan((value) => ({ ...value, x: value.x + step }));
+// ORIGINAL (preserved, not deleted — disabled by comment): else if (event.key === "ArrowRight")
+// ORIGINAL (preserved, not deleted — disabled by comment): setPan((value) => ({ ...value, x: value.x - step }));
+// ORIGINAL (preserved, not deleted — disabled by comment): else if (event.key === "ArrowUp")
+// ORIGINAL (preserved, not deleted — disabled by comment): setPan((value) => ({ ...value, y: value.y + step }));
+// ORIGINAL (preserved, not deleted — disabled by comment): else if (event.key === "ArrowDown")
+// ORIGINAL (preserved, not deleted — disabled by comment): setPan((value) => ({ ...value, y: value.y - step }));
+// ORIGINAL (preserved, not deleted — disabled by comment): else if (event.key === "+" || event.key === "=")
+// ORIGINAL (preserved, not deleted — disabled by comment): setZoom((value) => Math.min(1.8, value + 0.1));
+// ORIGINAL (preserved, not deleted — disabled by comment): else if (event.key === "-") setZoom((value) => Math.max(0.65, value - 0.1));
+// ORIGINAL (preserved, not deleted — disabled by comment): else return;
+// ORIGINAL (preserved, not deleted — disabled by comment): event.preventDefault();
+// ORIGINAL (preserved, not deleted — disabled by comment): };
+// ORIGINAL (preserved, not deleted — disabled by comment): */
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): const setModeAndLayout = (nextMode: GraphMode) => {
+// ORIGINAL (preserved, not deleted — disabled by comment): setMode(nextMode);
+// ORIGINAL (preserved, not deleted — disabled by comment): setPan({ x: 0, y: 0 });
+// ORIGINAL (preserved, not deleted — disabled by comment): };
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): const toggleAgentStatus = () => {
+// ORIGINAL (preserved, not deleted — disabled by comment): setAgents((current) =>
+// ORIGINAL (preserved, not deleted — disabled by comment): current.map((agent) =>
+// ORIGINAL (preserved, not deleted — disabled by comment): agent.id === selected.id
+// ORIGINAL (preserved, not deleted — disabled by comment): ? { ...agent, status: agent.status === "paused" ? "idle" : "paused" }
+// ORIGINAL (preserved, not deleted — disabled by comment): : agent,
+// ORIGINAL (preserved, not deleted — disabled by comment): ),
+// ORIGINAL (preserved, not deleted — disabled by comment): );
+// ORIGINAL (preserved, not deleted — disabled by comment): };
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): return (
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): DEMO GRAPH
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): }
+// ORIGINAL (preserved, not deleted — disabled by comment): />
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): Agent network
+// ORIGINAL (preserved, not deleted — disabled by comment): Local sample topology · no external connections
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): ADAPTERS
+// ORIGINAL (preserved, not deleted — disabled by comment): {agents.length.toString().padStart(2, "0")}
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): DEMO LINKS
+// ORIGINAL (preserved, not deleted — disabled by comment): {INITIAL_EDGES.length.toString().padStart(2, "0")}
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): ACTIVE TASKS
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): {agents
+// ORIGINAL (preserved, not deleted — disabled by comment): .reduce((sum, agent) => sum + agent.tasks, 0)
+// ORIGINAL (preserved, not deleted — disabled by comment): .toString()
+// ORIGINAL (preserved, not deleted — disabled by comment): .padStart(2, "0")}
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): Local demo data
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): Connection map
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): Drag nodes or canvas to explore
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): setModeAndLayout("network")}
+// ORIGINAL (preserved, not deleted — disabled by comment): >
+// ORIGINAL (preserved, not deleted — disabled by comment): Network
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): setModeAndLayout("radial")}
+// ORIGINAL (preserved, not deleted — disabled by comment): >
+// ORIGINAL (preserved, not deleted — disabled by comment): Radial
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): {
+// ORIGINAL (preserved, not deleted — disabled by comment): gesture.current = null;
+// ORIGINAL (preserved, not deleted — disabled by comment): }}
+// ORIGINAL (preserved, not deleted — disabled by comment): onPointerCancel={() => {
+// ORIGINAL (preserved, not deleted — disabled by comment): gesture.current = null;
+// ORIGINAL (preserved, not deleted — disabled by comment): }}
+// ORIGINAL (preserved, not deleted — disabled by comment): onWheel={onWheel}
+// ORIGINAL (preserved, not deleted — disabled by comment): >
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): Demo AI agent relationship map
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): {INITIAL_EDGES.map((edge) => {
+// ORIGINAL (preserved, not deleted — disabled by comment): const from = positions[edge.source];
+// ORIGINAL (preserved, not deleted — disabled by comment): const to = positions[edge.target];
+// ORIGINAL (preserved, not deleted — disabled by comment): const highlighted =
+// ORIGINAL (preserved, not deleted — disabled by comment): edge.source === selected.id ||
+// ORIGINAL (preserved, not deleted — disabled by comment): edge.target === selected.id;
+// ORIGINAL (preserved, not deleted — disabled by comment): const labelX = (from.x + to.x) / 2;
+// ORIGINAL (preserved, not deleted — disabled by comment): const labelY = (from.y + to.y) / 2;
+// ORIGINAL (preserved, not deleted — disabled by comment): return (
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): {highlighted && (
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): {edge.label}
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): )}
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): );
+// ORIGINAL (preserved, not deleted — disabled by comment): })}
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): {agents.map((agent) => {
+// ORIGINAL (preserved, not deleted — disabled by comment): const point = positions[agent.id];
+// ORIGINAL (preserved, not deleted — disabled by comment): const isSelected = agent.id === selectedId;
+// ORIGINAL (preserved, not deleted — disabled by comment): return (
+// ORIGINAL (preserved, not deleted — disabled by comment): /* biome-ignore lint/a11y/useSemanticElements: SVG node geometry needs a keyboard-accessible button role. */
+// ORIGINAL (preserved, not deleted — disabled by comment): {
+// ORIGINAL (preserved, not deleted — disabled by comment): event.stopPropagation();
+// ORIGINAL (preserved, not deleted — disabled by comment): setSelectedId(agent.id);
+// ORIGINAL (preserved, not deleted — disabled by comment): }}
+// ORIGINAL (preserved, not deleted — disabled by comment): onKeyDown={(event) => {
+// ORIGINAL (preserved, not deleted — disabled by comment): if (event.key === "Enter" || event.key === " ") {
+// ORIGINAL (preserved, not deleted — disabled by comment): event.preventDefault();
+// ORIGINAL (preserved, not deleted — disabled by comment): setSelectedId(agent.id);
+// ORIGINAL (preserved, not deleted — disabled by comment): }
+// ORIGINAL (preserved, not deleted — disabled by comment): }}
+// ORIGINAL (preserved, not deleted — disabled by comment): >
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): {agent.kind.toUpperCase()}
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): {agent.name}
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): {agent.tasks} TASK{agent.tasks === 1 ? "" : "S"} ·{" "}
+// ORIGINAL (preserved, not deleted — disabled by comment): {agent.status.toUpperCase()}
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): );
+// ORIGINAL (preserved, not deleted — disabled by comment): })}
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): DRAG TO REPOSITION · WHEEL TO ZOOM · SELECT A NODE FOR DETAILS
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): Canvas controls
+// ORIGINAL (preserved, not deleted — disabled by comment): {
+// ORIGINAL (preserved, not deleted — disabled by comment): const next = { ...pan, x: pan.x + 24 };
+// ORIGINAL (preserved, not deleted — disabled by comment): setPan(next);
+// ORIGINAL (preserved, not deleted — disabled by comment): updateCamera(next);
+// ORIGINAL (preserved, not deleted — disabled by comment): }}
+// ORIGINAL (preserved, not deleted — disabled by comment): >
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): {
+// ORIGINAL (preserved, not deleted — disabled by comment): const next = { ...pan, x: pan.x - 24 };
+// ORIGINAL (preserved, not deleted — disabled by comment): setPan(next);
+// ORIGINAL (preserved, not deleted — disabled by comment): updateCamera(next);
+// ORIGINAL (preserved, not deleted — disabled by comment): }}
+// ORIGINAL (preserved, not deleted — disabled by comment): >
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): {
+// ORIGINAL (preserved, not deleted — disabled by comment): const next = { ...pan, y: pan.y + 24 };
+// ORIGINAL (preserved, not deleted — disabled by comment): setPan(next);
+// ORIGINAL (preserved, not deleted — disabled by comment): updateCamera(next);
+// ORIGINAL (preserved, not deleted — disabled by comment): }}
+// ORIGINAL (preserved, not deleted — disabled by comment): >
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): {
+// ORIGINAL (preserved, not deleted — disabled by comment): const next = { ...pan, y: pan.y - 24 };
+// ORIGINAL (preserved, not deleted — disabled by comment): setPan(next);
+// ORIGINAL (preserved, not deleted — disabled by comment): updateCamera(next);
+// ORIGINAL (preserved, not deleted — disabled by comment): }}
+// ORIGINAL (preserved, not deleted — disabled by comment): >
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): setZoom((value) => {
+// ORIGINAL (preserved, not deleted — disabled by comment): const next = Math.max(0.65, value - 0.1);
+// ORIGINAL (preserved, not deleted — disabled by comment): updateCamera({ zoom: next });
+// ORIGINAL (preserved, not deleted — disabled by comment): return next;
+// ORIGINAL (preserved, not deleted — disabled by comment): })
+// ORIGINAL (preserved, not deleted — disabled by comment): }
+// ORIGINAL (preserved, not deleted — disabled by comment): >
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): {Math.round(zoom * 100)}%
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): setZoom((value) => {
+// ORIGINAL (preserved, not deleted — disabled by comment): const next = Math.min(1.8, value + 0.1);
+// ORIGINAL (preserved, not deleted — disabled by comment): updateCamera({ zoom: next });
+// ORIGINAL (preserved, not deleted — disabled by comment): return next;
+// ORIGINAL (preserved, not deleted — disabled by comment): })
+// ORIGINAL (preserved, not deleted — disabled by comment): }
+// ORIGINAL (preserved, not deleted — disabled by comment): >
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): {
+// ORIGINAL (preserved, not deleted — disabled by comment): setZoom(1);
+// ORIGINAL (preserved, not deleted — disabled by comment): setPan({ x: 0, y: 0 });
+// ORIGINAL (preserved, not deleted — disabled by comment): layoutEdited.current = true;
+// ORIGINAL (preserved, not deleted — disabled by comment): setLayout((value) => ({
+// ORIGINAL (preserved, not deleted — disabled by comment): ...value,
+// ORIGINAL (preserved, not deleted — disabled by comment): positions: {
+// ORIGINAL (preserved, not deleted — disabled by comment): ...value.positions,
+// ORIGINAL (preserved, not deleted — disabled by comment): [mode]: positionsFor(mode),
+// ORIGINAL (preserved, not deleted — disabled by comment): },
+// ORIGINAL (preserved, not deleted — disabled by comment): cameras: {
+// ORIGINAL (preserved, not deleted — disabled by comment): ...value.cameras,
+// ORIGINAL (preserved, not deleted — disabled by comment): [mode]: { x: 0, y: 0, zoom: 1 },
+// ORIGINAL (preserved, not deleted — disabled by comment): },
+// ORIGINAL (preserved, not deleted — disabled by comment): }));
+// ORIGINAL (preserved, not deleted — disabled by comment): }}
+// ORIGINAL (preserved, not deleted — disabled by comment): >
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): Active
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): Idle
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): Needs attention
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): Lines show illustrative adapter relationships
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): ADAPTER INSPECTOR
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): {String(
+// ORIGINAL (preserved, not deleted — disabled by comment): agents.findIndex((agent) => agent.id === selected.id) + 1,
+// ORIGINAL (preserved, not deleted — disabled by comment): ).padStart(2, "0")}{" "}
+// ORIGINAL (preserved, not deleted — disabled by comment): / {String(agents.length).padStart(2, "0")}
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): {selected.status}
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): {selected.name}
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): {selected.vendor} · {selected.kind.toUpperCase()} ·{" "}
+// ORIGINAL (preserved, not deleted — disabled by comment): {selected.role}
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): MODEL / ADAPTER
+// ORIGINAL (preserved, not deleted — disabled by comment): {selected.model}
+// ORIGINAL (preserved, not deleted — disabled by comment): Configured for demonstration only
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): DEMO TASKS
+// ORIGINAL (preserved, not deleted — disabled by comment): {String(selected.tasks).padStart(2, "0")}
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): LATENCY SAMPLE
+// ORIGINAL (preserved, not deleted — disabled by comment): {selected.latency}
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): CONNECTED TO
+// ORIGINAL (preserved, not deleted — disabled by comment): {String(selectedEdges.length).padStart(2, "0")}
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): {selectedEdges.length ? (
+// ORIGINAL (preserved, not deleted — disabled by comment): selectedEdges.map((edge) => {
+// ORIGINAL (preserved, not deleted — disabled by comment): const otherId =
+// ORIGINAL (preserved, not deleted — disabled by comment): edge.source === selected.id ? edge.target : edge.source;
+// ORIGINAL (preserved, not deleted — disabled by comment): const other = agents.find((agent) => agent.id === otherId);
+// ORIGINAL (preserved, not deleted — disabled by comment): if (!other) return null;
+// ORIGINAL (preserved, not deleted — disabled by comment): return (
+// ORIGINAL (preserved, not deleted — disabled by comment):
setSelectedId(other.id)}
+// ORIGINAL (preserved, not deleted — disabled by comment): >
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): {other.name}
+// ORIGINAL (preserved, not deleted — disabled by comment): {edge.label}
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): );
+// ORIGINAL (preserved, not deleted — disabled by comment): })
+// ORIGINAL (preserved, not deleted — disabled by comment): ) : (
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): No demo connections are shown for this adapter.
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): )}
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): GRAPH ENTITIES
+// ORIGINAL (preserved, not deleted — disabled by comment): {String(agents.length).padStart(2, "0")}
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): {agents.map((agent) => (
+// ORIGINAL (preserved, not deleted — disabled by comment): setSelectedId(agent.id)}
+// ORIGINAL (preserved, not deleted — disabled by comment): >
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): {agent.name}
+// ORIGINAL (preserved, not deleted — disabled by comment): {agent.kind}
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): ))}
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): {selected.status === "paused"
+// ORIGINAL (preserved, not deleted — disabled by comment): ? "Resume demo adapter"
+// ORIGINAL (preserved, not deleted — disabled by comment): : "Pause demo adapter"}
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): Status changes are temporary and remain in this page session only.
+// ORIGINAL (preserved, not deleted — disabled by comment): No software is installed or contacted.
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): Sample graph data only {" "}
+// ORIGINAL (preserved, not deleted — disabled by comment): setLive((value) => !value)}
+// ORIGINAL (preserved, not deleted — disabled by comment): aria-pressed={live}
+// ORIGINAL (preserved, not deleted — disabled by comment): >
+// ORIGINAL (preserved, not deleted — disabled by comment): Motion {live ? "on" : "off"}
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): Decorative connection motion can be switched off.
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): );
+// ORIGINAL (preserved, not deleted — disabled by comment): }
diff --git a/dashboard/src/pages/Automation.css b/dashboard/src/pages/Automation.css
index 6d3ee94..e8d35fa 100644
--- a/dashboard/src/pages/Automation.css
+++ b/dashboard/src/pages/Automation.css
@@ -63,6 +63,136 @@
font-size: 11px;
font-weight: 600;
}
+.automation-name {
+ width: 190px;
+ border: 0;
+ padding: 0;
+ outline: 0;
+ background: transparent;
+ color: var(--text);
+ font: 600 11px inherit;
+}
+.automation-name:focus-visible {
+ outline: 1px solid #6bc8a1;
+ outline-offset: 3px;
+}
+.automation-palette {
+ display: flex;
+ gap: 6px;
+ overflow-x: auto;
+ padding: 8px 10px;
+ border-bottom: 1px solid var(--border);
+ scrollbar-width: thin;
+}
+.automation-palette button {
+ display: flex;
+ flex: 0 0 auto;
+ align-items: center;
+ gap: 5px;
+ border: 1px solid var(--border-light);
+ border-radius: 5px;
+ padding: 5px 7px;
+ background: #111b23;
+ color: #9aabb2;
+ font-size: 9px;
+ text-transform: capitalize;
+ cursor: pointer;
+}
+.automation-palette button:hover {
+ color: #bcebd7;
+ border-color: #3b6655;
+}
+.automation-reactflow {
+ height: 450px;
+ background:
+ radial-gradient(ellipse at 50% 42%, rgba(24, 65, 60, 0.14), transparent 61%),
+ #0a1117;
+}
+.automation-reactflow .react-flow__controls,
+.automation-reactflow .react-flow__minimap {
+ overflow: hidden;
+ border: 1px solid #2a3942;
+ border-radius: 7px;
+ background: #10191fdd;
+}
+.automation-reactflow .react-flow__controls-button {
+ background: #10191f;
+ fill: #91a1a8;
+ border-color: #2a3942;
+}
+.automation-reactflow .react-flow__node-default {
+ width: 188px;
+ border: 1px solid #35504e;
+ border-radius: 8px;
+ padding: 14px;
+ color: var(--text);
+ background: linear-gradient(150deg, #17232b, #101920);
+ box-shadow: 0 8px 24px #0005;
+}
+.automation-reactflow .react-flow__node-workflow {
+ width: 190px;
+ border: 1px solid #35504e;
+ border-radius: 8px;
+ color: var(--text);
+ background: linear-gradient(150deg, #17232b, #101920);
+ box-shadow: 0 8px 24px #0005;
+}
+.flow-card {
+ display: grid;
+ grid-template-columns: 18px 1fr;
+ gap: 5px 8px;
+ align-items: center;
+ padding: 12px;
+}
+.flow-card > span {
+ grid-row: span 2;
+ color: #7fd1ad;
+}
+.flow-card strong {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: 10px;
+}
+.flow-card small {
+ color: #82939a;
+ font-size: 8px;
+ text-transform: uppercase;
+}
+.automation-reactflow .react-flow__node-workflow.selected {
+ border-color: #58b38b;
+ box-shadow:
+ 0 0 0 1px #58b38b55,
+ 0 9px 26px #0008;
+}
+.automation-reactflow .react-flow__node-default.selected {
+ border-color: #58b38b;
+ box-shadow:
+ 0 0 0 1px #58b38b55,
+ 0 9px 26px #0008;
+}
+.automation-reactflow .react-flow__handle {
+ width: 8px;
+ height: 8px;
+ border: 1px solid #12231d;
+ background: #6dd7a8;
+}
+.automation-reactflow .react-flow__edge-path {
+ stroke: #68b896;
+ stroke-width: 1.8;
+}
+.automation-reactflow .react-flow__edge-text {
+ fill: #9bb0aa;
+ font-size: 10px;
+}
+.automation-issues {
+ color: #e4b56b;
+ font-size: 10px;
+ line-height: 1.5;
+}
+.automation-issues p {
+ margin: 8px 0;
+}
.automation-workflow-label small,
.automation-path-item small,
.preview-result small {
diff --git a/dashboard/src/pages/Automation.tsx b/dashboard/src/pages/Automation.tsx
index 3b43f67..83fc376 100644
--- a/dashboard/src/pages/Automation.tsx
+++ b/dashboard/src/pages/Automation.tsx
@@ -1,298 +1,338 @@
+import {
+ addEdge,
+ Background,
+ type Connection,
+ Controls,
+ type Edge,
+ Handle,
+ MiniMap,
+ type Node,
+ type NodeProps,
+ Position,
+ ReactFlow,
+ ReactFlowProvider,
+ useEdgesState,
+ useNodesState,
+ useReactFlow,
+} from "@xyflow/react";
+import "@xyflow/react/dist/style.css";
import {
Activity,
Bot,
- Check,
CirclePlay,
Clock3,
GitBranch,
- Grip,
- Minus,
- MousePointer2,
Plus,
RotateCcw,
- Search,
- Settings2,
+ Save,
Sparkles,
Webhook,
- X,
} from "lucide-react";
-import type { PointerEvent as ReactPointerEvent, WheelEvent } from "react";
-import { useState } from "react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import * as UI from "../components/ui";
+import {
+ assertWorkflowImportSize,
+ isConnectionValid,
+ loadWorkflow,
+ parseWorkflowImport,
+ recordWorkflowChange,
+ redoWorkflow,
+ saveWorkflow,
+ starterWorkflow,
+ undoWorkflow,
+ type Workflow,
+ type WorkflowHistory,
+ type WorkflowKind,
+ type WorkflowNode,
+ workflowIssues,
+} from "../data/workflows";
import "./Automation.css";
-type StepKind = "trigger" | "action" | "condition" | "agent";
-type StepStatus = "idle" | "running" | "complete" | "skipped";
-type WorkflowStep = {
- id: string;
+type FlowData = {
title: string;
- subtitle: string;
- kind: StepKind;
- x: number;
- y: number;
- status: StepStatus;
detail: string;
+ kind: WorkflowKind;
+ config: WorkflowNode["config"];
};
-
-const starterSteps: WorkflowStep[] = [
- {
- id: "trigger",
- title: "Schedule trigger",
- subtitle: "Every 15 minutes",
- kind: "trigger",
- x: 54,
- y: 210,
- status: "idle",
- detail: "Starts a sample health check on a fixed interval.",
- },
- {
- id: "inspect",
- title: "Inspect cluster",
- subtitle: "Read node health",
- kind: "action",
- x: 300,
- y: 210,
- status: "idle",
- detail: "Collects mock health values from the selected demo cluster.",
- },
- {
- id: "threshold",
- title: "Health below 80%?",
- subtitle: "Condition · 80%",
- kind: "condition",
- x: 548,
- y: 210,
- status: "idle",
- detail: "Routes the sample run based on a configurable threshold.",
- },
- {
- id: "notify",
- title: "Notify operator",
- subtitle: "Create demo alert",
- kind: "action",
- x: 806,
- y: 102,
- status: "idle",
- detail: "Adds a preview notification to this simulated workflow run.",
- },
- {
- id: "agent",
- title: "Summarize status",
- subtitle: "OpenCluster assistant",
- kind: "agent",
- x: 806,
- y: 326,
- status: "idle",
- detail: "Creates a local sample summary. No AI service is contacted.",
- },
-];
-
-const kindIcon = {
+const icons: Record = {
trigger: Webhook,
- action: Activity,
- condition: GitBranch,
+ schedule: Clock3,
agent: Bot,
+ condition: GitBranch,
+ delay: Clock3,
+ transform: Activity,
+ notify: Activity,
+ success: CirclePlay,
+ failure: Activity,
};
-
-const edges = [
- { from: "trigger", to: "inspect", label: "on schedule" },
- { from: "inspect", to: "threshold", label: "health sample" },
- { from: "threshold", to: "notify", label: "below threshold", branch: true },
- { from: "threshold", to: "agent", label: "healthy", branch: false },
+const palette: WorkflowKind[] = [
+ "trigger",
+ "schedule",
+ "agent",
+ "condition",
+ "delay",
+ "transform",
+ "notify",
+ "success",
+ "failure",
];
-
-const cardWidth = 188;
-const cardHeight = 92;
-const stageWidth = 1060;
-const stageHeight = 620;
-
-function StepCard({
- step,
- selected,
- onSelect,
- onDragStart,
- onMove,
-}: {
- step: WorkflowStep;
- selected: boolean;
- onSelect: () => void;
- onDragStart: (event: ReactPointerEvent) => void;
- onMove: (x: number, y: number) => void;
-}) {
- const Icon = kindIcon[step.kind];
+function WorkflowCard({ data }: NodeProps>) {
+ const Icon = icons[data.kind];
return (
- {
- const delta = event.shiftKey ? 20 : 8;
- const movement = {
- ArrowUp: { x: 0, y: -delta },
- ArrowDown: { x: 0, y: delta },
- ArrowLeft: { x: -delta, y: 0 },
- ArrowRight: { x: delta, y: 0 },
- }[event.key as "ArrowUp" | "ArrowDown" | "ArrowLeft" | "ArrowRight"];
- if (movement) {
- event.preventDefault();
- onMove(movement.x, movement.y);
- }
- }}
- aria-pressed={selected}
- aria-label={`${step.title}, ${step.kind} step, ${step.status}`}
- >
-
-
-
-
- {step.title}
- {step.subtitle}
-
-
- {step.status === "complete" ? (
-
- ) : step.status === "running" ? (
-
- ) : (
-
- )}
-
-
+ <>
+
+
+
+
+
+ {data.title}
+ {data.kind}
+
+
+ >
);
}
+const nodeTypes = { workflow: WorkflowCard };
+const asNodes = (flow: Workflow): Node[] =>
+ flow.nodes.map((n) => ({
+ id: n.id,
+ type: "workflow",
+ position: { x: n.x, y: n.y },
+ data: { title: n.title, detail: n.detail, kind: n.kind, config: n.config },
+ }));
+const asEdges = (flow: Workflow): Edge[] =>
+ flow.edges.map((e) => ({
+ id: e.id,
+ source: e.source,
+ target: e.target,
+ label: e.label,
+ type: "smoothstep",
+ }));
-export function AutomationPage() {
- const [steps, setSteps] = useState(starterSteps);
- const [selectedId, setSelectedId] = useState("inspect");
- const [pan, setPan] = useState({ x: 22, y: 20 });
- const [zoom, setZoom] = useState(0.92);
- const [isRunning, setIsRunning] = useState(false);
- const [runMessage, setRunMessage] = useState("Ready for a simulated run");
- const [runCount, setRunCount] = useState(0);
- const [dragState, setDragState] = useState<
- | {
- type: "pan";
- pointerX: number;
- pointerY: number;
- originX: number;
- originY: number;
- }
- | {
- type: "step";
- id: string;
- pointerX: number;
- pointerY: number;
- originX: number;
- originY: number;
- }
- | null
- >(null);
- const [inspectorOpen, setInspectorOpen] = useState(true);
- const selected = steps.find((step) => step.id === selectedId) ?? steps[0];
-
- const resetWorkflow = () => {
- setSteps(starterSteps);
- setRunMessage("Workflow reset to its sample state");
- setIsRunning(false);
- };
-
- const runWorkflow = () => {
- if (isRunning) return;
- setIsRunning(true);
- setRunCount((count) => count + 1);
- const sequence = ["trigger", "inspect", "threshold", "notify", "agent"];
- setSteps((current) => current.map((step) => ({ ...step, status: "idle" })));
- setRunMessage("Simulated run started · no jobs are dispatched");
- sequence.forEach((id, index) => {
- window.setTimeout(() => {
- setSteps((current) =>
- current.map((step) => ({
- ...step,
- status:
- step.id === id
- ? "running"
- : sequence.indexOf(step.id) < index
- ? "complete"
- : step.status,
- })),
- );
- }, 380 * index);
- });
- window.setTimeout(() => {
- setSteps((current) =>
- current.map((step) => ({ ...step, status: "complete" })),
+function Editor() {
+ const initial = useMemo(loadWorkflow, []);
+ const [nodes, setNodes, onNodesChange] = useNodesState>(
+ asNodes(starterWorkflow),
+ );
+ const [edges, setEdges, onEdgesChange] = useEdgesState(
+ asEdges(starterWorkflow),
+ );
+ const [selectedId, setSelectedId] = useState(
+ starterWorkflow.nodes[1]?.id ?? "",
+ );
+ const [name, setName] = useState(starterWorkflow.name);
+ const [hydrated, setHydrated] = useState(false);
+ const [message, setMessage] = useState(
+ "Changes are stored locally in this browser.",
+ );
+ const [running, setRunning] = useState(false);
+ const [runs, setRuns] = useState(0);
+ const [history, setHistory] = useState({
+ past: [],
+ future: [],
+ });
+ const { screenToFlowPosition } = useReactFlow();
+ const fileRef = useRef(null);
+ const selected = nodes.find((n) => n.id === selectedId);
+ const model = useMemo(
+ () => ({
+ version: 1,
+ name,
+ nodes: nodes.map((n) => ({
+ id: n.id,
+ kind: n.data.kind,
+ title: n.data.title,
+ detail: n.data.detail,
+ x: n.position.x,
+ y: n.position.y,
+ config: n.data.config,
+ })),
+ edges: edges.map((e) => ({
+ id: e.id,
+ source: e.source,
+ target: e.target,
+ ...(typeof e.label === "string" ? { label: e.label } : {}),
+ })),
+ }),
+ [name, nodes, edges],
+ );
+ const issues = useMemo(() => workflowIssues(model), [model]);
+ useEffect(() => {
+ let active = true;
+ initial
+ .then((flow) => {
+ if (!active) return;
+ setName(flow.name);
+ setNodes(asNodes(flow));
+ setEdges(asEdges(flow));
+ setSelectedId(flow.nodes[1]?.id ?? flow.nodes[0]?.id ?? "");
+ setHydrated(true);
+ })
+ .catch(() => {
+ if (active)
+ setMessage(
+ "Saved workflows could not be loaded; demo remains available.",
+ );
+ });
+ return () => {
+ active = false;
+ };
+ }, [initial, setNodes, setEdges]);
+ useEffect(() => {
+ if (hydrated)
+ void saveWorkflow(model).catch(() =>
+ setMessage("Could not save workflow to this browser."),
);
- setIsRunning(false);
- setRunMessage("Demo run completed · sample result only");
- }, 380 * sequence.length);
+ }, [hydrated, model]);
+ const snapshot = () =>
+ setHistory((items) => recordWorkflowChange(items, model));
+ const connect = useCallback(
+ (connection: Connection) => {
+ setHistory((items) => recordWorkflowChange(items, model));
+ setEdges((current) =>
+ addEdge(
+ { ...connection, id: crypto.randomUUID(), type: "smoothstep" },
+ current,
+ ),
+ );
+ },
+ [model, setEdges],
+ );
+ const update = (patch: Partial) => {
+ snapshot();
+ setNodes((current) =>
+ current.map((n) =>
+ n.id === selectedId ? { ...n, data: { ...n.data, ...patch } } : n,
+ ),
+ );
};
-
- const startPan = (event: ReactPointerEvent) => {
- if ((event.target as HTMLElement).closest(".automation-step")) return;
- event.currentTarget.setPointerCapture(event.pointerId);
- setDragState({
- type: "pan",
- pointerX: event.clientX,
- pointerY: event.clientY,
- originX: pan.x,
- originY: pan.y,
- });
+ const addNode = (kind: WorkflowKind, position?: { x: number; y: number }) => {
+ snapshot();
+ const id = kind + "-" + crypto.randomUUID().slice(0, 6);
+ const title = kind[0].toUpperCase() + kind.slice(1);
+ setNodes((current) => [
+ ...current,
+ {
+ id,
+ type: "workflow",
+ position: position ?? {
+ x: 160 + (current.length % 4) * 220,
+ y: 120 + Math.floor(current.length / 4) * 140,
+ },
+ data: { title, detail: title + " demo step.", kind, config: {} },
+ },
+ ]);
+ setSelectedId(id);
};
-
- const startStepDrag = (
- step: WorkflowStep,
- event: ReactPointerEvent,
- ) => {
- event.stopPropagation();
- event.currentTarget.setPointerCapture(event.pointerId);
- setSelectedId(step.id);
- setDragState({
- type: "step",
- id: step.id,
- pointerX: event.clientX,
- pointerY: event.clientY,
- originX: step.x,
- originY: step.y,
- });
+ const restore = (flow: Workflow) => {
+ setName(flow.name);
+ setNodes(asNodes(flow));
+ setEdges(asEdges(flow));
+ setSelectedId(flow.nodes[0]?.id ?? "");
};
-
- const movePointer = (event: ReactPointerEvent) => {
- if (!dragState) return;
- const dx =
- (event.clientX - dragState.pointerX) /
- (dragState.type === "pan" ? 1 : zoom);
- const dy =
- (event.clientY - dragState.pointerY) /
- (dragState.type === "pan" ? 1 : zoom);
- if (dragState.type === "pan")
- setPan({ x: dragState.originX + dx, y: dragState.originY + dy });
- else
- setSteps((current) =>
- current.map((step) =>
- step.id === dragState.id
- ? {
- ...step,
- x: Math.max(0, dragState.originX + dx),
- y: Math.max(0, dragState.originY + dy),
- }
- : step,
- ),
+ const undo = () => {
+ const result = undoWorkflow(history, model);
+ if (!result) return;
+ setHistory(result.history);
+ restore(result.workflow);
+ };
+ const redo = () => {
+ const result = redoWorkflow(history, model);
+ if (!result) return;
+ setHistory(result.history);
+ restore(result.workflow);
+ };
+ const exportFlow = () => {
+ const url = URL.createObjectURL(
+ new Blob([JSON.stringify(model, null, 2)], { type: "application/json" }),
+ );
+ const a = document.createElement("a");
+ a.href = url;
+ a.download =
+ (name.toLowerCase().replace(/[^a-z0-9]+/g, "-") || "workflow") + ".json";
+ a.click();
+ URL.revokeObjectURL(url);
+ };
+ const importFlow = async (file?: File) => {
+ if (!file) return;
+ try {
+ assertWorkflowImportSize(file.size);
+ const flow = parseWorkflowImport(await file.text());
+ snapshot();
+ setName(flow.name);
+ setNodes(asNodes(flow));
+ setEdges(asEdges(flow));
+ setSelectedId(flow.nodes[0]?.id ?? "");
+ setMessage("Workflow imported and validated.");
+ } catch (error) {
+ setMessage(
+ error instanceof Error ? error.message : "Unable to import workflow.",
);
+ } finally {
+ if (fileRef.current) fileRef.current.value = "";
+ }
};
-
- const onWheel = (event: WheelEvent) => {
- if (!event.ctrlKey && !event.metaKey) return;
- event.preventDefault();
- setZoom((value) =>
- Math.min(1.25, Math.max(0.55, value - event.deltaY * 0.001)),
+ const run = () => {
+ if (issues.length || running) return;
+ setRunning(true);
+ setRuns((n) => n + 1);
+ setMessage("Simulated preview · no jobs dispatched");
+ window.setTimeout(() => {
+ setRunning(false);
+ setMessage("Demo run completed · sample result only");
+ }, 1000);
+ };
+ const saveNow = () => {
+ void saveWorkflow(model)
+ .then(() => setMessage("Workflow saved in this browser."))
+ .catch(() => setMessage("Could not save workflow to this browser."));
+ };
+ const deleteSelected = () => {
+ if (!selected) return;
+ snapshot();
+ setNodes((current) => current.filter((n) => n.id !== selectedId));
+ setEdges((current) =>
+ current.filter((e) => e.source !== selectedId && e.target !== selectedId),
);
+ setSelectedId("");
+ };
+ const duplicateSelected = () => {
+ if (!selected) return;
+ snapshot();
+ const id = selected.id + "-copy-" + crypto.randomUUID().slice(0, 5);
+ setNodes((current) => [
+ ...current,
+ {
+ ...selected,
+ id,
+ selected: true,
+ position: { x: selected.position.x + 36, y: selected.position.y + 36 },
+ data: { ...selected.data, title: selected.data.title + " copy" },
+ },
+ ]);
+ setSelectedId(id);
+ };
+ const onDrop = (event: React.DragEvent) => {
+ event.preventDefault();
+ const kind = event.dataTransfer.getData(
+ "application/opencluster-workflow-kind",
+ ) as WorkflowKind;
+ if (palette.includes(kind))
+ addNode(
+ kind,
+ screenToFlowPosition({ x: event.clientX, y: event.clientY }),
+ );
+ };
+ const beforeDelete = async ({
+ nodes: removedNodes,
+ edges: removedEdges,
+ }: {
+ nodes: Node[];
+ edges: Edge[];
+ }) => {
+ if (removedNodes.length || removedEdges.length) snapshot();
+ if (removedNodes.some((node) => node.id === selectedId)) setSelectedId("");
+ return true;
};
return (
@@ -300,15 +340,15 @@ export function AutomationPage() {
0}
>
- {isRunning ? "Running demo…" : "Run demo"}
+ {running ? "Running demo…" : "Run demo"}
}
/>
@@ -320,8 +360,13 @@ export function AutomationPage() {
- Cluster health check
- Draft workflow · {steps.length} steps
+ setName(e.target.value)}
+ />
+ {nodes.length} nodes · local draft