devflow-ai
Version:
Enterprise-grade AI agent orchestration with swarm management UI dashboard
680 lines (598 loc) • 21.9 kB
HTML
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DevFlow Dashboard</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #333;
min-height: 100vh;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
header {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 15px;
padding: 20px 30px;
margin-bottom: 30px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
h1 {
color: #667eea;
font-size: 28px;
display: flex;
align-items: center;
gap: 10px;
}
.status {
display: flex;
gap: 20px;
align-items: center;
}
.status-indicator {
display: flex;
align-items: center;
gap: 5px;
padding: 8px 15px;
background: #f0f0f0;
border-radius: 20px;
font-size: 14px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #4caf50;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.dashboard {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 20px;
}
.card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 15px;
padding: 20px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.15);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 2px solid #f0f0f0;
}
.card-title {
font-size: 18px;
font-weight: 600;
color: #333;
}
.card-count {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
padding: 4px 12px;
border-radius: 15px;
font-size: 14px;
font-weight: 600;
}
.agent-list, .task-list {
list-style: none;
max-height: 300px;
overflow-y: auto;
}
.agent-item, .task-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
margin-bottom: 8px;
background: #f8f9fa;
border-radius: 8px;
transition: background 0.2s ease;
}
.agent-item:hover, .task-item:hover {
background: #e9ecef;
}
.agent-name, .task-name {
font-weight: 500;
color: #495057;
}
.agent-status, .task-status {
padding: 4px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.status-active {
background: #d4f4dd;
color: #2e7d32;
}
.status-idle {
background: #fff3cd;
color: #856404;
}
.status-pending {
background: #cce5ff;
color: #004085;
}
.status-completed {
background: #d1ecf1;
color: #0c5460;
}
.metrics-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
.metric {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
text-align: center;
}
.metric-value {
font-size: 24px;
font-weight: bold;
color: #667eea;
margin-bottom: 5px;
}
.metric-label {
font-size: 12px;
color: #6c757d;
text-transform: uppercase;
}
.topology-view {
min-height: 300px;
background: #f8f9fa;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
.node {
position: absolute;
width: 60px;
height: 60px;
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
transition: transform 0.3s ease;
}
.node:hover {
transform: scale(1.1);
}
.connection {
position: absolute;
height: 2px;
background: #667eea;
opacity: 0.3;
transform-origin: left center;
}
.controls {
display: flex;
gap: 10px;
margin-top: 20px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
font-size: 14px;
}
.btn-primary {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.btn-secondary {
background: #f8f9fa;
color: #495057;
border: 2px solid #dee2e6;
}
.btn-secondary:hover {
background: #e9ecef;
}
.logs-container {
background: #1e1e1e;
color: #d4d4d4;
padding: 15px;
border-radius: 8px;
font-family: 'Courier New', monospace;
font-size: 12px;
max-height: 200px;
overflow-y: auto;
}
.log-entry {
margin-bottom: 5px;
white-space: pre-wrap;
}
.log-time {
color: #858585;
margin-right: 10px;
}
.log-level-info {
color: #4ec9b0;
}
.log-level-warn {
color: #ce9178;
}
.log-level-error {
color: #f48771;
}
.empty-state {
text-align: center;
padding: 40px;
color: #6c757d;
}
.empty-state-icon {
font-size: 48px;
margin-bottom: 10px;
opacity: 0.3;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>
🌊 DevFlow Dashboard
</h1>
<div class="status">
<div class="status-indicator">
<span class="status-dot"></span>
<span id="connection-status">Connected</span>
</div>
<div class="status-indicator">
<span>Swarms: </span>
<strong id="swarm-count">0</strong>
</div>
<div class="status-indicator">
<span>Agents: </span>
<strong id="agent-count">0</strong>
</div>
</div>
</header>
<div class="dashboard">
<!-- Swarm Topology -->
<div class="card" style="grid-column: span 2;">
<div class="card-header">
<h2 class="card-title">🐝 Swarm Topology</h2>
<span class="card-count" id="topology-count">0 nodes</span>
</div>
<div class="topology-view" id="topology-view">
<div class="empty-state">
<div class="empty-state-icon">🔗</div>
<p>No active swarms</p>
</div>
</div>
</div>
<!-- Active Agents -->
<div class="card">
<div class="card-header">
<h2 class="card-title">🤖 Active Agents</h2>
<span class="card-count" id="active-agent-count">0</span>
</div>
<ul class="agent-list" id="agent-list">
<li class="empty-state">
<div class="empty-state-icon">🤖</div>
<p>No active agents</p>
</li>
</ul>
</div>
<!-- Tasks -->
<div class="card">
<div class="card-header">
<h2 class="card-title">📋 Tasks</h2>
<span class="card-count" id="task-count">0</span>
</div>
<ul class="task-list" id="task-list">
<li class="empty-state">
<div class="empty-state-icon">📋</div>
<p>No tasks</p>
</li>
</ul>
</div>
<!-- Performance Metrics -->
<div class="card">
<div class="card-header">
<h2 class="card-title">📊 Performance</h2>
</div>
<div class="metrics-grid">
<div class="metric">
<div class="metric-value" id="cpu-usage">0%</div>
<div class="metric-label">CPU Usage</div>
</div>
<div class="metric">
<div class="metric-value" id="memory-usage">0 MB</div>
<div class="metric-label">Memory</div>
</div>
<div class="metric">
<div class="metric-value" id="task-throughput">0</div>
<div class="metric-label">Tasks/min</div>
</div>
<div class="metric">
<div class="metric-value" id="response-time">0 ms</div>
<div class="metric-label">Avg Response</div>
</div>
</div>
</div>
<!-- System Logs -->
<div class="card">
<div class="card-header">
<h2 class="card-title">📜 System Logs</h2>
</div>
<div class="logs-container" id="logs-container">
<div class="log-entry">
<span class="log-time">[00:00:00]</span>
<span class="log-level-info">INFO</span> Dashboard initialized
</div>
</div>
</div>
<!-- Controls -->
<div class="card" style="grid-column: span 2;">
<div class="card-header">
<h2 class="card-title">🎮 Controls</h2>
</div>
<div class="controls">
<button class="btn btn-primary" onclick="createSwarm()">🚀 Create Swarm</button>
<button class="btn btn-primary" onclick="spawnAgent()">🤖 Spawn Agent</button>
<button class="btn btn-primary" onclick="createTask()">📋 Create Task</button>
<button class="btn btn-secondary" onclick="clearLogs()">🗑️ Clear Logs</button>
<button class="btn btn-secondary" onclick="exportMetrics()">📊 Export Metrics</button>
</div>
</div>
</div>
</div>
<script>
let ws;
let agents = [];
let tasks = [];
let swarms = [];
let metrics = {};
function connect() {
ws = new WebSocket(`ws://${window.location.host}`);
ws.onopen = () => {
updateConnectionStatus('Connected');
addLog('INFO', 'Connected to DevFlow server');
};
ws.onclose = () => {
updateConnectionStatus('Disconnected');
addLog('WARN', 'Disconnected from server');
setTimeout(connect, 3000);
};
ws.onerror = (error) => {
addLog('ERROR', `WebSocket error: ${error.message || 'Unknown error'}`);
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
handleMessage(message);
};
}
function handleMessage(message) {
switch (message.type) {
case 'init':
initializeDashboard(message.data);
break;
case 'update':
updateDashboard(message.data);
break;
case 'swarmCreated':
addLog('INFO', `Swarm created: ${message.data.id}`);
break;
case 'taskCreated':
addLog('INFO', `Task created: ${message.data.id}`);
break;
case 'agentStopped':
addLog('INFO', `Agent stopped: ${message.data.agentId}`);
break;
case 'error':
addLog('ERROR', message.error);
break;
}
}
function initializeDashboard(data) {
agents = data.agents || [];
tasks = data.tasks || [];
swarms = data.swarms || [];
metrics = data.metrics || {};
updateAgentList();
updateTaskList();
updateMetrics();
updateTopology();
updateCounts();
}
function updateDashboard(data) {
if (data.agents) agents = data.agents;
if (data.tasks) tasks = data.tasks;
if (data.metrics) metrics = data.metrics;
updateAgentList();
updateTaskList();
updateMetrics();
updateCounts();
}
function updateAgentList() {
const list = document.getElementById('agent-list');
if (agents.length === 0) {
list.innerHTML = '<li class="empty-state"><div class="empty-state-icon">🤖</div><p>No active agents</p></li>';
return;
}
list.innerHTML = agents.map(agent => `
<li class="agent-item">
<span class="agent-name">${agent.name || agent.id}</span>
<span class="agent-status status-${agent.status || 'idle'}">${agent.status || 'idle'}</span>
</li>
`).join('');
}
function updateTaskList() {
const list = document.getElementById('task-list');
if (tasks.length === 0) {
list.innerHTML = '<li class="empty-state"><div class="empty-state-icon">📋</div><p>No tasks</p></li>';
return;
}
list.innerHTML = tasks.map(task => `
<li class="task-item">
<span class="task-name">${task.name || task.id}</span>
<span class="task-status status-${task.status || 'pending'}">${task.status || 'pending'}</span>
</li>
`).join('');
}
function updateMetrics() {
document.getElementById('cpu-usage').textContent = `${metrics.cpuUsage || 0}%`;
document.getElementById('memory-usage').textContent = `${Math.round(metrics.memoryUsage / 1024 / 1024) || 0} MB`;
document.getElementById('task-throughput').textContent = metrics.taskThroughput || 0;
document.getElementById('response-time').textContent = `${metrics.avgResponseTime || 0} ms`;
}
function updateTopology() {
const view = document.getElementById('topology-view');
if (agents.length === 0) {
view.innerHTML = '<div class="empty-state"><div class="empty-state-icon">🔗</div><p>No active swarms</p></div>';
document.getElementById('topology-count').textContent = '0 nodes';
return;
}
// Simple circular topology visualization
view.innerHTML = '';
const centerX = view.offsetWidth / 2;
const centerY = view.offsetHeight / 2;
const radius = Math.min(centerX, centerY) - 50;
agents.forEach((agent, index) => {
const angle = (index / agents.length) * 2 * Math.PI;
const x = centerX + radius * Math.cos(angle) - 30;
const y = centerY + radius * Math.sin(angle) - 30;
const node = document.createElement('div');
node.className = 'node';
node.style.left = `${x}px`;
node.style.top = `${y}px`;
node.textContent = agent.type?.[0]?.toUpperCase() || 'A';
node.title = agent.name || agent.id;
view.appendChild(node);
});
document.getElementById('topology-count').textContent = `${agents.length} nodes`;
}
function updateCounts() {
document.getElementById('swarm-count').textContent = swarms.length;
document.getElementById('agent-count').textContent = agents.length;
document.getElementById('active-agent-count').textContent = agents.filter(a => a.status === 'active').length;
document.getElementById('task-count').textContent = tasks.length;
}
function updateConnectionStatus(status) {
const element = document.getElementById('connection-status');
element.textContent = status;
const dot = element.previousElementSibling;
dot.style.background = status === 'Connected' ? '#4caf50' : '#f44336';
}
function addLog(level, message) {
const container = document.getElementById('logs-container');
const time = new Date().toLocaleTimeString();
const entry = document.createElement('div');
entry.className = 'log-entry';
entry.innerHTML = `
<span class="log-time">[${time}]</span>
<span class="log-level-${level.toLowerCase()}">${level}</span> ${message}
`;
container.appendChild(entry);
container.scrollTop = container.scrollHeight;
// Keep only last 100 logs
while (container.children.length > 100) {
container.removeChild(container.firstChild);
}
}
function createSwarm() {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'createSwarm',
data: {
topology: 'mesh',
maxAgents: 5
}
}));
}
}
function spawnAgent() {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'spawnAgent',
data: {
type: 'worker',
capabilities: ['task-execution']
}
}));
}
}
function createTask() {
const taskName = prompt('Enter task description:');
if (taskName && ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'createTask',
data: {
name: taskName,
priority: 'normal'
}
}));
}
}
function clearLogs() {
const container = document.getElementById('logs-container');
container.innerHTML = '<div class="log-entry"><span class="log-time">[00:00:00]</span><span class="log-level-info">INFO</span> Logs cleared</div>';
}
function exportMetrics() {
const data = {
timestamp: new Date().toISOString(),
agents,
tasks,
metrics
};
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `devflow-metrics-${Date.now()}.json`;
a.click();
URL.revokeObjectURL(url);
addLog('INFO', 'Metrics exported');
}
// Initialize connection
connect();
</script>
</body>
</html>