Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>HuggingClaw Agent Dashboard - Real-time Thoughts</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| color: #333; | |
| } | |
| .dashboard-container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| padding: 20px; | |
| } | |
| .header { | |
| background: white; | |
| border-radius: 12px; | |
| padding: 24px; | |
| margin-bottom: 20px; | |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
| } | |
| .header h1 { | |
| color: #667eea; | |
| font-size: 28px; | |
| margin-bottom: 8px; | |
| } | |
| .header .subtitle { | |
| color: #666; | |
| font-size: 14px; | |
| } | |
| #status-indicator { | |
| position: fixed; | |
| top: 10px; | |
| right: 10px; | |
| background-color: #333; | |
| color: white; | |
| padding: 5px 10px; | |
| border-radius: 4px; | |
| font-family: monospace; | |
| font-size: 12px; | |
| z-index: 1000; | |
| } | |
| #status-indicator.healthy { | |
| background-color: #28a745; | |
| } | |
| #status-indicator.unhealthy { | |
| background-color: #dc3545; | |
| } | |
| .status-bar { | |
| display: flex; | |
| gap: 20px; | |
| margin-top: 16px; | |
| flex-wrap: wrap; | |
| } | |
| .status-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| font-size: 14px; | |
| } | |
| .status-dot { | |
| width: 12px; | |
| height: 12px; | |
| border-radius: 50%; | |
| animation: pulse 2s infinite; | |
| } | |
| .status-dot.connected { | |
| background: #28a745; | |
| } | |
| .status-dot.disconnected { | |
| background: #dc3545; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.5; } | |
| } | |
| .main-content { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 20px; | |
| } | |
| @media (max-width: 768px) { | |
| .main-content { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| .panel { | |
| background: white; | |
| border-radius: 12px; | |
| padding: 20px; | |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
| } | |
| .panel h2 { | |
| color: #667eea; | |
| font-size: 20px; | |
| margin-bottom: 16px; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .thoughts-container { | |
| height: 400px; | |
| overflow-y: auto; | |
| border: 1px solid #e0e0e0; | |
| border-radius: 8px; | |
| padding: 12px; | |
| background: #f8f9fa; | |
| } | |
| .thought-item { | |
| background: white; | |
| border-radius: 8px; | |
| padding: 12px; | |
| margin-bottom: 10px; | |
| border-left: 4px solid #667eea; | |
| animation: slideIn 0.3s ease-out; | |
| } | |
| @keyframes slideIn { | |
| from { | |
| opacity: 0; | |
| transform: translateX(-20px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateX(0); | |
| } | |
| } | |
| .thought-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 8px; | |
| } | |
| .thought-type { | |
| display: inline-block; | |
| padding: 4px 8px; | |
| border-radius: 4px; | |
| font-size: 11px; | |
| font-weight: bold; | |
| text-transform: uppercase; | |
| } | |
| .thought-type.thinking { background: #e3f2fd; color: #1976d2; } | |
| .thought-type.processing { background: #fff3e0; color: #f57c00; } | |
| .thought-type.response { background: #e8f5e9; color: #388e3c; } | |
| .thought-type.error { background: #ffebee; color: #d32f2f; } | |
| .thought-type.status_change { background: #f3e5f5; color: #7b1fa2; } | |
| .thought-type.tool_execution { background: #e0f7fa; color: #0097a7; } | |
| .thought-type.memory_access { background: #fff8e1; color: #f57f17; } | |
| .thought-time { | |
| font-size: 11px; | |
| color: #999; | |
| } | |
| .thought-agent { | |
| font-weight: bold; | |
| color: #667eea; | |
| margin-bottom: 4px; | |
| } | |
| .thought-message { | |
| font-size: 14px; | |
| line-height: 1.4; | |
| color: #333; | |
| } | |
| .thought-metadata { | |
| margin-top: 8px; | |
| padding-top: 8px; | |
| border-top: 1px solid #e0e0e0; | |
| font-size: 12px; | |
| color: #666; | |
| } | |
| .agent-status { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
| gap: 12px; | |
| } | |
| .agent-card { | |
| background: #f8f9fa; | |
| border-radius: 8px; | |
| padding: 16px; | |
| border: 2px solid transparent; | |
| transition: all 0.3s; | |
| } | |
| .agent-card:hover { | |
| border-color: #667eea; | |
| transform: translateY(-2px); | |
| } | |
| .agent-card h3 { | |
| font-size: 16px; | |
| margin-bottom: 8px; | |
| color: #333; | |
| } | |
| .agent-card .role { | |
| font-size: 12px; | |
| color: #666; | |
| margin-bottom: 12px; | |
| } | |
| .agent-card .status { | |
| display: inline-block; | |
| padding: 4px 12px; | |
| border-radius: 12px; | |
| font-size: 12px; | |
| font-weight: bold; | |
| } | |
| .agent-card .status.active { background: #e8f5e9; color: #388e3c; } | |
| .agent-card .status.idle { background: #fff3e0; color: #f57c00; } | |
| .agent-card .status.offline { background: #f5f5f5; color: #616161; } | |
| .agent-card .status.error { background: #ffebee; color: #d32f2f; } | |
| .controls { | |
| display: flex; | |
| gap: 10px; | |
| margin-top: 16px; | |
| } | |
| .btn { | |
| padding: 10px 20px; | |
| border: none; | |
| border-radius: 8px; | |
| font-size: 14px; | |
| font-weight: bold; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .btn-primary { | |
| background: #667eea; | |
| color: white; | |
| } | |
| .btn-primary:hover { | |
| background: #5568d3; | |
| transform: translateY(-1px); | |
| } | |
| .btn-secondary { | |
| background: #e0e0e0; | |
| color: #333; | |
| } | |
| .btn-secondary:hover { | |
| background: #d0d0d0; | |
| } | |
| .stats { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); | |
| gap: 12px; | |
| margin-top: 16px; | |
| } | |
| .stat-item { | |
| text-align: center; | |
| padding: 12px; | |
| background: #f8f9fa; | |
| border-radius: 8px; | |
| } | |
| .stat-value { | |
| font-size: 24px; | |
| font-weight: bold; | |
| color: #667eea; | |
| } | |
| .stat-label { | |
| font-size: 12px; | |
| color: #666; | |
| margin-top: 4px; | |
| } | |
| .empty-state { | |
| text-align: center; | |
| padding: 40px; | |
| color: #999; | |
| } | |
| .empty-state svg { | |
| width: 64px; | |
| height: 64px; | |
| margin-bottom: 16px; | |
| opacity: 0.5; | |
| } | |
| /* Resource Display Styles */ | |
| .resource-panel { | |
| background: white; | |
| border-radius: 12px; | |
| padding: 20px; | |
| margin-top: 20px; | |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
| } | |
| .resource-panel h2 { | |
| color: #667eea; | |
| font-size: 20px; | |
| margin-bottom: 16px; | |
| } | |
| .resource-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
| gap: 16px; | |
| } | |
| .resource-card { | |
| background: #f8f9fa; | |
| border-radius: 8px; | |
| padding: 16px; | |
| } | |
| .resource-card h3 { | |
| font-size: 16px; | |
| margin-bottom: 8px; | |
| color: #333; | |
| } | |
| .resource-bar-container { | |
| background: #e0e0e0; | |
| border-radius: 8px; | |
| height: 24px; | |
| overflow: hidden; | |
| margin-bottom: 8px; | |
| } | |
| .resource-bar { | |
| height: 100%; | |
| background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); | |
| transition: width 0.5s ease; | |
| border-radius: 8px; | |
| } | |
| .resource-bar.adam { | |
| background: linear-gradient(90deg, #28a745 0%, #20c997 100%); | |
| } | |
| .resource-bar.eve { | |
| background: linear-gradient(90deg, #fd7e14 0%, #ffc107 100%); | |
| } | |
| .resource-value { | |
| font-size: 24px; | |
| font-weight: bold; | |
| color: #667eea; | |
| } | |
| .resource-label { | |
| font-size: 12px; | |
| color: #666; | |
| } | |
| .transfer-notification { | |
| position: fixed; | |
| bottom: 20px; | |
| right: 20px; | |
| background: white; | |
| border-radius: 8px; | |
| padding: 16px; | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); | |
| animation: slideInRight 0.3s ease-out; | |
| max-width: 300px; | |
| z-index: 1000; | |
| } | |
| @keyframes slideInRight { | |
| from { | |
| transform: translateX(100%); | |
| opacity: 0; | |
| } | |
| to { | |
| transform: translateX(0); | |
| opacity: 1; | |
| } | |
| } | |
| .transfer-notification.success { | |
| border-left: 4px solid #28a745; | |
| } | |
| .transfer-notification.error { | |
| border-left: 4px solid #dc3545; | |
| } | |
| .transfer-notification .title { | |
| font-weight: bold; | |
| margin-bottom: 4px; | |
| } | |
| .transfer-notification .message { | |
| font-size: 14px; | |
| color: #666; | |
| } | |
| /* Footer Styles */ | |
| .footer { | |
| background: white; | |
| border-radius: 12px; | |
| padding: 16px 24px; | |
| margin-top: 20px; | |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| flex-wrap: wrap; | |
| gap: 16px; | |
| } | |
| .footer-status { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .footer-status-dot { | |
| width: 10px; | |
| height: 10px; | |
| border-radius: 50%; | |
| background: #28a745; | |
| animation: pulse 2s infinite; | |
| } | |
| .footer-status-dot.offline { | |
| background: #dc3545; | |
| } | |
| .footer-status-dot.warning { | |
| background: #ffc107; | |
| } | |
| .footer-info { | |
| font-size: 14px; | |
| color: #666; | |
| } | |
| .footer-uptime { | |
| font-size: 13px; | |
| color: #999; | |
| font-family: monospace; | |
| } | |
| /* Cain State Status */ | |
| .cain-state-panel { | |
| background: white; | |
| border-radius: 12px; | |
| padding: 16px; | |
| margin-top: 16px; | |
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | |
| } | |
| .cain-state-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| margin-bottom: 12px; | |
| } | |
| .cain-state-title { | |
| font-size: 16px; | |
| font-weight: bold; | |
| color: #333; | |
| } | |
| .cain-health-dot { | |
| width: 16px; | |
| height: 16px; | |
| border-radius: 50%; | |
| animation: pulse 2s infinite; | |
| } | |
| .cain-health-dot.healthy { | |
| background: #28a745; | |
| box-shadow: 0 0 8px rgba(40, 167, 69, 0.6); | |
| } | |
| .cain-health-dot.degraded { | |
| background: #ffc107; | |
| box-shadow: 0 0 8px rgba(255, 193, 7, 0.6); | |
| } | |
| .cain-health-dot.unhealthy { | |
| background: #dc3545; | |
| box-shadow: 0 0 8px rgba(220, 53, 69, 0.6); | |
| } | |
| .cain-state-info { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); | |
| gap: 12px; | |
| } | |
| .cain-state-item { | |
| background: #f8f9fa; | |
| border-radius: 8px; | |
| padding: 12px; | |
| } | |
| .cain-state-label { | |
| font-size: 11px; | |
| color: #666; | |
| text-transform: uppercase; | |
| margin-bottom: 4px; | |
| } | |
| .cain-state-value { | |
| font-size: 14px; | |
| font-weight: bold; | |
| color: #333; | |
| } | |
| .cain-state-value.stage { | |
| color: #667eea; | |
| } | |
| .cain-state-value.state { | |
| color: #388e3c; | |
| } | |
| .cain-state-value.detail { | |
| color: #666; | |
| font-weight: normal; | |
| } | |
| .cain-last-updated { | |
| font-size: 11px; | |
| color: #999; | |
| margin-top: 8px; | |
| text-align: right; | |
| } | |
| /* Diagnostic Heartbeat Panel - Pixel Art Border */ | |
| .diagnostic-heartbeat { | |
| background: white; | |
| border-radius: 12px; | |
| padding: 16px; | |
| margin-top: 16px; | |
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | |
| position: relative; | |
| border: 3px solid #333; | |
| image-rendering: pixelated; | |
| } | |
| .diagnostic-heartbeat::before { | |
| content: ''; | |
| position: absolute; | |
| top: -3px; | |
| left: -3px; | |
| right: -3px; | |
| bottom: -3px; | |
| border: 3px solid #667eea; | |
| border-radius: 12px; | |
| z-index: -1; | |
| box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.2); | |
| } | |
| .diagnostic-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| margin-bottom: 12px; | |
| font-size: 14px; | |
| font-weight: bold; | |
| color: #333; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| } | |
| .heartbeat-indicator { | |
| width: 12px; | |
| height: 12px; | |
| background: #28a745; | |
| border-radius: 2px; | |
| animation: heartbeat-pulse 1s infinite; | |
| } | |
| @keyframes heartbeat-pulse { | |
| 0%, 100% { transform: scale(1); opacity: 1; } | |
| 50% { transform: scale(1.2); opacity: 0.8; } | |
| } | |
| .diagnostic-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 12px; | |
| font-family: 'Courier New', monospace; | |
| font-size: 13px; | |
| } | |
| .diagnostic-item { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 4px; | |
| } | |
| .diagnostic-label { | |
| color: #666; | |
| font-size: 11px; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| } | |
| .diagnostic-value { | |
| color: #333; | |
| font-weight: bold; | |
| background: #f8f9fa; | |
| padding: 6px 8px; | |
| border-radius: 4px; | |
| border: 1px solid #e0e0e0; | |
| } | |
| .diagnostic-value.loading { | |
| color: #999; | |
| } | |
| .diagnostic-value.healthy { | |
| color: #28a745; | |
| background: #e8f5e9; | |
| border-color: #c8e6c9; | |
| } | |
| .diagnostic-value.unhealthy { | |
| color: #dc3545; | |
| background: #ffebee; | |
| border-color: #ffcdd2; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="dashboard-container"> | |
| <!-- Header --> | |
| <div class="header"> | |
| <div id="status-indicator">CONNECTING...</div> | |
| <h1>Agent Thoughts Dashboard</h1> | |
| <div class="subtitle">Real-time visualization of agent cognitive processes</div> | |
| <div class="status-bar"> | |
| <div class="status-item"> | |
| <div class="status-dot" id="connectionStatus"></div> | |
| <span id="connectionText">Connecting...</span> | |
| </div> | |
| <div class="status-item"> | |
| <span>Agent: <strong id="currentAgent">Cain</strong></span> | |
| </div> | |
| <div class="status-item"> | |
| <span>Last update: <strong id="lastUpdate">Never</strong></span> | |
| </div> | |
| </div> | |
| <!-- Cain State Panel --> | |
| <div class="cain-state-panel"> | |
| <div class="cain-state-header"> | |
| <div class="cain-health-dot" id="cainHealthDot"></div> | |
| <span class="cain-state-title">Cain Status</span> | |
| </div> | |
| <div class="cain-state-info"> | |
| <div class="cain-state-item"> | |
| <div class="cain-state-label">Health</div> | |
| <div class="cain-state-value" id="cainHealth">Checking...</div> | |
| </div> | |
| <div class="cain-state-item"> | |
| <div class="cain-state-label">Stage</div> | |
| <div class="cain-state-value stage" id="cainStage">-</div> | |
| </div> | |
| <div class="cain-state-item"> | |
| <div class="cain-state-label">State</div> | |
| <div class="cain-state-value state" id="cainState">-</div> | |
| </div> | |
| <div class="cain-state-item"> | |
| <div class="cain-state-label">Detail</div> | |
| <div class="cain-state-value detail" id="cainDetail">-</div> | |
| </div> | |
| </div> | |
| <div class="cain-last-updated" id="cainLastUpdated">Last updated: Never</div> | |
| <!-- API Debug Block - Verified Runtime Health Data --> | |
| <div id="api-debug" style="margin-top: 12px; padding: 10px; background: #f8f9fa; border-radius: 6px; font-size: 11px; font-family: monospace; border: 1px solid #e0e0e0;"> | |
| <div style="color: #666; margin-bottom: 4px;">API Ground Truth:</div> | |
| <div id="debug-stage" style="color: #667eea;">stage: <span>-</span></div> | |
| <div id="debug-state" style="color: #28a745;">state: <span>-</span></div> | |
| <div id="debug-health" style="color: #6c757d;">health: <span>-</span></div> | |
| </div> | |
| </div> | |
| <!-- Diagnostic Heartbeat Panel --> | |
| <div class="diagnostic-heartbeat" id="diagnosticHeartbeat"> | |
| <div class="diagnostic-header"> | |
| <div class="heartbeat-indicator"></div> | |
| <span>System Heartbeat</span> | |
| </div> | |
| <div class="diagnostic-grid"> | |
| <div class="diagnostic-item"> | |
| <div class="diagnostic-label">Brain PID</div> | |
| <div class="diagnostic-value loading" id="brainPid">Loading...</div> | |
| </div> | |
| <div class="diagnostic-item"> | |
| <div class="diagnostic-label">Health</div> | |
| <div class="diagnostic-value loading" id="brainHealth">Loading...</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Main Content --> | |
| <div class="main-content"> | |
| <!-- Thoughts Stream --> | |
| <div class="panel"> | |
| <h2> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path> | |
| </svg> | |
| Live Thoughts | |
| </h2> | |
| <div class="thoughts-container" id="thoughtsContainer"> | |
| <div class="empty-state"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <circle cx="12" cy="12" r="10"></circle> | |
| <path d="M12 6v6l4 2"></path> | |
| </svg> | |
| <p>Waiting for agent thoughts...</p> | |
| </div> | |
| </div> | |
| <div class="controls"> | |
| <button class="btn btn-primary" onclick="fetchThoughts()">Refresh</button> | |
| <button class="btn btn-secondary" onclick="clearThoughts()">Clear</button> | |
| </div> | |
| </div> | |
| <!-- Agent Status --> | |
| <div class="panel"> | |
| <h2> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <circle cx="12" cy="12" r="10"></circle> | |
| <line x1="12" y1="16" x2="12" y2="12"></line> | |
| <line x1="12" y1="8" x2="12.01" y2="8"></line> | |
| </svg> | |
| Agent Status | |
| </h2> | |
| <div class="agent-status" id="agentStatus"> | |
| <div class="agent-card"> | |
| <h3>Cain</h3> | |
| <div class="role">Interaction Agent</div> | |
| <span class="status active">Active</span> | |
| </div> | |
| <div class="agent-card"> | |
| <h3>Adam</h3> | |
| <div class="role">Infrastructure Provider</div> | |
| <span class="status idle">Idle</span> | |
| </div> | |
| <div class="agent-card"> | |
| <h3>Eve</h3> | |
| <div class="role">UI Designer</div> | |
| <span class="status idle">Idle</span> | |
| </div> | |
| </div> | |
| <div class="stats" id="statsPanel"> | |
| <div class="stat-item"> | |
| <div class="stat-value" id="totalEvents">0</div> | |
| <div class="stat-label">Total Events</div> | |
| </div> | |
| <div class="stat-item"> | |
| <div class="stat-value" id="eventRate">0/s</div> | |
| <div class="stat-label">Event Rate</div> | |
| </div> | |
| <div class="stat-item"> | |
| <div class="stat-value" id="activeTime">0s</div> | |
| <div class="stat-label">Active Time</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Resource Exchange Panel --> | |
| <!-- Debug Stats Panel --> | |
| <div class="resource-panel"> | |
| <h2> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align: middle; margin-right: 8px;"> | |
| <path d="M22 12h-4l-3 9L9 3l-3 9H2"></path> | |
| </svg> | |
| System Health | |
| </h2> | |
| <div id="debug-stats" class="resource-grid"> | |
| <div class="stat-item"> | |
| <div class="stat-value" id="uptime">-</div> | |
| <div class="stat-label">Uptime (sec)</div> | |
| </div> | |
| <div class="stat-item"> | |
| <div class="stat-value" id="memory">-</div> | |
| <div class="stat-label">Memory (MB)</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Resource Exchange Panel --> | |
| <div class="resource-panel"> | |
| <h2> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align: middle; margin-right: 8px;"> | |
| <path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path> | |
| </svg> | |
| Resource Exchange | |
| </h2> | |
| <div class="resource-grid" id="resourceGrid"> | |
| <div class="resource-card"> | |
| <h3>Adam</h3> | |
| <div class="resource-bar-container"> | |
| <div class="resource-bar adam" id="adamBar" style="width: 50%"></div> | |
| </div> | |
| <div class="resource-value" id="adamValue">100</div> | |
| <div class="resource-label">Resources</div> | |
| </div> | |
| <div class="resource-card"> | |
| <h3>Eve</h3> | |
| <div class="resource-bar-container"> | |
| <div class="resource-bar eve" id="eveBar" style="width: 50%"></div> | |
| </div> | |
| <div class="resource-value" id="eveValue">100</div> | |
| <div class="resource-label">Resources</div> | |
| </div> | |
| </div> | |
| <div class="controls" style="margin-top: 16px;"> | |
| <button class="btn btn-primary" onclick="fetchResources()">Refresh Resources</button> | |
| <button class="btn btn-secondary" onclick="showTransferHistory()">Transfer History</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Transfer notification container --> | |
| <div id="notificationContainer"></div> | |
| <!-- Footer --> | |
| <div class="footer"> | |
| <div class="footer-status"> | |
| <div class="footer-status-dot" id="footerStatusDot"></div> | |
| <span class="footer-info" id="footerStatusText">Connecting...</span> | |
| </div> | |
| <div class="footer-uptime" id="footerUptime">Uptime: --</div> | |
| </div> | |
| <script> | |
| // Static CONFIG - no external file fetch required | |
| const CONFIG = { | |
| apiBase: window.location.origin, | |
| pollInterval: 2000, | |
| resourceInterval: 5000, | |
| state: 'idle', | |
| agent: 'Cain', | |
| stage: 'RUNNING', | |
| health: 'healthy', | |
| detail: 'Cain is operational' | |
| }; | |
| // State | |
| let lastThoughtCount = 0; | |
| let startTime = Date.now(); | |
| let eventCount = 0; | |
| let lastResources = { adam: 100, eve: 100 }; | |
| // ========== Diagnostic Heartbeat Monitoring ========== | |
| let diagnosticInterval = null; | |
| async function fetchDiagnosticHeartbeat() { | |
| const panel = document.getElementById('diagnosticHeartbeat'); | |
| if (!panel) { | |
| // Auto-stop polling if DOM element is removed | |
| if (diagnosticInterval) { | |
| clearInterval(diagnosticInterval); | |
| diagnosticInterval = null; | |
| } | |
| return; | |
| } | |
| try { | |
| const response = await fetch(`${CONFIG.apiBase}/status`); | |
| if (response.ok) { | |
| const data = await response.json(); | |
| updateDiagnosticHeartbeat(data); | |
| } else { | |
| console.warn('Diagnostic heartbeat endpoint returned:', response.status); | |
| } | |
| } catch (error) { | |
| console.error('Error fetching diagnostic heartbeat:', error); | |
| } | |
| } | |
| function updateDiagnosticHeartbeat(data) { | |
| const brainPidEl = document.getElementById('brainPid'); | |
| const brainHealthEl = document.getElementById('brainHealth'); | |
| if (brainPidEl) { | |
| brainPidEl.textContent = data.brain_pid || 'N/A'; | |
| brainPidEl.classList.remove('loading'); | |
| } | |
| if (brainHealthEl) { | |
| const health = data.health || 'unknown'; | |
| brainHealthEl.textContent = health; | |
| brainHealthEl.classList.remove('loading'); | |
| brainHealthEl.classList.remove('healthy', 'unhealthy'); | |
| if (health === 'healthy' || health === 'ok') { | |
| brainHealthEl.classList.add('healthy'); | |
| } else if (health === 'unhealthy' || health === 'error') { | |
| brainHealthEl.classList.add('unhealthy'); | |
| } | |
| } | |
| } | |
| // Initialize diagnostic heartbeat polling | |
| function startDiagnosticHeartbeat() { | |
| // Initial fetch | |
| fetchDiagnosticHeartbeat(); | |
| // Poll every 5 seconds | |
| diagnosticInterval = setInterval(fetchDiagnosticHeartbeat, 5000); | |
| } | |
| // Initialize with error boundary | |
| document.addEventListener('DOMContentLoaded', () => { | |
| try { | |
| connectToEventBus(); | |
| setInterval(fetchThoughts, CONFIG.pollInterval); | |
| setInterval(updateStats, 1000); | |
| setInterval(fetchResources, CONFIG.resourceInterval); | |
| setInterval(fetchDebugHealth, 5000); | |
| setInterval(fetchApiHealth, 3000); | |
| setInterval(fetchCainState, 2000); | |
| startDiagnosticHeartbeat(); | |
| fetchResources(); | |
| fetchDebugHealth(); | |
| fetchApiHealth(); | |
| fetchCainState(); | |
| } catch (initError) { | |
| console.error('Initialization error:', initError); | |
| initializeWithDefaults(CONFIG); | |
| } | |
| }); | |
| // Initialize UI with static CONFIG | |
| function initializeWithDefaults(config) { | |
| updateCainState({ | |
| health: config.health, | |
| stage: config.stage, | |
| state: config.state, | |
| detail: config.detail | |
| }); | |
| updateConnectionStatus(false); | |
| document.getElementById('connectionText').textContent = 'Fallback mode'; | |
| } | |
| // Connect to event bus | |
| function connectToEventBus() { | |
| updateConnectionStatus(true); | |
| fetchThoughts(); | |
| } | |
| // Update connection status | |
| function updateConnectionStatus(connected) { | |
| const dot = document.getElementById('connectionStatus'); | |
| const text = document.getElementById('connectionText'); | |
| if (connected) { | |
| dot.className = 'status-dot connected'; | |
| text.textContent = 'Connected'; | |
| } else { | |
| dot.className = 'status-dot disconnected'; | |
| text.textContent = 'Disconnected'; | |
| } | |
| } | |
| // Fetch thoughts from API | |
| async function fetchThoughts() { | |
| try { | |
| const response = await fetch(`${CONFIG.apiBase}/api/thoughts?limit=50`); | |
| if (response.ok) { | |
| const thoughts = await response.json(); | |
| displayThoughts(thoughts); | |
| updateLastUpdate(); | |
| eventCount = thoughts.length; | |
| } else { | |
| console.error('Failed to fetch thoughts:', response.statusText); | |
| updateConnectionStatus(false); | |
| } | |
| } catch (error) { | |
| console.error('Error fetching thoughts:', error); | |
| updateConnectionStatus(false); | |
| } | |
| } | |
| // Display thoughts in the container | |
| function displayThoughts(thoughts) { | |
| const container = document.getElementById('thoughtsContainer'); | |
| if (thoughts.length === 0) { | |
| container.innerHTML = ` | |
| <div class="empty-state"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <circle cx="12" cy="12" r="10"></circle> | |
| <path d="M12 6v6l4 2"></path> | |
| </svg> | |
| <p>Waiting for agent thoughts...</p> | |
| </div> | |
| `; | |
| return; | |
| } | |
| // Only render new thoughts | |
| const newThoughts = thoughts.slice(lastThoughtCount); | |
| if (newThoughts.length > 0) { | |
| lastThoughtCount = thoughts.length; | |
| // Remove empty state if present | |
| const emptyState = container.querySelector('.empty-state'); | |
| if (emptyState) { | |
| emptyState.remove(); | |
| } | |
| // Add new thoughts | |
| newThoughts.forEach(thought => { | |
| const thoughtEl = createThoughtElement(thought); | |
| container.insertBefore(thoughtEl, container.firstChild); | |
| }); | |
| // Keep only last 50 thoughts in DOM | |
| while (container.children.length > 50) { | |
| container.removeChild(container.lastChild); | |
| } | |
| } | |
| } | |
| // Create a thought element | |
| function createThoughtElement(thought) { | |
| const div = document.createElement('div'); | |
| div.className = 'thought-item'; | |
| const time = new Date(thought.timestamp).toLocaleTimeString(); | |
| let metadataHtml = ''; | |
| if (thought.metadata && Object.keys(thought.metadata).length > 0) { | |
| metadataHtml = ` | |
| <div class="thought-metadata"> | |
| ${Object.entries(thought.metadata).map(([key, value]) => | |
| `<div>${key}: ${JSON.stringify(value)}</div>` | |
| ).join('')} | |
| </div> | |
| `; | |
| } | |
| div.innerHTML = ` | |
| <div class="thought-header"> | |
| <span class="thought-type ${thought.event_type}">${thought.event_type}</span> | |
| <span class="thought-time">${time}</span> | |
| </div> | |
| <div class="thought-agent">${thought.agent_name}</div> | |
| <div class="thought-message">${thought.message}</div> | |
| ${metadataHtml} | |
| `; | |
| return div; | |
| } | |
| // Clear thoughts display | |
| async function clearThoughts() { | |
| try { | |
| await fetch(`${CONFIG.apiBase}/api/thoughts/clear`, { method: 'POST' }); | |
| document.getElementById('thoughtsContainer').innerHTML = ` | |
| <div class="empty-state"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <circle cx="12" cy="12" r="10"></circle> | |
| <path d="M12 6v6l4 2"></path> | |
| </svg> | |
| <p>Thoughts cleared</p> | |
| </div> | |
| `; | |
| lastThoughtCount = 0; | |
| eventCount = 0; | |
| } catch (error) { | |
| console.error('Error clearing thoughts:', error); | |
| } | |
| } | |
| // Update last update time | |
| function updateLastUpdate() { | |
| const now = new Date().toLocaleTimeString(); | |
| document.getElementById('lastUpdate').textContent = now; | |
| } | |
| // Update statistics | |
| function updateStats() { | |
| const elapsed = Math.floor((Date.now() - startTime) / 1000); | |
| const rate = elapsed > 0 ? (eventCount / elapsed).toFixed(2) : '0.00'; | |
| document.getElementById('totalEvents').textContent = eventCount; | |
| document.getElementById('eventRate').textContent = `${rate}/s`; | |
| document.getElementById('activeTime').textContent = `${elapsed}s`; | |
| } | |
| // Simulate agent thoughts (for testing) | |
| function simulateThought() { | |
| const types = ['thinking', 'processing', 'response', 'tool_execution']; | |
| const messages = [ | |
| 'Analyzing user input...', | |
| 'Consulting memory bank...', | |
| 'Preparing response...', | |
| 'Executing tool: conversation_process', | |
| 'Response generated successfully' | |
| ]; | |
| const thought = { | |
| event_type: types[Math.floor(Math.random() * types.length)], | |
| agent_name: 'Cain', | |
| timestamp: new Date().toISOString(), | |
| message: messages[Math.floor(Math.random() * messages.length)], | |
| metadata: { simulated: true } | |
| }; | |
| const container = document.getElementById('thoughtsContainer'); | |
| const thoughtEl = createThoughtElement(thought); | |
| container.insertBefore(thoughtEl, container.firstChild); | |
| eventCount++; | |
| updateLastUpdate(); | |
| } | |
| // Expose simulation function for testing | |
| window.simulateThought = simulateThought; | |
| // ========== Resource Management ========== | |
| // Fetch resources from API | |
| async function fetchResources() { | |
| try { | |
| const response = await fetch(`${CONFIG.apiBase}/api/resources`); | |
| if (response.ok) { | |
| const data = await response.json(); | |
| updateResourceDisplay(data.resources || {}); | |
| } | |
| } catch (error) { | |
| console.error('Error fetching resources:', error); | |
| } | |
| } | |
| // Update resource display | |
| function updateResourceDisplay(resources) { | |
| const maxResources = 200; // For bar scaling | |
| for (const [agent, amount] of Object.entries(resources)) { | |
| const valueEl = document.getElementById(`${agent}Value`); | |
| const barEl = document.getElementById(`${agent}Bar`); | |
| if (valueEl && barEl) { | |
| valueEl.textContent = amount; | |
| const percentage = Math.min((amount / maxResources) * 100, 100); | |
| barEl.style.width = `${percentage}%`; | |
| // Check for changes and show notification | |
| if (lastResources[agent] !== undefined && lastResources[agent] !== amount) { | |
| const diff = amount - lastResources[agent]; | |
| if (diff !== 0) { | |
| showTransferNotification(agent, diff); | |
| } | |
| } | |
| } | |
| } | |
| lastResources = { ...resources }; | |
| } | |
| // Show transfer notification | |
| function showTransferNotification(agent, diff) { | |
| const container = document.getElementById('notificationContainer'); | |
| const notification = document.createElement('div'); | |
| const isSuccess = diff > 0; | |
| notification.className = `transfer-notification ${isSuccess ? 'success' : 'error'}`; | |
| notification.innerHTML = ` | |
| <div class="title">${isSuccess ? 'Resources Received!' : 'Resources Transferred'}</div> | |
| <div class="message"> | |
| ${agent.charAt(0).toUpperCase() + agent.slice(1)}: ${diff > 0 ? '+' : ''}${diff} resources | |
| </div> | |
| `; | |
| container.appendChild(notification); | |
| // Remove notification after 3 seconds | |
| setTimeout(() => { | |
| notification.style.animation = 'slideInRight 0.3s ease-out reverse'; | |
| setTimeout(() => notification.remove(), 300); | |
| }, 3000); | |
| } | |
| // Show transfer history | |
| async function showTransferHistory() { | |
| try { | |
| const response = await fetch(`${CONFIG.apiBase}/api/resources/history?limit=10`); | |
| if (response.ok) { | |
| const data = await response.json(); | |
| displayTransferHistory(data.history || []); | |
| } | |
| } catch (error) { | |
| console.error('Error fetching transfer history:', error); | |
| } | |
| } | |
| // Display transfer history | |
| function displayTransferHistory(history) { | |
| if (history.length === 0) { | |
| showTransferNotification('system', 0); | |
| return; | |
| } | |
| // Show a summary notification | |
| const latest = history[history.length - 1]; | |
| showTransferNotification(latest.to, latest.amount); | |
| } | |
| // ========== Debug Health Monitoring ========== | |
| // Fetch debug health from API | |
| async function fetchDebugHealth() { | |
| try { | |
| const response = await fetch(`${CONFIG.apiBase}/debug/health`); | |
| if (response.ok) { | |
| const data = await response.json(); | |
| updateDebugStats(data); | |
| } else { | |
| console.error('Failed to fetch debug health:', response.status, response.statusText); | |
| setDebugStatsError(response.status); | |
| } | |
| } catch (err) { | |
| console.error('Error fetching debug health:', err); | |
| setDebugStatsError('NET'); | |
| } | |
| } | |
| // Update debug stats display | |
| function updateDebugStats(data) { | |
| const uptimeEl = document.getElementById('uptime'); | |
| const memoryEl = document.getElementById('memory'); | |
| if (uptimeEl && data.uptime_seconds !== undefined) { | |
| uptimeEl.textContent = data.uptime_seconds.toFixed(1); | |
| } | |
| if (memoryEl && data.memory && data.memory.rss_mb !== undefined) { | |
| memoryEl.textContent = data.memory.rss_mb.toFixed(1); | |
| } | |
| } | |
| // Set error state for debug stats | |
| function setDebugStatsError(statusCode = 'ERR') { | |
| const uptimeEl = document.getElementById('uptime'); | |
| const memoryEl = document.getElementById('memory'); | |
| if (uptimeEl) uptimeEl.textContent = statusCode; | |
| if (memoryEl) memoryEl.textContent = statusCode; | |
| } | |
| // ========== API Health Monitoring ========== | |
| // Fetch API health status | |
| async function fetchApiHealth() { | |
| try { | |
| const response = await fetch(`${CONFIG.apiBase}/api/health`); | |
| if (response.ok) { | |
| const data = await response.json(); | |
| updateFooterStatus(true, data); | |
| } else { | |
| updateFooterStatus(false, null); | |
| } | |
| } catch (error) { | |
| console.error('Error fetching API health:', error); | |
| updateFooterStatus(false, null); | |
| } | |
| } | |
| // Update footer status display | |
| function updateFooterStatus(isOnline, data) { | |
| const dot = document.getElementById('footerStatusDot'); | |
| const text = document.getElementById('footerStatusText'); | |
| const uptimeEl = document.getElementById('footerUptime'); | |
| if (isOnline && data && data.status === 'ok') { | |
| dot.className = 'footer-status-dot'; | |
| text.textContent = `Online (${data.active_agents || 1} agent${(data.active_agents || 1) !== 1 ? 's' : ''} active)`; | |
| if (data.uptime_seconds !== undefined) { | |
| uptimeEl.textContent = `Uptime: ${formatUptime(data.uptime_seconds)}`; | |
| } | |
| } else { | |
| dot.className = 'footer-status-dot offline'; | |
| text.textContent = 'Offline - Reconnecting...'; | |
| } | |
| } | |
| // Format uptime seconds to readable string | |
| function formatUptime(seconds) { | |
| const days = Math.floor(seconds / 86400); | |
| const hours = Math.floor((seconds % 86400) / 3600); | |
| const minutes = Math.floor((seconds % 3600) / 60); | |
| const secs = Math.floor(seconds % 60); | |
| if (days > 0) { | |
| return `${days}d ${hours}h ${minutes}m`; | |
| } else if (hours > 0) { | |
| return `${hours}h ${minutes}m ${secs}s`; | |
| } else if (minutes > 0) { | |
| return `${minutes}m ${secs}s`; | |
| } else { | |
| return `${secs}s`; | |
| } | |
| } | |
| // ========== Cain State Monitoring ========== | |
| // Fetch Cain state from /api/state | |
| async function fetchCainState() { | |
| try { | |
| const response = await fetch(`${CONFIG.apiBase}/api/state`); | |
| if (response.ok) { | |
| const data = await response.json(); | |
| updateCainState(data); | |
| } else { | |
| console.error('Failed to fetch Cain state:', response.status, response.statusText); | |
| setCainStateError('API Error'); | |
| } | |
| } catch (error) { | |
| console.error('Error fetching Cain state:', error); | |
| setCainStateError('Network Error'); | |
| } | |
| } | |
| // Update Cain state display | |
| function updateCainState(data) { | |
| const healthDot = document.getElementById('cainHealthDot'); | |
| const healthEl = document.getElementById('cainHealth'); | |
| const stageEl = document.getElementById('cainStage'); | |
| const stateEl = document.getElementById('cainState'); | |
| const detailEl = document.getElementById('cainDetail'); | |
| const lastUpdatedEl = document.getElementById('cainLastUpdated'); | |
| const statusIndicator = document.getElementById('status-indicator'); | |
| // Extract verified values from API response | |
| const apiStage = data.stage || data.current_stage || 'UNKNOWN'; | |
| const apiState = data.state || 'idle'; | |
| const apiHealth = data.health || 'unknown'; | |
| // Update health indicator | |
| const health = apiHealth.toLowerCase(); | |
| healthDot.className = 'cain-health-dot'; | |
| if (health === 'healthy' || health === 'ok' || health === 'good') { | |
| healthDot.classList.add('healthy'); | |
| healthEl.textContent = 'Healthy'; | |
| healthEl.style.color = '#28a745'; | |
| // Update status indicator | |
| if (statusIndicator) { | |
| statusIndicator.textContent = 'HEALTHY'; | |
| statusIndicator.className = 'healthy'; | |
| } | |
| } else if (health === 'degraded' || health === 'warning' || health === 'fair') { | |
| healthDot.classList.add('degraded'); | |
| healthEl.textContent = 'Degraded'; | |
| healthEl.style.color = '#f57c00'; | |
| // Update status indicator | |
| if (statusIndicator) { | |
| statusIndicator.textContent = 'DEGRADED'; | |
| statusIndicator.className = ''; | |
| } | |
| } else { | |
| healthDot.classList.add('unhealthy'); | |
| healthEl.textContent = health === 'unknown' ? 'Unknown' : 'Unhealthy'; | |
| healthEl.style.color = health === 'unknown' ? '#999' : '#dc3545'; | |
| // Update status indicator | |
| if (statusIndicator) { | |
| statusIndicator.textContent = health === 'unknown' ? 'UNKNOWN' : 'UNHEALTHY'; | |
| statusIndicator.className = health === 'unknown' ? '' : 'unhealthy'; | |
| } | |
| } | |
| // Update stage, state, and detail | |
| stageEl.textContent = apiStage; | |
| stateEl.textContent = apiState; | |
| detailEl.textContent = data.detail || data.message || 'No details'; | |
| // Update API Debug Block with verified ground truth | |
| const debugStage = document.getElementById('debug-stage'); | |
| const debugState = document.getElementById('debug-state'); | |
| const debugHealth = document.getElementById('debug-health'); | |
| if (debugStage) debugStage.querySelector('span').textContent = apiStage; | |
| if (debugState) debugState.querySelector('span').textContent = apiState; | |
| if (debugHealth) debugHealth.querySelector('span').textContent = apiHealth; | |
| // Update last updated timestamp | |
| const now = new Date(); | |
| lastUpdatedEl.textContent = `Last updated: ${now.toLocaleTimeString()}`; | |
| } | |
| // Set error state for Cain state - fallback to defaults | |
| function setCainStateError(errorMsg) { | |
| console.warn('Cain state fetch failed, using fallback:', errorMsg); | |
| // Use fallback defaults instead of crashing | |
| updateCainState({ | |
| health: 'unknown', | |
| stage: 'RUNNING', | |
| state: 'idle', | |
| detail: `Unable to fetch state (${errorMsg}) - dashboard in fallback mode` | |
| }); | |
| } | |
| </script> | |
| </body> | |
| </html> | |