HuggingClaw-Cain / frontend /agent-dashboard.html
Claude Code
Claude Code: Add diagnostic heartbeat panel to agent dashboard
50b0ecc
<!DOCTYPE html>
<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>