HuggingClaw-User-Deployment / frontend /agent-dashboard.html
Claude Code
Claude Code: Add /api/health endpoint with uptime and active_agents, and footer status polling in dashboard
774e62c
Raw
History Blame
33.5 kB
<!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-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;
}
</style>
</head>
<body>
<div class="dashboard-container">
<!-- Header -->
<div class="header">
<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>
</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>
// Configuration
const API_BASE = window.location.origin;
const POLL_INTERVAL = 2000; // 2 seconds
const RESOURCE_INTERVAL = 5000; // 5 seconds
// State
let lastThoughtCount = 0;
let startTime = Date.now();
let eventCount = 0;
let lastResources = { adam: 100, eve: 100 };
// Initialize
document.addEventListener('DOMContentLoaded', () => {
connectToEventBus();
setInterval(fetchThoughts, POLL_INTERVAL);
setInterval(updateStats, 1000);
setInterval(fetchResources, RESOURCE_INTERVAL);
setInterval(fetchDebugHealth, 5000); // Poll /debug/health every 5 seconds
setInterval(fetchApiHealth, 3000); // Poll /api/health every 3 seconds
fetchResources(); // Initial fetch
fetchDebugHealth(); // Initial health fetch
fetchApiHealth(); // Initial API health fetch
});
// 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(`${API_BASE}/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(`${API_BASE}/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(`${API_BASE}/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(`${API_BASE}/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(`${API_BASE}/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(`${API_BASE}/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`;
}
}
</script>
</body>
</html>