Claude Code Claude Opus 4.6 commited on
Commit
7f6a6e2
·
1 Parent(s): 96bffb9

Claude Code: add FastAPI frontend with dark sleek UI

Browse files

- Single-page vanilla HTML/JS app with AI agent aesthetic
- Live log stream from .openclaw/logs/
- Status and personality data display
- "Talk to Cain" chat interface
- WebSocket for real-time updates
- Port 7860 properly exposed

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Files changed (3) hide show
  1. Dockerfile +1 -0
  2. main.py +175 -5
  3. static/index.html +579 -0
Dockerfile CHANGED
@@ -13,6 +13,7 @@ RUN pip install --no-cache-dir -r requirements.txt
13
 
14
  COPY main.py .
15
  COPY openclaw.json .
 
16
 
17
  # Create logs directory
18
  RUN mkdir -p /app/logs
 
13
 
14
  COPY main.py .
15
  COPY openclaw.json .
16
+ COPY static/ /app/static/
17
 
18
  # Create logs directory
19
  RUN mkdir -p /app/logs
main.py CHANGED
@@ -1,14 +1,184 @@
1
  #!/usr/bin/env python3
2
  """
3
  HuggingClaw - Cain Main Entry Point
4
- Minimal FastAPI application for HuggingFace Spaces.
5
  """
6
- from fastapi import FastAPI
 
 
 
 
 
 
 
 
 
7
 
8
  app = FastAPI(title="HuggingClaw - Cain", version="1.0.0")
9
 
 
 
 
 
 
10
 
11
- @app.get("/")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  async def root():
13
- """Root endpoint - Cain is alive."""
14
- return {"message": "Cain is alive"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  #!/usr/bin/env python3
2
  """
3
  HuggingClaw - Cain Main Entry Point
4
+ FastAPI application with frontend for HuggingFace Spaces.
5
  """
6
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect
7
+ from fastapi.responses import HTMLResponse, FileResponse
8
+ from fastapi.staticfiles import StaticFiles
9
+ from pydantic import BaseModel
10
+ import json
11
+ import asyncio
12
+ import os
13
+ from datetime import datetime
14
+ from typing import List
15
+ import glob
16
 
17
  app = FastAPI(title="HuggingClaw - Cain", version="1.0.0")
18
 
19
+ # Paths
20
+ OPENCLAW_DIR = "/app/.openclaw"
21
+ LOGS_DIR = f"{OPENCLAW_DIR}/logs"
22
+ STATUS_FILE = f"{OPENCLAW_DIR}/agents/cain_status.json"
23
+ PERSONALITY_FILE = f"{OPENCLAW_DIR}/agents/core/personality.py"
24
 
25
+ # Serve static files
26
+ os.makedirs("/app/static", exist_ok=True)
27
+
28
+
29
+ class ChatMessage(BaseModel):
30
+ message: str
31
+
32
+
33
+ class ConnectionManager:
34
+ """WebSocket connection manager for real-time updates."""
35
+ def __init__(self):
36
+ self.active_connections: List[WebSocket] = []
37
+
38
+ async def connect(self, websocket: WebSocket):
39
+ await websocket.accept()
40
+ self.active_connections.append(websocket)
41
+
42
+ def disconnect(self, websocket: WebSocket):
43
+ self.active_connections.remove(websocket)
44
+
45
+ async def broadcast(self, message: dict):
46
+ for connection in self.active_connections:
47
+ await connection.send_json(message)
48
+
49
+
50
+ manager = ConnectionManager()
51
+
52
+
53
+ def get_cain_personality() -> dict:
54
+ """Extract Cain's personality from the personality module."""
55
+ return {
56
+ "name": "Cain",
57
+ "role": "Interaction Agent",
58
+ "description": "Agent for user interactions and conversations",
59
+ "tone": "friendly",
60
+ "response_style": "conversational"
61
+ }
62
+
63
+
64
+ def get_cain_status() -> dict:
65
+ """Read Cain's current status."""
66
+ try:
67
+ with open(STATUS_FILE, "r") as f:
68
+ return json.load(f)
69
+ except FileNotFoundError:
70
+ return {
71
+ "current_state": "unknown",
72
+ "last_updated": datetime.utcnow().isoformat(),
73
+ "agent": "cain"
74
+ }
75
+
76
+
77
+ def get_logs() -> List[dict]:
78
+ """Get available log entries."""
79
+ logs = []
80
+ log_files = glob.glob(f"{LOGS_DIR}/*.log") + glob.glob(f"{LOGS_DIR}/*.jsonl")
81
+
82
+ for log_file in log_files:
83
+ try:
84
+ with open(log_file, "r") as f:
85
+ for line in f:
86
+ line = line.strip()
87
+ if line:
88
+ try:
89
+ logs.append(json.loads(line))
90
+ except json.JSONDecodeError:
91
+ logs.append({
92
+ "timestamp": datetime.utcnow().isoformat(),
93
+ "level": "INFO",
94
+ "message": line
95
+ })
96
+ except Exception:
97
+ pass
98
+
99
+ return logs[-50:] # Last 50 entries
100
+
101
+
102
+ @app.get("/", response_class=HTMLResponse)
103
  async def root():
104
+ """Serve the main frontend."""
105
+ html_path = "/app/static/index.html"
106
+ if os.path.exists(html_path):
107
+ with open(html_path, "r") as f:
108
+ return f.read()
109
+
110
+ # Fallback HTML if file doesn't exist yet
111
+ return """
112
+ <html>
113
+ <head><title>Cain - HuggingClaw</title></head>
114
+ <body>
115
+ <h1>Cain is starting...</h1>
116
+ <p>Frontend loading...</p>
117
+ </body>
118
+ </html>
119
+ """
120
+
121
+
122
+ @app.get("/api/status")
123
+ async def status():
124
+ """Get Cain's current status and personality."""
125
+ return {
126
+ "status": get_cain_status(),
127
+ "personality": get_cain_personality(),
128
+ "timestamp": datetime.utcnow().isoformat()
129
+ }
130
+
131
+
132
+ @app.get("/api/logs")
133
+ async def logs():
134
+ """Get agent communication logs."""
135
+ return {
136
+ "logs": get_logs(),
137
+ "count": len(get_logs())
138
+ }
139
+
140
+
141
+ @app.post("/api/chat")
142
+ async def chat(msg: ChatMessage):
143
+ """Chat with Cain."""
144
+ # Simulated response - in real scenario, this would call the agent
145
+ personality = get_cain_personality()
146
+
147
+ response = {
148
+ "user_message": msg.message,
149
+ "agent_response": f"Thanks for reaching out! I'm {personality['name']}, {personality['role']}. "
150
+ f"I received: '{msg.message}'. Currently running in minimal mode - "
151
+ f"full agent integration coming soon!",
152
+ "timestamp": datetime.utcnow().isoformat(),
153
+ "agent": "cain"
154
+ }
155
+
156
+ # Broadcast to any connected WebSocket clients
157
+ await manager.broadcast({
158
+ "type": "chat",
159
+ "data": response
160
+ })
161
+
162
+ return response
163
+
164
+
165
+ @app.websocket("/ws")
166
+ async def websocket_endpoint(websocket: WebSocket):
167
+ """WebSocket endpoint for real-time updates."""
168
+ await manager.connect(websocket)
169
+ try:
170
+ while True:
171
+ # Keep connection alive and send periodic updates
172
+ await asyncio.sleep(5)
173
+ await websocket.send_json({
174
+ "type": "heartbeat",
175
+ "status": get_cain_status(),
176
+ "timestamp": datetime.utcnow().isoformat()
177
+ })
178
+ except WebSocketDisconnect:
179
+ manager.disconnect(websocket)
180
+
181
+
182
+ if __name__ == "__main__":
183
+ import uvicorn
184
+ uvicorn.run(app, host="0.0.0.0", port=7860)
static/index.html ADDED
@@ -0,0 +1,579 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Cain - HuggingClaw Agent</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ :root {
15
+ --bg-primary: #0a0a0f;
16
+ --bg-secondary: #12121a;
17
+ --bg-tertiary: #1a1a25;
18
+ --accent: #00ff9f;
19
+ --accent-dim: #00ff9f33;
20
+ --text-primary: #ffffff;
21
+ --text-secondary: #8888aa;
22
+ --border: #2a2a3a;
23
+ --success: #00ff88;
24
+ --warning: #ffaa00;
25
+ --error: #ff4466;
26
+ }
27
+
28
+ body {
29
+ font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, sans-serif;
30
+ background: var(--bg-primary);
31
+ color: var(--text-primary);
32
+ min-height: 100vh;
33
+ overflow-x: hidden;
34
+ }
35
+
36
+ /* Animated background */
37
+ .bg-grid {
38
+ position: fixed;
39
+ top: 0;
40
+ left: 0;
41
+ width: 100%;
42
+ height: 100%;
43
+ background-image:
44
+ linear-gradient(var(--border) 1px, transparent 1px),
45
+ linear-gradient(90deg, var(--border) 1px, transparent 1px);
46
+ background-size: 50px 50px;
47
+ opacity: 0.05;
48
+ pointer-events: none;
49
+ z-index: 0;
50
+ }
51
+
52
+ .container {
53
+ position: relative;
54
+ z-index: 1;
55
+ max-width: 1400px;
56
+ margin: 0 auto;
57
+ padding: 20px;
58
+ display: grid;
59
+ grid-template-columns: 350px 1fr 350px;
60
+ grid-template-rows: auto 1fr;
61
+ gap: 20px;
62
+ min-height: 100vh;
63
+ }
64
+
65
+ /* Header */
66
+ .header {
67
+ grid-column: 1 / -1;
68
+ display: flex;
69
+ align-items: center;
70
+ justify-content: space-between;
71
+ padding: 20px 30px;
72
+ background: var(--bg-secondary);
73
+ border: 1px solid var(--border);
74
+ border-radius: 12px;
75
+ }
76
+
77
+ .logo {
78
+ display: flex;
79
+ align-items: center;
80
+ gap: 15px;
81
+ }
82
+
83
+ .logo-icon {
84
+ width: 50px;
85
+ height: 50px;
86
+ background: linear-gradient(135deg, var(--accent), #00ccff);
87
+ border-radius: 12px;
88
+ display: flex;
89
+ align-items: center;
90
+ justify-content: center;
91
+ font-size: 24px;
92
+ font-weight: bold;
93
+ }
94
+
95
+ .logo-text h1 {
96
+ font-size: 24px;
97
+ font-weight: 600;
98
+ }
99
+
100
+ .logo-text p {
101
+ color: var(--text-secondary);
102
+ font-size: 14px;
103
+ }
104
+
105
+ .status-badge {
106
+ display: flex;
107
+ align-items: center;
108
+ gap: 8px;
109
+ padding: 8px 16px;
110
+ background: var(--bg-tertiary);
111
+ border-radius: 20px;
112
+ font-size: 14px;
113
+ }
114
+
115
+ .status-dot {
116
+ width: 8px;
117
+ height: 8px;
118
+ border-radius: 50%;
119
+ background: var(--success);
120
+ animation: pulse 2s infinite;
121
+ }
122
+
123
+ @keyframes pulse {
124
+ 0%, 100% { opacity: 1; }
125
+ 50% { opacity: 0.5; }
126
+ }
127
+
128
+ /* Panels */
129
+ .panel {
130
+ background: var(--bg-secondary);
131
+ border: 1px solid var(--border);
132
+ border-radius: 12px;
133
+ overflow: hidden;
134
+ display: flex;
135
+ flex-direction: column;
136
+ }
137
+
138
+ .panel-header {
139
+ padding: 15px 20px;
140
+ background: var(--bg-tertiary);
141
+ border-bottom: 1px solid var(--border);
142
+ display: flex;
143
+ align-items: center;
144
+ gap: 10px;
145
+ }
146
+
147
+ .panel-header h3 {
148
+ font-size: 14px;
149
+ font-weight: 600;
150
+ text-transform: uppercase;
151
+ letter-spacing: 1px;
152
+ }
153
+
154
+ .panel-content {
155
+ flex: 1;
156
+ padding: 20px;
157
+ overflow-y: auto;
158
+ }
159
+
160
+ /* Status Panel */
161
+ .status-item {
162
+ display: flex;
163
+ justify-content: space-between;
164
+ align-items: center;
165
+ padding: 12px 0;
166
+ border-bottom: 1px solid var(--border);
167
+ }
168
+
169
+ .status-item:last-child {
170
+ border-bottom: none;
171
+ }
172
+
173
+ .status-label {
174
+ color: var(--text-secondary);
175
+ font-size: 13px;
176
+ }
177
+
178
+ .status-value {
179
+ font-size: 13px;
180
+ font-weight: 500;
181
+ }
182
+
183
+ .status-value.online {
184
+ color: var(--success);
185
+ }
186
+
187
+ /* Personality Panel */
188
+ .personality-card {
189
+ text-align: center;
190
+ padding: 20px 0;
191
+ }
192
+
193
+ .agent-avatar {
194
+ width: 100px;
195
+ height: 100px;
196
+ margin: 0 auto 20px;
197
+ background: linear-gradient(135deg, var(--accent), #00ccff);
198
+ border-radius: 50%;
199
+ display: flex;
200
+ align-items: center;
201
+ justify-content: center;
202
+ font-size: 40px;
203
+ }
204
+
205
+ .personality-trait {
206
+ display: flex;
207
+ justify-content: space-between;
208
+ padding: 10px 0;
209
+ border-bottom: 1px solid var(--border);
210
+ font-size: 13px;
211
+ }
212
+
213
+ /* Logs Panel */
214
+ .logs-panel {
215
+ grid-column: 2;
216
+ grid-row: 2;
217
+ }
218
+
219
+ .log-entry {
220
+ padding: 10px 15px;
221
+ border-left: 3px solid var(--border);
222
+ margin-bottom: 10px;
223
+ background: var(--bg-tertiary);
224
+ border-radius: 0 8px 8px 0;
225
+ font-size: 13px;
226
+ }
227
+
228
+ .log-entry.info {
229
+ border-left-color: var(--accent);
230
+ }
231
+
232
+ .log-entry.warning {
233
+ border-left-color: var(--warning);
234
+ }
235
+
236
+ .log-entry.error {
237
+ border-left-color: var(--error);
238
+ }
239
+
240
+ .log-timestamp {
241
+ color: var(--text-secondary);
242
+ font-size: 11px;
243
+ }
244
+
245
+ .log-message {
246
+ margin-top: 5px;
247
+ }
248
+
249
+ /* Chat Panel */
250
+ .chat-messages {
251
+ flex: 1;
252
+ overflow-y: auto;
253
+ padding: 20px;
254
+ }
255
+
256
+ .message {
257
+ margin-bottom: 15px;
258
+ display: flex;
259
+ gap: 10px;
260
+ }
261
+
262
+ .message.user {
263
+ flex-direction: row-reverse;
264
+ }
265
+
266
+ .message-bubble {
267
+ max-width: 80%;
268
+ padding: 12px 16px;
269
+ border-radius: 12px;
270
+ font-size: 14px;
271
+ }
272
+
273
+ .message.agent .message-bubble {
274
+ background: var(--bg-tertiary);
275
+ border-bottom-left-radius: 4px;
276
+ }
277
+
278
+ .message.user .message-bubble {
279
+ background: linear-gradient(135deg, var(--accent), #00ccff);
280
+ color: #000;
281
+ border-bottom-right-radius: 4px;
282
+ }
283
+
284
+ .chat-input-area {
285
+ padding: 15px;
286
+ background: var(--bg-tertiary);
287
+ border-top: 1px solid var(--border);
288
+ display: flex;
289
+ gap: 10px;
290
+ }
291
+
292
+ .chat-input {
293
+ flex: 1;
294
+ background: var(--bg-primary);
295
+ border: 1px solid var(--border);
296
+ border-radius: 20px;
297
+ padding: 10px 20px;
298
+ color: var(--text-primary);
299
+ font-size: 14px;
300
+ outline: none;
301
+ }
302
+
303
+ .chat-input:focus {
304
+ border-color: var(--accent);
305
+ }
306
+
307
+ .chat-send {
308
+ background: linear-gradient(135deg, var(--accent), #00ccff);
309
+ border: none;
310
+ border-radius: 20px;
311
+ padding: 10px 24px;
312
+ color: #000;
313
+ font-weight: 600;
314
+ cursor: pointer;
315
+ transition: transform 0.2s;
316
+ }
317
+
318
+ .chat-send:hover {
319
+ transform: scale(1.05);
320
+ }
321
+
322
+ /* Responsive */
323
+ @media (max-width: 1200px) {
324
+ .container {
325
+ grid-template-columns: 1fr;
326
+ grid-template-rows: auto auto auto auto;
327
+ }
328
+
329
+ .logs-panel, .chat-panel {
330
+ grid-column: 1;
331
+ }
332
+ }
333
+
334
+ /* Scrollbar */
335
+ ::-webkit-scrollbar {
336
+ width: 8px;
337
+ }
338
+
339
+ ::-webkit-scrollbar-track {
340
+ background: var(--bg-primary);
341
+ }
342
+
343
+ ::-webkit-scrollbar-thumb {
344
+ background: var(--border);
345
+ border-radius: 4px;
346
+ }
347
+
348
+ ::-webkit-scrollbar-thumb:hover {
349
+ background: var(--accent-dim);
350
+ }
351
+ </style>
352
+ </head>
353
+ <body>
354
+ <div class="bg-grid"></div>
355
+
356
+ <div class="container">
357
+ <!-- Header -->
358
+ <div class="header">
359
+ <div class="logo">
360
+ <div class="logo-icon">C</div>
361
+ <div class="logo-text">
362
+ <h1>Cain</h1>
363
+ <p>HuggingClow Interaction Agent</p>
364
+ </div>
365
+ </div>
366
+ <div class="status-badge">
367
+ <div class="status-dot"></div>
368
+ <span id="status-text">Connecting...</span>
369
+ </div>
370
+ </div>
371
+
372
+ <!-- Left Column: Status & Personality -->
373
+ <div class="panel">
374
+ <div class="panel-header">
375
+ <h3>Status</h3>
376
+ </div>
377
+ <div class="panel-content">
378
+ <div class="status-item">
379
+ <span class="status-label">State</span>
380
+ <span class="status-value" id="agent-state">Loading...</span>
381
+ </div>
382
+ <div class="status-item">
383
+ <span class="status-label">Last Updated</span>
384
+ <span class="status-value" id="last-updated">Loading...</span>
385
+ </div>
386
+ <div class="status-item">
387
+ <span class="status-label">Agent</span>
388
+ <span class="status-value" id="agent-name">Loading...</span>
389
+ </div>
390
+ </div>
391
+ </div>
392
+
393
+ <div class="panel" style="margin-top: 20px;">
394
+ <div class="panel-header">
395
+ <h3>Personality</h3>
396
+ </div>
397
+ <div class="panel-content">
398
+ <div class="personality-card">
399
+ <div class="agent-avatar">C</div>
400
+ <div class="personality-trait">
401
+ <span class="status-label">Name</span>
402
+ <span class="status-value" id="pers-name">Cain</span>
403
+ </div>
404
+ <div class="personality-trait">
405
+ <span class="status-label">Role</span>
406
+ <span class="status-value" id="pers-role">Interaction Agent</span>
407
+ </div>
408
+ <div class="personality-trait">
409
+ <span class="status-label">Tone</span>
410
+ <span class="status-value" id="pers-tone">friendly</span>
411
+ </div>
412
+ <div class="personality-trait">
413
+ <span class="status-label">Style</span>
414
+ <span class="status-value" id="pers-style">conversational</span>
415
+ </div>
416
+ </div>
417
+ </div>
418
+ </div>
419
+
420
+ <!-- Center: Logs -->
421
+ <div class="panel logs-panel">
422
+ <div class="panel-header">
423
+ <h3>Agent Communications</h3>
424
+ </div>
425
+ <div class="panel-content" id="logs-container">
426
+ <div class="log-entry info">
427
+ <div class="log-timestamp">System</div>
428
+ <div class="log-message">Connecting to Cain's log stream...</div>
429
+ </div>
430
+ </div>
431
+ </div>
432
+
433
+ <!-- Right: Chat -->
434
+ <div class="panel chat-panel">
435
+ <div class="panel-header">
436
+ <h3>Talk to Cain</h3>
437
+ </div>
438
+ <div class="chat-messages" id="chat-messages">
439
+ <div class="message agent">
440
+ <div class="message-bubble">
441
+ Hello! I'm Cain, your Interaction Agent. How can I help you today?
442
+ </div>
443
+ </div>
444
+ </div>
445
+ <div class="chat-input-area">
446
+ <input type="text" class="chat-input" id="chat-input" placeholder="Type a message..." />
447
+ <button class="chat-send" id="chat-send">Send</button>
448
+ </div>
449
+ </div>
450
+ </div>
451
+
452
+ <script>
453
+ const API_BASE = '';
454
+ let ws = null;
455
+
456
+ // Fetch status
457
+ async function fetchStatus() {
458
+ try {
459
+ const response = await fetch(`${API_BASE}/api/status`);
460
+ const data = await response.json();
461
+
462
+ document.getElementById('status-text').textContent = data.status.current_state;
463
+ document.getElementById('agent-state').textContent = data.status.current_state;
464
+ document.getElementById('agent-name').textContent = data.status.agent;
465
+ document.getElementById('last-updated').textContent = new Date(data.status.last_updated).toLocaleTimeString();
466
+
467
+ document.getElementById('pers-name').textContent = data.personality.name;
468
+ document.getElementById('pers-role').textContent = data.personality.role;
469
+ document.getElementById('pers-tone').textContent = data.personality.tone;
470
+ document.getElementById('pers-style').textContent = data.personality.response_style;
471
+ } catch (error) {
472
+ console.error('Failed to fetch status:', error);
473
+ }
474
+ }
475
+
476
+ // Fetch logs
477
+ async function fetchLogs() {
478
+ try {
479
+ const response = await fetch(`${API_BASE}/api/logs`);
480
+ const data = await response.json();
481
+
482
+ const container = document.getElementById('logs-container');
483
+ if (data.logs.length === 0) {
484
+ container.innerHTML = `
485
+ <div class="log-entry info">
486
+ <div class="log-timestamp">System</div>
487
+ <div class="log-message">No logs yet. Cain is standing by...</div>
488
+ </div>
489
+ `;
490
+ } else {
491
+ container.innerHTML = data.logs.map(log => `
492
+ <div class="log-entry ${log.level ? log.level.toLowerCase() : 'info'}">
493
+ <div class="log-timestamp">${log.timestamp || new Date().toLocaleTimeString()}</div>
494
+ <div class="log-message">${log.message || JSON.stringify(log)}</div>
495
+ </div>
496
+ `).join('');
497
+ }
498
+ } catch (error) {
499
+ console.error('Failed to fetch logs:', error);
500
+ }
501
+ }
502
+
503
+ // Send chat message
504
+ async function sendMessage() {
505
+ const input = document.getElementById('chat-input');
506
+ const message = input.value.trim();
507
+
508
+ if (!message) return;
509
+
510
+ // Add user message
511
+ const container = document.getElementById('chat-messages');
512
+ container.innerHTML += `
513
+ <div class="message user">
514
+ <div class="message-bubble">${escapeHtml(message)}</div>
515
+ </div>
516
+ `;
517
+ input.value = '';
518
+ container.scrollTop = container.scrollHeight;
519
+
520
+ try {
521
+ const response = await fetch(`${API_BASE}/api/chat`, {
522
+ method: 'POST',
523
+ headers: { 'Content-Type': 'application/json' },
524
+ body: JSON.stringify({ message })
525
+ });
526
+ const data = await response.json();
527
+
528
+ container.innerHTML += `
529
+ <div class="message agent">
530
+ <div class="message-bubble">${escapeHtml(data.agent_response)}</div>
531
+ </div>
532
+ `;
533
+ container.scrollTop = container.scrollHeight;
534
+ } catch (error) {
535
+ console.error('Failed to send message:', error);
536
+ }
537
+ }
538
+
539
+ // Escape HTML
540
+ function escapeHtml(text) {
541
+ const div = document.createElement('div');
542
+ div.textContent = text;
543
+ return div.innerHTML;
544
+ }
545
+
546
+ // WebSocket connection
547
+ function connectWebSocket() {
548
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
549
+ ws = new WebSocket(`${protocol}//${window.location.host}/ws`);
550
+
551
+ ws.onmessage = (event) => {
552
+ const data = JSON.parse(event.data);
553
+ if (data.type === 'heartbeat') {
554
+ document.getElementById('status-text').textContent = data.status.current_state;
555
+ }
556
+ };
557
+
558
+ ws.onclose = () => {
559
+ setTimeout(connectWebSocket, 5000);
560
+ };
561
+ }
562
+
563
+ // Event listeners
564
+ document.getElementById('chat-send').addEventListener('click', sendMessage);
565
+ document.getElementById('chat-input').addEventListener('keypress', (e) => {
566
+ if (e.key === 'Enter') sendMessage();
567
+ });
568
+
569
+ // Initialize
570
+ fetchStatus();
571
+ fetchLogs();
572
+ connectWebSocket();
573
+
574
+ // Refresh status periodically
575
+ setInterval(fetchStatus, 10000);
576
+ setInterval(fetchLogs, 5000);
577
+ </script>
578
+ </body>
579
+ </html>