vfven commited on
Commit
83030b7
Β·
verified Β·
1 Parent(s): 7d85eb9

Upload 5 files

Browse files
Files changed (5) hide show
  1. Dockerfile +12 -0
  2. README.md +7 -5
  3. main.py +121 -0
  4. requirements.txt +4 -0
  5. templates/index.html +1082 -0
Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY . .
9
+
10
+ EXPOSE 7860
11
+
12
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,10 +1,12 @@
1
  ---
2
- title: Test
3
- emoji: πŸ”₯
4
- colorFrom: purple
5
- colorTo: purple
6
  sdk: docker
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
1
  ---
2
+ title: Mission Control AI
3
+ emoji: πŸš€
4
+ colorFrom: blue
5
+ colorTo: gray
6
  sdk: docker
7
  pinned: false
8
  ---
9
 
10
+ # Mission Control AI
11
+
12
+ Agent orchestration dashboard β€” FastAPI + HTML frontend.
main.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, Request
2
+ from fastapi.staticfiles import StaticFiles
3
+ from fastapi.responses import HTMLResponse, JSONResponse
4
+ from fastapi.middleware.cors import CORSMiddleware
5
+ import httpx
6
+ import json
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+
10
+ app = FastAPI()
11
+
12
+ app.add_middleware(
13
+ CORSMiddleware,
14
+ allow_origins=["*"],
15
+ allow_methods=["*"],
16
+ allow_headers=["*"],
17
+ )
18
+
19
+ app.mount("/static", StaticFiles(directory="static"), name="static")
20
+
21
+ N8N_WEBHOOK = "https://n8n-mission-control.fly.dev/webhook/mission-control"
22
+
23
+ mission_history = []
24
+
25
+ AGENTS = [
26
+ {"key": "manager", "name": "Manager", "role": "Project Coordinator"},
27
+ {"key": "developer", "name": "Developer", "role": "Senior Engineer"},
28
+ {"key": "analyst", "name": "Analyst", "role": "Business Analyst"},
29
+ ]
30
+
31
+
32
+ @app.get("/", response_class=HTMLResponse)
33
+ async def root():
34
+ html = Path("templates/index.html").read_text()
35
+ return HTMLResponse(content=html)
36
+
37
+
38
+ @app.get("/api/agents")
39
+ async def get_agents():
40
+ return {"agents": AGENTS}
41
+
42
+
43
+ @app.get("/api/history")
44
+ async def get_history():
45
+ return {"history": mission_history[-20:]}
46
+
47
+
48
+ @app.post("/api/mission")
49
+ async def run_mission(request: Request):
50
+ body = await request.json()
51
+ task = body.get("task", "").strip()
52
+
53
+ if not task:
54
+ return JSONResponse({"error": "No task provided"}, status_code=400)
55
+
56
+ started_at = datetime.now().isoformat()
57
+
58
+ try:
59
+ async with httpx.AsyncClient(timeout=300) as client:
60
+ resp = await client.post(
61
+ N8N_WEBHOOK,
62
+ json={"task": task},
63
+ headers={"Content-Type": "application/json"},
64
+ )
65
+
66
+ if resp.status_code != 200:
67
+ return JSONResponse({"error": f"n8n returned {resp.status_code}"}, status_code=502)
68
+
69
+ data = resp.json()
70
+ if isinstance(data, list):
71
+ data = data[0]
72
+
73
+ results = {}
74
+ for agent in AGENTS:
75
+ k = agent["key"]
76
+ raw = data.get(k, {})
77
+
78
+ if isinstance(raw, dict):
79
+ status = raw.get("status", "active")
80
+ message = raw.get("message", "")
81
+ model = raw.get("model", "") or raw.get("provider", "")
82
+ else:
83
+ status = "active"
84
+ message = str(raw)
85
+ model = ""
86
+
87
+ if status in ("resting", "rate_limit"):
88
+ status = "resting"
89
+
90
+ results[k] = {
91
+ "status": status,
92
+ "message": message,
93
+ "model": model,
94
+ }
95
+
96
+ final_raw = data.get("final", {})
97
+ final_msg = final_raw.get("message", "") if isinstance(final_raw, dict) else str(final_raw)
98
+
99
+ entry = {
100
+ "id": len(mission_history) + 1,
101
+ "task": task,
102
+ "started_at": started_at,
103
+ "ended_at": datetime.now().isoformat(),
104
+ "status": "completed",
105
+ "results": results,
106
+ "final": final_msg,
107
+ }
108
+ mission_history.append(entry)
109
+
110
+ return JSONResponse({
111
+ "success": True,
112
+ "task": task,
113
+ "results": results,
114
+ "final": final_msg,
115
+ "mission_id": entry["id"],
116
+ })
117
+
118
+ except httpx.TimeoutException:
119
+ return JSONResponse({"error": "Request timed out"}, status_code=504)
120
+ except Exception as e:
121
+ return JSONResponse({"error": str(e)}, status_code=500)
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ fastapi==0.115.0
2
+ uvicorn==0.30.6
3
+ httpx==0.27.2
4
+ python-multipart==0.0.9
templates/index.html ADDED
@@ -0,0 +1,1082 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="es">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Mission Control AI</title>
7
+ <style>
8
+ :root {
9
+ --bg: #f4f5f7;
10
+ --surface: #ffffff;
11
+ --surface2: #f8f9fb;
12
+ --border: #e1e4e8;
13
+ --border2: #d0d4da;
14
+ --text: #1a1d23;
15
+ --text2: #5a6270;
16
+ --text3: #8a93a0;
17
+ --accent: #2563eb;
18
+ --accent-light: #eff4ff;
19
+ --success: #16a34a;
20
+ --success-light: #f0fdf4;
21
+ --warning: #d97706;
22
+ --warning-light: #fffbeb;
23
+ --danger: #dc2626;
24
+ --danger-light: #fef2f2;
25
+ --shadow: 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.04);
26
+ --shadow-md: 0 4px 12px rgba(0,0,0,.08), 0 2px 4px rgba(0,0,0,.04);
27
+ --radius: 8px;
28
+ --radius-sm: 6px;
29
+ }
30
+
31
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
32
+
33
+ body {
34
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
35
+ background: var(--bg);
36
+ color: var(--text);
37
+ min-height: 100vh;
38
+ display: flex;
39
+ flex-direction: column;
40
+ font-size: 14px;
41
+ line-height: 1.5;
42
+ }
43
+
44
+ /* TOPBAR */
45
+ .topbar {
46
+ background: var(--surface);
47
+ border-bottom: 1px solid var(--border);
48
+ padding: 0 24px;
49
+ height: 56px;
50
+ display: flex;
51
+ align-items: center;
52
+ justify-content: space-between;
53
+ position: sticky;
54
+ top: 0;
55
+ z-index: 100;
56
+ box-shadow: var(--shadow);
57
+ }
58
+
59
+ .topbar-left { display: flex; align-items: center; gap: 12px; }
60
+
61
+ .logo {
62
+ width: 32px; height: 32px;
63
+ background: var(--accent);
64
+ border-radius: var(--radius-sm);
65
+ display: flex; align-items: center; justify-content: center;
66
+ }
67
+
68
+ .logo svg { width: 18px; height: 18px; fill: white; }
69
+
70
+ .topbar-title {
71
+ font-size: 15px;
72
+ font-weight: 600;
73
+ color: var(--text);
74
+ letter-spacing: -.01em;
75
+ }
76
+
77
+ .topbar-subtitle {
78
+ font-size: 12px;
79
+ color: var(--text3);
80
+ margin-left: 4px;
81
+ }
82
+
83
+ .topbar-right { display: flex; align-items: center; gap: 16px; }
84
+
85
+ .system-status {
86
+ display: flex;
87
+ align-items: center;
88
+ gap: 6px;
89
+ font-size: 12px;
90
+ color: var(--text2);
91
+ }
92
+
93
+ .status-dot {
94
+ width: 7px; height: 7px;
95
+ border-radius: 50%;
96
+ background: var(--success);
97
+ }
98
+
99
+ .status-dot.pulse { animation: pulse-green 2s ease infinite; }
100
+
101
+ @keyframes pulse-green {
102
+ 0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(22,163,74,.4); }
103
+ 50% { box-shadow: 0 0 0 5px rgba(22,163,74,0); }
104
+ }
105
+
106
+ .clock {
107
+ font-size: 12px;
108
+ color: var(--text3);
109
+ font-variant-numeric: tabular-nums;
110
+ font-weight: 500;
111
+ }
112
+
113
+ /* MISSION BAR */
114
+ .mission-bar {
115
+ background: var(--surface);
116
+ border-bottom: 1px solid var(--border);
117
+ padding: 16px 24px;
118
+ }
119
+
120
+ .mission-form {
121
+ display: flex;
122
+ gap: 10px;
123
+ max-width: 900px;
124
+ }
125
+
126
+ .mission-input {
127
+ flex: 1;
128
+ height: 40px;
129
+ border: 1px solid var(--border2);
130
+ border-radius: var(--radius-sm);
131
+ padding: 0 14px;
132
+ font-size: 14px;
133
+ color: var(--text);
134
+ background: var(--surface);
135
+ outline: none;
136
+ transition: border-color .15s, box-shadow .15s;
137
+ font-family: inherit;
138
+ }
139
+
140
+ .mission-input:focus {
141
+ border-color: var(--accent);
142
+ box-shadow: 0 0 0 3px rgba(37,99,235,.1);
143
+ }
144
+
145
+ .mission-input::placeholder { color: var(--text3); }
146
+
147
+ .launch-btn {
148
+ height: 40px;
149
+ padding: 0 20px;
150
+ background: var(--accent);
151
+ color: white;
152
+ border: none;
153
+ border-radius: var(--radius-sm);
154
+ font-size: 14px;
155
+ font-weight: 500;
156
+ cursor: pointer;
157
+ display: flex;
158
+ align-items: center;
159
+ gap: 7px;
160
+ transition: background .15s, transform .1s;
161
+ font-family: inherit;
162
+ white-space: nowrap;
163
+ }
164
+
165
+ .launch-btn:hover { background: #1d4ed8; }
166
+ .launch-btn:active { transform: scale(.98); }
167
+ .launch-btn:disabled { background: #93afd8; cursor: not-allowed; transform: none; }
168
+
169
+ .launch-btn svg { width: 15px; height: 15px; fill: white; flex-shrink: 0; }
170
+
171
+ /* MAIN LAYOUT */
172
+ .main {
173
+ display: grid;
174
+ grid-template-columns: 1fr 300px;
175
+ gap: 0;
176
+ flex: 1;
177
+ min-height: 0;
178
+ }
179
+
180
+ @media (max-width: 1024px) {
181
+ .main { grid-template-columns: 1fr; }
182
+ .sidebar { display: none; }
183
+ }
184
+
185
+ /* AGENTS AREA */
186
+ .agents-area {
187
+ padding: 20px 24px;
188
+ overflow-y: auto;
189
+ }
190
+
191
+ .section-label {
192
+ font-size: 11px;
193
+ font-weight: 600;
194
+ color: var(--text3);
195
+ letter-spacing: .06em;
196
+ text-transform: uppercase;
197
+ margin-bottom: 14px;
198
+ }
199
+
200
+ .agents-grid {
201
+ display: grid;
202
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
203
+ gap: 14px;
204
+ margin-bottom: 24px;
205
+ }
206
+
207
+ /* AGENT CARD */
208
+ .agent-card {
209
+ background: var(--surface);
210
+ border: 1px solid var(--border);
211
+ border-radius: var(--radius);
212
+ overflow: hidden;
213
+ box-shadow: var(--shadow);
214
+ transition: box-shadow .2s, transform .2s;
215
+ display: flex;
216
+ flex-direction: column;
217
+ }
218
+
219
+ .agent-card.is-active {
220
+ box-shadow: var(--shadow-md);
221
+ border-color: var(--accent);
222
+ }
223
+
224
+ .agent-card.is-resting {
225
+ border-color: var(--warning);
226
+ }
227
+
228
+ /* AGENT HEADER */
229
+ .agent-header {
230
+ padding: 14px 16px 12px;
231
+ border-bottom: 1px solid var(--border);
232
+ display: flex;
233
+ align-items: center;
234
+ gap: 12px;
235
+ }
236
+
237
+ .agent-avatar {
238
+ width: 40px; height: 40px;
239
+ border-radius: 50%;
240
+ display: flex; align-items: center; justify-content: center;
241
+ font-size: 16px;
242
+ flex-shrink: 0;
243
+ position: relative;
244
+ }
245
+
246
+ .agent-avatar.idle { background: var(--surface2); border: 1px solid var(--border); }
247
+ .agent-avatar.working { background: var(--accent-light); border: 1px solid #bfcfef; }
248
+ .agent-avatar.active { background: var(--success-light); border: 1px solid #bbdfc8; }
249
+ .agent-avatar.resting { background: var(--warning-light); border: 1px solid #f0d89e; }
250
+ .agent-avatar.error { background: var(--danger-light); border: 1px solid #f5baba; }
251
+
252
+ .agent-avatar-img {
253
+ width: 28px; height: 28px;
254
+ image-rendering: pixelated;
255
+ display: block;
256
+ }
257
+
258
+ .agent-info { flex: 1; min-width: 0; }
259
+
260
+ .agent-name {
261
+ font-size: 14px;
262
+ font-weight: 600;
263
+ color: var(--text);
264
+ white-space: nowrap;
265
+ overflow: hidden;
266
+ text-overflow: ellipsis;
267
+ }
268
+
269
+ .agent-role {
270
+ font-size: 12px;
271
+ color: var(--text3);
272
+ margin-top: 1px;
273
+ }
274
+
275
+ .agent-badge {
276
+ flex-shrink: 0;
277
+ font-size: 11px;
278
+ font-weight: 500;
279
+ padding: 3px 9px;
280
+ border-radius: 20px;
281
+ letter-spacing: .01em;
282
+ }
283
+
284
+ .badge-idle { background: var(--surface2); color: var(--text3); }
285
+ .badge-working { background: var(--accent-light); color: var(--accent); }
286
+ .badge-active { background: var(--success-light); color: var(--success); }
287
+ .badge-resting { background: var(--warning-light); color: var(--warning); }
288
+ .badge-error { background: var(--danger-light); color: var(--danger); }
289
+
290
+ /* AGENT BODY - pixel desktop scene */
291
+ .agent-scene {
292
+ padding: 14px 16px;
293
+ background: var(--surface2);
294
+ border-bottom: 1px solid var(--border);
295
+ min-height: 110px;
296
+ position: relative;
297
+ display: flex;
298
+ align-items: flex-end;
299
+ gap: 12px;
300
+ }
301
+
302
+ .pixel-figure { flex-shrink: 0; }
303
+
304
+ .pixel-desk-area { flex: 1; display: flex; flex-direction: column; gap: 4px; }
305
+
306
+ .pixel-monitor-wrap {
307
+ background: #1a1a2e;
308
+ border: 2px solid #2d2d4a;
309
+ border-radius: 3px;
310
+ padding: 4px;
311
+ height: 52px;
312
+ overflow: hidden;
313
+ position: relative;
314
+ }
315
+
316
+ .pixel-monitor-wrap.thinking {
317
+ border-color: var(--accent);
318
+ }
319
+
320
+ /* Thinking dots animation */
321
+ .think-dots {
322
+ display: flex; gap: 4px; align-items: center;
323
+ position: absolute; top: 50%; left: 50%;
324
+ transform: translate(-50%,-50%);
325
+ }
326
+
327
+ .think-dot {
328
+ width: 5px; height: 5px; border-radius: 50%;
329
+ background: var(--accent);
330
+ animation: think-bounce .8s ease-in-out infinite;
331
+ }
332
+
333
+ .think-dot:nth-child(2) { animation-delay: .15s; }
334
+ .think-dot:nth-child(3) { animation-delay: .3s; }
335
+
336
+ @keyframes think-bounce {
337
+ 0%, 80%, 100% { transform: translateY(0); opacity: .4; }
338
+ 40% { transform: translateY(-6px); opacity: 1; }
339
+ }
340
+
341
+ /* Screen content lines */
342
+ .screen-lines { padding: 2px; display: flex; flex-direction: column; gap: 3px; }
343
+
344
+ .screen-line {
345
+ height: 3px;
346
+ background: #4ade80;
347
+ border-radius: 1px;
348
+ opacity: .7;
349
+ }
350
+
351
+ .screen-line.dim { opacity: .25; }
352
+ .screen-line.mid { opacity: .5; }
353
+
354
+ .screen-cursor {
355
+ width: 4px; height: 10px;
356
+ background: #4ade80;
357
+ position: absolute;
358
+ bottom: 5px; left: 6px;
359
+ animation: blink-cursor .8s step-end infinite;
360
+ }
361
+
362
+ @keyframes blink-cursor { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
363
+
364
+ /* typing animation on screen lines */
365
+ @keyframes scan-line {
366
+ 0% { width: 10%; }
367
+ 100% { width: 85%; }
368
+ }
369
+
370
+ .screen-line.scanning {
371
+ animation: scan-line .6s ease-in-out infinite alternate;
372
+ }
373
+
374
+ .pixel-desk-surface {
375
+ height: 8px;
376
+ background: #c8b89a;
377
+ border-radius: 2px;
378
+ border-top: 2px solid #a09070;
379
+ }
380
+
381
+ .pixel-keyboard {
382
+ height: 6px;
383
+ background: #d4ccc0;
384
+ border-radius: 2px;
385
+ border: 1px solid #b0a898;
386
+ display: flex; gap: 1px; padding: 1px 3px; align-items: center;
387
+ }
388
+
389
+ .pixel-key { flex: 1; height: 3px; background: #b8b0a4; border-radius: .5px; }
390
+
391
+ /* Speech bubble */
392
+ .speech-bubble {
393
+ position: absolute;
394
+ top: 8px; left: 55px;
395
+ background: white;
396
+ border: 1px solid var(--border2);
397
+ border-radius: var(--radius-sm);
398
+ padding: 5px 9px;
399
+ font-size: 11px;
400
+ color: var(--text);
401
+ max-width: 160px;
402
+ box-shadow: var(--shadow);
403
+ z-index: 10;
404
+ opacity: 0;
405
+ transform: translateY(-4px);
406
+ transition: opacity .3s, transform .3s;
407
+ pointer-events: none;
408
+ line-height: 1.3;
409
+ }
410
+
411
+ .speech-bubble.visible {
412
+ opacity: 1;
413
+ transform: translateY(0);
414
+ }
415
+
416
+ .speech-bubble::before {
417
+ content: '';
418
+ position: absolute;
419
+ left: -6px; top: 10px;
420
+ width: 0; height: 0;
421
+ border-top: 5px solid transparent;
422
+ border-bottom: 5px solid transparent;
423
+ border-right: 6px solid var(--border2);
424
+ }
425
+
426
+ .speech-bubble::after {
427
+ content: '';
428
+ position: absolute;
429
+ left: -5px; top: 10px;
430
+ width: 0; height: 0;
431
+ border-top: 5px solid transparent;
432
+ border-bottom: 5px solid transparent;
433
+ border-right: 6px solid white;
434
+ }
435
+
436
+ /* OOO card */
437
+ .ooo-card {
438
+ position: absolute; inset: 10px;
439
+ background: var(--warning-light);
440
+ border: 1px dashed #e8c96a;
441
+ border-radius: var(--radius-sm);
442
+ display: flex;
443
+ flex-direction: column;
444
+ align-items: center;
445
+ justify-content: center;
446
+ gap: 4px;
447
+ font-size: 11px;
448
+ color: var(--warning);
449
+ font-weight: 500;
450
+ }
451
+
452
+ .ooo-icon { font-size: 20px; }
453
+
454
+ /* AGENT RESULT */
455
+ .agent-result {
456
+ padding: 12px 16px;
457
+ flex: 1;
458
+ min-height: 60px;
459
+ }
460
+
461
+ .result-placeholder {
462
+ font-size: 12px;
463
+ color: var(--text3);
464
+ font-style: italic;
465
+ }
466
+
467
+ .result-text {
468
+ font-size: 13px;
469
+ color: var(--text);
470
+ line-height: 1.6;
471
+ }
472
+
473
+ .result-model {
474
+ margin-top: 6px;
475
+ font-size: 11px;
476
+ color: var(--text3);
477
+ display: flex; align-items: center; gap: 4px;
478
+ }
479
+
480
+ .model-dot { width: 5px; height: 5px; border-radius: 50%; background: var(--success); }
481
+
482
+ /* SIDEBAR */
483
+ .sidebar {
484
+ border-left: 1px solid var(--border);
485
+ background: var(--surface);
486
+ display: flex;
487
+ flex-direction: column;
488
+ overflow: hidden;
489
+ }
490
+
491
+ .sidebar-section {
492
+ border-bottom: 1px solid var(--border);
493
+ flex-shrink: 0;
494
+ }
495
+
496
+ .sidebar-title {
497
+ font-size: 11px;
498
+ font-weight: 600;
499
+ color: var(--text3);
500
+ letter-spacing: .06em;
501
+ text-transform: uppercase;
502
+ padding: 14px 16px 10px;
503
+ border-bottom: 1px solid var(--border);
504
+ }
505
+
506
+ /* Activity feed */
507
+ .activity-feed {
508
+ flex: 1;
509
+ overflow-y: auto;
510
+ padding: 8px 0;
511
+ }
512
+
513
+ .activity-item {
514
+ padding: 8px 16px;
515
+ display: flex;
516
+ gap: 10px;
517
+ align-items: flex-start;
518
+ transition: background .1s;
519
+ }
520
+
521
+ .activity-item:hover { background: var(--surface2); }
522
+
523
+ .activity-dot {
524
+ width: 6px; height: 6px;
525
+ border-radius: 50%;
526
+ margin-top: 5px;
527
+ flex-shrink: 0;
528
+ }
529
+
530
+ .activity-content { flex: 1; min-width: 0; }
531
+ .activity-msg { font-size: 12px; color: var(--text); line-height: 1.4; }
532
+ .activity-time { font-size: 11px; color: var(--text3); margin-top: 1px; }
533
+
534
+ /* Mission history */
535
+ .history-list { overflow-y: auto; max-height: 240px; }
536
+
537
+ .history-item {
538
+ padding: 10px 16px;
539
+ border-bottom: 1px solid var(--border);
540
+ cursor: pointer;
541
+ transition: background .1s;
542
+ }
543
+
544
+ .history-item:hover { background: var(--surface2); }
545
+ .history-item:last-child { border-bottom: none; }
546
+
547
+ .history-task {
548
+ font-size: 12px;
549
+ color: var(--text);
550
+ white-space: nowrap;
551
+ overflow: hidden;
552
+ text-overflow: ellipsis;
553
+ font-weight: 500;
554
+ }
555
+
556
+ .history-meta {
557
+ font-size: 11px;
558
+ color: var(--text3);
559
+ margin-top: 2px;
560
+ display: flex; gap: 8px;
561
+ }
562
+
563
+ /* NOTIFICATION */
564
+ .notification {
565
+ position: fixed;
566
+ top: 70px; right: 20px;
567
+ background: var(--surface);
568
+ border: 1px solid var(--border);
569
+ border-left: 3px solid var(--success);
570
+ border-radius: var(--radius);
571
+ padding: 12px 16px;
572
+ box-shadow: var(--shadow-md);
573
+ max-width: 300px;
574
+ z-index: 999;
575
+ transform: translateX(320px);
576
+ transition: transform .3s ease;
577
+ font-size: 13px;
578
+ }
579
+
580
+ .notification.show { transform: translateX(0); }
581
+ .notification-title { font-weight: 600; margin-bottom: 2px; }
582
+ .notification-msg { color: var(--text2); font-size: 12px; }
583
+
584
+ /* PROGRESS BAR */
585
+ .progress-bar {
586
+ height: 2px;
587
+ background: var(--accent-light);
588
+ position: relative;
589
+ overflow: hidden;
590
+ display: none;
591
+ }
592
+
593
+ .progress-bar.active { display: block; }
594
+
595
+ .progress-bar::after {
596
+ content: '';
597
+ position: absolute;
598
+ top: 0; left: -100%;
599
+ width: 40%;
600
+ height: 100%;
601
+ background: var(--accent);
602
+ animation: progress-slide 1.5s ease-in-out infinite;
603
+ }
604
+
605
+ @keyframes progress-slide {
606
+ 0% { left: -40%; }
607
+ 100% { left: 100%; }
608
+ }
609
+
610
+ /* SCROLLBAR */
611
+ ::-webkit-scrollbar { width: 6px; }
612
+ ::-webkit-scrollbar-track { background: transparent; }
613
+ ::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 3px; }
614
+ ::-webkit-scrollbar-thumb:hover { background: var(--text3); }
615
+
616
+ /* RESPONSIVE */
617
+ @media (max-width: 768px) {
618
+ .topbar { padding: 0 16px; }
619
+ .topbar-subtitle { display: none; }
620
+ .mission-bar { padding: 12px 16px; }
621
+ .agents-area { padding: 16px; }
622
+ .agents-grid { grid-template-columns: 1fr; }
623
+ }
624
+ </style>
625
+ </head>
626
+ <body>
627
+
628
+ <!-- TOPBAR -->
629
+ <header class="topbar">
630
+ <div class="topbar-left">
631
+ <div class="logo">
632
+ <svg viewBox="0 0 24 24"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg>
633
+ </div>
634
+ <div>
635
+ <span class="topbar-title">Mission Control AI</span>
636
+ <span class="topbar-subtitle">β€” Agent Orchestration</span>
637
+ </div>
638
+ </div>
639
+ <div class="topbar-right">
640
+ <div class="system-status">
641
+ <div class="status-dot pulse" id="sys-dot"></div>
642
+ <span id="sys-label">System Online</span>
643
+ </div>
644
+ <div class="clock" id="clock">--:--:--</div>
645
+ </div>
646
+ </header>
647
+
648
+ <div class="progress-bar" id="progress-bar"></div>
649
+
650
+ <!-- MISSION BAR -->
651
+ <div class="mission-bar">
652
+ <div class="mission-form">
653
+ <input
654
+ class="mission-input"
655
+ id="task-input"
656
+ placeholder="Describe the mission for your agents..."
657
+ autocomplete="off"
658
+ />
659
+ <button class="launch-btn" id="launch-btn" onclick="launchMission()">
660
+ <svg viewBox="0 0 24 24"><path d="M22 2L11 13M22 2L15 22 11 13 2 9l20-7z"/></svg>
661
+ Launch Mission
662
+ </button>
663
+ </div>
664
+ </div>
665
+
666
+ <!-- MAIN -->
667
+ <div class="main">
668
+
669
+ <!-- AGENTS -->
670
+ <div class="agents-area">
671
+ <div class="section-label" id="agents-label">Agents β€” Waiting for mission</div>
672
+ <div class="agents-grid" id="agents-grid"></div>
673
+ </div>
674
+
675
+ <!-- SIDEBAR -->
676
+ <aside class="sidebar">
677
+ <div class="sidebar-section">
678
+ <div class="sidebar-title">Activity</div>
679
+ <div class="activity-feed" id="activity-feed"></div>
680
+ </div>
681
+ <div class="sidebar-section">
682
+ <div class="sidebar-title">Mission History</div>
683
+ <div class="history-list" id="history-list">
684
+ <div style="padding:16px;font-size:12px;color:var(--text3);text-align:center">No missions yet</div>
685
+ </div>
686
+ </div>
687
+ </aside>
688
+
689
+ </div>
690
+
691
+ <!-- NOTIFICATION -->
692
+ <div class="notification" id="notification">
693
+ <div class="notification-title" id="notif-title">Mission Complete</div>
694
+ <div class="notification-msg" id="notif-msg"></div>
695
+ </div>
696
+
697
+ <script>
698
+ const AGENT_DEFS = [
699
+ { key: 'manager', name: 'Manager', role: 'Project Coordinator', emoji: 'πŸ‘”' },
700
+ { key: 'developer', name: 'Developer', role: 'Senior Engineer', emoji: 'πŸ’»' },
701
+ { key: 'analyst', name: 'Analyst', role: 'Business Analyst', emoji: 'πŸ“Š' },
702
+ ];
703
+
704
+ const SPEECH = {
705
+ manager: ['Delegating tasks...', 'Reviewing plan...', 'Coordinating team...'],
706
+ developer: ['Writing code...', 'Debugging...', 'Building solution...'],
707
+ analyst: ['Analyzing data...', 'Evaluating risks...', 'Processing insights...'],
708
+ };
709
+
710
+ const INTER_SPEECH = {
711
+ manager: ['Checking with Developer...', 'Reviewing Analyst output...'],
712
+ developer: ['Manager approved scope', 'Implementing per spec...'],
713
+ analyst: ['Flagging to Manager...', 'Sharing findings...'],
714
+ };
715
+
716
+ let agentStates = {};
717
+ let activityLog = [];
718
+ let missionHistory = [];
719
+ let missionCount = 0;
720
+ let speechTimers = {};
721
+ let interactionTimers = [];
722
+
723
+ AGENT_DEFS.forEach(a => {
724
+ agentStates[a.key] = { status: 'idle', message: '', model: '' };
725
+ });
726
+
727
+ // CLOCK
728
+ function updateClock() {
729
+ const now = new Date();
730
+ document.getElementById('clock').textContent =
731
+ [now.getHours(), now.getMinutes(), now.getSeconds()]
732
+ .map(n => String(n).padStart(2, '0')).join(':');
733
+ }
734
+ setInterval(updateClock, 1000);
735
+ updateClock();
736
+
737
+ // PIXEL PERSON SVG
738
+ function pixelPerson(status) {
739
+ const isWorking = status === 'working';
740
+ const isResting = status === 'resting';
741
+ const bodyOp = isResting ? '0.35' : '1';
742
+ const armAnim = isWorking
743
+ ? 'style="animation:arm-type .3s ease-in-out infinite alternate;transform-origin:14px 18px"'
744
+ : '';
745
+
746
+ return `<svg viewBox="0 0 36 56" width="36" height="56" style="image-rendering:pixelated;display:block;opacity:${bodyOp}">
747
+ <style>@keyframes arm-type{0%{transform:translateY(0)rotate(0)}100%{transform:translateY(2px)rotate(-4deg)}}</style>
748
+ <rect x="12" y="0" width="12" height="3" fill="#1a1a2a"/>
749
+ <rect x="10" y="3" width="16" height="2" fill="#1a1a2a"/>
750
+ <rect x="10" y="5" width="2" height="5" fill="#1a1a2a"/>
751
+ <rect x="24" y="5" width="2" height="5" fill="#1a1a2a"/>
752
+ <rect x="10" y="5" width="16" height="12" fill="#f0c8a0"/>
753
+ <rect x="8" y="8" width="2" height="5" fill="#f0c8a0"/>
754
+ <rect x="26" y="8" width="2" height="5" fill="#f0c8a0"/>
755
+ <rect x="13" y="9" width="4" height="3" fill="#2563eb" opacity=".9"/>
756
+ <rect x="19" y="9" width="4" height="3" fill="#2563eb" opacity=".9"/>
757
+ <rect x="14" y="10" width="2" height="2" fill="#111"/>
758
+ <rect x="20" y="10" width="2" height="2" fill="#111"/>
759
+ <rect x="17" y="10" width="2" height="1" fill="#2563eb"/>
760
+ <rect x="15" y="14" width="6" height="1" fill="#c88870" opacity=".6"/>
761
+ <rect x="10" y="17" width="16" height="12" fill="#e8f0ff"/>
762
+ <rect x="13" y="17" width="10" height="4" fill="#e8f0ff"/>
763
+ <rect x="6" y="18" width="4" height="3" fill="#e8f0ff"/>
764
+ <rect x="26" y="18" width="4" height="3" fill="#e8f0ff"/>
765
+ <g ${armAnim}>
766
+ <rect x="4" y="18" width="6" height="8" fill="#e8f0ff"/>
767
+ <rect x="26" y="18" width="6" height="8" fill="#e8f0ff"/>
768
+ <rect x="4" y="26" width="6" height="4" fill="#f0c8a0"/>
769
+ <rect x="26" y="26" width="6" height="4" fill="#f0c8a0"/>
770
+ </g>
771
+ <rect x="10" y="29" width="16" height="14" fill="#2d3748"/>
772
+ <rect x="15" y="43" width="6" height="8" fill="#2d3748"/>
773
+ <rect x="10" y="43" width="6" height="8" fill="#2d3748"/>
774
+ <rect x="8" y="51" width="8" height="4" fill="#1a1a2a"/>
775
+ <rect x="20" y="51" width="8" height="4" fill="#1a1a2a"/>
776
+ </svg>`;
777
+ }
778
+
779
+ // MONITOR SCREEN CONTENT
780
+ function monitorContent(status) {
781
+ if (status === 'working') {
782
+ return `
783
+ <div class="think-dots">
784
+ <div class="think-dot"></div>
785
+ <div class="think-dot"></div>
786
+ <div class="think-dot"></div>
787
+ </div>`;
788
+ }
789
+ if (status === 'active') {
790
+ return `<div class="screen-lines">
791
+ <div class="screen-line" style="width:80%"></div>
792
+ <div class="screen-line mid" style="width:60%"></div>
793
+ <div class="screen-line dim" style="width:70%"></div>
794
+ <div class="screen-line mid" style="width:45%"></div>
795
+ </div>
796
+ <div class="screen-cursor"></div>`;
797
+ }
798
+ if (status === 'resting') {
799
+ return `<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:9px;color:#e8c96a;font-family:monospace;letter-spacing:1px;opacity:.7">AWAY</div>`;
800
+ }
801
+ return `<div class="screen-lines">
802
+ <div class="screen-line dim" style="width:50%"></div>
803
+ <div class="screen-line dim" style="width:35%"></div>
804
+ </div>`;
805
+ }
806
+
807
+ // BUILD AGENT CARD HTML
808
+ function buildAgentCard(agent) {
809
+ const st = agentStates[agent.key];
810
+ const status = st.status;
811
+
812
+ const avatarClass = `agent-avatar ${status === 'working' ? 'working' : status === 'active' ? 'active' : status === 'resting' ? 'resting' : 'idle'}`;
813
+ const cardClass = `agent-card ${status === 'working' || status === 'active' ? 'is-active' : status === 'resting' ? 'is-resting' : ''}`;
814
+ const badgeClass = `agent-badge badge-${status === 'active' ? 'active' : status}`;
815
+ const badgeLabel = { idle: 'Idle', working: 'Processing', active: 'Done', resting: 'Away', error: 'Error' }[status] || status;
816
+ const monThinking = status === 'working' ? 'thinking' : '';
817
+
818
+ const sceneContent = status === 'resting'
819
+ ? `<div class="ooo-card">
820
+ <div class="ooo-icon">β˜•</div>
821
+ <div>Out of Office</div>
822
+ <div style="font-size:10px;font-weight:400;color:var(--warning);opacity:.7">Rate limit β€” back soon</div>
823
+ </div>`
824
+ : `<div class="pixel-figure">${pixelPerson(status)}</div>
825
+ <div class="pixel-desk-area">
826
+ <div class="pixel-monitor-wrap ${monThinking}">
827
+ ${monitorContent(status)}
828
+ </div>
829
+ <div class="pixel-desk-surface"></div>
830
+ <div class="pixel-keyboard">
831
+ <div class="pixel-key"></div>
832
+ <div class="pixel-key"></div>
833
+ <div class="pixel-key"></div>
834
+ <div class="pixel-key"></div>
835
+ </div>
836
+ </div>`;
837
+
838
+ const resultContent = status === 'idle'
839
+ ? `<div class="result-placeholder">Waiting for mission...</div>`
840
+ : status === 'working'
841
+ ? `<div class="result-placeholder" style="color:var(--accent)">Thinking...</div>`
842
+ : status === 'resting'
843
+ ? `<div class="result-placeholder" style="color:var(--warning)">Rate limit β€” available in ~1-2 min</div>`
844
+ : `<div class="result-text" id="result-${agent.key}"></div>
845
+ ${st.model ? `<div class="result-model"><div class="model-dot"></div>${st.model}</div>` : ''}`;
846
+
847
+ return `<div class="${cardClass}" id="card-${agent.key}">
848
+ <div class="agent-header">
849
+ <div class="${avatarClass}" id="avatar-${agent.key}">
850
+ ${pixelPerson(status)}
851
+ </div>
852
+ <div class="agent-info">
853
+ <div class="agent-name">${agent.name}</div>
854
+ <div class="agent-role">${agent.role}</div>
855
+ </div>
856
+ <span class="${badgeClass}" id="badge-${agent.key}">${badgeLabel}</span>
857
+ </div>
858
+ <div class="agent-scene" id="scene-${agent.key}">
859
+ ${sceneContent}
860
+ <div class="speech-bubble" id="bubble-${agent.key}"></div>
861
+ </div>
862
+ <div class="agent-result" id="result-area-${agent.key}">
863
+ ${resultContent}
864
+ </div>
865
+ </div>`;
866
+ }
867
+
868
+ function renderAgents() {
869
+ const grid = document.getElementById('agents-grid');
870
+ grid.innerHTML = AGENT_DEFS.map(buildAgentCard).join('');
871
+ }
872
+
873
+ // TYPEWRITER EFFECT
874
+ function typeWriter(el, text, speed = 18) {
875
+ el.textContent = '';
876
+ let i = 0;
877
+ const interval = setInterval(() => {
878
+ if (i < text.length) {
879
+ el.textContent += text[i++];
880
+ } else {
881
+ clearInterval(interval);
882
+ }
883
+ }, speed);
884
+ }
885
+
886
+ // SPEECH BUBBLE
887
+ function showBubble(agentKey, text, duration = 2800) {
888
+ const bubble = document.getElementById(`bubble-${agentKey}`);
889
+ if (!bubble) return;
890
+ if (speechTimers[agentKey]) clearTimeout(speechTimers[agentKey]);
891
+ bubble.textContent = text;
892
+ bubble.classList.add('visible');
893
+ speechTimers[agentKey] = setTimeout(() => {
894
+ bubble.classList.remove('visible');
895
+ }, duration);
896
+ }
897
+
898
+ // ACTIVITY LOG
899
+ function addActivity(msg, color = '#2563eb') {
900
+ const now = new Date();
901
+ const time = [now.getHours(), now.getMinutes(), now.getSeconds()]
902
+ .map(n => String(n).padStart(2, '0')).join(':');
903
+ activityLog.unshift({ msg, time, color });
904
+
905
+ const feed = document.getElementById('activity-feed');
906
+ feed.innerHTML = activityLog.slice(0, 30).map(a => `
907
+ <div class="activity-item">
908
+ <div class="activity-dot" style="background:${a.color}"></div>
909
+ <div class="activity-content">
910
+ <div class="activity-msg">${a.msg}</div>
911
+ <div class="activity-time">${a.time}</div>
912
+ </div>
913
+ </div>`).join('');
914
+ }
915
+
916
+ // NOTIFICATION
917
+ function notify(title, msg) {
918
+ document.getElementById('notif-title').textContent = title;
919
+ document.getElementById('notif-msg').textContent = msg;
920
+ const el = document.getElementById('notification');
921
+ el.classList.add('show');
922
+ setTimeout(() => el.classList.remove('show'), 5000);
923
+ }
924
+
925
+ // AGENT INTERACTIONS (manager talks to others while working)
926
+ function startInteractions() {
927
+ interactionTimers.forEach(t => clearTimeout(t));
928
+ interactionTimers = [];
929
+
930
+ const pairs = [
931
+ { from: 'manager', msg: 'Briefing Developer...' },
932
+ { from: 'manager', msg: 'Checking with Analyst...' },
933
+ { from: 'developer', msg: 'Manager approved β€” building...' },
934
+ { from: 'analyst', msg: 'Sending report to Manager...' },
935
+ ];
936
+
937
+ pairs.forEach((p, i) => {
938
+ const t = setTimeout(() => {
939
+ if (agentStates[p.from]?.status === 'working') {
940
+ showBubble(p.from, p.msg, 2500);
941
+ addActivity(`${p.from.charAt(0).toUpperCase() + p.from.slice(1)}: ${p.msg}`, '#6366f1');
942
+ }
943
+ }, 3000 + i * 5000);
944
+ interactionTimers.push(t);
945
+ });
946
+ }
947
+
948
+ // UPDATE HISTORY
949
+ function updateHistory() {
950
+ const list = document.getElementById('history-list');
951
+ if (!missionHistory.length) {
952
+ list.innerHTML = `<div style="padding:16px;font-size:12px;color:var(--text3);text-align:center">No missions yet</div>`;
953
+ return;
954
+ }
955
+ list.innerHTML = missionHistory.slice().reverse().map(m => `
956
+ <div class="history-item">
957
+ <div class="history-task">${m.task}</div>
958
+ <div class="history-meta">
959
+ <span>${m.time}</span>
960
+ <span style="color:${m.ok ? 'var(--success)' : 'var(--warning)'}">
961
+ ${m.ok ? 'Completed' : 'Partial'}
962
+ </span>
963
+ </div>
964
+ </div>`).join('');
965
+ }
966
+
967
+ // LAUNCH MISSION
968
+ async function launchMission() {
969
+ const task = document.getElementById('task-input').value.trim();
970
+ if (!task) return;
971
+
972
+ const btn = document.getElementById('launch-btn');
973
+ btn.disabled = true;
974
+ document.getElementById('progress-bar').classList.add('active');
975
+ document.getElementById('agents-label').textContent = `Agents β€” Processing: "${task.substring(0, 50)}${task.length > 50 ? '...' : ''}"`;
976
+ document.getElementById('sys-label').textContent = 'Mission Running';
977
+
978
+ // Set all to working
979
+ AGENT_DEFS.forEach((a, i) => {
980
+ agentStates[a.key] = { status: 'working', message: '', model: '' };
981
+ });
982
+ renderAgents();
983
+
984
+ // Stagger bubble appearances
985
+ AGENT_DEFS.forEach((a, i) => {
986
+ setTimeout(() => {
987
+ const phrases = SPEECH[a.key];
988
+ showBubble(a.key, phrases[Math.floor(Math.random() * phrases.length)], 3000);
989
+ }, i * 800);
990
+ });
991
+
992
+ addActivity(`Mission launched: "${task.substring(0, 40)}"`, '#2563eb');
993
+ startInteractions();
994
+
995
+ try {
996
+ const resp = await fetch('/api/mission', {
997
+ method: 'POST',
998
+ headers: { 'Content-Type': 'application/json' },
999
+ body: JSON.stringify({ task }),
1000
+ });
1001
+
1002
+ const data = await resp.json();
1003
+
1004
+ if (!resp.ok || data.error) {
1005
+ throw new Error(data.error || 'Server error');
1006
+ }
1007
+
1008
+ let anyResting = false;
1009
+
1010
+ AGENT_DEFS.forEach(a => {
1011
+ const r = data.results[a.key] || {};
1012
+ const status = r.status === 'resting' ? 'resting' : 'active';
1013
+ if (status === 'resting') anyResting = true;
1014
+
1015
+ agentStates[a.key] = {
1016
+ status,
1017
+ message: r.message || '',
1018
+ model: r.model || '',
1019
+ };
1020
+ });
1021
+
1022
+ renderAgents();
1023
+
1024
+ // Typewriter for each active agent
1025
+ AGENT_DEFS.forEach((a, i) => {
1026
+ const st = agentStates[a.key];
1027
+ if (st.status === 'active' && st.message) {
1028
+ setTimeout(() => {
1029
+ const el = document.getElementById(`result-${a.key}`);
1030
+ if (el) typeWriter(el, st.message);
1031
+ showBubble(a.key, 'Done!', 2000);
1032
+ addActivity(`${a.name} completed task`, '#16a34a');
1033
+ }, i * 600);
1034
+ } else if (st.status === 'resting') {
1035
+ addActivity(`${a.name} is rate-limited β€” resting`, '#d97706');
1036
+ }
1037
+ });
1038
+
1039
+ const time = new Date().toLocaleTimeString();
1040
+ missionHistory.push({ task, time, ok: !anyResting });
1041
+ missionCount++;
1042
+ updateHistory();
1043
+
1044
+ setTimeout(() => {
1045
+ notify(
1046
+ anyResting ? 'Mission Partial' : 'Mission Complete',
1047
+ anyResting
1048
+ ? 'Some agents hit rate limits. Try again in ~2 minutes.'
1049
+ : `All agents responded. Mission #${missionCount} done.`
1050
+ );
1051
+ }, AGENT_DEFS.length * 600 + 500);
1052
+
1053
+ document.getElementById('agents-label').textContent = anyResting
1054
+ ? `Agents β€” Partial results (some rate-limited)`
1055
+ : `Agents β€” Mission #${missionCount} complete`;
1056
+
1057
+ document.getElementById('sys-label').textContent = 'System Online';
1058
+
1059
+ } catch (err) {
1060
+ AGENT_DEFS.forEach(a => {
1061
+ agentStates[a.key] = { status: 'error', message: err.message, model: '' };
1062
+ });
1063
+ renderAgents();
1064
+ addActivity(`Error: ${err.message}`, '#dc2626');
1065
+ document.getElementById('sys-dot').style.background = 'var(--danger)';
1066
+ }
1067
+
1068
+ btn.disabled = false;
1069
+ document.getElementById('progress-bar').classList.remove('active');
1070
+ }
1071
+
1072
+ // ENTER KEY
1073
+ document.getElementById('task-input').addEventListener('keydown', e => {
1074
+ if (e.key === 'Enter') launchMission();
1075
+ });
1076
+
1077
+ // INIT
1078
+ renderAgents();
1079
+ addActivity('System online β€” agents ready', '#16a34a');
1080
+ </script>
1081
+ </body>
1082
+ </html>