/** * Live Agent Supervision Dashboard * Main Application Logic */ class AgentSupervisionDashboard { constructor() { // DOM Elements this.canvasContainer = document.getElementById('canvasContainer'); this.nodesContainer = document.getElementById('nodesContainer'); this.connectionsLayer = document.getElementById('connectionsLayer'); this.minimap = document.getElementById('minimap'); this.minimapViewport = document.getElementById('minimapViewport'); // State this.nodes = new Map(); this.connections = []; this.zoom = 1; this.pan = { x: 0, y: 0 }; this.isDragging = false; this.dragNode = null; this.dragOffset = { x: 0, y: 0 }; // SSE state this.eventSource = null; this.currentRunId = this.getRunIdFromUrl(); // Agent name to node ID mapping for SSE events this.agentNameToNodeId = { 'ML_EFFORT': 'route_planner', // Maps to route planner visualization 'ROUTE_PLANNER': 'route_planner', 'FAIRNESS_MANAGER': 'fairness_manager', 'DRIVER_LIAISON': 'driver_liaison', 'EXPLAINABILITY': 'explainability', 'FINAL_RESOLUTION': 'route_planner', }; // Layout config this.layout = this.getDefaultLayout(); // Initialize this.init(); } /** * Get run_id from URL query parameter */ getRunIdFromUrl() { const urlParams = new URLSearchParams(window.location.search); return urlParams.get('run_id'); } /** * Get default node positions */ getDefaultLayout() { const containerWidth = window.innerWidth; const containerHeight = window.innerHeight - 100; // Account for header/footer const centerX = containerWidth / 2; const topY = 80; const bottomY = topY + 220; // Calculate spacing for 6 bottom nodes const nodeWidth = 180; const spacing = 20; const totalWidth = 6 * nodeWidth + 5 * spacing; const startX = (containerWidth - totalWidth) / 2; return { central_orchestrator: { x: centerX - 130, y: topY }, route_database: { x: startX, y: bottomY }, route_planner: { x: startX + nodeWidth + spacing, y: bottomY }, fairness_manager: { x: startX + 2 * (nodeWidth + spacing), y: bottomY }, driver_liaison: { x: startX + 3 * (nodeWidth + spacing), y: bottomY }, explainability: { x: startX + 4 * (nodeWidth + spacing), y: bottomY }, learning: { x: startX + 5 * (nodeWidth + spacing), y: bottomY } }; } /** * Initialize the dashboard */ async init() { // Load saved layout or use default this.loadLayout(); // Get initial workflow state (try real data first, fallback to mock) // IMPORTANT: If we have a run_id from URL, fetch data for THAT specific run let state; try { if (this.currentRunId) { // Fetch workflow state for the specific run from URL state = await API.getRealWorkflowState(null, this.currentRunId); } else { // No specific run - get latest for today state = await API.getRealWorkflowState(); } } catch (error) { console.log('Using mock data for initial load:', error.message); state = API.getMockWorkflowState(); } // Store current state this.currentWorkflowState = state; // Render agents this.renderAgents(state.agents); // Draw connections this.updateConnections(state.connections); // Update stats this.updateStats(state.stats); // Setup event listeners this.setupEventListeners(); // Setup minimap this.updateMinimap(); // Start API polling with workflow updates this.setupAPIPolling(); // Connect to SSE for real-time agent events this.connectSSE(); // Show run_id banner or no-run-selected message if (this.currentRunId) { this.showRunBanner(this.currentRunId); this.fetchRoutesForRun(this.currentRunId); this.fetchRunSummary(this.currentRunId); // Fetch and replay historical events (for when allocation already completed) this.fetchHistoricalEvents(this.currentRunId); } else { this.showNoRunSelectedBanner(); } // Show data source indicator if (state.isRealData) { this.showToast('Connected to live backend data!'); } } /** * Show banner with current run ID */ showRunBanner(runId) { const shortId = runId.substring(0, 8); const banner = document.createElement('div'); banner.id = 'run-banner'; banner.className = 'run-banner'; banner.innerHTML = `
🎯 Tracking Run: ${shortId}...
`; banner.style.cssText = ` position: fixed; top: 70px; left: 50%; transform: translateX(-50%); background: linear-gradient(135deg, #00d4aa 0%, #00b894 100%); color: #0a0f1a; padding: 8px 20px; border-radius: 20px; z-index: 1000; font-size: 14px; font-weight: 500; box-shadow: 0 4px 20px rgba(0, 212, 170, 0.3); `; document.body.appendChild(banner); } /** * Show banner indicating no run is selected */ showNoRunSelectedBanner() { const banner = document.createElement('div'); banner.id = 'no-run-banner'; banner.innerHTML = `
⚠️ No allocation run selected. Open from API Demo or enter a Run ID.
`; banner.style.cssText = ` position: fixed; top: 70px; left: 50%; transform: translateX(-50%); background: rgba(251, 191, 36, 0.2); color: #fbbf24; padding: 12px 24px; border-radius: 8px; border: 1px solid #fbbf24; z-index: 1000; font-size: 14px; `; const link = banner.querySelector('a'); if (link) { link.style.color = '#00d4aa'; link.style.textDecoration = 'underline'; } document.body.appendChild(banner); } /** * Fetch routes for map from run-scoped endpoint */ async fetchRoutesForRun(runId) { try { const response = await fetch(`http://localhost:8000/api/v1/runs/${runId}/routes-on-map`); if (!response.ok) { console.error('Failed to fetch routes for run:', response.status); return; } const data = await response.json(); console.log('Fetched routes for run:', data.routes.length); // Store routes for map display this.currentRoutes = data.routes; // Update map if available if (typeof this.updateMapWithRoutes === 'function') { this.updateMapWithRoutes(data.routes); } } catch (error) { console.error('Error fetching routes for run:', error); } } /** * Fetch run summary to update stats */ async fetchRunSummary(runId) { try { const response = await fetch(`http://localhost:8000/api/v1/runs/${runId}/summary`); if (!response.ok) { console.error('Failed to fetch run summary:', response.status); return; } const data = await response.json(); console.log('Fetched run summary:', data); // Update stats display with run metrics this.updateStats({ total_drivers: data.num_drivers, active_routes: data.num_routes, delivered: 0, // Not tracked in summary pending: data.num_packages, gini_index: data.global_gini_index, std_dev: data.global_std_dev, }); } catch (error) { console.error('Error fetching run summary:', error); } } /** * Fetch and replay historical events for a run * This updates agent statuses when the dashboard is opened after allocation completes */ async fetchHistoricalEvents(runId) { try { const response = await fetch(`http://localhost:8000/api/v1/runs/${runId}/recent-events`); if (!response.ok) { console.log('No recent events endpoint or no events:', response.status); return; } const data = await response.json(); const events = data.events || []; console.log(`Replaying ${events.length} historical events for run`); // Process each event to update agent statuses // Sort by timestamp to replay in order events.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp)); // Track the last state for each agent to show final status const agentStates = new Map(); events.forEach(event => { const key = event.agent_name; agentStates.set(key, event); }); // Update each agent to its final state agentStates.forEach((event, agentName) => { this.handleAgentEvent(event); }); if (events.length > 0) { this.showToast(`Loaded ${events.length} agent events for this run`); } } catch (error) { console.log('Could not fetch historical events:', error.message); } } /** * Update map with routes from run-scoped API * Uses driver_name directly from the API response */ updateMapWithRoutes(routes) { if (!this.map || !this.mapLayer) { console.log('Map not initialized, skipping route update'); return; } // Clear existing layers this.mapLayer.clearLayers(); // Route colors palette const routeColors = [ { main: '#00d4aa', glow: '#00b894' }, { main: '#6c5ce7', glow: '#a29bfe' }, { main: '#fd79a8', glow: '#e84393' }, { main: '#fdcb6e', glow: '#f39c12' }, { main: '#74b9ff', glow: '#0984e3' }, { main: '#ff7675', glow: '#d63031' }, { main: '#55efc4', glow: '#00cec9' }, { main: '#fab1a0', glow: '#e17055' }, ]; // Add warehouse marker const warehouseIcon = L.divIcon({ className: 'warehouse-marker', html: `
W
`, iconSize: [20, 20], iconAnchor: [10, 10] }); L.marker([12.9716, 77.5946], { icon: warehouseIcon }) .addTo(this.mapLayer) .bindPopup(`
🏭 Central Warehouse
Bangalore Hub
`); const renderedRoutes = []; // Render each route routes.forEach((route, i) => { const colorScheme = routeColors[i % routeColors.length]; // Use driver_name from API response directly const driverName = route.driver_name || `Driver ${i + 1}`; if (route.stops && route.stops.length > 0) { const latlngs = [[12.9716, 77.5946]]; // Start at warehouse // Add stop markers route.stops.forEach((stop, idx) => { const latlng = [stop.lat, stop.lng]; latlngs.push(latlng); const stopIcon = L.divIcon({ className: 'stop-marker-premium', html: `
${idx + 1}
`, iconSize: [24, 24], iconAnchor: [12, 12] }); L.marker(latlng, { icon: stopIcon }) .addTo(this.mapLayer) .bindPopup(`
${idx + 1}
Stop ${idx + 1}
${driverName}
πŸ“ ${stop.address || 'Delivery Location'}
`); }); // Draw route line with glow L.polyline(latlngs, { color: colorScheme.glow, weight: 8, opacity: 0.3, lineCap: 'round', lineJoin: 'round' }).addTo(this.mapLayer); // Main line L.polyline(latlngs, { color: colorScheme.main, weight: 4, opacity: 0.9, lineCap: 'round', lineJoin: 'round' }).addTo(this.mapLayer); renderedRoutes.push({ color: colorScheme.main, name: driverName, stops: route.stops.length }); } }); // Add legend with correct driver names this.addMapLegend(renderedRoutes); // Fit bounds if (this.mapLayer.getLayers().length > 0) { try { const group = L.featureGroup(this.mapLayer.getLayers()); this.map.fitBounds(group.getBounds().pad(0.1)); } catch (e) { // Ignore bounds error } } console.log('Map updated with', renderedRoutes.length, 'routes'); } /** * Connect to Server-Sent Events for real-time agent status updates */ connectSSE() { // Close existing connection if any if (this.eventSource) { this.eventSource.close(); } // Only connect if we have a run_id - otherwise no events to subscribe to if (!this.currentRunId) { console.log('No run_id present, skipping SSE connection'); return; } // Use run-scoped SSE endpoint const sseUrl = `http://localhost:8000/api/v1/runs/${encodeURIComponent(this.currentRunId)}/agent-events`; try { this.eventSource = new EventSource(sseUrl); this.eventSource.onopen = () => { console.log('SSE connection established for run:', this.currentRunId); }; this.eventSource.onmessage = (event) => { try { const eventData = JSON.parse(event.data); this.handleAgentEvent(eventData); } catch (err) { console.warn('Failed to parse SSE event:', err); } }; this.eventSource.onerror = (error) => { console.warn('SSE connection error, will retry...', error); }; } catch (error) { console.error('Failed to create SSE connection:', error); } } /** * Handle incoming agent event from SSE stream */ handleAgentEvent(event) { // Skip connection events if (event.type === 'connected') { console.log('SSE connected:', event.message); return; } const { agent_name, state: eventState, step_type, payload } = event; // Map agent name to node ID const nodeId = this.agentNameToNodeId[agent_name]; if (!nodeId) { console.log('Unknown agent:', agent_name); return; } // Determine status based on event state let status = 'idle'; if (eventState === 'STARTED') { status = 'processing'; } else if (eventState === 'COMPLETED') { status = 'active'; // Use 'active' for completed to show success } else if (eventState === 'ERROR') { status = 'error'; } // Update the agent status this.updateAgentStatusFromEvent(nodeId, status, step_type, payload); // Log for debugging console.log(`Agent event: ${agent_name} - ${step_type} - ${eventState}`); } /** * Update a single agent's status from SSE event */ updateAgentStatusFromEvent(nodeId, status, stepType, payload) { const node = this.nodes.get(nodeId); if (!node) return; // Update status badge const statusBadge = node.element.querySelector('.agent-status'); if (statusBadge) { // Remove all status classes statusBadge.classList.remove('active', 'processing', 'idle', 'error'); statusBadge.classList.add(status); // Update status text const statusIcons = { active: 'βœ“', processing: '⟳', idle: 'β—‹', error: 'βœ—' }; const statusText = status.charAt(0).toUpperCase() + status.slice(1); statusBadge.innerHTML = ` ${statusIcons[status] || 'β—‹'} ${statusText}`; } // Add visual pulse effect node.element.classList.remove('node-active-pulse', 'node-processing-pulse'); if (status === 'active') { node.element.classList.add('node-active-pulse'); } else if (status === 'processing') { node.element.classList.add('node-processing-pulse'); } // Auto-clear processing status after a timeout (for visual feedback) if (status === 'active') { setTimeout(() => { this.updateAgentStatusFromEvent(nodeId, 'idle', '', {}); }, 5000); // Reset to idle after 5 seconds } } /** * Render agent nodes */ renderAgents(agents) { this.nodesContainer.innerHTML = ''; agents.forEach(agent => { const node = this.createAgentNode(agent); this.nodesContainer.appendChild(node); this.nodes.set(agent.id, { element: node, data: agent, position: this.layout[agent.id] || { x: 100, y: 100 } }); // Position the node this.updateNodePosition(agent.id); }); } /** * Create an agent node element */ createAgentNode(agent) { const node = document.createElement('div'); node.className = `agent-node ${agent.type}`; node.dataset.id = agent.id; // Status class if (agent.status) { node.classList.add(agent.status); } // Icon based on type const iconSVG = this.getAgentIcon(agent.type); // Processing badge for processing status const processingBadge = agent.status === 'processing' ? 'Processing' : ''; node.innerHTML = `
${iconSVG}
${agent.name}
${agent.description}
`; return node; } /** * Get icon SVG for agent type */ getAgentIcon(type) { const icons = { orchestrator: ` `, database: ` `, agent: ` ` }; return icons[type] || icons.agent; } /** * Format status text */ formatStatus(status) { const statusMap = { active: 'Active', processing: 'Processing', idle: 'Idle', error: 'Error' }; return statusMap[status] || status; } /** * Update node position */ updateNodePosition(nodeId) { const node = this.nodes.get(nodeId); if (!node) return; const pos = node.position; node.element.style.left = `${pos.x}px`; node.element.style.top = `${pos.y}px`; } /** * Update agent nodes with new status data (called during polling) */ updateAgentStatuses(agents) { if (!agents || !Array.isArray(agents)) return; agents.forEach(agent => { const node = this.nodes.get(agent.id); if (!node) return; // Update status badge const statusBadge = node.element.querySelector('.agent-status'); if (statusBadge) { // Remove all status classes statusBadge.classList.remove('active', 'processing', 'idle', 'error'); // Add current status class const status = agent.status || 'idle'; statusBadge.classList.add(status); // Update status text with icon const statusText = status.charAt(0).toUpperCase() + status.slice(1); const statusIcons = { active: 'βœ“', processing: '⟳', idle: 'β—‹', error: 'βœ—' }; statusBadge.innerHTML = ` ${statusIcons[status] || 'β—‹'} ${statusText}`; } // Update description if provided const descEl = node.element.querySelector('.node-description'); if (descEl && agent.description) { descEl.textContent = agent.description; } // Update meta badge if provided const metaBadge = node.element.querySelector('.meta-badge'); if (metaBadge && agent.meta) { metaBadge.textContent = agent.meta; metaBadge.style.display = 'inline-block'; } // Add visual pulse effect for active agents node.element.classList.remove('node-active-pulse', 'node-processing-pulse'); if (agent.status === 'active') { node.element.classList.add('node-active-pulse'); } else if (agent.status === 'processing') { node.element.classList.add('node-processing-pulse'); } }); } /** * Draw connections between nodes */ updateConnections(connections) { this.connections = connections; this.drawConnections(); } /** * Draw all connection lines */ drawConnections() { // Clear existing connections (keep defs) const defs = this.connectionsLayer.querySelector('defs'); this.connectionsLayer.innerHTML = ''; this.connectionsLayer.appendChild(defs); this.connections.forEach(conn => { const fromNode = this.nodes.get(conn.from); const toNode = this.nodes.get(conn.to); if (!fromNode || !toNode) return; // Get node centers const fromRect = fromNode.element.getBoundingClientRect(); const toRect = toNode.element.getBoundingClientRect(); const containerRect = this.canvasContainer.getBoundingClientRect(); // Calculate positions relative to container const fromX = fromRect.left - containerRect.left + fromRect.width / 2; const fromY = fromRect.top - containerRect.top + fromRect.height; const toX = toRect.left - containerRect.left + toRect.width / 2; const toY = toRect.top - containerRect.top; // Create curved path const path = this.createConnectionPath(fromX, fromY, toX, toY, conn.active); this.connectionsLayer.appendChild(path); // Add click handler for data payload path.addEventListener('click', (e) => { e.stopPropagation(); this.showDataPayload(conn); }); }); } /** * Create a curved SVG path for connection */ createConnectionPath(x1, y1, x2, y2, isActive) { const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); // Calculate control points for bezier curve const midY = (y1 + y2) / 2; const controlOffset = Math.abs(y2 - y1) * 0.5; const d = `M ${x1} ${y1} C ${x1} ${y1 + controlOffset}, ${x2} ${y2 - controlOffset}, ${x2} ${y2}`; path.setAttribute('d', d); path.setAttribute('class', isActive ? 'connection-line-animated' : 'connection-line'); path.style.pointerEvents = 'stroke'; path.style.cursor = 'pointer'; return path; } /** * Setup event listeners */ setupEventListeners() { // Node click handler this.nodesContainer.addEventListener('click', (e) => { const node = e.target.closest('.agent-node'); if (node) { this.openTerminal(node.dataset.id); } }); // Node drag handlers this.nodesContainer.addEventListener('mousedown', (e) => { const node = e.target.closest('.agent-node'); if (node) { this.startDrag(node, e); } }); document.addEventListener('mousemove', (e) => { if (this.isDragging) { this.handleDrag(e); } }); document.addEventListener('mouseup', () => { if (this.isDragging) { this.endDrag(); } }); // Button handlers document.getElementById('saveLayoutBtn').addEventListener('click', () => { this.saveLayout(); }); document.getElementById('resetBtn').addEventListener('click', () => { this.resetLayout(); }); // Start Allocation handler document.getElementById('startAllocationBtn').addEventListener('click', () => { this.handleStartAllocation(); }); // Map View Toggles document.getElementById('graphViewBtn').addEventListener('click', () => { this.toggleView('graph'); }); document.getElementById('mapViewBtn').addEventListener('click', () => { this.toggleView('map'); }); // Date Picker const dateInput = document.getElementById('historyDate'); // Set today as default - use LOCAL timezone, not UTC const now = new Date(); const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`; console.log('Initializing date picker with today (local):', today); dateInput.value = today; API.setSelectedDate(today); // Sync with API // Store current date for reference this._currentDisplayDate = today; dateInput.addEventListener('change', (e) => { const newDate = e.target.value; console.log('Date picker changed to:', newDate); this._currentDisplayDate = newDate; API.setSelectedDate(newDate); // Sync with API this.loadHistory(newDate); }); // Zoom handlers document.getElementById('zoomIn').addEventListener('click', () => { this.setZoom(this.zoom + 0.1); }); document.getElementById('zoomOut').addEventListener('click', () => { this.setZoom(this.zoom - 0.1); }); // Modal close handlers document.getElementById('closeTerminal').addEventListener('click', () => { this.closeModal('terminalModal'); }); document.getElementById('closeData').addEventListener('click', () => { this.closeModal('dataModal'); }); // Close modal on backdrop click document.querySelectorAll('.modal').forEach(modal => { modal.addEventListener('click', (e) => { if (e.target === modal) { modal.classList.remove('active'); } }); }); // Window resize window.addEventListener('resize', () => { this.drawConnections(); this.updateMinimap(); }); } /** * Start dragging a node */ startDrag(nodeElement, e) { const nodeId = nodeElement.dataset.id; const node = this.nodes.get(nodeId); if (!node) return; this.isDragging = true; this.dragNode = node; const rect = nodeElement.getBoundingClientRect(); this.dragOffset = { x: e.clientX - rect.left, y: e.clientY - rect.top }; nodeElement.classList.add('dragging'); } /** * Handle drag movement */ handleDrag(e) { if (!this.dragNode) return; const containerRect = this.canvasContainer.getBoundingClientRect(); const newX = e.clientX - containerRect.left - this.dragOffset.x; const newY = e.clientY - containerRect.top - this.dragOffset.y; this.dragNode.position = { x: Math.max(0, newX), y: Math.max(0, newY) }; // Update node visual position this.dragNode.element.style.left = `${this.dragNode.position.x}px`; this.dragNode.element.style.top = `${this.dragNode.position.y}px`; // Redraw connections this.drawConnections(); // Update minimap this.updateMinimap(); } /** * End dragging */ endDrag() { if (this.dragNode) { this.dragNode.element.classList.remove('dragging'); // Update layout this.layout[this.dragNode.data.id] = this.dragNode.position; } this.isDragging = false; this.dragNode = null; } /** * Open terminal modal for an agent */ openTerminal(nodeId) { if (this.isDragging) return; const node = this.nodes.get(nodeId); if (!node) return; const modal = document.getElementById('terminalModal'); const title = document.getElementById('terminalTitle'); const output = document.getElementById('terminalOutput'); title.textContent = `${node.data.name} - Terminal`; // Try to get real agent logs first const realLogs = API.getAgentLogs(nodeId); if (realLogs && realLogs.length > 0) { output.textContent = this.formatRealLogs(realLogs, node.data); } else { // Fallback to mock terminal output output.textContent = this.generateTerminalOutput(node.data); } modal.classList.add('active'); } /** * Format real agent logs for terminal display */ formatRealLogs(logs, agent) { let output = `=== ${agent.name} Decision Log ===\n\n`; logs.forEach((log, index) => { const timestamp = log.timestamp || new Date().toISOString(); const stepType = log.step_type || log.action || 'Decision'; output += `[${timestamp}] Step ${index + 1}: ${stepType}\n`; // Show short message if available if (log.short_message) { output += ` Message: ${log.short_message}\n`; } // Show details if available if (log.details && Object.keys(log.details).length > 0) { output += ` Details:\n`; Object.entries(log.details).forEach(([key, value]) => { let displayValue; if (typeof value === 'number') { displayValue = Number.isInteger(value) ? value : value.toFixed(2); } else if (typeof value === 'object') { displayValue = JSON.stringify(value); } else { displayValue = value; } // Format key to be more readable const formattedKey = key.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); output += ` - ${formattedKey}: ${displayValue}\n`; }); } // Legacy fields support if (log.decision) { output += ` Decision: ${log.decision}\n`; } if (log.reasoning) { output += ` Reasoning: ${log.reasoning}\n`; } if (log.input_snapshot) { const summary = JSON.stringify(log.input_snapshot).substring(0, 150); output += ` Input: ${summary}${summary.length >= 150 ? '...' : ''}\n`; } if (log.output_snapshot) { const summary = JSON.stringify(log.output_snapshot).substring(0, 150); output += ` Output: ${summary}${summary.length >= 150 ? '...' : ''}\n`; } output += '\n'; }); output += `=== Total: ${logs.length} decision(s) ===`; return output; } /** * Generate mock terminal output */ generateTerminalOutput(agent) { const timestamp = new Date().toISOString(); const allocationRun = API.getCurrentAllocationRun(); // If we have a real allocation run, show real info if (allocationRun) { const outputs = { central_orchestrator: `[${timestamp}] Central Orchestrator Agent - LIVE\n[${timestamp}] Current Allocation Run: ${allocationRun.id.substring(0, 8)}...\n[${timestamp}] Status: ${allocationRun.status || 'COMPLETED'}\n[${timestamp}] Gini Index: ${allocationRun.gini_index?.toFixed(4) || 'N/A'}\n[${timestamp}] Std Dev: ${allocationRun.std_dev?.toFixed(2) || 'N/A'}\n[${timestamp}] Avg Workload: ${allocationRun.avg_workload?.toFixed(2) || 'N/A'}`, route_database: `[${timestamp}] Route Database Connection - LIVE\n[${timestamp}] Host: localhost (SQLite)\n[${timestamp}] Database: fair_dispatch.db\n[${timestamp}] Allocation Run ID: ${allocationRun.id}\n[${timestamp}] Last sync: ${allocationRun.created_at || timestamp}\n[${timestamp}] Connection: Active` }; if (outputs[agent.id]) { return outputs[agent.id]; } } // Default mock outputs const mockOutputs = { central_orchestrator: `[${timestamp}] Central Orchestrator Agent initialized\n[${timestamp}] Monitoring 6 child agents\n[${timestamp}] Data flow channels: ACTIVE\n[${timestamp}] Current task: Coordinating route allocation\n[${timestamp}] Status: All systems nominal\n[${timestamp}] Last heartbeat: 2ms ago`, route_database: `[${timestamp}] Route Database Connection\n[${timestamp}] Host: localhost\n[${timestamp}] Database: fair_dispatch\n[${timestamp}] Last sync: ${timestamp}\n[${timestamp}] Connection pool: healthy`, route_planner: `[${timestamp}] Route Planner Agent - Standby\n[${timestamp}] Awaiting allocation request...\n[${timestamp}] K-Means clustering: Ready\n[${timestamp}] OR-Tools optimization: Ready`, fairness_manager: `[${timestamp}] Fairness Manager Agent\n[${timestamp}] Gini threshold: 0.30\n[${timestamp}] Awaiting route proposals...`, driver_liaison: `[${timestamp}] Driver Liaison Agent\n[${timestamp}] Processing driver feedback...\n[${timestamp}] Driver contexts loaded\n[${timestamp}] Awaiting route proposals...`, explainability: `[${timestamp}] Explainability Agent\n[${timestamp}] Template engine: ACTIVE\n[${timestamp}] Language support: en, ta, hi\n[${timestamp}] Gemini integration: READY`, learning: `[${timestamp}] Learning Agent\n[${timestamp}] Model: Ready\n[${timestamp}] Mode: Online learning` }; return mockOutputs[agent.id] || `[${timestamp}] Agent ${agent.name} initialized`; } /** * Show data payload modal */ showDataPayload(connection) { const modal = document.getElementById('dataModal'); const title = document.getElementById('dataTitle'); const payload = document.getElementById('dataPayload'); const fromNode = this.nodes.get(connection.from); const toNode = this.nodes.get(connection.to); title.textContent = `Data Flow: ${fromNode?.data.name || connection.from} β†’ ${toNode?.data.name || connection.to}`; // Generate mock payload payload.textContent = JSON.stringify(this.generateDataPayload(connection), null, 2); modal.classList.add('active'); } /** * Generate mock data payload */ generateDataPayload(connection) { const payloads = { 'central_orchestrator-route_planner': { type: 'allocation_request', timestamp: new Date().toISOString(), packages: 47, drivers: 5, warehouse: { lat: 12.9716, lng: 77.5946 }, config: { target_packages_per_route: 20, fairness_threshold: 0.30 } }, 'route_planner-fairness_manager': { type: 'route_proposal', proposal_id: 1, assignments: [ { driver_id: 'driver_001', route_id: 'route_001', effort: 65.3 }, { driver_id: 'driver_002', route_id: 'route_002', effort: 72.1 } ], total_effort: 324.5 } }; const key = `${connection.from}-${connection.to}`; return payloads[key] || { type: 'data_flow', from: connection.from, to: connection.to, active: connection.active, timestamp: new Date().toISOString() }; } /** * Close modal */ closeModal(modalId) { document.getElementById(modalId).classList.remove('active'); } /** * Update zoom level */ setZoom(level) { this.zoom = Math.max(0.5, Math.min(2, level)); this.nodesContainer.style.transform = `scale(${this.zoom})`; this.nodesContainer.style.transformOrigin = 'center center'; this.updateMinimap(); } /** * Save layout to localStorage */ saveLayout() { const layoutData = {}; this.nodes.forEach((node, id) => { layoutData[id] = node.position; }); localStorage.setItem('agentDashboardLayout', JSON.stringify(layoutData)); // Show brief confirmation this.showToast('Layout saved!'); } /** * Load layout from localStorage */ loadLayout() { try { const saved = localStorage.getItem('agentDashboardLayout'); if (saved) { const layoutData = JSON.parse(saved); this.layout = { ...this.getDefaultLayout(), ...layoutData }; } } catch (e) { console.warn('Could not load saved layout:', e); } } /** * Reset layout to default */ resetLayout() { this.layout = this.getDefaultLayout(); localStorage.removeItem('agentDashboardLayout'); // Update all node positions this.nodes.forEach((node, id) => { node.position = this.layout[id] || { x: 100, y: 100 }; this.updateNodePosition(id); }); // Redraw connections this.drawConnections(); this.updateMinimap(); this.showToast('Layout reset!'); } /** * Show toast notification */ showToast(message) { // Simple toast implementation const toast = document.createElement('div'); toast.style.cssText = ` position: fixed; bottom: 60px; left: 50%; transform: translateX(-50%); background: var(--accent-primary); color: var(--bg-primary); padding: 10px 20px; border-radius: 6px; font-size: 13px; font-weight: 500; z-index: 2000; animation: fadeInOut 2s ease; `; toast.textContent = message; document.body.appendChild(toast); setTimeout(() => toast.remove(), 2000); } /** * Update minimap */ updateMinimap() { // Clear minimap this.minimap.innerHTML = '
'; const minimapWidth = 180; const minimapHeight = 120; const containerWidth = this.canvasContainer.clientWidth; const containerHeight = this.canvasContainer.clientHeight; const scaleX = minimapWidth / containerWidth; const scaleY = minimapHeight / containerHeight; const scale = Math.min(scaleX, scaleY); // Draw nodes on minimap this.nodes.forEach((node) => { const miniNode = document.createElement('div'); miniNode.className = 'minimap-node'; miniNode.style.left = `${node.position.x * scale}px`; miniNode.style.top = `${node.position.y * scale}px`; miniNode.style.width = '8px'; miniNode.style.height = '5px'; this.minimap.appendChild(miniNode); }); // Update viewport indicator const viewport = document.getElementById('minimapViewport'); if (viewport) { viewport.style.width = `${minimapWidth}px`; viewport.style.height = `${minimapHeight}px`; viewport.style.left = '0'; viewport.style.top = '0'; } } /** * Update stats display */ updateStats(stats) { document.getElementById('processingCount').textContent = stats.processing; document.getElementById('dataFlowCount').textContent = stats.dataFlows; document.getElementById('agentCount').textContent = stats.totalAgents; } /** * Setup API polling for real-time updates */ setupAPIPolling() { // Handle connection status updates API.onStatusUpdate((status) => { // Update connection indicator const liveIndicator = document.querySelector('.status-indicator'); const liveStatus = document.getElementById('liveStatus'); if (status.connected) { if (liveStatus) liveStatus.textContent = 'Live'; if (liveIndicator) { liveIndicator.classList.remove('disconnected'); liveIndicator.querySelector('.status-dot').style.background = ''; } } else { if (liveStatus) liveStatus.textContent = 'Offline'; if (liveIndicator) { liveIndicator.classList.add('disconnected'); liveIndicator.querySelector('.status-dot').style.background = 'var(--status-error)'; } } }); // Handle workflow updates (real-time data) API.onWorkflowUpdate((state) => { // Debug log console.log(`[${new Date().toLocaleTimeString()}] Poll update received. Real data: ${state.isRealData}, Run ID: ${state.allocationRun?.id?.substring(0, 8) || 'none'}`); if (state && state.isRealData !== undefined) { // Check if this is new data const isNewData = !this.currentWorkflowState || (state.allocationRun?.id !== this.currentWorkflowState?.allocationRun?.id); // Update current state this.currentWorkflowState = state; // Re-render agents with new data - ALWAYS update to show real statuses this.updateAgentStatuses(state.agents); // Update connections - ALWAYS update to show active flows this.updateConnections(state.connections); // Update stats this.updateStats(state.stats); // Update minimap this.updateMinimap(); // Show notification if new allocation (but don't change date picker) if (isNewData && state.isRealData) { this.showToast(`πŸš€ New allocation! Run: ${state.allocationRun.id.substring(0, 8)}...`); // Refresh map if active if (document.getElementById('mapWrapper')?.classList.contains('active')) { this.renderMap(state.allocationRun); } // Trigger Replay Animation for the workflow if (state.timeline) { this.playWorkflowAnimation(state.timeline); } } // Also update map in background if we have allocation data if (state.allocationRun && this.map) { this.renderMap(state.allocationRun); } } }); API.onError((error) => { console.error('API Error:', error); }); // Start polling API.startPolling(); } /** * Simulate agent activity for demo */ simulateActivity() { const agentIds = ['route_planner', 'fairness_manager', 'driver_liaison', 'explainability', 'learning']; let currentIndex = 0; setInterval(() => { // Reset all to idle agentIds.forEach(id => { const node = this.nodes.get(id); if (node) { node.element.classList.remove('processing'); node.data.status = 'idle'; } }); // Set current to processing const currentId = agentIds[currentIndex]; const currentNode = this.nodes.get(currentId); if (currentNode) { currentNode.element.classList.add('processing'); currentNode.data.status = 'processing'; } // Update connections const state = API.getMockWorkflowState(); state.connections.forEach((conn, i) => { conn.active = i <= currentIndex + 1; }); this.updateConnections(state.connections); // Move to next currentIndex = (currentIndex + 1) % agentIds.length; // Update stats this.updateStats({ processing: 1, dataFlows: currentIndex + 2, totalAgents: 6 }); }, 3000); } /** * Handle start allocation click */ async handleStartAllocation() { const btn = document.getElementById('startAllocationBtn'); if (btn.disabled) return; // FIRST: Force update date picker to today immediately (LOCAL timezone) const now = new Date(); const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`; const dateInput = document.getElementById('historyDate'); if (dateInput) { dateInput.value = today; this._currentDisplayDate = today; console.log('[Start Allocation] Date picker forced to:', today); } API.setSelectedDate(today); // Clear any cached old data API._lastKnownRunId = null; API._lastRunCount = 0; API._currentAllocationRun = null; // Disable button and show loading state btn.disabled = true; const originalText = btn.innerHTML; btn.innerHTML = ` Starting...`; // Set all agents to "processing" state for visual feedback this.setAgentsProcessing(); this.showToast('πŸš€ Initiating Agent Workflow with 30 packages & 5 drivers...'); try { // Generate comprehensive test data for Bangalore area const payload = this.generateTestAllocationData(); // Call API const result = await API.triggerAllocation(payload); // Success this.showToast(`πŸŽ‰ Workflow Complete! Run ID: ${result.allocation_run_id.substring(0, 8)}... | Gini: ${result.global_fairness.gini_index.toFixed(3)}`); console.log('Allocation result:', result); // Force refresh the dashboard data API._lastKnownRunId = null; API._lastRunCount = 0; API._currentAllocationRun = null; const workflowState = await API.getRealWorkflowState(today); if (workflowState) { // Update UI with new data this.currentWorkflowState = workflowState; this.updateAgentStatuses(workflowState.agents); this.updateConnections(workflowState.connections); this.updateStats(workflowState.stats); // Render map if allocation run exists if (workflowState.allocationRun) { this.renderMap(workflowState.allocationRun); } // Play workflow animation if (workflowState.timeline) { this.playWorkflowAnimation(workflowState.timeline); } } } catch (error) { console.error('Allocation start failed:', error); this.showToast('❌ Failed to start: ' + error.message); // Reset agents to idle on error this.resetAgentsToIdle(); } finally { // Restore button after delay setTimeout(() => { btn.disabled = false; btn.innerHTML = originalText; }, 2000); } } /** * Set all agents to processing state for visual feedback */ setAgentsProcessing() { const agentIds = ['route_planner', 'fairness_manager', 'driver_liaison', 'explainability', 'learning']; agentIds.forEach(id => { const node = this.nodes.get(id); if (node) { node.element.classList.add('processing'); const statusBadge = node.element.querySelector('.agent-status'); if (statusBadge) { statusBadge.className = 'agent-status processing'; statusBadge.innerHTML = ' Processing'; } } }); } /** * Reset all agents to idle state */ resetAgentsToIdle() { const agentIds = ['route_planner', 'fairness_manager', 'driver_liaison', 'explainability', 'learning']; agentIds.forEach(id => { const node = this.nodes.get(id); if (node) { node.element.classList.remove('processing'); const statusBadge = node.element.querySelector('.agent-status'); if (statusBadge) { statusBadge.className = 'agent-status idle'; statusBadge.innerHTML = ' Idle'; } } }); } /** * Generate comprehensive test allocation data * Creates realistic packages and drivers for Bangalore area */ generateTestAllocationData() { const timestamp = Date.now(); // Bangalore area locations with realistic addresses const locations = [ { name: "Electronic City Phase 1", lat: 12.8456, lng: 77.6603, area: "South" }, { name: "Electronic City Phase 2", lat: 12.8520, lng: 77.6680, area: "South" }, { name: "Whitefield Main Road", lat: 12.9698, lng: 77.7500, area: "East" }, { name: "ITPL Main Road", lat: 12.9854, lng: 77.7081, area: "East" }, { name: "Marathahalli Bridge", lat: 12.9591, lng: 77.6974, area: "East" }, { name: "Indiranagar 100ft Road", lat: 12.9784, lng: 77.6408, area: "Central" }, { name: "Indiranagar CMH Road", lat: 12.9716, lng: 77.6412, area: "Central" }, { name: "Koramangala 4th Block", lat: 12.9352, lng: 77.6245, area: "Central" }, { name: "Koramangala 5th Block", lat: 12.9344, lng: 77.6150, area: "Central" }, { name: "HSR Layout Sector 1", lat: 12.9116, lng: 77.6389, area: "South" }, { name: "HSR Layout Sector 7", lat: 12.9081, lng: 77.6476, area: "South" }, { name: "BTM Layout 1st Stage", lat: 12.9166, lng: 77.6101, area: "South" }, { name: "JP Nagar 5th Phase", lat: 12.9063, lng: 77.5857, area: "South" }, { name: "Jayanagar 4th Block", lat: 12.9308, lng: 77.5838, area: "South" }, { name: "Bannerghatta Road", lat: 12.8876, lng: 77.5973, area: "South" }, { name: "MG Road", lat: 12.9756, lng: 77.6068, area: "Central" }, { name: "Brigade Road", lat: 12.9716, lng: 77.6070, area: "Central" }, { name: "Commercial Street", lat: 12.9824, lng: 77.6074, area: "Central" }, { name: "Rajajinagar 1st Block", lat: 12.9914, lng: 77.5521, area: "West" }, { name: "Malleswaram 18th Cross", lat: 13.0067, lng: 77.5713, area: "North" }, { name: "Yeshwanthpur", lat: 13.0271, lng: 77.5450, area: "North" }, { name: "Hebbal Flyover", lat: 13.0358, lng: 77.5970, area: "North" }, { name: "Yelahanka New Town", lat: 13.1007, lng: 77.5963, area: "North" }, { name: "RT Nagar", lat: 13.0206, lng: 77.5970, area: "North" }, { name: "Basavanagudi Bull Temple", lat: 12.9429, lng: 77.5688, area: "South" }, { name: "Vijayanagar BDA Complex", lat: 12.9710, lng: 77.5360, area: "West" }, { name: "Kengeri Satellite Town", lat: 12.9048, lng: 77.4823, area: "West" }, { name: "Banashankari 2nd Stage", lat: 12.9255, lng: 77.5468, area: "South" }, { name: "Sarjapur Road", lat: 12.9107, lng: 77.6868, area: "East" }, { name: "Bellandur Lake View", lat: 12.9260, lng: 77.6762, area: "East" }, ]; // Generate 30 packages with varied properties const priorities = ["NORMAL", "NORMAL", "NORMAL", "HIGH", "HIGH", "EXPRESS"]; const packages = locations.map((loc, index) => ({ id: `pkg_${timestamp}_${String(index + 1).padStart(3, '0')}`, weight_kg: parseFloat((Math.random() * 8 + 0.5).toFixed(2)), // 0.5 to 8.5 kg fragility_level: Math.floor(Math.random() * 4) + 1, // 1-4 address: `${loc.name}, Bangalore - ${560000 + Math.floor(Math.random() * 100)}`, latitude: loc.lat + (Math.random() - 0.5) * 0.01, // Small random offset longitude: loc.lng + (Math.random() - 0.5) * 0.01, priority: priorities[Math.floor(Math.random() * priorities.length)] })); // Generate 5 drivers with different capacities and languages const driverNames = [ { name: "Rajesh Kumar", lang: "hi", capacity: 200 }, { name: "Venkatesh S", lang: "kn", capacity: 180 }, { name: "Mohammed Ashraf", lang: "en", capacity: 220 }, { name: "Suresh Babu", lang: "ta", capacity: 150 }, { name: "Prashanth Gowda", lang: "kn", capacity: 250 } ]; const drivers = driverNames.map((d, index) => ({ id: `driver_${timestamp}_${String(index + 1).padStart(2, '0')}`, name: d.name, vehicle_capacity_kg: d.capacity, preferred_language: d.lang })); return { allocation_date: API.getTodayDate(), warehouse: { lat: 12.9716, lng: 77.5946 }, // MG Road area - central Bangalore packages, drivers }; } /** * Map Integration Methods */ initMap() { if (this.map) return; // Initialize Leaflet map this.map = L.map('map').setView([12.9716, 77.5946], 12); L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { attribution: '© OpenStreetMap contributors © CARTO', subdomains: 'abcd', maxZoom: 19 }).addTo(this.map); this.mapLayer = L.layerGroup().addTo(this.map); this.renderMap(this.currentWorkflowState?.allocationRun); } toggleView(view) { const graphBtn = document.getElementById('graphViewBtn'); const mapBtn = document.getElementById('mapViewBtn'); const mapWrapper = document.getElementById('mapWrapper'); if (view === 'map') { mapBtn.classList.add('active'); graphBtn.classList.remove('active'); mapWrapper.classList.add('active'); if (!this.map) this.initMap(); } else { graphBtn.classList.add('active'); mapBtn.classList.remove('active'); mapWrapper.classList.remove('active'); } } async renderMap(allocationRun) { if (!this.map || !allocationRun) return; this.mapLayer.clearLayers(); // Premium color palette with gradients const routeColors = [ { main: '#3b82f6', glow: '#60a5fa', name: 'Route A' }, { main: '#f59e0b', glow: '#fbbf24', name: 'Route B' }, { main: '#ec4899', glow: '#f472b6', name: 'Route C' }, { main: '#10b981', glow: '#34d399', name: 'Route D' }, { main: '#8b5cf6', glow: '#a78bfa', name: 'Route E' }, { main: '#f97316', glow: '#fb923c', name: 'Route F' } ]; // Premium Warehouse marker with pulsing animation const warehouseIcon = L.divIcon({ className: 'warehouse-marker-premium', html: `
W
`, iconSize: [20, 20], iconAnchor: [10, 10] }); L.marker([12.9716, 77.5946], { icon: warehouseIcon }) .addTo(this.mapLayer) .bindPopup(`
🏭 Central Warehouse
Bangalore Hub
πŸ“ 12.9716Β°N, 77.5946Β°E
`); // Get assignments let assignments = allocationRun.assignments; if (!assignments || assignments.length === 0) { try { const dateStr = allocationRun.run_date || allocationRun.allocation_date || API.getTodayDate(); const response = await API.getAssignments(dateStr); assignments = response?.items || []; } catch (e) { console.error('Failed to fetch assignments:', e); assignments = []; } } if (!assignments || assignments.length === 0) { // Add "No routes" indicator this.addMapLegend([]); return; } // Render each route with premium styling const renderedRoutes = []; for (let i = 0; i < assignments.length; i++) { const assign = assignments[i]; try { const routeId = assign.route_id || assign.route?.id; if (!routeId) continue; const route = await API.getRoute(routeId); const colorScheme = routeColors[i % routeColors.length]; const driverName = assign.driver?.name || assign.driver_name || `Driver ${i + 1}`; if (route.stops && route.stops.length > 0) { const latlngs = [[12.9716, 77.5946]]; // Render stops with premium markers route.stops.forEach((stop, idx) => { const latlng = [stop.latitude, stop.longitude]; latlngs.push(latlng); // Premium stop marker with number const stopIcon = L.divIcon({ className: 'stop-marker-premium', html: `
${idx + 1}
`, iconSize: [24, 24], iconAnchor: [12, 12] }); L.marker(latlng, { icon: stopIcon }) .addTo(this.mapLayer) .bindPopup(`
${idx + 1}
Stop ${idx + 1}
${driverName}
πŸ“ ${stop.address || 'Delivery Location'}
πŸ“¦ ${stop.weight_kg?.toFixed(1) || '?'} kg ⚑ ${stop.priority || 'Normal'}
`); }); // Animated route line with glow effect // Background glow L.polyline(latlngs, { color: colorScheme.glow, weight: 8, opacity: 0.3, lineCap: 'round', lineJoin: 'round' }).addTo(this.mapLayer); // Main line L.polyline(latlngs, { color: colorScheme.main, weight: 4, opacity: 0.9, lineCap: 'round', lineJoin: 'round' }).addTo(this.mapLayer); // Direction arrows const decorator = L.polylineDecorator ? L.polylineDecorator(L.polyline(latlngs), { patterns: [{ offset: 25, repeat: 50, symbol: L.Symbol.arrowHead({ pixelSize: 8, polygon: false, pathOptions: { stroke: true, color: '#fff', weight: 2, opacity: 0.8 } }) }] }).addTo(this.mapLayer) : null; renderedRoutes.push({ color: colorScheme.main, name: driverName, stops: route.stops.length, distance: route.total_distance_km || 'N/A' }); } } catch (e) { console.error('Error rendering route:', e); } } // Add legend this.addMapLegend(renderedRoutes); // Fit bounds to show all markers if (this.mapLayer.getLayers().length > 0) { try { const group = L.featureGroup(this.mapLayer.getLayers()); this.map.fitBounds(group.getBounds().pad(0.1)); } catch (e) { // Ignore bounds error } } } addMapLegend(routes) { // Remove existing legend if (this.mapLegend) { this.map.removeControl(this.mapLegend); } // Create custom legend control const LegendControl = L.Control.extend({ options: { position: 'bottomright' }, onAdd: function () { const div = L.DomUtil.create('div', 'map-legend'); div.style.cssText = ` background: rgba(15, 23, 42, 0.95); backdrop-filter: blur(10px); padding: 12px 16px; border-radius: 12px; border: 1px solid rgba(255,255,255,0.1); box-shadow: 0 4px 20px rgba(0,0,0,0.3); font-family: 'Inter', sans-serif; min-width: 160px; `; let html = `
πŸ—ΊοΈ Route Legend
`; if (routes.length === 0) { html += `
No routes to display
`; } else { routes.forEach((route, i) => { html += `
${route.name}
${route.stops} stops
`; }); } div.innerHTML = html; return div; } }); this.mapLegend = new LegendControl(); this.map.addControl(this.mapLegend); } async loadHistory(date) { this.showToast(`Loading history for ${date}...`); try { const response = await API.getAllocationRuns(date); const runs = response?.runs || []; if (runs && runs.length > 0) { // Load the first run of the day for now const latestRun = runs[runs.length - 1]; // Get the latest // Construct a state object similar to verify/real data // Note: We need to manually construct the state structure as getRealWorkflowState does // Or refactor to reuse that logic. For now, simple update. this.currentWorkflowState = { allocationRun: latestRun, isRealData: true, agents: {}, // We might miss agent logs for history unless we fetch timeline too stats: {}, connections: [] }; // Fetch timeline for logs const timeline = await API.getAgentTimeline(latestRun.id); const state = API.transformTimelineToWorkflowState(timeline, latestRun); this.currentWorkflowState = state; // Update UI this.updateAgentStatuses(state.agents); this.updateStats(state.stats); this.updateConnections(state.connections); this.renderMap(latestRun); this.showToast(`Loaded ${runs.length} runs for ${date}`); } else { this.showToast(`No data found for ${date}`); } } catch (e) { console.error(e); this.showToast('Failed to load history'); } } /** * Animate the workflow steps */ async playWorkflowAnimation(timeline) { // Handle both new format (timeline.timeline) and legacy format (timeline.steps) const steps = timeline?.timeline || timeline?.steps || []; if (!steps || steps.length === 0) return; this.showToast('Replaying agent workflow...'); console.log('Starting workflow replay with steps:', steps.length); // Helper to reset all nodes visual state this.nodes.forEach(node => { // Remove status classes const statusBadge = node.element.querySelector('.agent-status'); if (statusBadge) { statusBadge.className = 'agent-status idle'; statusBadge.innerHTML = ' Idle'; } node.element.classList.remove('active', 'processing'); }); // Map backend agent names to frontend node IDs const agentMap = { 'MLEffortAgent': 'route_planner', 'RoutePlannerAgent': 'route_planner', 'FairnessManagerAgent': 'fairness_manager', 'DriverLiaisonAgent': 'driver_liaison', 'ExplainabilityAgent': 'explainability', 'LearningAgent': 'learning', }; for (const step of steps) { const nodeId = agentMap[step.agent_name]; if (!nodeId) continue; const node = this.nodes.get(nodeId); if (node) { // Update to processing const statusBadge = node.element.querySelector('.agent-status'); if (statusBadge) { statusBadge.className = 'agent-status processing'; statusBadge.innerHTML = ' Processing'; } node.element.classList.add('processing'); // Log to terminal const logMsg = `[${step.agent_name}] ${step.step_type}`; console.log(logMsg); // Use the existing terminal if open, or update the mock one // We'll assuming updateConnections highlights the flow too await new Promise(r => setTimeout(r, 1200)); // Visible delay // Update to active/done if (statusBadge) { statusBadge.className = 'agent-status active'; statusBadge.innerHTML = ' Active'; } node.element.classList.remove('processing'); node.element.classList.add('active'); } } this.showToast('Workflow Replay Complete'); } } // Add fadeInOut animation const style = document.createElement('style'); style.textContent = ` @keyframes fadeInOut { 0% { opacity: 0; transform: translateX(-50%) translateY(20px); } 20% { opacity: 1; transform: translateX(-50%) translateY(0); } 80% { opacity: 1; transform: translateX(-50%) translateY(0); } 100% { opacity: 0; transform: translateX(-50%) translateY(-20px); } } `; document.head.appendChild(style); // Initialize dashboard when DOM is ready document.addEventListener('DOMContentLoaded', () => { window.dashboard = new AgentSupervisionDashboard(); // Start activity simulation for demo // window.dashboard.simulateActivity(); });