nyk
feat(refactor): ready for manual QA after main sync (#274)
b6ecafa unverified
Raw
History Blame Contribute Delete
9.59 kB
'use client'
import { useCallback, useEffect, useState, useMemo } from 'react'
import {
ReactFlow,
Node,
Edge,
addEdge,
useNodesState,
useEdgesState,
Controls,
Background,
BackgroundVariant,
Connection,
} from '@xyflow/react'
import '@xyflow/react/dist/style.css'
import { Agent, Session } from '@/types'
import { sessionToAgent, generateNodePosition } from '@/lib/utils'
import { AgentCoreNode } from '@/components/ui/agent-core-node'
interface AgentNetworkProps {
agents: Agent[]
sessions: Session[]
}
// SVG icons for agent types (16x16, stroke-based)
function CrownIcon() {
return (
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className="w-4 h-4">
<path d="M2 12l1.5-7L6 8l2-5 2 5 2.5-3L14 12H2z" />
</svg>
)
}
function BotIcon() {
return (
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className="w-4 h-4">
<rect x="3" y="5" width="10" height="8" rx="1.5" />
<circle cx="6" cy="9" r="1" />
<circle cx="10" cy="9" r="1" />
<path d="M8 2v3M6 2h4" />
</svg>
)
}
function ClockIcon() {
return (
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className="w-4 h-4">
<circle cx="8" cy="8" r="6" />
<path d="M8 4.5v3.5l2.5 2" />
</svg>
)
}
function GroupIcon() {
return (
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className="w-4 h-4">
<circle cx="6" cy="5" r="2" />
<circle cx="11" cy="5" r="2" />
<path d="M2 13c0-2.2 1.8-4 4-4s4 1.8 4 4" />
<path d="M13.5 13c0-2.2-1.5-4-3-4" />
</svg>
)
}
function FileIcon() {
return (
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className="w-4 h-4">
<path d="M4 1h5l4 4v9a1 1 0 01-1 1H4a1 1 0 01-1-1V2a1 1 0 011-1z" />
<path d="M9 1v4h4" />
</svg>
)
}
// Custom node component for agents
function AgentNode({ data }: { data: any }) {
const { agent, status } = data
const getStatusClasses = () => {
switch (status) {
case 'active': return 'border-void-cyan glow-cyan'
case 'idle': return 'border-void-amber/50'
case 'error': return 'border-void-crimson badge-glow-error'
default: return 'border-border'
}
}
const getTypeIcon = () => {
switch (agent.type) {
case 'main': return <CrownIcon />
case 'subagent': return <BotIcon />
case 'cron': return <ClockIcon />
case 'group': return <GroupIcon />
default: return <FileIcon />
}
}
const getRoleBadge = () => {
switch (agent.type) {
case 'main':
return { label: 'LEAD', color: 'bg-void-violet/20 text-void-violet border-void-violet/30' }
case 'subagent':
return { label: 'WORKER', color: 'bg-void-cyan/20 text-void-cyan border-void-cyan/30' }
case 'cron':
return { label: 'CRON', color: 'bg-void-amber/20 text-void-amber border-void-amber/30' }
default:
return { label: 'SYSTEM', color: 'bg-muted text-muted-foreground border-border' }
}
}
const roleBadge = getRoleBadge()
const isWorking = status === 'active'
return (
<div className={`void-panel px-3 py-3 border-2 ${getStatusClasses()} min-w-[140px]`}>
<div className="flex items-start justify-between">
<span className={`text-void-cyan ${isWorking ? 'animate-glow-pulse' : ''}`}>
{getTypeIcon()}
</span>
{isWorking && (
<span className="px-1.5 py-0.5 text-xs font-bold font-mono bg-void-mint/20 text-void-mint border border-void-mint/30 rounded-full animate-pulse">
WORKING
</span>
)}
</div>
<div className="mt-2">
<div className="flex items-center space-x-1 mb-1">
<div className="font-medium text-foreground text-sm truncate">
{agent.name}
</div>
<span className={`px-1.5 py-0.5 text-xs font-bold font-mono border rounded-full ${roleBadge.color}`}>
{roleBadge.label}
</span>
</div>
<div className="text-xs font-mono text-muted-foreground truncate">
{(typeof agent.model === 'string' ? agent.model : '').split('/').pop() || 'unknown'}
</div>
{agent.session && (
<div className="text-xs font-mono text-muted-foreground/70 mt-1 truncate">
{agent.session.key.split(':').pop()}
</div>
)}
</div>
</div>
)
}
const nodeTypes = {
agent: AgentNode,
core: AgentCoreNode,
}
export function AgentNetwork({ agents, sessions }: AgentNetworkProps) {
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([])
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([])
// Convert sessions to nodes and edges
const { nodeData, edgeData } = useMemo(() => {
const agentList = sessions.map(sessionToAgent)
// Add CORE hub node at center
const coreNode: Node = {
id: '__core__',
type: 'core',
position: { x: 0, y: 0 },
data: { label: 'CORE', agentCount: agentList.length },
style: { background: 'transparent', border: 'none' },
}
const agentNodes: Node[] = agentList.map((agent, index) => ({
id: agent.id,
type: 'agent',
position: generateNodePosition(index, agentList.length),
data: {
agent,
status: agent.status,
label: agent.name
},
style: {
background: 'transparent',
border: 'none',
}
}))
const nodes = [coreNode, ...agentNodes]
// Create edges — all agents connect to CORE, plus hierarchical edges
const edges: Edge[] = []
const cyanStroke = 'hsl(var(--void-cyan))'
const cyanDimStroke = 'hsl(var(--void-cyan) / 0.4)'
const amberStroke = 'hsl(var(--void-amber) / 0.5)'
const mainAgents = agentList.filter(a => a.type === 'main')
const subagents = agentList.filter(a => a.type === 'subagent')
const cronAgents = agentList.filter(a => a.type === 'cron')
// Connect all agents to CORE hub
agentList.forEach(agent => {
edges.push({
id: `core-${agent.id}`,
source: '__core__',
target: agent.id,
animated: agent.status === 'active',
style: {
stroke: agent.status === 'active' ? cyanStroke : cyanDimStroke,
strokeWidth: 1.5,
},
type: 'smoothstep',
})
})
// Connect main agents to subagents
mainAgents.forEach(main => {
subagents.forEach(sub => {
edges.push({
id: `${main.id}-${sub.id}`,
source: main.id,
target: sub.id,
animated: sub.status === 'active',
style: {
stroke: cyanStroke,
strokeWidth: 2,
},
type: 'smoothstep'
})
})
// Connect main agents to cron jobs
cronAgents.forEach(cron => {
edges.push({
id: `${main.id}-${cron.id}`,
source: main.id,
target: cron.id,
animated: false,
style: {
stroke: amberStroke,
strokeWidth: 1,
strokeDasharray: '5,5',
},
type: 'smoothstep'
})
})
})
return { nodeData: nodes, edgeData: edges }
}, [sessions])
useEffect(() => {
setNodes(nodeData)
setEdges(edgeData)
}, [nodeData, edgeData, setNodes, setEdges])
const onConnect = useCallback(
(params: Connection) => setEdges((eds) => addEdge(params, eds)),
[setEdges]
)
if (sessions.length === 0) {
return (
<div className="void-panel h-96 flex items-center justify-center">
<div className="text-center text-muted-foreground">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className="w-10 h-10 mx-auto mb-2 text-void-cyan/40">
<circle cx="4" cy="4" r="2" />
<circle cx="12" cy="4" r="2" />
<circle cx="4" cy="12" r="2" />
<circle cx="12" cy="12" r="2" />
<path d="M6 4h4M4 6v4M12 6v4M6 12h4" />
</svg>
<p>No agent network to display</p>
<p className="text-xs mt-1">Agent connections will appear here</p>
</div>
</div>
)
}
return (
<div className="void-panel">
<div className="p-4 border-b border-border">
<h3 className="font-semibold text-foreground">Agent Network</h3>
<p className="text-sm text-muted-foreground">
Visual representation of agent relationships
</p>
</div>
<div className="h-96">
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
nodeTypes={nodeTypes}
fitView
className="bg-transparent"
>
<Controls
style={{
background: 'hsl(var(--surface-1))',
border: '1px solid hsl(var(--surface-3))',
borderRadius: '10px',
}}
/>
<Background
variant={BackgroundVariant.Dots}
gap={40}
size={0.6}
color="hsl(var(--void-cyan) / 0.12)"
/>
</ReactFlow>
</div>
</div>
)
}