| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>AgentGlancer β Hayula Agent Connectome Dashboard</title> |
| <style> |
| * { margin: 0; padding: 0; box-sizing: border-box; } |
| body { background: #0a0a0f; color: #e0e0e0; font-family: 'SF Mono', 'Fira Code', monospace; overflow: hidden; height: 100vh; } |
| #canvas { position: fixed; top: 0; left: 0; width: 100%; height: 100%; } |
| #overlay { position: fixed; top: 16px; left: 16px; z-index: 10; pointer-events: none; } |
| #overlay h1 { font-size: 20px; color: #7ec87e; margin-bottom: 4px; } |
| #overlay .sub { font-size: 11px; color: #666; } |
| #stats { position: fixed; bottom: 16px; left: 16px; z-index: 10; font-size: 11px; color: #888; line-height: 1.6; } |
| #legend { position: fixed; bottom: 16px; right: 16px; z-index: 10; font-size: 11px; color: #888; } |
| .legend-item { display: flex; align-items: center; gap: 6px; margin: 4px 0; } |
| .legend-dot { width: 8px; height: 8px; border-radius: 50%; } |
| .hub { background: #ff4444; box-shadow: 0 0 8px #ff4444; } |
| .normal { background: #4488ff; } |
| .orphan { background: #555; } |
| .bottleneck { background: #ff8800; box-shadow: 0 0 6px #ff8800; } |
| .worker { background: #44cc88; } |
| #tooltip { position: fixed; z-index: 20; pointer-events: none; background: #1a1a2e; border: 1px solid #333; padding: 10px 14px; border-radius: 6px; font-size: 12px; display: none; max-width: 280px; } |
| #tooltip .name { color: #7ec87e; font-size: 14px; font-weight: bold; } |
| #tooltip .row { display: flex; gap: 12px; margin: 2px 0; } |
| #tooltip .label { color: #666; } |
| #tooltip .val { color: #ccc; } |
| #alerts { position: fixed; top: 80px; right: 16px; z-index: 10; font-size: 11px; } |
| .alert { background: #331a1a; border: 1px solid #662222; color: #ff6666; padding: 6px 10px; border-radius: 4px; margin: 4px 0; animation: fadeIn 0.3s; } |
| @keyframes fadeIn { from { opacity: 0; transform: translateX(20px); } to { opacity: 1; transform: translateX(0); } } |
| .pulse { animation: pulse 2s infinite; } |
| @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } } |
| </style> |
| </head> |
| <body> |
| <canvas id="canvas"></canvas> |
|
|
| <div id="overlay"> |
| <h1>π§ AgentGlancer</h1> |
| <div class="sub">Hayula Agent Connectome β Live</div> |
| </div> |
|
|
| <div id="stats"> |
| <div id="agentCount">Agents: 0</div> |
| <div id="eventCount">Events: 0</div> |
| <div id="hubCount">Hubs: 0</div> |
| <div id="bneckCount">Bottlenecks: 0</div> |
| <div id="fps">FPS: 0</div> |
| </div> |
|
|
| <div id="alerts"></div> |
|
|
| <div id="legend"> |
| <div class="legend-item"><span class="legend-dot hub"></span> Hub (high degree)</div> |
| <div class="legend-item"><span class="legend-dot bottleneck"></span> Bottleneck</div> |
| <div class="legend-item"><span class="legend-dot normal"></span> Router</div> |
| <div class="legend-item"><span class="legend-dot worker"></span> Worker</div> |
| <div class="legend-item"><span class="legend-dot orphan"></span> Orphan</div> |
| </div> |
|
|
| <div id="tooltip"></div> |
|
|
| <script type="module"> |
| |
| |
| |
| |
| const canvas = document.getElementById('canvas'); |
| const ctx = canvas.getContext('2d'); |
| |
| |
| const CONNECTOME_URL = '/api/connectome'; |
| const REFRESH_MS = 2000; |
| const ANIMATION_MS = 16; |
| |
| |
| let agents = {}; |
| let edges = []; |
| let alerts = []; |
| let mouse = { x: 0, y: 0 }; |
| let camera = { x: 0, y: 0, zoom: 1, targetX: 0, targetY: 0 }; |
| let dragging = false; |
| let dragStart = { x: 0, y: 0 }; |
| let selectedAgent = null; |
| let lastFrameTime = 0; |
| let fpsCounter = 0; |
| let fpsDisplay = 0; |
| let fpsTimer = 0; |
| |
| |
| const COLORS = { |
| router: '#4488ff', |
| worker: '#44cc88', |
| verifier: '#cc44cc', |
| memory: '#cccc44', |
| observer: '#44cccc', |
| hub: '#ff4444', |
| bottleneck: '#ff8800', |
| orphan: '#555555', |
| unknown: '#888888', |
| }; |
| |
| |
| class AgentSimulation { |
| constructor() { |
| this.nodes = []; |
| this.edges = []; |
| } |
| |
| updateData(agentsData, edgesData) { |
| |
| const existingIds = new Set(this.nodes.map(n => n.id)); |
| for (const [id, data] of Object.entries(agentsData)) { |
| if (!existingIds.has(id)) { |
| this.nodes.push({ |
| id, |
| x: Math.random() * canvas.width, |
| y: Math.random() * canvas.height, |
| vx: 0, vy: 0, |
| type: data.type || 'unknown', |
| degree: data.degree || 0, |
| events: data.events_sent || 0, |
| skills: data.skills_used?.length || 0, |
| targetX: 0, targetY: 0, |
| }); |
| } else { |
| const node = this.nodes.find(n => n.id === id); |
| if (node) { |
| node.type = data.type || node.type; |
| node.degree = data.degree || node.degree; |
| node.events = data.events_sent || node.events; |
| node.skills = data.skills_used?.length || node.skills; |
| } |
| } |
| } |
| this.edges = edgesData.slice(-200); |
| } |
| |
| step() { |
| const cx = canvas.width / 2; |
| const cy = canvas.height / 2; |
| |
| |
| for (const node of this.nodes) { |
| |
| node.vx += (cx - node.x) * 0.0001; |
| node.vy += (cy - node.y) * 0.0001; |
| |
| |
| node.vx *= 0.95; |
| node.vy *= 0.95; |
| |
| node.x += node.vx; |
| node.y += node.vy; |
| |
| |
| node.x = Math.max(30, Math.min(canvas.width - 30, node.x)); |
| node.y = Math.max(30, Math.min(canvas.height - 30, node.y)); |
| } |
| |
| |
| for (let i = 0; i < this.nodes.length; i++) { |
| for (let j = i + 1; j < this.nodes.length; j++) { |
| const dx = this.nodes[i].x - this.nodes[j].x; |
| const dy = this.nodes[i].y - this.nodes[j].y; |
| const dist = Math.sqrt(dx * dx + dy * dy) || 1; |
| const force = 500 / (dist * dist); |
| const fx = dx * force; |
| const fy = dy * force; |
| this.nodes[i].vx += fx; |
| this.nodes[i].vy += fy; |
| this.nodes[j].vx -= fx; |
| this.nodes[j].vy -= fy; |
| } |
| } |
| |
| |
| for (const edge of this.edges) { |
| const from = this.nodes.find(n => n.id === edge.from); |
| const to = this.nodes.find(n => n.id === edge.to); |
| if (from && to) { |
| const dx = to.x - from.x; |
| const dy = to.y - from.y; |
| from.vx += dx * 0.001; |
| from.vy += dy * 0.001; |
| to.vx -= dx * 0.001; |
| to.vy -= dy * 0.001; |
| } |
| } |
| } |
| } |
| |
| const sim = new AgentSimulation(); |
| |
| |
| function render(timestamp) { |
| |
| fpsCounter++; |
| if (timestamp - fpsTimer > 1000) { |
| fpsDisplay = fpsCounter; |
| fpsCounter = 0; |
| fpsTimer = timestamp; |
| } |
| |
| |
| camera.x += (camera.targetX - camera.x) * 0.1; |
| camera.y += (camera.targetY - camera.y) * 0.1; |
| |
| ctx.save(); |
| ctx.clearRect(0, 0, canvas.width, canvas.height); |
| |
| |
| ctx.strokeStyle = '#111122'; |
| ctx.lineWidth = 0.5; |
| const gridSize = 40 * camera.zoom; |
| const gx = (camera.x % gridSize) - gridSize; |
| const gy = (camera.y % gridSize) - gridSize; |
| for (let x = gx; x < canvas.width + gridSize; x += gridSize) { |
| ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvas.height); ctx.stroke(); |
| } |
| for (let y = gy; y < canvas.height + gridSize; y += gridSize) { |
| ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvas.width, y); ctx.stroke(); |
| } |
| |
| |
| ctx.translate(canvas.width/2 - camera.x * camera.zoom, canvas.height/2 - camera.y * camera.zoom); |
| ctx.scale(camera.zoom, camera.zoom); |
| |
| |
| const edgeOpacity = {}; |
| for (const edge of sim.edges) { |
| const key = `${edge.from}-${edge.to}`; |
| edgeOpacity[key] = (edgeOpacity[key] || 0) + 0.15; |
| } |
| |
| for (const [key, opacity] of Object.entries(edgeOpacity)) { |
| const [from, to] = key.split('-'); |
| const fn = sim.nodes.find(n => n.id === from); |
| const tn = sim.nodes.find(n => n.id === to); |
| if (fn && tn) { |
| ctx.strokeStyle = `rgba(68, 136, 255, ${Math.min(opacity, 0.6)})`; |
| ctx.lineWidth = 0.5; |
| ctx.beginPath(); |
| ctx.moveTo(fn.x, fn.y); |
| ctx.lineTo(tn.x, tn.y); |
| ctx.stroke(); |
| } |
| } |
| |
| |
| for (const node of sim.nodes) { |
| const color = COLORS[node.type] || COLORS.unknown; |
| const radius = Math.min(6 + node.degree * 2, 20); |
| |
| |
| if (node.degree > 5) { |
| ctx.shadowColor = color; |
| ctx.shadowBlur = 12; |
| } |
| |
| |
| const isBneck = node.type === 'bottleneck'; |
| |
| ctx.beginPath(); |
| ctx.arc(node.x, node.y, radius, 0, Math.PI * 2); |
| ctx.fillStyle = color; |
| ctx.fill(); |
| |
| if (isBneck) { |
| ctx.shadowColor = '#ff8800'; |
| ctx.shadowBlur = 20 + Math.sin(timestamp * 0.005) * 8; |
| ctx.strokeStyle = '#ff8800'; |
| ctx.lineWidth = 2; |
| ctx.stroke(); |
| } |
| |
| |
| if (selectedAgent === node.id) { |
| ctx.strokeStyle = '#fff'; |
| ctx.lineWidth = 2; |
| ctx.stroke(); |
| } |
| |
| |
| ctx.shadowBlur = 0; |
| ctx.font = `${Math.max(8, 10/ camera.zoom)}px monospace`; |
| ctx.fillStyle = '#aaa'; |
| ctx.textAlign = 'center'; |
| ctx.fillText(node.id, node.x, node.y - radius - 4); |
| } |
| |
| ctx.restore(); |
| |
| |
| document.getElementById('agentCount').textContent = `Agents: ${sim.nodes.length}`; |
| document.getElementById('eventCount').textContent = `Edges: ${sim.edges.length}`; |
| const hubs = sim.nodes.filter(n => n.degree > 5).length; |
| document.getElementById('hubCount').textContent = `Hubs: ${hubs}`; |
| document.getElementById('bneckCount').textContent = `Bottlenecks: ${sim.nodes.filter(n => n.type === 'bottleneck').length}`; |
| document.getElementById('fps').textContent = `FPS: ${fpsDisplay}`; |
| |
| lastFrameTime = timestamp; |
| requestAnimationFrame(render); |
| } |
| |
| |
| canvas.addEventListener('mousedown', e => { |
| dragging = true; |
| dragStart = { x: e.clientX - camera.targetX, y: e.clientY - camera.targetY }; |
| }); |
| |
| canvas.addEventListener('mousemove', e => { |
| mouse.x = e.clientX; |
| mouse.y = e.clientY; |
| |
| if (dragging) { |
| camera.targetX = e.clientX - dragStart.x; |
| camera.targetY = e.clientY - dragStart.y; |
| } |
| |
| |
| const mx = (e.clientX - canvas.width/2) / camera.zoom + camera.x; |
| const my = (e.clientY - canvas.height/2) / camera.zoom + camera.y; |
| const hovered = sim.nodes.find(n => { |
| const dx = n.x - mx; |
| const dy = n.y - my; |
| return Math.sqrt(dx*dx + dy*dy) < 15; |
| }); |
| |
| const tt = document.getElementById('tooltip'); |
| if (hovered) { |
| tt.style.display = 'block'; |
| tt.style.left = (e.clientX + 15) + 'px'; |
| tt.style.top = (e.clientY + 15) + 'px'; |
| tt.innerHTML = ` |
| <div class="name">${hovered.id}</div> |
| <div class="row"><span class="label">Type:</span><span class="val">${hovered.type}</span></div> |
| <div class="row"><span class="label">Degree:</span><span class="val">${hovered.degree}</span></div> |
| <div class="row"><span class="label">Events:</span><span class="val">${hovered.events}</span></div> |
| <div class="row"><span class="label">Skills:</span><span class="val">${hovered.skills}</span></div> |
| `; |
| } else { |
| tt.style.display = 'none'; |
| } |
| }); |
| |
| canvas.addEventListener('mouseup', () => { dragging = false; }); |
| canvas.addEventListener('wheel', e => { |
| e.preventDefault(); |
| camera.zoom *= e.deltaY > 0 ? 0.9 : 1.1; |
| camera.zoom = Math.max(0.2, Math.min(5, camera.zoom)); |
| }); |
| |
| canvas.addEventListener('click', e => { |
| if (dragging) return; |
| const mx = (e.clientX - canvas.width/2) / camera.zoom + camera.x; |
| const my = (e.clientY - canvas.height/2) / camera.zoom + camera.y; |
| const clicked = sim.nodes.find(n => { |
| const dx = n.x - mx, dy = n.y - my; |
| return Math.sqrt(dx*dx + dy*dy) < 15; |
| }); |
| selectedAgent = clicked ? clicked.id : null; |
| |
| if (clicked) { |
| |
| camera.targetX = clicked.x; |
| camera.targetY = clicked.y; |
| camera.zoom = Math.min(3, camera.zoom + 0.5); |
| } |
| }); |
| |
| |
| canvas.addEventListener('touchstart', e => { |
| if (e.touches.length === 1) { |
| dragging = true; |
| dragStart = { x: e.touches[0].clientX - camera.targetX, y: e.touches[0].clientY - camera.targetY }; |
| } |
| }); |
| canvas.addEventListener('touchmove', e => { |
| if (dragging && e.touches.length === 1) { |
| camera.targetX = e.touches[0].clientX - dragStart.x; |
| camera.targetY = e.touches[0].clientY - dragStart.y; |
| } |
| e.preventDefault(); |
| }, { passive: false }); |
| canvas.addEventListener('touchend', () => { dragging = false; }); |
| |
| |
| async function fetchConnectome() { |
| try { |
| const resp = await fetch(CONNECTOME_URL); |
| if (!resp.ok) throw new Error('API not ready'); |
| const data = await resp.json(); |
| |
| |
| const agentsData = {}; |
| for (const agent of (data.agent_list || [])) { |
| agentsData[agent] = { type: 'unknown', degree: 0, events_sent: 0 }; |
| } |
| |
| |
| const hubNames = new Set((data.hubs || []).slice(0, 10).map(h => h.agent)); |
| const bneckNames = new Set((data.bottlenecks || []).map(b => b.agent)); |
| |
| for (const hub of (data.hubs || [])) { |
| if (agentsData[hub.agent]) { |
| agentsData[hub.agent].type = hubNames.has(hub.agent) ? 'hub' : 'router'; |
| agentsData[hub.agent].degree = hub.degree || 0; |
| } |
| } |
| |
| for (const bneck of (data.bottlenecks || [])) { |
| if (agentsData[bneck.agent]) { |
| agentsData[bneck.agent].type = 'bottleneck'; |
| } |
| } |
| |
| |
| const edgesData = []; |
| for (const path of (data.critical_paths || [])) { |
| const p = path.path || []; |
| for (let i = 0; i < p.length - 1; i++) { |
| edgesData.push({ from: p[i], to: p[i+1] }); |
| } |
| } |
| |
| sim.updateData(agentsData, edgesData); |
| |
| |
| if ((data.bottlenecks || []).length > 0 && alerts.length === 0) { |
| addAlert(`β οΈ ${data.bottlenecks.length} bottleneck(s) detected`); |
| } |
| } catch (e) { |
| |
| generateDemoData(); |
| } |
| } |
| |
| |
| function generateDemoData() { |
| const demoAgents = [ |
| 'rushd', 'wafa', 'awf', 'dragon', 'hermes', 'musa', 'zeus', 'haytham', |
| 'saif', 'averroes', 'orphanim', 'uta', 'bait', 'exploiter', 'devil', |
| '0xZeus', 'dragon-agent', 'dragon-sec', 'awf-agent', 'musa-writer' |
| ]; |
| |
| const agentsData = {}; |
| for (const name of demoAgents) { |
| const degree = Math.floor(Math.random() * 15) + 1; |
| agentsData[name] = { |
| type: degree > 10 ? 'hub' : degree > 7 ? 'router' : degree > 3 ? 'worker' : 'orphan', |
| degree, |
| events_sent: Math.floor(Math.random() * 100), |
| skills_used: new Set([`skill_${Math.floor(Math.random() * 5)}`]), |
| }; |
| } |
| |
| const edgesData = []; |
| for (let i = 0; i < 60; i++) { |
| const from = demoAgents[Math.floor(Math.random() * demoAgents.length)]; |
| const to = demoAgents[Math.floor(Math.random() * demoAgents.length)]; |
| if (from !== to) edgesData.push({ from, to }); |
| } |
| |
| sim.updateData(agentsData, edgesData); |
| |
| if (Math.random() < 0.1) { |
| addAlert('π‘ Demo mode β connect FFAM API for live data'); |
| } |
| } |
| |
| function addAlert(msg) { |
| if (alerts.includes(msg)) return; |
| alerts.push(msg); |
| const div = document.createElement('div'); |
| div.className = 'alert pulse'; |
| div.textContent = msg; |
| document.getElementById('alerts').appendChild(div); |
| setTimeout(() => { div.remove(); alerts = alerts.filter(a => a !== msg); }, 8000); |
| } |
| |
| |
| function resize() { |
| canvas.width = window.innerWidth; |
| canvas.height = window.innerHeight; |
| } |
| window.addEventListener('resize', resize); |
| resize(); |
| |
| |
| fetchConnectome(); |
| setInterval(fetchConnectome, REFRESH_MS); |
| setInterval(() => sim.step(), ANIMATION_MS); |
| requestAnimationFrame(render); |
| </script> |
| </body> |
| </html> |
|
|