agent-glancer / index.html
BinSaqban's picture
Upload index.html with huggingface_hub
d9afa36 verified
Raw
History Blame Contribute Delete
16.2 kB
<!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">
// ─── AgentGlancer β€” Live Multi-Agent Connectome Dashboard ───
// Inspired by Google's Neuroglancer, built for Hayula's 91 agents
// Uses Canvas 2D for ultra-fast rendering, WebSocket for live data
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// Configuration
const CONNECTOME_URL = '/api/connectome'; // Will fetch from local FFAM snapshots
const REFRESH_MS = 2000; // 2 second refresh
const ANIMATION_MS = 16; // 60fps
// State
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;
// ─── Agent type colors ───
const COLORS = {
router: '#4488ff',
worker: '#44cc88',
verifier: '#cc44cc',
memory: '#cccc44',
observer: '#44cccc',
hub: '#ff4444',
bottleneck: '#ff8800',
orphan: '#555555',
unknown: '#888888',
};
// ─── Physics simulation ───
class AgentSimulation {
constructor() {
this.nodes = [];
this.edges = [];
}
updateData(agentsData, edgesData) {
// Merge new agents
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); // Keep last 200 edges for rendering
}
step() {
const cx = canvas.width / 2;
const cy = canvas.height / 2;
// Force-directed layout
for (const node of this.nodes) {
// Center gravity
node.vx += (cx - node.x) * 0.0001;
node.vy += (cy - node.y) * 0.0001;
// Damping
node.vx *= 0.95;
node.vy *= 0.95;
node.x += node.vx;
node.y += node.vy;
// Bounds
node.x = Math.max(30, Math.min(canvas.width - 30, node.x));
node.y = Math.max(30, Math.min(canvas.height - 30, node.y));
}
// Repulsion between all nodes
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;
}
}
// Attraction along edges
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();
// ─── Render ───
function render(timestamp) {
// FPS
fpsCounter++;
if (timestamp - fpsTimer > 1000) {
fpsDisplay = fpsCounter;
fpsCounter = 0;
fpsTimer = timestamp;
}
// Smooth camera
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);
// Background grid
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();
}
// Transform
ctx.translate(canvas.width/2 - camera.x * camera.zoom, canvas.height/2 - camera.y * camera.zoom);
ctx.scale(camera.zoom, camera.zoom);
// Edges
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();
}
}
// Nodes
for (const node of sim.nodes) {
const color = COLORS[node.type] || COLORS.unknown;
const radius = Math.min(6 + node.degree * 2, 20);
// Glow for hubs
if (node.degree > 5) {
ctx.shadowColor = color;
ctx.shadowBlur = 12;
}
// Animated pulse for bottlenecks
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();
}
// Selected ring
if (selectedAgent === node.id) {
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();
}
// Label
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();
// Update stats
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);
}
// ─── Mouse/Touch interaction ───
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;
}
// Check hover
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) {
// Zoom to agent
camera.targetX = clicked.x;
camera.targetY = clicked.y;
camera.zoom = Math.min(3, camera.zoom + 0.5);
}
});
// Touch
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; });
// ─── Data fetching ───
async function fetchConnectome() {
try {
const resp = await fetch(CONNECTOME_URL);
if (!resp.ok) throw new Error('API not ready');
const data = await resp.json();
// Process agents
const agentsData = {};
for (const agent of (data.agent_list || [])) {
agentsData[agent] = { type: 'unknown', degree: 0, events_sent: 0 };
}
// Process hubs/bottlenecks for colors
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';
}
}
// Process edges (from event log or simulated)
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);
// Check for new alerts
if ((data.bottlenecks || []).length > 0 && alerts.length === 0) {
addAlert(`⚠️ ${data.bottlenecks.length} bottleneck(s) detected`);
}
} catch (e) {
// If API not connected, use demo mode
generateDemoData();
}
}
// ─── Demo mode (when FFAM API not connected) ───
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);
}
// ─── Init ───
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
window.addEventListener('resize', resize);
resize();
// Start
fetchConnectome();
setInterval(fetchConnectome, REFRESH_MS);
setInterval(() => sim.step(), ANIMATION_MS);
requestAnimationFrame(render);
</script>
</body>
</html>