HuggingClaw-Cain / static /index.html
Claude Code
Claude Code: add FastAPI frontend with dark sleek UI
7f6a6e2
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cain - HuggingClaw Agent</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--bg-primary: #0a0a0f;
--bg-secondary: #12121a;
--bg-tertiary: #1a1a25;
--accent: #00ff9f;
--accent-dim: #00ff9f33;
--text-primary: #ffffff;
--text-secondary: #8888aa;
--border: #2a2a3a;
--success: #00ff88;
--warning: #ffaa00;
--error: #ff4466;
}
body {
font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
overflow-x: hidden;
}
/* Animated background */
.bg-grid {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image:
linear-gradient(var(--border) 1px, transparent 1px),
linear-gradient(90deg, var(--border) 1px, transparent 1px);
background-size: 50px 50px;
opacity: 0.05;
pointer-events: none;
z-index: 0;
}
.container {
position: relative;
z-index: 1;
max-width: 1400px;
margin: 0 auto;
padding: 20px;
display: grid;
grid-template-columns: 350px 1fr 350px;
grid-template-rows: auto 1fr;
gap: 20px;
min-height: 100vh;
}
/* Header */
.header {
grid-column: 1 / -1;
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 30px;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 12px;
}
.logo {
display: flex;
align-items: center;
gap: 15px;
}
.logo-icon {
width: 50px;
height: 50px;
background: linear-gradient(135deg, var(--accent), #00ccff);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
font-weight: bold;
}
.logo-text h1 {
font-size: 24px;
font-weight: 600;
}
.logo-text p {
color: var(--text-secondary);
font-size: 14px;
}
.status-badge {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: var(--bg-tertiary);
border-radius: 20px;
font-size: 14px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--success);
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* Panels */
.panel {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 12px;
overflow: hidden;
display: flex;
flex-direction: column;
}
.panel-header {
padding: 15px 20px;
background: var(--bg-tertiary);
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
gap: 10px;
}
.panel-header h3 {
font-size: 14px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
}
.panel-content {
flex: 1;
padding: 20px;
overflow-y: auto;
}
/* Status Panel */
.status-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid var(--border);
}
.status-item:last-child {
border-bottom: none;
}
.status-label {
color: var(--text-secondary);
font-size: 13px;
}
.status-value {
font-size: 13px;
font-weight: 500;
}
.status-value.online {
color: var(--success);
}
/* Personality Panel */
.personality-card {
text-align: center;
padding: 20px 0;
}
.agent-avatar {
width: 100px;
height: 100px;
margin: 0 auto 20px;
background: linear-gradient(135deg, var(--accent), #00ccff);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 40px;
}
.personality-trait {
display: flex;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid var(--border);
font-size: 13px;
}
/* Logs Panel */
.logs-panel {
grid-column: 2;
grid-row: 2;
}
.log-entry {
padding: 10px 15px;
border-left: 3px solid var(--border);
margin-bottom: 10px;
background: var(--bg-tertiary);
border-radius: 0 8px 8px 0;
font-size: 13px;
}
.log-entry.info {
border-left-color: var(--accent);
}
.log-entry.warning {
border-left-color: var(--warning);
}
.log-entry.error {
border-left-color: var(--error);
}
.log-timestamp {
color: var(--text-secondary);
font-size: 11px;
}
.log-message {
margin-top: 5px;
}
/* Chat Panel */
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.message {
margin-bottom: 15px;
display: flex;
gap: 10px;
}
.message.user {
flex-direction: row-reverse;
}
.message-bubble {
max-width: 80%;
padding: 12px 16px;
border-radius: 12px;
font-size: 14px;
}
.message.agent .message-bubble {
background: var(--bg-tertiary);
border-bottom-left-radius: 4px;
}
.message.user .message-bubble {
background: linear-gradient(135deg, var(--accent), #00ccff);
color: #000;
border-bottom-right-radius: 4px;
}
.chat-input-area {
padding: 15px;
background: var(--bg-tertiary);
border-top: 1px solid var(--border);
display: flex;
gap: 10px;
}
.chat-input {
flex: 1;
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: 20px;
padding: 10px 20px;
color: var(--text-primary);
font-size: 14px;
outline: none;
}
.chat-input:focus {
border-color: var(--accent);
}
.chat-send {
background: linear-gradient(135deg, var(--accent), #00ccff);
border: none;
border-radius: 20px;
padding: 10px 24px;
color: #000;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s;
}
.chat-send:hover {
transform: scale(1.05);
}
/* Responsive */
@media (max-width: 1200px) {
.container {
grid-template-columns: 1fr;
grid-template-rows: auto auto auto auto;
}
.logs-panel, .chat-panel {
grid-column: 1;
}
}
/* Scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-primary);
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--accent-dim);
}
</style>
</head>
<body>
<div class="bg-grid"></div>
<div class="container">
<!-- Header -->
<div class="header">
<div class="logo">
<div class="logo-icon">C</div>
<div class="logo-text">
<h1>Cain</h1>
<p>HuggingClow Interaction Agent</p>
</div>
</div>
<div class="status-badge">
<div class="status-dot"></div>
<span id="status-text">Connecting...</span>
</div>
</div>
<!-- Left Column: Status & Personality -->
<div class="panel">
<div class="panel-header">
<h3>Status</h3>
</div>
<div class="panel-content">
<div class="status-item">
<span class="status-label">State</span>
<span class="status-value" id="agent-state">Loading...</span>
</div>
<div class="status-item">
<span class="status-label">Last Updated</span>
<span class="status-value" id="last-updated">Loading...</span>
</div>
<div class="status-item">
<span class="status-label">Agent</span>
<span class="status-value" id="agent-name">Loading...</span>
</div>
</div>
</div>
<div class="panel" style="margin-top: 20px;">
<div class="panel-header">
<h3>Personality</h3>
</div>
<div class="panel-content">
<div class="personality-card">
<div class="agent-avatar">C</div>
<div class="personality-trait">
<span class="status-label">Name</span>
<span class="status-value" id="pers-name">Cain</span>
</div>
<div class="personality-trait">
<span class="status-label">Role</span>
<span class="status-value" id="pers-role">Interaction Agent</span>
</div>
<div class="personality-trait">
<span class="status-label">Tone</span>
<span class="status-value" id="pers-tone">friendly</span>
</div>
<div class="personality-trait">
<span class="status-label">Style</span>
<span class="status-value" id="pers-style">conversational</span>
</div>
</div>
</div>
</div>
<!-- Center: Logs -->
<div class="panel logs-panel">
<div class="panel-header">
<h3>Agent Communications</h3>
</div>
<div class="panel-content" id="logs-container">
<div class="log-entry info">
<div class="log-timestamp">System</div>
<div class="log-message">Connecting to Cain's log stream...</div>
</div>
</div>
</div>
<!-- Right: Chat -->
<div class="panel chat-panel">
<div class="panel-header">
<h3>Talk to Cain</h3>
</div>
<div class="chat-messages" id="chat-messages">
<div class="message agent">
<div class="message-bubble">
Hello! I'm Cain, your Interaction Agent. How can I help you today?
</div>
</div>
</div>
<div class="chat-input-area">
<input type="text" class="chat-input" id="chat-input" placeholder="Type a message..." />
<button class="chat-send" id="chat-send">Send</button>
</div>
</div>
</div>
<script>
const API_BASE = '';
let ws = null;
// Fetch status
async function fetchStatus() {
try {
const response = await fetch(`${API_BASE}/api/status`);
const data = await response.json();
document.getElementById('status-text').textContent = data.status.current_state;
document.getElementById('agent-state').textContent = data.status.current_state;
document.getElementById('agent-name').textContent = data.status.agent;
document.getElementById('last-updated').textContent = new Date(data.status.last_updated).toLocaleTimeString();
document.getElementById('pers-name').textContent = data.personality.name;
document.getElementById('pers-role').textContent = data.personality.role;
document.getElementById('pers-tone').textContent = data.personality.tone;
document.getElementById('pers-style').textContent = data.personality.response_style;
} catch (error) {
console.error('Failed to fetch status:', error);
}
}
// Fetch logs
async function fetchLogs() {
try {
const response = await fetch(`${API_BASE}/api/logs`);
const data = await response.json();
const container = document.getElementById('logs-container');
if (data.logs.length === 0) {
container.innerHTML = `
<div class="log-entry info">
<div class="log-timestamp">System</div>
<div class="log-message">No logs yet. Cain is standing by...</div>
</div>
`;
} else {
container.innerHTML = data.logs.map(log => `
<div class="log-entry ${log.level ? log.level.toLowerCase() : 'info'}">
<div class="log-timestamp">${log.timestamp || new Date().toLocaleTimeString()}</div>
<div class="log-message">${log.message || JSON.stringify(log)}</div>
</div>
`).join('');
}
} catch (error) {
console.error('Failed to fetch logs:', error);
}
}
// Send chat message
async function sendMessage() {
const input = document.getElementById('chat-input');
const message = input.value.trim();
if (!message) return;
// Add user message
const container = document.getElementById('chat-messages');
container.innerHTML += `
<div class="message user">
<div class="message-bubble">${escapeHtml(message)}</div>
</div>
`;
input.value = '';
container.scrollTop = container.scrollHeight;
try {
const response = await fetch(`${API_BASE}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message })
});
const data = await response.json();
container.innerHTML += `
<div class="message agent">
<div class="message-bubble">${escapeHtml(data.agent_response)}</div>
</div>
`;
container.scrollTop = container.scrollHeight;
} catch (error) {
console.error('Failed to send message:', error);
}
}
// Escape HTML
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// WebSocket connection
function connectWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
ws = new WebSocket(`${protocol}//${window.location.host}/ws`);
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'heartbeat') {
document.getElementById('status-text').textContent = data.status.current_state;
}
};
ws.onclose = () => {
setTimeout(connectWebSocket, 5000);
};
}
// Event listeners
document.getElementById('chat-send').addEventListener('click', sendMessage);
document.getElementById('chat-input').addEventListener('keypress', (e) => {
if (e.key === 'Enter') sendMessage();
});
// Initialize
fetchStatus();
fetchLogs();
connectWebSocket();
// Refresh status periodically
setInterval(fetchStatus, 10000);
setInterval(fetchLogs, 5000);
</script>
</body>
</html>