vfven commited on
Commit
f38a870
·
verified ·
1 Parent(s): 877c670

Upload 6 files

Browse files
Files changed (3) hide show
  1. main.py +22 -8
  2. main_backup.py +591 -0
  3. templates/index.html +346 -820
main.py CHANGED
@@ -474,15 +474,29 @@ async def run_mission(request: Request):
474
 
475
  elif key == "writer":
476
  try:
 
477
  writer_prompt = (
478
- f"Redacta un informe formal y completo sobre: {task}\n\n"
479
- "Usa este formato:\n"
480
- "## Resumen Ejecutivo\n[contenido]\n\n"
481
- "### Introducción\n[contenido]\n\n"
482
- "### Desarrollo\n[contenido con subsecciones]\n\n"
483
- "### Conclusiones\n[contenido]\n\n"
484
- "### Recomendaciones\n[contenido]\n\n"
485
- " formal, detallado y profesional."
 
 
 
 
 
 
 
 
 
 
 
 
 
486
  )
487
  writer_text = await call_agent_llm(agent, writer_prompt)
488
  results["writer"] = {
 
474
 
475
  elif key == "writer":
476
  try:
477
+ manager_context = results.get("manager", {}).get("message", "")
478
  writer_prompt = (
479
+ f"Eres un redactor experto. Escribe el contenido REAL y COMPLETO del siguiente informe.\n\n"
480
+ f"TEMA DEL INFORME: {task}\n\n"
481
+ f"INSTRUCCIONES DEL MANAGER:\n{manager_context[:500]}\n\n"
482
+ "INSTRUCCIONES IMPORTANTES:\n"
483
+ "- Escribe contenido REAL, detallado y específico sobre el tema. NO uses placeholders ni corchetes.\n"
484
+ "- El informe debe tener mínimo 600 palabras de contenido real.\n"
485
+ "- Usa datos, hechos y ejemplos concretos relacionados con el tema.\n"
486
+ "- Estructura con estos encabezados EXACTOS (sin corchetes, solo el contenido real):\n\n"
487
+ "## Resumen Ejecutivo\n"
488
+ "(Escribe aquí 2-3 párrafos resumiendo los puntos clave del informe)\n\n"
489
+ "### Introducción\n"
490
+ "(Escribe aquí contexto e importancia del tema)\n\n"
491
+ "### Desarrollo\n"
492
+ "(Escribe aquí el análisis detallado con datos y hechos reales)\n\n"
493
+ "### Hallazgos Principales\n"
494
+ "(Escribe aquí los descubrimientos o puntos más importantes)\n\n"
495
+ "### Conclusiones\n"
496
+ "(Escribe aquí las conclusiones derivadas del análisis)\n\n"
497
+ "### Recomendaciones\n"
498
+ "(Escribe aquí recomendaciones concretas y accionables)\n\n"
499
+ "IMPORTANTE: Escribe contenido real y sustancioso. Nada de '[contenido]' ni placeholders."
500
  )
501
  writer_text = await call_agent_llm(agent, writer_prompt)
502
  results["writer"] = {
main_backup.py ADDED
@@ -0,0 +1,591 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import asyncio
3
+ import httpx
4
+ import json
5
+ import re
6
+ import uuid
7
+ import base64
8
+ from io import BytesIO
9
+ from fastapi import FastAPI, Request
10
+ from fastapi.responses import HTMLResponse, JSONResponse, FileResponse
11
+ from fastapi.middleware.cors import CORSMiddleware
12
+ from datetime import datetime
13
+ from pathlib import Path
14
+
15
+ # python-docx imports
16
+ from docx import Document as DocxDocument
17
+ from docx.shared import Inches, Pt, RGBColor
18
+ from docx.enum.text import WD_ALIGN_PARAGRAPH
19
+ from docx.oxml.ns import qn
20
+ from docx.oxml import OxmlElement
21
+
22
+ app = FastAPI()
23
+ app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
24
+
25
+ GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY", "")
26
+ OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY", "")
27
+ GROQ_API_KEY = os.getenv("GROQ_API_KEY", "")
28
+ PEXELS_API_KEY = os.getenv("PEXELS_API_KEY", "") # free at pexels.com/api
29
+ HF_API_KEY = os.getenv("HF_TOKEN", "") # HuggingFace token for image gen
30
+
31
+ DOCS_DIR = Path("docs")
32
+ DOCS_DIR.mkdir(exist_ok=True)
33
+
34
+ PROVIDERS = {
35
+ "gemini": {
36
+ "name": "Google Gemini", "type": "gemini", "key": GOOGLE_API_KEY,
37
+ },
38
+ "openrouter": {
39
+ "name": "OpenRouter", "type": "openai_compat", "key": OPENROUTER_API_KEY,
40
+ "base_url": "https://openrouter.ai/api/v1/chat/completions",
41
+ "headers": {
42
+ "HTTP-Referer": "https://huggingface.co/spaces/vfven/mission-control-ui",
43
+ "X-Title": "Mission Control AI",
44
+ },
45
+ },
46
+ "groq": {
47
+ "name": "Groq", "type": "openai_compat", "key": GROQ_API_KEY,
48
+ "base_url": "https://api.groq.com/openai/v1/chat/completions",
49
+ "headers": {},
50
+ },
51
+ }
52
+
53
+ # ── DEFAULT AGENTS (can be extended via UI) ───────────────────────────────
54
+ DEFAULT_AGENTS = [
55
+ {
56
+ "key": "manager", "name": "Manager", "provider": "gemini",
57
+ "role": "Gerente de proyecto experto en coordinar equipos y planificar estrategias. Cuando el usuario pide un documento o informe, DEBES indicar en tu respuesta qué agentes necesitas activar usando el formato JSON: {\"delegate\": [\"writer\", \"analyst\"]} al final de tu respuesta.",
58
+ "models": ["gemini-2.5-flash-preview-04-17", "gemini-2.0-flash", "gemini-1.5-flash"],
59
+ },
60
+ {
61
+ "key": "developer", "name": "Developer", "provider": "openrouter",
62
+ "role": "Programador senior especialista en crear aplicaciones y soluciones técnicas.",
63
+ "models": ["qwen/qwen3-4b:free", "meta-llama/llama-3.3-70b-instruct:free", "mistralai/mistral-small-3.1-24b-instruct:free", "google/gemma-3-12b-it:free"],
64
+ },
65
+ {
66
+ "key": "analyst", "name": "Analyst", "provider": "openrouter",
67
+ "role": "Analista de negocios experto en evaluar viabilidad, riesgos y oportunidades. También revisa y critica documentos formales.",
68
+ "models": ["meta-llama/llama-3.3-70b-instruct:free", "mistralai/mistral-small-3.1-24b-instruct:free", "google/gemma-3-27b-it:free", "qwen/qwen3-4b:free"],
69
+ },
70
+ {
71
+ "key": "writer", "name": "Writer", "provider": "openrouter",
72
+ "role": "Especialista en redacción de documentos formales, informes ejecutivos y reportes técnicos. Escribe en formato estructurado con secciones claras usando ### para títulos y ** para subtítulos.",
73
+ "models": ["meta-llama/llama-3.3-70b-instruct:free", "mistralai/mistral-small-3.1-24b-instruct:free", "qwen/qwen3-4b:free", "google/gemma-3-12b-it:free"],
74
+ },
75
+ {
76
+ "key": "image_agent", "name": "ImageAgent", "provider": "gemini",
77
+ "role": "Agente especializado en buscar y proveer imágenes relevantes. Cuando se te pida imágenes sobre un tema, responde con una lista JSON de términos de búsqueda en inglés: {\"image_queries\": [\"term1\", \"term2\", \"term3\"]}",
78
+ "models": ["gemini-2.0-flash", "gemini-1.5-flash"],
79
+ },
80
+ ]
81
+
82
+ SUBSTITUTE_MODELS = {
83
+ "groq": ["llama-3.3-70b-versatile", "llama3-70b-8192", "gemma2-9b-it"],
84
+ }
85
+
86
+ # Runtime agent registry (can be extended)
87
+ agent_registry = {a["key"]: dict(a) for a in DEFAULT_AGENTS}
88
+ mission_history = []
89
+
90
+
91
+ # ── LLM CALLERS ───────────────────────────────────────────────────────────
92
+ async def call_gemini(model: str, system: str, user: str, key: str) -> str:
93
+ url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={key}"
94
+ payload = {
95
+ "contents": [{"role": "user", "parts": [{"text": f"{system}\n\n{user}"}]}],
96
+ "generationConfig": {"maxOutputTokens": 2048, "temperature": 0.7},
97
+ }
98
+ async with httpx.AsyncClient(timeout=90) as client:
99
+ r = await client.post(url, json=payload)
100
+ r.raise_for_status()
101
+ return r.json()["candidates"][0]["content"]["parts"][0]["text"]
102
+
103
+
104
+ async def call_openai_compat(base_url: str, model: str, system: str, user: str,
105
+ key: str, extra_headers: dict) -> str:
106
+ headers = {"Authorization": f"Bearer {key}", "Content-Type": "application/json", **extra_headers}
107
+ payload = {
108
+ "model": model,
109
+ "messages": [{"role": "system", "content": system}, {"role": "user", "content": user}],
110
+ "max_tokens": 2048, "temperature": 0.7,
111
+ }
112
+ async with httpx.AsyncClient(timeout=90) as client:
113
+ r = await client.post(base_url, json=payload, headers=headers)
114
+ r.raise_for_status()
115
+ return r.json()["choices"][0]["message"]["content"]
116
+
117
+
118
+ async def call_agent_llm(agent: dict, task: str) -> str:
119
+ prov_key = agent["provider"]
120
+ provider = PROVIDERS[prov_key]
121
+ system = f"Eres {agent['name']}. {agent['role']} Responde en español. Sé conciso y profesional."
122
+ last_err = None
123
+
124
+ for model in agent["models"]:
125
+ try:
126
+ if provider["type"] == "gemini":
127
+ return await call_gemini(model, system, task, provider["key"])
128
+ else:
129
+ return await call_openai_compat(
130
+ provider["base_url"], model, system, task,
131
+ provider["key"], provider.get("headers", {}))
132
+ except Exception as e:
133
+ last_err = str(e)
134
+
135
+ # Groq fallback
136
+ if GROQ_API_KEY:
137
+ groq = PROVIDERS["groq"]
138
+ for m in SUBSTITUTE_MODELS["groq"]:
139
+ try:
140
+ return await call_openai_compat(
141
+ groq["base_url"], m, system, task, groq["key"], {})
142
+ except Exception as e:
143
+ last_err = str(e)
144
+
145
+ raise Exception(f"All providers failed: {last_err}")
146
+
147
+
148
+ # ── IMAGE FETCHING ─────────────────────────────────────────────────────────
149
+ async def fetch_pexels_image(query: str) -> bytes | None:
150
+ if not PEXELS_API_KEY:
151
+ return None
152
+ try:
153
+ async with httpx.AsyncClient(timeout=20) as client:
154
+ r = await client.get(
155
+ "https://api.pexels.com/v1/search",
156
+ params={"query": query, "per_page": 1, "orientation": "landscape"},
157
+ headers={"Authorization": PEXELS_API_KEY}
158
+ )
159
+ data = r.json()
160
+ if data.get("photos"):
161
+ img_url = data["photos"][0]["src"]["medium"]
162
+ img_r = await client.get(img_url)
163
+ return img_r.content
164
+ except Exception:
165
+ pass
166
+ return None
167
+
168
+
169
+ async def generate_hf_image(prompt: str) -> bytes | None:
170
+ if not HF_API_KEY:
171
+ return None
172
+ try:
173
+ async with httpx.AsyncClient(timeout=60) as client:
174
+ r = await client.post(
175
+ "https://api-inference.huggingface.co/models/stabilityai/stable-diffusion-xl-base-1.0",
176
+ headers={"Authorization": f"Bearer {HF_API_KEY}"},
177
+ json={"inputs": prompt, "parameters": {"width": 512, "height": 384}},
178
+ )
179
+ if r.status_code == 200 and r.headers.get("content-type", "").startswith("image"):
180
+ return r.content
181
+ except Exception:
182
+ pass
183
+ return None
184
+
185
+
186
+ async def get_image_for_query(query: str) -> bytes | None:
187
+ img = await fetch_pexels_image(query)
188
+ if img:
189
+ return img
190
+ return await generate_hf_image(query)
191
+
192
+
193
+ # ── DOCX BUILDER ──────────────────────────────────────────────────────────
194
+ def build_docx(title: str, sections: dict, images: list[bytes], analyst_review: str) -> bytes:
195
+ doc = DocxDocument()
196
+
197
+ # Page margins
198
+ for section in doc.sections:
199
+ section.top_margin = Inches(1)
200
+ section.bottom_margin = Inches(1)
201
+ section.left_margin = Inches(1.2)
202
+ section.right_margin = Inches(1.2)
203
+
204
+ # Title
205
+ title_para = doc.add_heading(title, 0)
206
+ title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
207
+ run = title_para.runs[0]
208
+ run.font.color.rgb = RGBColor(0x1a, 0x56, 0xdb)
209
+
210
+ # Date + subtitle
211
+ sub = doc.add_paragraph()
212
+ sub.alignment = WD_ALIGN_PARAGRAPH.CENTER
213
+ sub.add_run(f"Mission Control AI — {datetime.now().strftime('%B %d, %Y')}").italic = True
214
+ doc.add_paragraph()
215
+
216
+ # Horizontal rule
217
+ p = doc.add_paragraph()
218
+ pPr = p._p.get_or_add_pPr()
219
+ pBdr = OxmlElement("w:pBdr")
220
+ bottom = OxmlElement("w:bottom")
221
+ bottom.set(qn("w:val"), "single")
222
+ bottom.set(qn("w:sz"), "6")
223
+ bottom.set(qn("w:color"), "1a56db")
224
+ pBdr.append(bottom)
225
+ pPr.append(pBdr)
226
+
227
+ img_index = 0
228
+
229
+ # Writer content sections
230
+ writer_text = sections.get("writer", "")
231
+ current_lines = []
232
+
233
+ def flush_lines():
234
+ nonlocal current_lines
235
+ if current_lines:
236
+ para_text = " ".join(current_lines).strip()
237
+ if para_text:
238
+ p = doc.add_paragraph(para_text)
239
+ p.paragraph_format.space_after = Pt(6)
240
+ current_lines = []
241
+
242
+ for line in writer_text.split("\n"):
243
+ stripped = line.strip()
244
+ if not stripped:
245
+ flush_lines()
246
+ continue
247
+ if stripped.startswith("### "):
248
+ flush_lines()
249
+ h = doc.add_heading(stripped[4:], level=2)
250
+ # Insert image after each major section heading if available
251
+ if img_index < len(images) and images[img_index]:
252
+ try:
253
+ img_stream = BytesIO(images[img_index])
254
+ doc.add_picture(img_stream, width=Inches(5))
255
+ last_para = doc.paragraphs[-1]
256
+ last_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
257
+ cap = doc.add_paragraph(f"Figure {img_index + 1}")
258
+ cap.alignment = WD_ALIGN_PARAGRAPH.CENTER
259
+ cap.runs[0].italic = True
260
+ cap.runs[0].font.size = Pt(9)
261
+ img_index += 1
262
+ except Exception:
263
+ pass
264
+ elif stripped.startswith("## "):
265
+ flush_lines()
266
+ doc.add_heading(stripped[3:], level=1)
267
+ elif stripped.startswith("**") and stripped.endswith("**"):
268
+ flush_lines()
269
+ p = doc.add_paragraph()
270
+ run = p.add_run(stripped.strip("**"))
271
+ run.bold = True
272
+ elif stripped.startswith("- ") or stripped.startswith("* "):
273
+ flush_lines()
274
+ doc.add_paragraph(stripped[2:], style="List Bullet")
275
+ else:
276
+ current_lines.append(stripped)
277
+
278
+ flush_lines()
279
+
280
+ # Remaining images
281
+ while img_index < len(images):
282
+ if images[img_index]:
283
+ try:
284
+ img_stream = BytesIO(images[img_index])
285
+ doc.add_picture(img_stream, width=Inches(5))
286
+ last_para = doc.paragraphs[-1]
287
+ last_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
288
+ img_index += 1
289
+ except Exception:
290
+ img_index += 1
291
+ else:
292
+ img_index += 1
293
+
294
+ # Analyst review section
295
+ if analyst_review:
296
+ doc.add_page_break()
297
+ doc.add_heading("Análisis y Revisión", level=1)
298
+ for line in analyst_review.split("\n"):
299
+ if line.strip():
300
+ doc.add_paragraph(line.strip())
301
+
302
+ # Footer
303
+ doc.add_paragraph()
304
+ footer_p = doc.add_paragraph()
305
+ footer_p.alignment = WD_ALIGN_PARAGRAPH.CENTER
306
+ footer_run = footer_p.add_run("— Generado por Mission Control AI —")
307
+ footer_run.italic = True
308
+ footer_run.font.size = Pt(9)
309
+ footer_run.font.color.rgb = RGBColor(0x6b, 0x72, 0x80)
310
+
311
+ buf = BytesIO()
312
+ doc.save(buf)
313
+ buf.seek(0)
314
+ return buf.read()
315
+
316
+
317
+ # ── PARSE MANAGER DELEGATION ───────────────────────────────────────────────
318
+ def parse_delegation(manager_text: str) -> list[str]:
319
+ match = re.search(r'\{[^}]*"delegate"\s*:\s*\[([^\]]*)\][^}]*\}', manager_text)
320
+ if match:
321
+ raw = match.group(1)
322
+ keys = re.findall(r'"(\w+)"', raw)
323
+ return keys
324
+ # Heuristic fallback: detect keywords in manager response
325
+ delegates = []
326
+ lower = manager_text.lower()
327
+ if any(w in lower for w in ["informe", "documento", "redact", "escrib", "report", "word"]):
328
+ delegates.extend(["writer"])
329
+ if any(w in lower for w in ["imagen", "image", "foto", "visual", "ilustr"]):
330
+ delegates.extend(["image_agent"])
331
+ if any(w in lower for w in ["analiz", "revisar", "evalua", "critic"]):
332
+ delegates.extend(["analyst"])
333
+ return list(dict.fromkeys(delegates)) # deduplicate
334
+
335
+
336
+ def parse_image_queries(image_agent_text: str) -> list[str]:
337
+ match = re.search(r'"image_queries"\s*:\s*\[([^\]]*)\]', image_agent_text)
338
+ if match:
339
+ return re.findall(r'"([^"]+)"', match.group(1))
340
+ return []
341
+
342
+
343
+ def clean_text(text: str) -> str:
344
+ # Remove JSON blocks from display text
345
+ text = re.sub(r'\{[^}]*"delegate"[^}]*\}', '', text)
346
+ text = re.sub(r'\{[^}]*"image_queries"[^}]*\}', '', text)
347
+ return text.strip()
348
+
349
+
350
+ # ── ROUTES ─────────────────────────────────────────────────────────────────
351
+ @app.get("/", response_class=HTMLResponse)
352
+ async def root():
353
+ return HTMLResponse(Path("templates/index.html").read_text())
354
+
355
+
356
+ @app.get("/api/agents")
357
+ async def get_agents():
358
+ return {"agents": [
359
+ {"key": a["key"], "name": a["name"], "role": a["role"]}
360
+ for a in agent_registry.values()
361
+ ]}
362
+
363
+
364
+ @app.post("/api/agents/add")
365
+ async def add_agent(request: Request):
366
+ body = await request.json()
367
+ key = re.sub(r'\W+', '_', body.get("key", "").lower().strip())
368
+ if not key:
369
+ return JSONResponse({"error": "key required"}, status_code=400)
370
+ agent_registry[key] = {
371
+ "key": key,
372
+ "name": body.get("name", key.capitalize()),
373
+ "role": body.get("role", "Agente de propósito general."),
374
+ "provider": body.get("provider", "openrouter"),
375
+ "models": body.get("models", ["meta-llama/llama-3.3-70b-instruct:free"]),
376
+ }
377
+ return {"success": True, "agent": agent_registry[key]}
378
+
379
+
380
+ @app.delete("/api/agents/{key}")
381
+ async def delete_agent(key: str):
382
+ if key in ("manager", "developer", "analyst"):
383
+ return JSONResponse({"error": "Cannot delete core agents"}, status_code=400)
384
+ if key in agent_registry:
385
+ del agent_registry[key]
386
+ return {"success": True}
387
+
388
+
389
+ @app.get("/api/history")
390
+ async def get_history():
391
+ return {"history": mission_history[-20:]}
392
+
393
+
394
+ @app.get("/api/docs/{filename}")
395
+ async def download_doc(filename: str):
396
+ path = DOCS_DIR / filename
397
+ if not path.exists() or not filename.endswith(".docx"):
398
+ return JSONResponse({"error": "File not found"}, status_code=404)
399
+ return FileResponse(
400
+ path,
401
+ media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
402
+ filename=filename,
403
+ )
404
+
405
+
406
+ @app.post("/api/mission")
407
+ async def run_mission(request: Request):
408
+ body = await request.json()
409
+ task = body.get("task", "").strip()
410
+ if not task:
411
+ return JSONResponse({"error": "No task provided"}, status_code=400)
412
+
413
+ started_at = datetime.now().isoformat()
414
+ results = {}
415
+ doc_file = None
416
+ events = [] # orchestration event log
417
+
418
+ def log(msg: str):
419
+ events.append({"time": datetime.now().strftime("%H:%M:%S"), "msg": msg})
420
+
421
+ # ── STEP 1: Manager plans ──────────────────────────────────────────────
422
+ log("Manager analyzing task...")
423
+ manager_agent = agent_registry["manager"]
424
+ try:
425
+ manager_raw = await call_agent_llm(manager_agent, task)
426
+ delegates = parse_delegation(manager_raw)
427
+ manager_msg = clean_text(manager_raw)
428
+ results["manager"] = {
429
+ "status": "active", "message": manager_msg,
430
+ "model": manager_agent["models"][0],
431
+ "delegates": delegates,
432
+ }
433
+ log(f"Manager delegated to: {delegates or 'none'}")
434
+ except Exception as e:
435
+ results["manager"] = {"status": "resting", "message": str(e), "model": ""}
436
+ delegates = []
437
+ log(f"Manager failed: {e}")
438
+
439
+ # ── STEP 2: Run delegated agents ───────────────────────────────────────
440
+ image_bytes = []
441
+ writer_text = ""
442
+ analyst_text = ""
443
+
444
+ async def run_delegated(key: str):
445
+ nonlocal writer_text, analyst_text
446
+ if key not in agent_registry:
447
+ log(f"Agent '{key}' not found in registry")
448
+ return
449
+
450
+ agent = agent_registry[key]
451
+ log(f"{agent['name']} starting...")
452
+
453
+ if key == "image_agent":
454
+ try:
455
+ img_prompt = f"Proporciona consultas de búsqueda de imágenes en inglés para ilustrar: {task}"
456
+ raw = await call_agent_llm(agent, img_prompt)
457
+ queries = parse_image_queries(raw)
458
+ if not queries:
459
+ queries = [task[:40]]
460
+ log(f"ImageAgent searching: {queries}")
461
+ imgs = await asyncio.gather(*[get_image_for_query(q) for q in queries[:3]])
462
+ for img in imgs:
463
+ if img:
464
+ image_bytes.append(img)
465
+ results["image_agent"] = {
466
+ "status": "active",
467
+ "message": f"Encontradas {len(image_bytes)} imágenes para: {', '.join(queries[:3])}",
468
+ "model": agent["models"][0],
469
+ }
470
+ log(f"ImageAgent: {len(image_bytes)} images found")
471
+ except Exception as e:
472
+ results["image_agent"] = {"status": "resting", "message": str(e), "model": ""}
473
+ log(f"ImageAgent failed: {e}")
474
+
475
+ elif key == "writer":
476
+ try:
477
+ writer_prompt = (
478
+ f"Redacta un informe formal y completo sobre: {task}\n\n"
479
+ "Usa este formato:\n"
480
+ "## Resumen Ejecutivo\n[contenido]\n\n"
481
+ "### Introducción\n[contenido]\n\n"
482
+ "### Desarrollo\n[contenido con subsecciones]\n\n"
483
+ "### Conclusiones\n[contenido]\n\n"
484
+ "### Recomendaciones\n[contenido]\n\n"
485
+ "Sé formal, detallado y profesional."
486
+ )
487
+ writer_text = await call_agent_llm(agent, writer_prompt)
488
+ results["writer"] = {
489
+ "status": "active",
490
+ "message": writer_text[:200] + "..." if len(writer_text) > 200 else writer_text,
491
+ "model": agent["models"][0],
492
+ }
493
+ log("Writer completed document")
494
+ except Exception as e:
495
+ results["writer"] = {"status": "resting", "message": str(e), "model": ""}
496
+ log(f"Writer failed: {e}")
497
+
498
+ elif key == "analyst":
499
+ try:
500
+ content_to_review = writer_text or manager_msg
501
+ analyst_prompt = (
502
+ f"Revisa y analiza el siguiente contenido sobre: {task}\n\n"
503
+ f"Contenido:\n{content_to_review[:1000]}\n\n"
504
+ "Proporciona: 1) Evaluación de calidad, 2) Puntos fuertes, "
505
+ "3) Áreas de mejora, 4) Conclusión final."
506
+ )
507
+ analyst_text = await call_agent_llm(agent, analyst_prompt)
508
+ results["analyst"] = {
509
+ "status": "active",
510
+ "message": analyst_text[:200] + "..." if len(analyst_text) > 200 else analyst_text,
511
+ "model": agent["models"][0],
512
+ }
513
+ log("Analyst review completed")
514
+ except Exception as e:
515
+ results["analyst"] = {"status": "resting", "message": str(e), "model": ""}
516
+ log(f"Analyst failed: {e}")
517
+
518
+ else:
519
+ # Generic agent
520
+ try:
521
+ raw = await call_agent_llm(agent, task)
522
+ results[key] = {"status": "active", "message": raw, "model": agent["models"][0]}
523
+ log(f"{agent['name']} completed")
524
+ except Exception as e:
525
+ results[key] = {"status": "resting", "message": str(e), "model": ""}
526
+
527
+ # Run image_agent and writer in parallel, then analyst
528
+ parallel_first = [k for k in delegates if k in ("writer", "image_agent")]
529
+ sequential_after = [k for k in delegates if k == "analyst"]
530
+ other = [k for k in delegates if k not in ("writer", "image_agent", "analyst")]
531
+
532
+ if parallel_first or other:
533
+ await asyncio.gather(*[run_delegated(k) for k in parallel_first + other])
534
+
535
+ if sequential_after:
536
+ for k in sequential_after:
537
+ await run_delegated(k)
538
+
539
+ # ── STEP 3: Build .docx if writer was involved ─────────────────────────
540
+ if writer_text:
541
+ log("Assembling Word document...")
542
+ try:
543
+ doc_bytes = build_docx(task, {"writer": writer_text}, image_bytes, analyst_text)
544
+ safe_name = re.sub(r'[^\w\-]', '_', task[:40])
545
+ doc_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.docx"
546
+ (DOCS_DIR / doc_filename).write_bytes(doc_bytes)
547
+ doc_file = doc_filename
548
+ results["manager"]["doc_file"] = doc_filename
549
+ log(f"Document ready: {doc_filename}")
550
+ except Exception as e:
551
+ log(f"Document build failed: {e}")
552
+
553
+ # Mark idle agents
554
+ for key in agent_registry:
555
+ if key not in results:
556
+ results[key] = {"status": "idle", "message": "", "model": ""}
557
+
558
+ final = results.get("manager", {}).get("message", "")[:300]
559
+
560
+ entry = {
561
+ "id": len(mission_history) + 1,
562
+ "task": task,
563
+ "started_at": started_at,
564
+ "ended_at": datetime.now().isoformat(),
565
+ "results": results,
566
+ "final": final,
567
+ "doc_file": doc_file,
568
+ "events": events,
569
+ }
570
+ mission_history.append(entry)
571
+
572
+ return JSONResponse({
573
+ "success": True, "task": task,
574
+ "results": results, "final": final,
575
+ "doc_file": doc_file, "events": events,
576
+ "mission_id": entry["id"],
577
+ })
578
+
579
+
580
+ @app.get("/api/health")
581
+ async def health():
582
+ return {
583
+ "status": "ok",
584
+ "providers": {
585
+ "gemini": "ok" if GOOGLE_API_KEY else "missing",
586
+ "openrouter": "ok" if OPENROUTER_API_KEY else "missing",
587
+ "groq": "ok" if GROQ_API_KEY else "missing",
588
+ "pexels": "ok" if PEXELS_API_KEY else "missing (optional)",
589
+ "hf_images": "ok" if HF_API_KEY else "missing (optional)",
590
+ }
591
+ }
templates/index.html CHANGED
@@ -5,906 +5,432 @@
5
  <meta name="viewport" content="width=device-width,initial-scale=1">
6
  <title>Mission Control AI</title>
7
  <style>
8
- @import url('https://fonts.googleapis.com/css2?family=VT323&family=IBM+Plex+Mono:wght@400;500&display=swap');
9
-
10
- :root {
11
- --bg:#0d1117; --surface:#161b22; --surface2:#1c2128; --surface3:#21262d;
12
- --border:#30363d; --border2:#444c56;
13
- --text:#e6edf3; --text2:#8b949e; --text3:#484f58;
14
- --accent:#58a6ff; --accent-dim:#1f3f6e;
15
- --green:#3fb950; --green-dim:#1a3d24;
16
- --amber:#d29922; --amber-dim:#3d2f0a;
17
- --red:#f85149; --red-dim:#3d1210;
18
- --purple:#bc8cff;
19
- --wood:#6b4e0a; --wood-l:#8b6914;
20
- --metal:#2d333b; --metal-l:#444c56;
21
- --carpet:#0d1620; --wall:#1a2030;
22
- --shadow:0 8px 24px rgba(0,0,0,.4);
23
- --r:6px;
24
  }
25
-
26
  *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
27
-
28
- body{
29
- font-family:'IBM Plex Mono',monospace;
30
- background:var(--bg); color:var(--text);
31
- min-height:100vh; display:flex; flex-direction:column;
32
- font-size:13px; line-height:1.5; overflow-x:hidden;
33
- }
34
-
35
- /* ── TOPBAR ── */
36
- .topbar{
37
- background:var(--surface); border-bottom:1px solid var(--border);
38
- height:50px; padding:0 20px;
39
- display:flex; align-items:center; justify-content:space-between;
40
- flex-shrink:0; position:sticky; top:0; z-index:100;
41
- }
42
- .tb-left{display:flex;align-items:center;gap:10px}
43
- .tb-logo{
44
- background:var(--accent); color:#000;
45
- font-family:'VT323',monospace; font-size:15px; letter-spacing:1px;
46
- padding:2px 8px; border-radius:3px;
47
- }
48
- .tb-title{font-family:'VT323',monospace;font-size:20px;color:var(--accent);letter-spacing:2px}
49
- .tb-right{display:flex;align-items:center;gap:16px}
50
  .tb-stat{font-size:11px;color:var(--text2);display:flex;align-items:center;gap:5px}
51
  .sdot{width:7px;height:7px;border-radius:50%}
52
- .dot-g{background:var(--green);animation:pulse-g 2s ease infinite}
53
- .dot-a{background:var(--amber);animation:pulse-g 1.2s ease infinite}
54
- .dot-r{background:var(--red);animation:blink-r .5s step-end infinite}
55
- .dot-d{background:var(--text3)}
56
- @keyframes pulse-g{0%,100%{box-shadow:0 0 0 0 currentColor}50%{box-shadow:0 0 0 4px transparent}}
57
- @keyframes blink-r{0%,100%{opacity:1}50%{opacity:0}}
58
- .clock{font-family:'VT323',monospace;font-size:20px;color:var(--accent);letter-spacing:2px}
59
-
60
- /* ── PROGRESS ── */
61
- .progress{height:2px;background:var(--surface2);overflow:hidden;display:none}
62
- .progress.on{display:block}
63
- .progress::after{
64
- content:'';display:block;height:100%;width:30%;background:var(--accent);
65
- animation:prog 1.4s ease-in-out infinite;
66
- }
67
- @keyframes prog{0%{margin-left:-30%}100%{margin-left:110%}}
68
-
69
- /* ── MISSION BAR ── */
70
- .mbar{
71
- background:var(--surface); border-bottom:1px solid var(--border);
72
- padding:12px 20px; display:flex; gap:10px; flex-shrink:0; flex-wrap:wrap;
73
- }
74
- .minput{
75
- flex:1; min-width:200px; height:36px;
76
- background:var(--surface2); border:1px solid var(--border);
77
- color:var(--text); font-family:'IBM Plex Mono',monospace; font-size:13px;
78
- padding:0 12px; outline:none; border-radius:var(--r);
79
- transition:border-color .15s;
80
- }
81
- .minput:focus{border-color:var(--accent)}
82
  .minput::placeholder{color:var(--text3)}
83
- .launch{
84
- height:36px; padding:0 18px;
85
- background:var(--accent); border:none; color:#000;
86
- font-family:'VT323',monospace; font-size:18px; letter-spacing:2px;
87
- cursor:pointer; border-radius:var(--r); transition:background .15s;
88
- display:flex; align-items:center; gap:6px;
89
- }
90
- .launch:hover{background:#79b8ff}
91
- .launch:active{transform:scale(.98)}
92
- .launch:disabled{background:var(--accent-dim);color:var(--text3);cursor:not-allowed}
93
- .add-agent-btn{
94
- height:36px; padding:0 14px;
95
- background:transparent; border:1px solid var(--border);
96
- color:var(--text2); font-family:'IBM Plex Mono',monospace; font-size:12px;
97
- cursor:pointer; border-radius:var(--r); transition:all .15s;
98
- display:flex; align-items:center; gap:6px;
99
- }
100
- .add-agent-btn:hover{border-color:var(--accent);color:var(--accent)}
101
- .mc{font-family:'VT323',monospace;font-size:16px;color:var(--text3)}
102
-
103
- /* ── MAIN LAYOUT ── */
104
- .main{display:grid;grid-template-columns:1fr 280px;flex:1;min-height:0;overflow:hidden}
105
  @media(max-width:900px){.main{grid-template-columns:1fr}.sidebar{display:none}}
106
-
107
- /* ── OFFICE ── */
108
- .office{
109
- background:var(--carpet);
110
- overflow-y:auto; padding:16px; position:relative;
111
- }
112
- .office::before{
113
- content:''; position:fixed; inset:0;
114
- background-image:
115
- linear-gradient(rgba(255,255,255,.03) 1px,transparent 1px),
116
- linear-gradient(90deg,rgba(255,255,255,.03) 1px,transparent 1px);
117
- background-size:32px 32px; pointer-events:none;
118
- }
119
- .floor-label{
120
- font-family:'VT323',monospace; font-size:13px; letter-spacing:3px;
121
- color:var(--text3); margin-bottom:14px; text-transform:uppercase;
122
- display:flex; align-items:center; justify-content:space-between;
123
- }
124
-
125
- /* ── OFFICE GRID ── */
126
- .ogrid{
127
- display:grid; grid-template-columns:repeat(3,1fr); gap:12px;
128
- position:relative; z-index:1;
129
- }
130
  @media(max-width:1100px){.ogrid{grid-template-columns:repeat(2,1fr)}}
131
  @media(max-width:700px){.ogrid{grid-template-columns:1fr}}
132
-
133
- /* ── ROOM ── */
134
- .room{
135
- background:var(--wall); border:1px solid var(--border);
136
- border-radius:var(--r); overflow:hidden;
137
- display:flex; flex-direction:column; transition:border-color .3s;
138
- min-height:220px;
139
- }
140
- .room.working{border-color:var(--accent)}
141
- .room.active{border-color:var(--green)}
142
- .room.resting{border-color:var(--amber)}
143
- .room.idle{border-color:var(--border)}
144
-
145
- /* room wall header */
146
- .rwall{
147
- background:var(--surface2); height:30px;
148
- border-bottom:1px solid var(--border);
149
- display:flex; align-items:center; justify-content:space-between;
150
- padding:0 10px; flex-shrink:0;
151
- }
152
  .rname{font-family:'VT323',monospace;font-size:15px;letter-spacing:2px}
153
- .rbadge{
154
- font-family:'VT323',monospace;font-size:12px;
155
- padding:1px 7px; border:1px solid; letter-spacing:1px;
156
- }
157
- .b-idle{color:var(--text3);border-color:var(--text3)}
158
- .b-working{color:var(--accent);border-color:var(--accent);animation:badgep .9s ease infinite}
159
- .b-active{color:var(--green);border-color:var(--green);background:var(--green-dim)}
160
- .b-resting{color:var(--amber);border-color:var(--amber)}
161
- .b-error{color:var(--red);border-color:var(--red)}
162
- @keyframes badgep{0%,100%{opacity:1}50%{opacity:.5}}
163
-
164
- /* room scene */
165
- .rscene{
166
- flex:1; background:var(--carpet); position:relative;
167
- display:flex; align-items:flex-end; padding:8px; gap:8px;
168
- overflow:hidden; min-height:120px;
169
- }
170
- .rscene::before{
171
- content:''; position:absolute; inset:0;
172
- background-image:
173
- linear-gradient(45deg,rgba(255,255,255,.015) 25%,transparent 25%),
174
- linear-gradient(-45deg,rgba(255,255,255,.015) 25%,transparent 25%),
175
- linear-gradient(45deg,transparent 75%,rgba(255,255,255,.015) 75%),
176
- linear-gradient(-45deg,transparent 75%,rgba(255,255,255,.015) 75%);
177
- background-size:16px 16px;
178
- }
179
-
180
- /* SERVER RACK */
181
- .srack{
182
- width:38px; flex-shrink:0; align-self:flex-end; margin-bottom:6px;
183
- background:#0d1117; border:1px solid #1e2a40; padding:4px 3px;
184
- display:flex; flex-direction:column; gap:2px; position:relative; z-index:1;
185
- }
186
- .srack::before{
187
- content:'SRV';position:absolute;top:-16px;left:0;right:0;
188
- text-align:center;font-family:'VT323',monospace;font-size:10px;color:var(--text3);
189
- }
190
- .runit{height:8px;background:#111827;border:1px solid #1e2a40;display:flex;align-items:center;gap:2px;padding:0 2px}
191
- .rled{width:4px;height:4px;border-radius:50%}
192
- .lg{background:var(--green);animation:led calc(var(--d,1.5s)) ease-in-out infinite}
193
- .la{background:var(--amber);animation:led calc(var(--d,2s)) ease-in-out infinite}
194
- .lb{background:var(--accent);animation:led calc(var(--d,.9s)) ease-in-out infinite}
195
- .lo{background:#1a2235;border:1px solid #2a3550}
196
- .rbar{flex:1;height:2px;background:#1e2a40}
197
- .rbar.on{background:var(--accent);animation:rba .4s ease-in-out infinite alternate}
198
- @keyframes led{0%,100%{opacity:.3}50%{opacity:1;box-shadow:0 0 4px currentColor}}
199
  @keyframes rba{0%{opacity:.3}100%{opacity:1}}
200
-
201
- /* DESK AREA */
202
- .deskarea{flex:1;display:flex;flex-direction:column;gap:3px;position:relative;z-index:1}
203
-
204
- .monitor{
205
- width:100%;height:48px;background:#050a0f;
206
- border:2px solid var(--metal-l);border-radius:2px;
207
- position:relative;overflow:hidden;display:flex;flex-direction:column;
208
- }
209
- .monitor.thinking{border-color:var(--accent)}
210
- .monitor.done{border-color:var(--green)}
211
- .monitor.away{border-color:var(--amber)}
212
-
213
  .mscreen{flex:1;padding:4px;display:flex;flex-direction:column;gap:3px;position:relative;overflow:hidden}
214
  .mbase{height:5px;background:var(--metal);position:relative}
215
- .mbase::after{
216
- content:'';position:absolute;bottom:-3px;left:50%;transform:translateX(-50%);
217
- width:14px;height:3px;background:var(--metal-l);
218
- }
219
-
220
- .sline{height:3px;background:var(--green);border-radius:1px;opacity:.6}
221
- .sline.scan{width:10%;animation:scan .6s ease-in-out infinite alternate}
222
- @keyframes scan{0%{width:10%}100%{width:85%}}
223
- .scur{
224
- position:absolute;bottom:3px;left:4px;
225
- width:4px;height:8px;background:var(--green);
226
- animation:cur .7s step-end infinite;
227
- }
228
  @keyframes cur{0%,100%{opacity:1}50%{opacity:0}}
229
  .tdots{display:flex;gap:3px;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}
230
- .tdot{width:4px;height:4px;border-radius:50%;background:var(--accent);animation:td .8s ease-in-out infinite}
231
- .tdot:nth-child(2){animation-delay:.15s}.tdot:nth-child(3){animation-delay:.3s}
232
- @keyframes td{0%,80%,100%{transform:translateY(0);opacity:.4}40%{transform:translateY(-5px);opacity:1}}
233
-
234
- .desk-surface{height:7px;background:var(--wood-l);border-radius:1px;border-top:2px solid #c09828}
235
- .keyboard{
236
- height:6px;background:#c8b89a;border:1px solid #a09070;
237
- border-radius:1px;display:flex;gap:1px;padding:1px 2px;align-items:center;
238
- }
239
- .key{flex:1;height:3px;background:#a09070;border-radius:.5px}
240
-
241
- /* COFFEE */
242
- .coffee{position:absolute;bottom:30px;right:10px;z-index:2}
243
-
244
- /* PERSON */
245
- .person{position:absolute;bottom:22px;left:50%;transform:translateX(-50%);z-index:2}
246
-
247
- /* CHAIR */
248
- .chair{position:absolute;bottom:0;right:4px;z-index:1}
249
-
250
- /* OOO */
251
- .ooo{
252
- position:absolute;inset:0;background:rgba(20,15,0,.75);
253
- display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;
254
- z-index:10;
255
- }
256
- .ooo-t{font-family:'VT323',monospace;font-size:18px;color:var(--amber);letter-spacing:2px}
257
- .ooo-s{font-size:10px;color:rgba(210,153,34,.6)}
258
-
259
- /* SPEECH BUBBLE */
260
- .bubble{
261
- position:absolute;top:6px;left:44px;
262
- background:var(--surface);border:1px solid var(--border2);
263
- border-radius:var(--r);padding:4px 8px;font-size:10px;
264
- max-width:140px;line-height:1.3;z-index:20;
265
- opacity:0;transform:translateY(-4px);pointer-events:none;
266
- transition:opacity .3s,transform .3s;
267
- box-shadow:var(--shadow);
268
- }
269
  .bubble.show{opacity:1;transform:translateY(0)}
270
- .bubble::before{
271
- content:'';position:absolute;left:-6px;top:8px;
272
- width:0;height:0;border-top:5px solid transparent;
273
- border-bottom:5px solid transparent;border-right:6px solid var(--border2);
274
- }
275
-
276
- /* RESULT */
277
- .rresult{
278
- background:var(--surface); border-top:1px solid var(--border);
279
- padding:10px 12px; flex-shrink:0; min-height:60px;
280
- }
281
  .rph{font-size:11px;color:var(--text3);font-style:italic}
282
  .rtxt{font-size:11px;color:var(--text);line-height:1.6;word-break:break-word}
283
- .rmodel{font-size:10px;color:var(--text3);margin-top:5px;display:flex;align-items:center;gap:4px}
284
-
285
- /* DOWNLOAD BUTTON */
286
- .dl-btn{
287
- display:inline-flex;align-items:center;gap:6px;
288
- margin-top:8px;padding:6px 14px;
289
- background:var(--green-dim);border:1px solid var(--green);
290
- color:var(--green);font-family:'IBM Plex Mono',monospace;
291
- font-size:11px;font-weight:500;cursor:pointer;border-radius:var(--r);
292
- text-decoration:none;transition:all .15s;
293
- }
294
- .dl-btn:hover{background:var(--green);color:#000}
295
-
296
- /* ORCHESTRATION LOG */
297
- .orch-log{
298
- grid-column:1/-1;
299
- background:var(--surface);border:1px solid var(--border);
300
- border-radius:var(--r);padding:10px 14px;margin-top:4px;
301
- display:none;
302
- }
303
- .orch-log.show{display:block}
304
- .orch-title{font-family:'VT323',monospace;font-size:14px;color:var(--text2);letter-spacing:2px;margin-bottom:8px}
305
- .orch-events{display:flex;flex-direction:column;gap:3px}
306
- .orch-event{font-size:10px;color:var(--text2);display:flex;gap:8px}
307
- .orch-time{color:var(--text3);flex-shrink:0}
308
-
309
- /* SERVER ROOM */
310
- .server-room{
311
- grid-column:1/-1;
312
- background:var(--surface);border:1px solid var(--border);
313
- border-radius:var(--r);padding:10px 14px;
314
- display:flex;align-items:center;gap:14px;
315
- position:relative;overflow:hidden;
316
- }
317
- .server-room::before{
318
- content:'SERVER ROOM';
319
- position:absolute;top:8px;left:14px;
320
- font-family:'VT323',monospace;font-size:12px;color:var(--text3);letter-spacing:3px;
321
- }
322
  .sroom-racks{display:flex;gap:8px;margin-top:16px;flex-wrap:wrap}
323
- .big-rack{background:#0d1117;border:1px solid #1e2a40;padding:4px;width:44px;display:flex;flex-direction:column;gap:2px}
324
- .big-unit{height:8px;background:#111827;border:1px solid #1e2a40;display:flex;align-items:center;gap:2px;padding:0 2px}
325
  .srv-stats{margin-left:auto;display:flex;flex-direction:column;gap:4px;font-size:10px;color:var(--text2)}
326
  .srv-stat{display:flex;align-items:center;gap:5px}
327
-
328
- /* ── ADD AGENT MODAL ── */
329
- .modal-bg{
330
- position:fixed;inset:0;background:rgba(0,0,0,.7);
331
- z-index:200;display:none;align-items:center;justify-content:center;
332
- }
333
  .modal-bg.show{display:flex}
334
- .modal{
335
- background:var(--surface);border:1px solid var(--border2);
336
- border-radius:var(--r);padding:24px;width:400px;
337
- box-shadow:var(--shadow);
338
- }
339
- .modal-title{font-family:'VT323',monospace;font-size:22px;color:var(--accent);letter-spacing:2px;margin-bottom:18px}
340
- .field{margin-bottom:12px}
341
- .field label{display:block;font-size:11px;color:var(--text2);letter-spacing:1px;margin-bottom:4px;text-transform:uppercase}
342
- .field input,.field select,.field textarea{
343
- width:100%;background:var(--surface2);border:1px solid var(--border);
344
- color:var(--text);font-family:'IBM Plex Mono',monospace;font-size:12px;
345
- padding:7px 10px;outline:none;border-radius:var(--r);
346
- transition:border-color .15s;
347
- }
348
- .field input:focus,.field select:focus,.field textarea:focus{border-color:var(--accent)}
349
- .field textarea{resize:vertical;min-height:60px}
350
- .field select option{background:var(--surface2)}
351
- .modal-actions{display:flex;gap:8px;justify-content:flex-end;margin-top:16px}
352
- .mbtn{
353
- padding:7px 18px;border-radius:var(--r);
354
- font-family:'IBM Plex Mono',monospace;font-size:12px;cursor:pointer;
355
- transition:all .15s;border:1px solid;
356
- }
357
- .mbtn-confirm{background:var(--accent);border-color:var(--accent);color:#000}
358
- .mbtn-confirm:hover{background:#79b8ff}
359
- .mbtn-cancel{background:transparent;border-color:var(--border);color:var(--text2)}
360
- .mbtn-cancel:hover{border-color:var(--text2)}
361
-
362
- /* ── SIDEBAR ── */
363
- .sidebar{background:var(--surface);border-left:1px solid var(--border);display:flex;flex-direction:column;overflow:hidden}
364
  .sb-sec{border-bottom:1px solid var(--border);flex-shrink:0}
365
- .sb-title{font-family:'VT323',monospace;font-size:14px;letter-spacing:3px;color:var(--text2);padding:10px 14px 8px;border-bottom:1px solid var(--border)}
366
  .act-scroll{flex:1;overflow-y:auto;max-height:260px}
367
- .act-item{padding:7px 14px;border-bottom:1px solid rgba(48,54,61,.4);display:flex;gap:8px}
368
  .act-dot{width:5px;height:5px;border-radius:50%;margin-top:5px;flex-shrink:0}
369
  .act-msg{font-size:11px;color:var(--text);line-height:1.4}
370
  .act-time{font-size:10px;color:var(--text3);margin-top:1px}
371
  .hist-scroll{overflow-y:auto;max-height:200px}
372
- .hist-item{padding:8px 14px;border-bottom:1px solid rgba(48,54,61,.4);cursor:pointer;transition:background .1s}
373
- .hist-item:hover{background:var(--surface2)}
374
  .hist-task{font-size:11px;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:500}
375
  .hist-meta{font-size:10px;color:var(--text3);margin-top:2px;display:flex;gap:8px}
376
-
377
- /* notification */
378
- .notif{
379
- position:fixed;top:60px;right:16px;
380
- background:var(--surface);border:1px solid var(--border);border-left:3px solid var(--green);
381
- padding:12px 16px;max-width:280px;z-index:999;
382
- transform:translateX(300px);transition:transform .3s ease;
383
- border-radius:var(--r);box-shadow:var(--shadow);
384
- }
385
- .notif.show{transform:translateX(0)}
386
- .notif-t{font-family:'VT323',monospace;font-size:18px;letter-spacing:1px}
387
- .notif-m{font-size:11px;color:var(--text2);margin-top:2px;line-height:1.4}
388
-
389
- ::-webkit-scrollbar{width:4px}
390
- ::-webkit-scrollbar-track{background:transparent}
391
- ::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
392
  </style>
393
  </head>
394
  <body>
395
-
396
  <header class="topbar">
397
- <div class="tb-left">
398
- <div class="tb-logo">MC</div>
399
- <span class="tb-title">MISSION CONTROL AI</span>
400
  </div>
401
  <div class="tb-right">
402
- <div class="tb-stat"><div class="sdot dot-g"></div><span id="top-active">0 active</span></div>
403
- <div class="tb-stat"><div class="sdot dot-d" id="top-dot"></div><span id="top-status">Idle</span></div>
404
  <div class="clock" id="clock">00:00:00</div>
405
  </div>
406
  </header>
407
-
408
- <div class="progress" id="progress"></div>
409
-
410
  <div class="mbar">
411
- <input class="minput" id="task-input" placeholder="Describe the mission... (e.g. 'Write a formal report about space exploration with images')" autocomplete="off"/>
412
  <button class="launch" id="launch-btn" onclick="launchMission()">&#9658; LAUNCH</button>
413
- <button class="add-agent-btn" onclick="openModal()">+ Add Agent</button>
414
  <span class="mc" id="mc-count">MISSIONS: 0</span>
415
  </div>
416
-
417
  <div class="main">
418
  <div class="office">
419
- <div class="floor-label">
420
- <span>&#9632; Operations Floor</span>
421
- <span style="font-size:10px;color:var(--text3)" id="floor-subtitle">Waiting for mission</span>
422
  </div>
423
  <div class="ogrid" id="ogrid"></div>
424
  </div>
425
-
426
  <aside class="sidebar">
427
- <div class="sb-sec">
428
- <div class="sb-title">ACTIVITY</div>
429
- <div class="act-scroll" id="act-log"></div>
430
- </div>
431
- <div class="sb-sec">
432
- <div class="sb-title">MISSIONS</div>
433
- <div class="hist-scroll" id="hist-log">
434
- <div style="padding:14px;font-size:11px;color:var(--text3);text-align:center">No missions yet</div>
435
- </div>
436
- </div>
437
  </aside>
438
  </div>
439
-
440
- <!-- ADD AGENT MODAL -->
 
 
 
441
  <div class="modal-bg" id="modal-bg" onclick="if(event.target===this)closeModal()">
442
  <div class="modal">
443
- <div class="modal-title">+ NEW AGENT</div>
444
- <div class="field">
445
- <label>Key (unique ID)</label>
446
- <input id="m-key" placeholder="e.g. researcher" />
447
- </div>
448
- <div class="field">
449
- <label>Display Name</label>
450
- <input id="m-name" placeholder="e.g. Researcher" />
451
- </div>
452
- <div class="field">
453
- <label>Role / System Prompt</label>
454
- <textarea id="m-role" placeholder="e.g. Especialista en investigación científica..."></textarea>
455
- </div>
456
- <div class="field">
457
- <label>Provider</label>
458
- <select id="m-provider">
459
- <option value="openrouter">OpenRouter (free models)</option>
460
- <option value="gemini">Google Gemini</option>
461
- <option value="groq">Groq</option>
462
- </select>
463
- </div>
464
- <div class="modal-actions">
465
- <button class="mbtn mbtn-cancel" onclick="closeModal()">Cancel</button>
466
- <button class="mbtn mbtn-confirm" onclick="submitAgent()">Add Agent</button>
467
  </div>
468
  </div>
469
  </div>
470
-
471
- <!-- NOTIFICATION -->
472
  <div class="notif" id="notif">
473
  <div class="notif-t" id="notif-t">Mission Complete</div>
474
  <div class="notif-m" id="notif-m"></div>
475
  </div>
476
-
477
  <script>
478
- const PROVIDER_MODELS = {
479
- openrouter: ['meta-llama/llama-3.3-70b-instruct:free','qwen/qwen3-4b:free','mistralai/mistral-small-3.1-24b-instruct:free'],
480
- gemini: ['gemini-2.5-flash-preview-04-17','gemini-2.0-flash','gemini-1.5-flash'],
481
- groq: ['llama-3.3-70b-versatile','llama3-70b-8192','gemma2-9b-it'],
482
- };
483
-
484
- const SPEECH = {
485
- manager: ['Analyzing task...','Planning approach...','Delegating to team...','Reviewing scope...'],
486
- developer: ['Building solution...','Writing code...','Testing logic...'],
487
- analyst: ['Analyzing data...','Evaluating risks...','Reviewing document...'],
488
- writer: ['Drafting content...','Structuring report...','Writing sections...'],
489
- image_agent: ['Searching images...','Finding visuals...','Fetching resources...'],
490
- };
491
-
492
- const COLORS = {
493
- manager:'#58a6ff', developer:'#3fb950', analyst:'#d29922',
494
- writer:'#bc8cff', image_agent:'#f78166',
495
- };
496
-
497
- let agentDefs = [];
498
- let states = {};
499
- let activity = [];
500
- let history = [];
501
- let mCount = 0;
502
- let bubbleT = {};
503
-
504
- // Load agents from API
505
- async function loadAgents() {
506
- const r = await fetch('/api/agents');
507
- const d = await r.json();
508
- agentDefs = d.agents;
509
- agentDefs.forEach(a => {
510
- if (!states[a.key]) states[a.key] = { status:'idle', message:'', model:'' };
511
- });
512
  renderOffice();
513
  }
514
 
515
- // CLOCK
516
- setInterval(() => {
517
- const n = new Date();
518
- document.getElementById('clock').textContent =
519
- [n.getHours(),n.getMinutes(),n.getSeconds()].map(x=>String(x).padStart(2,'0')).join(':');
520
- }, 1000);
521
-
522
- // ── SVG BUILDERS ─────────────────────────────────────────────────────────
523
- function personSVG(color, status) {
524
- const op = status === 'resting' ? '0.2' : '1';
525
- const anim = status === 'working'
526
- ? 'style="animation:arm-t .28s ease-in-out infinite alternate;transform-origin:13px 18px"'
527
- : '';
528
- return `<svg viewBox="0 0 32 52" width="32" height="52" style="image-rendering:pixelated;display:block;opacity:${op}">
529
- <style>@keyframes arm-t{0%{transform:translateY(0)rotate(0)}100%{transform:translateY(2px)rotate(-5deg)}}</style>
530
- <rect x="11" y="0" width="10" height="3" fill="#1a1a2a"/>
531
- <rect x="9" y="3" width="14" height="2" fill="#1a1a2a"/>
532
- <rect x="9" y="3" width="2" height="5" fill="#1a1a2a"/>
533
- <rect x="21" y="3" width="2" height="5" fill="#1a1a2a"/>
534
- <rect x="9" y="5" width="14" height="12" fill="#f0c8a0"/>
535
- <rect x="7" y="8" width="2" height="4" fill="#f0c8a0"/>
536
- <rect x="23" y="8" width="2" height="4" fill="#f0c8a0"/>
537
- <rect x="11" y="9" width="4" height="3" fill="${color}" opacity=".9"/>
538
- <rect x="17" y="9" width="4" height="3" fill="${color}" opacity=".9"/>
539
- <rect x="12" y="10" width="2" height="2" fill="#111"/>
540
- <rect x="18" y="10" width="2" height="2" fill="#111"/>
541
- <rect x="15" y="10" width="2" height="1" fill="${color}"/>
542
- <rect x="13" y="14" width="6" height="1" fill="#c88060" opacity=".5"/>
543
- <rect x="9" y="17" width="14" height="11" fill="${color}" opacity=".85"/>
544
- <rect x="11" y="17" width="10" height="3" fill="${color}"/>
545
- <g ${anim}>
546
- <rect x="4" y="18" width="5" height="8" fill="${color}" opacity=".85"/>
547
- <rect x="23" y="18" width="5" height="8" fill="${color}" opacity=".85"/>
548
- <rect x="3" y="25" width="5" height="4" fill="#f0c8a0"/>
549
- <rect x="24" y="25" width="5" height="4" fill="#f0c8a0"/>
550
- </g>
551
- <rect x="9" y="28" width="14" height="12" fill="#1e2535"/>
552
- <rect x="13" y="40" width="5" height="7" fill="#1e2535"/>
553
- <rect x="9" y="40" width="5" height="7" fill="#1e2535"/>
554
- <rect x="7" y="47" width="7" height="4" fill="#111"/>
555
- <rect x="18" y="47" width="7" height="4" fill="#111"/>
556
- </svg>`;
557
- }
558
-
559
- function chairSVG(empty) {
560
- const c = empty ? '#111827' : '#1e2a40', s = empty ? '#0d1117' : '#161b22';
561
- return `<svg viewBox="0 0 26 30" width="26" height="30" style="image-rendering:pixelated;display:block">
562
- <rect x="3" y="3" width="20" height="13" fill="${c}" rx="1"/>
563
- <rect x="4" y="4" width="18" height="11" fill="${s}"/>
564
- <rect x="3" y="16" width="20" height="4" fill="${c}"/>
565
- <rect x="3" y="0" width="4" height="16" fill="${c}"/>
566
- <rect x="5" y="20" width="3" height="8" fill="${c}"/>
567
- <rect x="18" y="20" width="3" height="8" fill="${c}"/>
568
- <rect x="3" y="27" width="5" height="3" fill="#0d1117"/>
569
- <rect x="18" y="27" width="5" height="3" fill="#0d1117"/>
570
- </svg>`;
571
- }
572
-
573
- function coffeeSVG() {
574
- return `<svg viewBox="0 0 13 13" width="13" height="13" style="image-rendering:pixelated;display:block">
575
- <rect x="2" y="4" width="7" height="7" fill="#5c3a1e"/>
576
- <rect x="2" y="4" width="7" height="2" fill="#7a4e2a"/>
577
- <rect x="9" y="6" width="2" height="3" fill="#5c3a1e"/>
578
- <rect x="1" y="11" width="9" height="2" fill="#4a2e14"/>
579
- <rect x="4" y="2" width="1" height="2" fill="#555" opacity=".5"/>
580
- <rect x="6" y="1" width="1" height="3" fill="#555" opacity=".4"/>
581
- </svg>`;
582
- }
583
-
584
- function serverRack() {
585
- const units = [
586
- ['g','g','1.1s','1.7s',true],['a','b','2.3s','.8s',true],
587
- ['g','o','1.8s',null,false], ['b','g','.7s','2.1s',true],
588
- ['a','a','1.4s','1.9s',false],['g','b','2.6s','.6s',true],
589
- ];
590
- return units.map(([l1,l2,d1,d2,act]) =>
591
- `<div class="runit">
592
- <div class="rled l${l1}" style="--d:${d1}"></div>
593
- <div class="rled l${l2 !== 'o' ? l2 : 'o'}" ${d2 ? `style="--d:${d2}"` : ''}></div>
594
- <div class="rbar ${act?'on':''}"></div>
595
- </div>`
596
- ).join('');
597
- }
598
-
599
- function screenContent(status) {
600
- if (status === 'working') return `<div class="tdots"><div class="tdot"></div><div class="tdot"></div><div class="tdot"></div></div>`;
601
- if (status === 'active') return `<div class="sline" style="width:80%"></div><div class="sline" style="width:55%;opacity:.4"></div><div class="sline" style="width:68%;opacity:.3"></div><div class="scur"></div>`;
602
- if (status === 'resting') return `<div style="display:flex;align-items:center;justify-content:center;height:100%;font-family:'VT323',monospace;font-size:11px;color:var(--amber);letter-spacing:1px;opacity:.6">AWAY</div>`;
603
- return `<div class="sline" style="width:40%;opacity:.2"></div><div class="sline" style="width:28%;opacity:.15"></div>`;
604
- }
605
-
606
- // ── BUILD ROOM ─────────────────────────────────────────────────────────────
607
- function buildRoom(agent) {
608
- const st = states[agent.key] || { status:'idle', message:'', model:'' };
609
- const status = st.status;
610
- const color = COLORS[agent.key] || '#58a6ff';
611
- const empty = status === 'resting';
612
-
613
- const badgeMap = { idle:'IDLE', working:'PROCESSING', active:'DONE', resting:'AWAY', error:'ERROR' };
614
- const badgeCls = { idle:'b-idle', working:'b-working', active:'b-active', resting:'b-resting', error:'b-error' };
615
- const roomCls = { idle:'idle', working:'working', active:'active', resting:'resting', error:'resting' };
616
-
617
- const monCls = status === 'working' ? 'thinking' : status === 'active' ? 'done' : status === 'resting' ? 'away' : '';
618
-
619
- let resultHTML = '';
620
- if (status === 'idle') {
621
- resultHTML = `<div class="rph">Waiting for mission...</div>`;
622
- } else if (status === 'working') {
623
- resultHTML = `<div class="rph" style="color:var(--accent)">Processing task...</div>`;
624
- } else if (status === 'resting') {
625
- resultHTML = `<div class="rph" style="color:var(--amber)">Rate limit — back in ~1-2 min</div>`;
626
- } else {
627
- const msg = st.message || '';
628
- resultHTML = `<div class="rtxt" id="result-${agent.key}"></div>
629
- ${st.model ? `<div class="rmodel"><div class="sdot dot-g" style="width:5px;height:5px"></div>${st.model}</div>` : ''}
630
- ${st.doc_file ? `<a class="dl-btn" href="/api/docs/${st.doc_file}" download>⬇ Download .docx</a>` : ''}`;
631
- }
632
-
633
- return `<div class="room ${roomCls[status]||'idle'}" id="room-${agent.key}">
634
- <div class="rwall">
635
- <span class="rname" style="color:${color}">${agent.name}</span>
636
- <span class="rbadge ${badgeCls[status]||'b-idle'}">${badgeMap[status]||'IDLE'}</span>
637
- </div>
638
  <div class="rscene" id="scene-${agent.key}">
639
- <div class="srack">${serverRack()}</div>
640
- <div class="deskarea">
641
- <div class="monitor ${monCls}">
642
- <div class="mscreen">${screenContent(status)}</div>
643
- <div class="mbase"></div>
644
- </div>
645
- <div class="desk-surface"></div>
646
- <div class="keyboard"><div class="key"></div><div class="key"></div><div class="key"></div><div class="key"></div></div>
647
- </div>
648
- <div style="position:absolute;bottom:0;right:4px;z-index:1">${chairSVG(empty)}</div>
649
  <div class="coffee">${coffeeSVG()}</div>
650
- ${!empty ? `<div class="person">${personSVG(color, status)}</div>` : ''}
651
- ${empty ? `<div class="ooo"><div class="ooo-t"> AWAY</div><div class="ooo-s">Rate limited</div></div>` : ''}
652
  <div class="bubble" id="bubble-${agent.key}"></div>
653
  </div>
654
  <div class="rresult" id="result-area-${agent.key}">${resultHTML}</div>
655
  </div>`;
656
  }
657
 
658
- function buildServerRoom() {
659
- const racks = Array.from({length:5},()=>{
660
- const units = [['g','1.1s'],['b','.8s'],['a','2s'],['g','1.6s'],['b','.5s']].map(([c,d])=>
661
- `<div class="big-unit"><div class="rled l${c}" style="--d:${d}"></div><div class="rbar on"></div></div>`
662
- ).join('');
663
- return `<div class="big-rack">${units}</div>`;
664
- }).join('');
665
-
666
- return `<div class="server-room">
667
- <div class="sroom-racks">${racks}</div>
668
- <div class="srv-stats">
669
- <div class="srv-stat"><div class="sdot dot-g"></div><span>FastAPI online</span></div>
670
- <div class="srv-stat"><div class="sdot dot-g" id="dot-gemini"></div><span>Gemini</span></div>
671
- <div class="srv-stat"><div class="sdot dot-g" id="dot-orouter"></div><span>OpenRouter</span></div>
672
- <div class="srv-stat"><div class="sdot dot-g" id="dot-groq"></div><span>Groq (backup)</span></div>
673
- </div>
674
- </div>`;
675
- }
676
-
677
- function renderOffice(orchEvents) {
678
- const grid = document.getElementById('ogrid');
679
- let html = agentDefs.map(buildRoom).join('');
680
- html += buildServerRoom();
681
-
682
- if (orchEvents && orchEvents.length) {
683
- const evHtml = orchEvents.map(e =>
684
- `<div class="orch-event"><span class="orch-time">${e.time}</span><span>${e.msg}</span></div>`
685
- ).join('');
686
- html += `<div class="orch-log show">
687
- <div class="orch-title">ORCHESTRATION LOG</div>
688
- <div class="orch-events">${evHtml}</div>
689
- </div>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
690
  }
691
 
692
- grid.innerHTML = html;
693
-
694
- // Fill typewriter results
695
- agentDefs.forEach(a => {
696
- const st = states[a.key];
697
- if (st && st.status === 'active' && st.message && st._typewrite) {
698
- const el = document.getElementById(`result-${a.key}`);
699
- if (el) typeWriter(el, st.message);
700
- }
701
- });
702
-
703
- const active = agentDefs.filter(a => (states[a.key]||{}).status === 'working').length;
704
- document.getElementById('top-active').textContent = `${active} active`;
705
- }
706
-
707
- // TYPEWRITER
708
- function typeWriter(el, text, speed=14) {
709
- el.textContent = ''; let i = 0;
710
- const iv = setInterval(() => {
711
- if (i < text.length) el.textContent += text[i++];
712
- else clearInterval(iv);
713
- }, speed);
714
- }
715
-
716
- // SPEECH
717
- function showBubble(key, text, ms=2800) {
718
- const b = document.getElementById(`bubble-${key}`);
719
- if (!b) return;
720
- if (bubbleT[key]) clearTimeout(bubbleT[key]);
721
- b.textContent = text;
722
- b.classList.add('show');
723
- bubbleT[key] = setTimeout(() => b.classList.remove('show'), ms);
724
- }
725
-
726
- // ACTIVITY
727
- function addAct(msg, color='#58a6ff') {
728
- const now = new Date();
729
- const t = [now.getHours(),now.getMinutes(),now.getSeconds()].map(x=>String(x).padStart(2,'0')).join(':');
730
- activity.unshift({ msg, t, color });
731
- document.getElementById('act-log').innerHTML = activity.slice(0,30).map(a=>
732
- `<div class="act-item">
733
- <div class="act-dot" style="background:${a.color}"></div>
734
- <div><div class="act-msg">${a.msg}</div><div class="act-time">${a.t}</div></div>
735
- </div>`
736
- ).join('');
737
- }
738
-
739
- // NOTIFY
740
- function notify(title, msg, color='#3fb950') {
741
- document.getElementById('notif-t').textContent = title;
742
- document.getElementById('notif-t').style.color = color;
743
- document.getElementById('notif-m').textContent = msg;
744
- document.getElementById('notif').style.borderLeftColor = color;
745
- const el = document.getElementById('notif');
746
- el.classList.add('show');
747
- setTimeout(() => el.classList.remove('show'), 5000);
748
- }
749
-
750
- // HISTORY
751
- function updateHistory() {
752
- const list = document.getElementById('hist-log');
753
- if (!history.length) {
754
- list.innerHTML = `<div style="padding:14px;font-size:11px;color:var(--text3);text-align:center">No missions yet</div>`;
755
- return;
756
  }
757
- list.innerHTML = [...history].reverse().map(m=>
758
- `<div class="hist-item">
759
- <div class="hist-task">${m.task}</div>
760
- <div class="hist-meta">
761
- <span>${m.time}</span>
762
- <span style="color:${m.ok?'var(--green)':'var(--amber)'}">${m.ok?'Done':'Partial'}</span>
763
- ${m.doc_file ? '<span style="color:var(--purple)">📄 docx</span>' : ''}
764
- </div>
765
- </div>`
766
- ).join('');
767
- }
768
-
769
- // MODAL
770
- function openModal() { document.getElementById('modal-bg').classList.add('show'); }
771
- function closeModal() { document.getElementById('modal-bg').classList.remove('show'); }
772
-
773
- async function submitAgent() {
774
- const key = document.getElementById('m-key').value.trim();
775
- const name = document.getElementById('m-name').value.trim() || key;
776
- const role = document.getElementById('m-role').value.trim() || 'General purpose agent.';
777
- const prov = document.getElementById('m-provider').value;
778
- if (!key) { alert('Key is required'); return; }
779
 
780
- const r = await fetch('/api/agents/add', {
781
- method:'POST', headers:{'Content-Type':'application/json'},
782
- body: JSON.stringify({ key, name, role, provider: prov, models: PROVIDER_MODELS[prov] }),
783
- });
784
- const d = await r.json();
785
- if (d.success) {
786
- agentDefs.push({ key: d.agent.key, name: d.agent.name, role: d.agent.role });
787
- states[d.agent.key] = { status:'idle', message:'', model:'' };
788
- renderOffice();
789
- addAct(`Agent "${name}" added`, '#bc8cff');
790
- closeModal();
791
- ['m-key','m-name','m-role'].forEach(id => document.getElementById(id).value = '');
792
  }
793
- }
794
-
795
- // ── LAUNCH ─────────────────────────────────────────────────────────────────
796
- async function launchMission() {
797
- const task = document.getElementById('task-input').value.trim();
798
- if (!task) return;
799
 
800
- const btn = document.getElementById('launch-btn');
801
- btn.disabled = true;
802
- document.getElementById('progress').classList.add('on');
803
- document.getElementById('top-status').textContent = 'Running';
804
- document.getElementById('top-dot').className = 'sdot dot-a';
805
- document.getElementById('floor-subtitle').textContent = `Mission: "${task.substring(0,50)}${task.length>50?'...':''}"`;
806
-
807
- // Set all agents working
808
- agentDefs.forEach(a => { states[a.key] = { status:'working', message:'', model:'' }; });
809
- renderOffice();
810
-
811
- // Stagger speech bubbles
812
- agentDefs.forEach((a, i) => {
813
- setTimeout(() => {
814
- const phrases = SPEECH[a.key] || ['Working...'];
815
- showBubble(a.key, phrases[Math.floor(Math.random()*phrases.length)], 3000);
816
- }, i * 600);
817
- });
818
-
819
- addAct(`Mission: "${task.substring(0,40)}"`, '#58a6ff');
820
-
821
- try {
822
- const resp = await fetch('/api/mission', {
823
- method:'POST', headers:{'Content-Type':'application/json'},
824
- body: JSON.stringify({ task }),
825
- });
826
- const data = await resp.json();
827
- if (!resp.ok || data.error) throw new Error(data.error || 'Server error');
828
-
829
- let anyResting = false;
830
- let hasDoc = false;
831
-
832
- // Update states from results
833
- agentDefs.forEach(a => {
834
- const r = data.results[a.key];
835
- if (!r) {
836
- states[a.key] = { status:'idle', message:'', model:'' };
837
- return;
838
  }
839
- if (r.status === 'resting') anyResting = true;
840
- const docFile = (a.key === 'manager' && data.doc_file) ? data.doc_file : null;
841
- if (docFile) hasDoc = true;
842
- states[a.key] = {
843
- status: r.status === 'resting' ? 'resting' : r.status === 'idle' ? 'idle' : 'active',
844
- message: r.message || '',
845
- model: r.model || '',
846
- doc_file: docFile,
847
- _typewrite: true,
848
- };
849
- });
850
-
851
- renderOffice(data.events);
852
-
853
- // Stagger bubbles for active agents
854
- agentDefs.forEach((a, i) => {
855
- if ((states[a.key]||{}).status === 'active') {
856
- setTimeout(() => {
857
- showBubble(a.key, 'Done ✓', 2000);
858
- addAct(`${a.name}: complete`, '#3fb950');
859
- }, i * 400);
860
- }
861
- });
862
-
863
- if (hasDoc) addAct('Word document ready for download', '#bc8cff');
864
-
865
- mCount++;
866
- document.getElementById('mc-count').textContent = `MISSIONS: ${mCount}`;
867
- const time = new Date().toLocaleTimeString();
868
- history.push({ task, time, ok: !anyResting, doc_file: data.doc_file });
869
- updateHistory();
870
-
871
- setTimeout(() => {
872
- notify(
873
- anyResting ? 'Mission Partial' : hasDoc ? 'Document Ready ✓' : 'Mission Complete ✓',
874
- anyResting
875
- ? 'Some agents rate-limited. Try again in ~2 min.'
876
- : hasDoc
877
- ? 'Your .docx document is ready to download!'
878
- : `All agents responded. Mission #${mCount} done.`,
879
- anyResting ? '#d29922' : hasDoc ? '#bc8cff' : '#3fb950'
880
- );
881
- }, agentDefs.length * 400 + 300);
882
-
883
- document.getElementById('top-status').textContent = anyResting ? 'Partial' : 'Done';
884
- document.getElementById('top-dot').className = `sdot ${anyResting ? 'dot-a' : 'dot-g'}`;
885
- document.getElementById('floor-subtitle').textContent = anyResting ? 'Partial results' : 'Mission complete';
886
-
887
- } catch(err) {
888
- agentDefs.forEach(a => { states[a.key] = { status:'resting', message: err.message, model:'' }; });
889
- renderOffice();
890
- addAct(`Error: ${err.message}`, '#f85149');
891
- notify('Mission Failed', err.message, '#f85149');
892
- document.getElementById('top-dot').className = 'sdot dot-r';
893
- document.getElementById('top-status').textContent = 'Error';
894
  }
895
-
896
- btn.disabled = false;
897
- document.getElementById('progress').classList.remove('on');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
898
  }
899
-
900
- document.getElementById('task-input').addEventListener('keydown', e => {
901
- if (e.key === 'Enter') launchMission();
902
- });
903
-
904
- // INIT
905
- loadAgents();
906
- addAct('System online — agents ready', '#3fb950');
907
- addAct('Server rack nominal', '#58a6ff');
908
  </script>
909
  </body>
910
  </html>
 
5
  <meta name="viewport" content="width=device-width,initial-scale=1">
6
  <title>Mission Control AI</title>
7
  <style>
8
+ @import url('https://fonts.googleapis.com/css2?family=VT323&family=Share+Tech+Mono&display=swap');
9
+ :root{
10
+ --bg:#0a0e14;--floor:#0d1320;--wall:#101828;--wall2:#131f2e;
11
+ --carpet:#080f1a;--carpet2:#0a1220;--border:#1e2d42;--border2:#2a3d58;
12
+ --text:#c8d8f0;--text2:#5a7a9a;--text3:#2a3d58;
13
+ --G:#00e5a0;--Gdim:#00995a;--Gdark:#002a18;
14
+ --B:#4a9eff;--Bdim:#1a5aaa;--A:#ffcc44;--Adim:#aa8800;
15
+ --R:#ff4455;--P:#cc88ff;
16
+ --wood:#7a5c0a;--woodl:#9a7a18;--metal:#1a2535;--metall:#243040;
17
+ --shadow:0 8px 32px rgba(0,0,0,.6);
 
 
 
 
 
 
18
  }
 
19
  *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
20
+ body{font-family:'Share Tech Mono',monospace;background:var(--bg);color:var(--text);min-height:100vh;display:flex;flex-direction:column;overflow-x:hidden;image-rendering:pixelated}
21
+ body::after{content:'';position:fixed;inset:0;background:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(0,0,0,.07) 2px,rgba(0,0,0,.07) 4px);pointer-events:none;z-index:500}
22
+ .topbar{background:#050810;border-bottom:2px solid var(--border2);height:48px;padding:0 20px;display:flex;align-items:center;justify-content:space-between;flex-shrink:0;position:sticky;top:0;z-index:200}
23
+ .tb-logo{font-family:'VT323',monospace;font-size:22px;color:var(--G);letter-spacing:3px;text-shadow:0 0 12px rgba(0,229,160,.4)}
24
+ .tb-ver{font-size:10px;color:var(--text3);margin-left:4px;letter-spacing:1px}
25
+ .tb-right{display:flex;align-items:center;gap:18px}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  .tb-stat{font-size:11px;color:var(--text2);display:flex;align-items:center;gap:5px}
27
  .sdot{width:7px;height:7px;border-radius:50%}
28
+ .dg{background:var(--G);animation:dg 2s ease infinite}
29
+ .da{background:var(--A);animation:dg 1.2s ease infinite}
30
+ .dr{background:var(--R);animation:bdr .5s step-end infinite}
31
+ .dd{background:var(--text3)}
32
+ @keyframes dg{0%,100%{box-shadow:0 0 0 0 currentColor}50%{box-shadow:0 0 0 4px transparent}}
33
+ @keyframes bdr{0%,100%{opacity:1}50%{opacity:0}}
34
+ .clock{font-family:'VT323',monospace;font-size:22px;color:var(--G);letter-spacing:2px}
35
+ .prog{height:2px;background:var(--floor);overflow:hidden;display:none;flex-shrink:0}
36
+ .prog.on{display:block}
37
+ .prog::after{content:'';display:block;height:100%;width:30%;background:var(--G);animation:prun 1.4s ease-in-out infinite}
38
+ @keyframes prun{0%{margin-left:-30%}100%{margin-left:110%}}
39
+ .mbar{background:#050810;border-bottom:1px solid var(--border);padding:10px 20px;display:flex;gap:10px;flex-shrink:0;flex-wrap:wrap}
40
+ .minput{flex:1;min-width:200px;height:36px;background:var(--floor);border:1px solid var(--border2);color:var(--text);font-family:'Share Tech Mono',monospace;font-size:12px;padding:0 12px;outline:none;transition:border-color .15s}
41
+ .minput:focus{border-color:var(--G);box-shadow:0 0 0 2px rgba(0,229,160,.1)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  .minput::placeholder{color:var(--text3)}
43
+ .launch{height:36px;padding:0 18px;background:transparent;border:2px solid var(--G);color:var(--G);font-family:'VT323',monospace;font-size:18px;letter-spacing:2px;cursor:pointer;transition:all .15s;display:flex;align-items:center;gap:6px}
44
+ .launch:hover{background:rgba(0,229,160,.1);box-shadow:0 0 12px rgba(0,229,160,.2)}
45
+ .launch:active{transform:scale(.97)}
46
+ .launch:disabled{border-color:var(--text3);color:var(--text3);cursor:not-allowed}
47
+ .add-btn{height:36px;padding:0 14px;background:transparent;border:1px solid var(--border2);color:var(--text2);font-family:'Share Tech Mono',monospace;font-size:11px;cursor:pointer;transition:all .15s}
48
+ .add-btn:hover{border-color:var(--B);color:var(--B)}
49
+ .mc{font-family:'VT323',monospace;font-size:17px;color:var(--text3)}
50
+ .main{display:grid;grid-template-columns:1fr 260px;flex:1;min-height:0;overflow:hidden}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  @media(max-width:900px){.main{grid-template-columns:1fr}.sidebar{display:none}}
52
+ .office{background:var(--carpet);overflow-y:auto;padding:16px;position:relative}
53
+ .office::before{content:'';position:fixed;inset:0;background-image:linear-gradient(rgba(255,255,255,.02) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.02) 1px,transparent 1px);background-size:32px 32px;pointer-events:none;z-index:0}
54
+ .floor-hdr{font-family:'VT323',monospace;font-size:13px;letter-spacing:4px;color:var(--text3);margin-bottom:14px;display:flex;align-items:center;justify-content:space-between;position:relative;z-index:1}
55
+ .floor-sub{font-size:10px;color:var(--Gdim);letter-spacing:2px}
56
+ .ogrid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;position:relative;z-index:1}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  @media(max-width:1100px){.ogrid{grid-template-columns:repeat(2,1fr)}}
58
  @media(max-width:700px){.ogrid{grid-template-columns:1fr}}
59
+ .room{background:var(--wall);border:1px solid var(--border);display:flex;flex-direction:column;min-height:230px;transition:border-color .3s,box-shadow .3s;overflow:hidden}
60
+ .room.working{border-color:var(--G);box-shadow:0 0 16px rgba(0,229,160,.12)}
61
+ .room.active{border-color:var(--Gdim)}
62
+ .room.resting{border-color:var(--A)}
63
+ .room.visiting{border-color:var(--B);box-shadow:0 0 16px rgba(74,158,255,.15);animation:vp 1s ease-in-out infinite}
64
+ @keyframes vp{0%,100%{box-shadow:0 0 8px rgba(74,158,255,.1)}50%{box-shadow:0 0 20px rgba(74,158,255,.25)}}
65
+ .rwall{background:var(--wall2);height:28px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;padding:0 10px;flex-shrink:0}
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  .rname{font-family:'VT323',monospace;font-size:15px;letter-spacing:2px}
67
+ .rbadge{font-family:'VT323',monospace;font-size:11px;padding:1px 6px;border:1px solid;letter-spacing:1px}
68
+ .bi{color:var(--text3);border-color:var(--text3)}.bw{color:var(--G);border-color:var(--G);animation:bp .9s ease infinite}.ba{color:var(--Gdim);border-color:var(--Gdim);background:rgba(0,229,160,.08)}.br{color:var(--A);border-color:var(--A)}.bv{color:var(--B);border-color:var(--B);animation:bp .6s ease infinite}
69
+ @keyframes bp{0%,100%{opacity:1}50%{opacity:.4}}
70
+ .rscene{flex:1;background:var(--carpet2);position:relative;display:flex;align-items:flex-end;padding:8px;gap:8px;overflow:hidden;min-height:130px}
71
+ .rscene::before{content:'';position:absolute;inset:0;background-image:linear-gradient(45deg,rgba(255,255,255,.012) 25%,transparent 25%),linear-gradient(-45deg,rgba(255,255,255,.012) 25%,transparent 25%),linear-gradient(45deg,transparent 75%,rgba(255,255,255,.012) 75%),linear-gradient(-45deg,transparent 75%,rgba(255,255,255,.012) 75%);background-size:16px 16px}
72
+ .srack{width:36px;flex-shrink:0;align-self:flex-end;margin-bottom:4px;background:#070e18;border:1px solid #1a2a40;padding:4px 3px;display:flex;flex-direction:column;gap:2px;position:relative;z-index:2}
73
+ .srack::before{content:'SRV';position:absolute;top:-16px;left:0;right:0;text-align:center;font-family:'VT323',monospace;font-size:10px;color:var(--text3)}
74
+ .ru{height:8px;background:#0d1525;border:1px solid #1a2a3a;display:flex;align-items:center;gap:2px;padding:0 2px}
75
+ .rl{width:4px;height:4px;border-radius:50%}
76
+ .lg{background:var(--G);animation:led var(--d,1.5s) ease-in-out infinite}
77
+ .la{background:var(--A);animation:led var(--d,2s) ease-in-out infinite}
78
+ .lb{background:var(--B);animation:led var(--d,.9s) ease-in-out infinite}
79
+ .lo{background:#0d1525;border:1px solid #1a2a3a}
80
+ .rb{flex:1;height:2px;background:#1a2a3a}
81
+ .rb.on{background:var(--B);animation:rba .4s ease-in-out infinite alternate}
82
+ @keyframes led{0%,100%{opacity:.25}50%{opacity:1;box-shadow:0 0 5px currentColor}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  @keyframes rba{0%{opacity:.3}100%{opacity:1}}
84
+ .deskarea{flex:1;display:flex;flex-direction:column;gap:3px;position:relative;z-index:2}
85
+ .monitor{width:100%;height:46px;background:#030608;border:2px solid var(--metal);display:flex;flex-direction:column;position:relative;overflow:hidden;transition:border-color .3s}
86
+ .monitor.thinking{border-color:var(--B)}.monitor.done{border-color:var(--G)}.monitor.away{border-color:var(--Adim)}
 
 
 
 
 
 
 
 
 
 
87
  .mscreen{flex:1;padding:4px;display:flex;flex-direction:column;gap:3px;position:relative;overflow:hidden}
88
  .mbase{height:5px;background:var(--metal);position:relative}
89
+ .mbase::after{content:'';position:absolute;bottom:-3px;left:50%;transform:translateX(-50%);width:14px;height:3px;background:var(--metall)}
90
+ .sl{height:3px;background:var(--G);border-radius:1px;opacity:.6}
91
+ .scur{position:absolute;bottom:3px;left:4px;width:4px;height:8px;background:var(--G);animation:cur .7s step-end infinite}
 
 
 
 
 
 
 
 
 
 
92
  @keyframes cur{0%,100%{opacity:1}50%{opacity:0}}
93
  .tdots{display:flex;gap:3px;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}
94
+ .td{width:4px;height:4px;border-radius:50%;background:var(--B);animation:tdb .8s ease-in-out infinite}
95
+ .td:nth-child(2){animation-delay:.15s}.td:nth-child(3){animation-delay:.3s}
96
+ @keyframes tdb{0%,80%,100%{transform:translateY(0);opacity:.3}40%{transform:translateY(-5px);opacity:1}}
97
+ .dtop{height:7px;background:var(--woodl);border-top:2px solid #c09828}
98
+ .kbd{height:6px;background:#c0b090;border:1px solid #a09070;display:flex;gap:1px;padding:1px 2px;align-items:center}
99
+ .kk{flex:1;height:3px;background:#a09070}
100
+ .coffee{position:absolute;bottom:28px;right:8px;z-index:3}
101
+ .person-slot{position:absolute;bottom:20px;left:50%;transform:translateX(-50%);z-index:3}
102
+ .chair-slot{position:absolute;bottom:0;right:3px;z-index:2}
103
+ .ooo{position:absolute;inset:0;background:rgba(20,14,0,.75);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;z-index:10}
104
+ .ooot{font-family:'VT323',monospace;font-size:17px;color:var(--A);letter-spacing:2px}
105
+ .ooos{font-size:10px;color:rgba(255,204,68,.5)}
106
+ .bubble{position:absolute;top:6px;left:42px;background:#050810;border:1px solid var(--border2);padding:4px 8px;font-size:10px;color:var(--text);max-width:130px;line-height:1.3;z-index:20;opacity:0;transform:translateY(-4px);pointer-events:none;transition:opacity .25s,transform .25s;box-shadow:0 4px 12px rgba(0,0,0,.5)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  .bubble.show{opacity:1;transform:translateY(0)}
108
+ .bubble::before{content:'';position:absolute;left:-6px;top:8px;border:5px solid transparent;border-right-color:var(--border2)}
109
+ .rresult{background:#060d18;border-top:1px solid var(--border);padding:10px 12px;flex-shrink:0;min-height:56px}
 
 
 
 
 
 
 
 
 
110
  .rph{font-size:11px;color:var(--text3);font-style:italic}
111
  .rtxt{font-size:11px;color:var(--text);line-height:1.6;word-break:break-word}
112
+ .rmodel{font-size:10px;color:var(--text3);margin-top:4px;display:flex;align-items:center;gap:4px}
113
+ .dl-btn{display:inline-flex;align-items:center;gap:6px;margin-top:8px;padding:5px 14px;background:rgba(0,229,160,.08);border:1px solid var(--G);color:var(--G);font-family:'Share Tech Mono',monospace;font-size:11px;cursor:pointer;text-decoration:none;transition:all .15s}
114
+ .dl-btn:hover{background:rgba(0,229,160,.18)}
115
+ .walk-overlay{position:fixed;inset:0;background:rgba(5,8,16,.93);z-index:400;display:none;align-items:center;justify-content:center;flex-direction:column;gap:16px}
116
+ .walk-overlay.show{display:flex}
117
+ .walk-wrap{position:relative;width:min(700px,95vw);height:min(400px,58vh);background:var(--wall);border:2px solid var(--border2);overflow:hidden}
118
+ .walk-lbl{font-family:'VT323',monospace;font-size:14px;color:var(--G);letter-spacing:3px;text-align:center}
119
+ .walk-log{font-family:'Share Tech Mono',monospace;font-size:11px;color:var(--text2);text-align:center;letter-spacing:1px;height:18px}
120
+ .srv-strip{grid-column:1/-1;background:var(--wall2);border:1px solid var(--border);padding:10px 16px;display:flex;align-items:center;gap:14px;position:relative;overflow:hidden}
121
+ .srv-strip::before{content:'SERVER ROOM';position:absolute;top:7px;left:14px;font-family:'VT323',monospace;font-size:11px;color:var(--text3);letter-spacing:3px}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  .sroom-racks{display:flex;gap:8px;margin-top:16px;flex-wrap:wrap}
123
+ .big-rack{background:#070e18;border:1px solid #1a2a40;padding:3px;width:42px;display:flex;flex-direction:column;gap:2px}
124
+ .big-unit{height:7px;background:#0d1525;border:1px solid #1a2a3a;display:flex;align-items:center;gap:2px;padding:0 2px}
125
  .srv-stats{margin-left:auto;display:flex;flex-direction:column;gap:4px;font-size:10px;color:var(--text2)}
126
  .srv-stat{display:flex;align-items:center;gap:5px}
127
+ .orch-log{grid-column:1/-1;background:var(--wall2);border:1px solid var(--border);padding:10px 14px;display:none}
128
+ .orch-log.show{display:block}
129
+ .orch-title{font-family:'VT323',monospace;font-size:13px;color:var(--text2);letter-spacing:3px;margin-bottom:6px}
130
+ .orch-ev{font-size:10px;color:var(--text2);display:flex;gap:8px;padding:2px 0}
131
+ .orch-t{color:var(--text3);flex-shrink:0}
132
+ .modal-bg{position:fixed;inset:0;background:rgba(0,0,0,.8);z-index:300;display:none;align-items:center;justify-content:center}
133
  .modal-bg.show{display:flex}
134
+ .modal{background:var(--wall);border:1px solid var(--border2);padding:24px;width:380px;box-shadow:var(--shadow)}
135
+ .modal-t{font-family:'VT323',monospace;font-size:20px;color:var(--G);letter-spacing:2px;margin-bottom:16px}
136
+ .field{margin-bottom:11px}
137
+ .field label{display:block;font-size:10px;color:var(--text2);letter-spacing:2px;margin-bottom:4px;text-transform:uppercase}
138
+ .field input,.field select,.field textarea{width:100%;background:var(--floor);border:1px solid var(--border2);color:var(--text);font-family:'Share Tech Mono',monospace;font-size:11px;padding:7px 10px;outline:none;transition:border-color .15s}
139
+ .field input:focus,.field select:focus,.field textarea:focus{border-color:var(--G)}
140
+ .field textarea{resize:vertical;min-height:56px}
141
+ .field select option{background:var(--floor)}
142
+ .modal-acts{display:flex;gap:8px;justify-content:flex-end;margin-top:14px}
143
+ .mbtn{padding:7px 16px;font-family:'Share Tech Mono',monospace;font-size:11px;cursor:pointer;border:1px solid;transition:all .15s;background:transparent}
144
+ .mbtn-ok{border-color:var(--G);color:var(--G)}.mbtn-ok:hover{background:rgba(0,229,160,.1)}
145
+ .mbtn-no{border-color:var(--border2);color:var(--text2)}.mbtn-no:hover{border-color:var(--text2)}
146
+ .notif{position:fixed;top:58px;right:16px;background:var(--wall);border:1px solid var(--border);border-left:3px solid var(--G);padding:12px 16px;max-width:280px;z-index:600;transform:translateX(300px);transition:transform .3s ease;box-shadow:var(--shadow);font-size:12px}
147
+ .notif.show{transform:translateX(0)}
148
+ .notif-t{font-family:'VT323',monospace;font-size:18px;letter-spacing:1px}
149
+ .notif-m{color:var(--text2);margin-top:2px;line-height:1.4;font-size:11px}
150
+ .sidebar{background:#050810;border-left:1px solid var(--border);display:flex;flex-direction:column;overflow:hidden}
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  .sb-sec{border-bottom:1px solid var(--border);flex-shrink:0}
152
+ .sb-t{font-family:'VT323',monospace;font-size:13px;letter-spacing:3px;color:var(--text2);padding:9px 14px 7px;border-bottom:1px solid var(--border)}
153
  .act-scroll{flex:1;overflow-y:auto;max-height:260px}
154
+ .act-item{padding:7px 14px;border-bottom:1px solid rgba(30,45,66,.4);display:flex;gap:8px}
155
  .act-dot{width:5px;height:5px;border-radius:50%;margin-top:5px;flex-shrink:0}
156
  .act-msg{font-size:11px;color:var(--text);line-height:1.4}
157
  .act-time{font-size:10px;color:var(--text3);margin-top:1px}
158
  .hist-scroll{overflow-y:auto;max-height:200px}
159
+ .hist-item{padding:8px 14px;border-bottom:1px solid rgba(30,45,66,.4);cursor:pointer;transition:background .1s}
160
+ .hist-item:hover{background:var(--wall)}
161
  .hist-task{font-size:11px;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:500}
162
  .hist-meta{font-size:10px;color:var(--text3);margin-top:2px;display:flex;gap:8px}
163
+ ::-webkit-scrollbar{width:4px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
164
+ @keyframes arm-type{0%{transform:translateY(0)rotate(0)}100%{transform:translateY(2px)rotate(-5deg)}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  </style>
166
  </head>
167
  <body>
 
168
  <header class="topbar">
169
+ <div style="display:flex;align-items:center;gap:8px">
170
+ <span class="tb-logo">MISSION CONTROL AI</span>
171
+ <span class="tb-ver">v4</span>
172
  </div>
173
  <div class="tb-right">
174
+ <div class="tb-stat"><div class="sdot dg"></div><span id="top-active">0 active</span></div>
175
+ <div class="tb-stat"><div class="sdot dd" id="top-dot"></div><span id="top-status">Idle</span></div>
176
  <div class="clock" id="clock">00:00:00</div>
177
  </div>
178
  </header>
179
+ <div class="prog" id="prog"></div>
 
 
180
  <div class="mbar">
181
+ <input class="minput" id="task-input" placeholder="Assign mission... (e.g. 'Write a formal report about space exploration with images')" autocomplete="off"/>
182
  <button class="launch" id="launch-btn" onclick="launchMission()">&#9658; LAUNCH</button>
183
+ <button class="add-btn" onclick="openModal()">+ Agent</button>
184
  <span class="mc" id="mc-count">MISSIONS: 0</span>
185
  </div>
 
186
  <div class="main">
187
  <div class="office">
188
+ <div class="floor-hdr">
189
+ <span style="font-family:'VT323',monospace;font-size:13px;letter-spacing:4px">&#9632; OPERATIONS FLOOR</span>
190
+ <span class="floor-sub" id="floor-sub">WAITING FOR MISSION</span>
191
  </div>
192
  <div class="ogrid" id="ogrid"></div>
193
  </div>
 
194
  <aside class="sidebar">
195
+ <div class="sb-sec"><div class="sb-t">ACTIVITY</div><div class="act-scroll" id="act-log"></div></div>
196
+ <div class="sb-sec"><div class="sb-t">MISSIONS</div><div class="hist-scroll" id="hist-log"><div style="padding:14px;font-size:11px;color:var(--text3);text-align:center">No missions yet</div></div></div>
 
 
 
 
 
 
 
 
197
  </aside>
198
  </div>
199
+ <div class="walk-overlay" id="walk-overlay">
200
+ <div class="walk-lbl">MANAGER BRIEFING TEAM</div>
201
+ <div class="walk-wrap"><canvas id="walk-canvas"></canvas></div>
202
+ <div class="walk-log" id="walk-log">Preparing mission plan...</div>
203
+ </div>
204
  <div class="modal-bg" id="modal-bg" onclick="if(event.target===this)closeModal()">
205
  <div class="modal">
206
+ <div class="modal-t">+ NEW AGENT</div>
207
+ <div class="field"><label>Key (unique ID)</label><input id="m-key" placeholder="e.g. researcher"/></div>
208
+ <div class="field"><label>Display Name</label><input id="m-name" placeholder="e.g. Researcher"/></div>
209
+ <div class="field"><label>Role</label><textarea id="m-role" placeholder="Especialista en..."></textarea></div>
210
+ <div class="field"><label>Provider</label><select id="m-prov"><option value="openrouter">OpenRouter (free)</option><option value="gemini">Google Gemini</option><option value="groq">Groq</option></select></div>
211
+ <div class="modal-acts">
212
+ <button class="mbtn mbtn-no" onclick="closeModal()">Cancel</button>
213
+ <button class="mbtn mbtn-ok" onclick="submitAgent()">Add Agent</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  </div>
215
  </div>
216
  </div>
 
 
217
  <div class="notif" id="notif">
218
  <div class="notif-t" id="notif-t">Mission Complete</div>
219
  <div class="notif-m" id="notif-m"></div>
220
  </div>
 
221
  <script>
222
+ const PROV_MODELS={openrouter:['meta-llama/llama-3.3-70b-instruct:free','qwen/qwen3-4b:free'],gemini:['gemini-2.5-flash-preview-04-17','gemini-2.0-flash'],groq:['llama-3.3-70b-versatile','gemma2-9b-it']};
223
+ const COLORS={manager:'#00e5a0',developer:'#4a9eff',analyst:'#ffcc44',writer:'#cc88ff',image_agent:'#ff6688'};
224
+ const SPEECH={manager:['Analyzing task...','Planning approach...','Coordinating team...'],developer:['Building solution...','Writing code...'],analyst:['Analyzing data...','Reviewing doc...'],writer:['Drafting report...','Writing sections...'],image_agent:['Searching images...','Finding visuals...']};
225
+ let agentDefs=[],states={},activity=[],history=[],mCount=0,bubbleT={};
226
+
227
+ async function loadAgents(){
228
+ const r=await fetch('/api/agents'),d=await r.json();
229
+ agentDefs=d.agents;
230
+ agentDefs.forEach(a=>{if(!states[a.key])states[a.key]={status:'idle',message:'',model:''};});
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  renderOffice();
232
  }
233
 
234
+ setInterval(()=>{
235
+ const n=new Date();
236
+ document.getElementById('clock').textContent=[n.getHours(),n.getMinutes(),n.getSeconds()].map(x=>String(x).padStart(2,'0')).join(':');
237
+ },1000);
238
+
239
+ function personSVG(color,status){
240
+ const op=status==='resting'?'0.18':'1';
241
+ const a=status==='working'?'style="animation:arm-type .28s ease-in-out infinite alternate;transform-origin:13px 18px"':'';
242
+ return`<svg viewBox="0 0 32 52" width="25" height="40" style="image-rendering:pixelated;display:block;opacity:${op}"><rect x="11" y="0" width="10" height="3" fill="#1a1a2e"/><rect x="9" y="3" width="14" height="2" fill="#1a1a2e"/><rect x="9" y="3" width="2" height="5" fill="#1a1a2e"/><rect x="21" y="3" width="2" height="5" fill="#1a1a2e"/><rect x="9" y="5" width="14" height="12" fill="#f0c8a0"/><rect x="7" y="8" width="2" height="4" fill="#f0c8a0"/><rect x="23" y="8" width="2" height="4" fill="#f0c8a0"/><rect x="11" y="9" width="4" height="3" fill="${color}" opacity=".9"/><rect x="17" y="9" width="4" height="3" fill="${color}" opacity=".9"/><rect x="12" y="10" width="2" height="2" fill="#111"/><rect x="18" y="10" width="2" height="2" fill="#111"/><rect x="15" y="10" width="2" height="1" fill="${color}"/><rect x="13" y="14" width="6" height="1" fill="#c88060" opacity=".5"/><rect x="9" y="17" width="14" height="11" fill="${color}" opacity=".85"/><rect x="11" y="17" width="10" height="3" fill="${color}"/><g ${a}><rect x="4" y="18" width="5" height="8" fill="${color}" opacity=".85"/><rect x="23" y="18" width="5" height="8" fill="${color}" opacity=".85"/><rect x="3" y="25" width="5" height="4" fill="#f0c8a0"/><rect x="24" y="25" width="5" height="4" fill="#f0c8a0"/></g><rect x="9" y="28" width="14" height="12" fill="#1e2535"/><rect x="13" y="40" width="5" height="7" fill="#1e2535"/><rect x="9" y="40" width="5" height="7" fill="#1e2535"/><rect x="7" y="47" width="7" height="4" fill="#111"/><rect x="18" y="47" width="7" height="4" fill="#111"/></svg>`;
243
+ }
244
+ function chairSVG(e){const c=e?'#0d1117':'#1a2535',s=e?'#080d14':'#0f1a28';return`<svg viewBox="0 0 26 30" width="26" height="30" style="image-rendering:pixelated;display:block"><rect x="3" y="3" width="20" height="13" fill="${c}" rx="1"/><rect x="4" y="4" width="18" height="11" fill="${s}"/><rect x="3" y="16" width="20" height="4" fill="${c}"/><rect x="3" y="0" width="4" height="16" fill="${c}"/><rect x="5" y="20" width="3" height="8" fill="${c}"/><rect x="18" y="20" width="3" height="8" fill="${c}"/><rect x="3" y="27" width="5" height="3" fill="#080d14"/><rect x="18" y="27" width="5" height="3" fill="#080d14"/></svg>`;}
245
+ function coffeeSVG(){return`<svg viewBox="0 0 13 13" width="13" height="13" style="image-rendering:pixelated;display:block"><rect x="2" y="4" width="7" height="7" fill="#5c3a1e"/><rect x="2" y="4" width="7" height="2" fill="#7a4e2a"/><rect x="9" y="6" width="2" height="3" fill="#5c3a1e"/><rect x="1" y="11" width="9" height="2" fill="#4a2e14"/><rect x="4" y="2" width="1" height="2" fill="#555" opacity=".5"/><rect x="6" y="1" width="1" height="3" fill="#555" opacity=".4"/></svg>`;}
246
+ function sRack(){const us=[['lg','lb','1.1s','.7s',true],['la','lb','2.3s','.9s',true],['lg','lo','1.8s',null,false],['lb','lg','.7s','2.1s',true],['la','la','1.4s','1.9s',false],['lg','lb','2.6s','.5s',true]];return us.map(([l1,l2,d1,d2,a])=>`<div class="ru"><div class="rl ${l1}" style="--d:${d1}"></div><div class="rl ${l2}" ${d2?`style="--d:${d2}"`:''} ></div><div class="rb ${a?'on':''}"></div></div>`).join('');}
247
+ function screenContent(s){if(s==='working')return`<div class="tdots"><div class="td"></div><div class="td"></div><div class="td"></div></div>`;if(s==='active')return`<div class="sl" style="width:78%"></div><div class="sl" style="width:52%;opacity:.4"></div><div class="sl" style="width:65%;opacity:.3"></div><div class="scur"></div>`;if(s==='resting')return`<div style="display:flex;align-items:center;justify-content:center;height:100%;font-family:'VT323',monospace;font-size:11px;color:var(--Adim);letter-spacing:1px;opacity:.7">AWAY</div>`;return`<div class="sl" style="width:38%;opacity:.18"></div><div class="sl" style="width:25%;opacity:.12"></div>`;}
248
+
249
+ function buildRoom(agent){
250
+ const st=states[agent.key]||{status:'idle',message:'',model:''};
251
+ const status=st.status,color=COLORS[agent.key]||'#00e5a0',empty=status==='resting';
252
+ const badge={idle:'IDLE',working:'PROCESSING',active:'DONE',resting:'AWAY',visiting:'BRIEFING',error:'ERROR'};
253
+ const bcls={idle:'bi',working:'bw',active:'ba',resting:'br',visiting:'bv',error:'br'};
254
+ const rcls={idle:'idle',working:'working',active:'active',resting:'resting',visiting:'visiting',error:'resting'};
255
+ const mcls=status==='working'?'thinking':status==='active'?'done':status==='resting'?'away':'';
256
+ let resultHTML='';
257
+ if(status==='idle')resultHTML=`<div class="rph">Waiting for mission...</div>`;
258
+ else if(status==='working'||status==='visiting')resultHTML=`<div class="rph" style="color:var(--B)">Processing...</div>`;
259
+ else if(status==='resting')resultHTML=`<div class="rph" style="color:var(--A)">Rate limited — back soon</div>`;
260
+ else{const msg=st.message||'';resultHTML=`<div class="rtxt" id="result-${agent.key}"></div>${st.model?`<div class="rmodel"><div class="sdot dg" style="width:5px;height:5px;margin:0"></div>${st.model}</div>`:''} ${st.doc_file?`<a class="dl-btn" href="/api/docs/${st.doc_file}" download>&#8595; Download .docx</a>`:''}`;}
261
+ return`<div class="room ${rcls[status]||'idle'}" id="room-${agent.key}">
262
+ <div class="rwall"><span class="rname" style="color:${color}">${agent.name}</span><span class="rbadge ${bcls[status]||'bi'}">${badge[status]||'IDLE'}</span></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
  <div class="rscene" id="scene-${agent.key}">
264
+ <div class="srack">${sRack()}</div>
265
+ <div class="deskarea"><div class="monitor ${mcls}"><div class="mscreen">${screenContent(status)}</div><div class="mbase"></div></div><div class="dtop"></div><div class="kbd"><div class="kk"></div><div class="kk"></div><div class="kk"></div><div class="kk"></div></div></div>
266
+ <div class="chair-slot">${chairSVG(empty)}</div>
 
 
 
 
 
 
 
267
  <div class="coffee">${coffeeSVG()}</div>
268
+ ${!empty?`<div class="person-slot">${personSVG(color,status)}</div>`:''}
269
+ ${empty?`<div class="ooo"><div class="ooot">&#9749; AWAY</div><div class="ooos">Rate limited</div></div>`:''}
270
  <div class="bubble" id="bubble-${agent.key}"></div>
271
  </div>
272
  <div class="rresult" id="result-area-${agent.key}">${resultHTML}</div>
273
  </div>`;
274
  }
275
 
276
+ function buildSrvRoom(){
277
+ const racks=Array.from({length:6},()=>`<div class="big-rack">${[['lg','.9s'],['lb','1.3s'],['la','2s'],['lg','1.6s'],['lb','.6s']].map(([c,d])=>`<div class="big-unit"><div class="rl ${c}" style="--d:${d}"></div><div class="rb on"></div></div>`).join('')}</div>`).join('');
278
+ return`<div class="srv-strip"><div class="sroom-racks">${racks}</div><div class="srv-stats"><div class="srv-stat"><div class="sdot dg"></div><span>FastAPI OK</span></div><div class="srv-stat"><div class="sdot dg"></div><span>Gemini</span></div><div class="srv-stat"><div class="sdot dg"></div><span>OpenRouter</span></div><div class="srv-stat"><div class="sdot dg"></div><span>Groq backup</span></div></div></div>`;
279
+ }
280
+
281
+ function renderOffice(orchEvents){
282
+ const grid=document.getElementById('ogrid');
283
+ let html=agentDefs.map(buildRoom).join('')+buildSrvRoom();
284
+ if(orchEvents&&orchEvents.length){const ev=orchEvents.map(e=>`<div class="orch-ev"><span class="orch-t">${e.time}</span><span>${e.msg}</span></div>`).join('');html+=`<div class="orch-log show"><div class="orch-title">ORCHESTRATION LOG</div>${ev}</div>`;}
285
+ grid.innerHTML=html;
286
+ agentDefs.forEach(a=>{const st=states[a.key];if(st&&st.status==='active'&&st.message&&st._tw){const el=document.getElementById(`result-${a.key}`);if(el)typeWriter(el,st.message);}});
287
+ document.getElementById('top-active').textContent=`${agentDefs.filter(a=>(states[a.key]||{}).status==='working').length} active`;
288
+ }
289
+
290
+ function typeWriter(el,text,speed=14){el.textContent='';let i=0;const iv=setInterval(()=>{if(i<text.length)el.textContent+=text[i++];else clearInterval(iv);},speed);}
291
+ function showBubble(key,text,ms=2800){const b=document.getElementById(`bubble-${key}`);if(!b)return;if(bubbleT[key])clearTimeout(bubbleT[key]);b.textContent=text;b.classList.add('show');bubbleT[key]=setTimeout(()=>b.classList.remove('show'),ms);}
292
+ function addAct(msg,color='#4a9eff'){const n=new Date();const t=[n.getHours(),n.getMinutes(),n.getSeconds()].map(x=>String(x).padStart(2,'0')).join(':');activity.unshift({msg,t,color});document.getElementById('act-log').innerHTML=activity.slice(0,30).map(a=>`<div class="act-item"><div class="act-dot" style="background:${a.color}"></div><div><div class="act-msg">${a.msg}</div><div class="act-time">${a.t}</div></div></div>`).join('');}
293
+ function notify(title,msg,color='#00e5a0'){document.getElementById('notif-t').textContent=title;document.getElementById('notif-t').style.color=color;document.getElementById('notif-m').textContent=msg;document.getElementById('notif').style.borderLeftColor=color;const el=document.getElementById('notif');el.classList.add('show');setTimeout(()=>el.classList.remove('show'),5500);}
294
+ function updateHistory(){const list=document.getElementById('hist-log');if(!history.length){list.innerHTML=`<div style="padding:14px;font-size:11px;color:var(--text3);text-align:center">No missions yet</div>`;return;}list.innerHTML=[...history].reverse().map(m=>`<div class="hist-item"><div class="hist-task">${m.task}</div><div class="hist-meta"><span>${m.time}</span><span style="color:${m.ok?'var(--G)':'var(--A)'}">${m.ok?'Done':'Partial'}</span>${m.doc_file?`<span style="color:var(--P)">docx</span>`:''}</div></div>`).join('');}
295
+
296
+ // WALK ANIMATION
297
+ function runWalkAnimation(task,delegates,onDone){
298
+ const overlay=document.getElementById('walk-overlay');
299
+ const canvas=document.getElementById('walk-canvas');
300
+ const logEl=document.getElementById('walk-log');
301
+ const wrap=canvas.parentElement;
302
+ canvas.width=wrap.clientWidth||640;
303
+ canvas.height=wrap.clientHeight||380;
304
+ const W=canvas.width,H=canvas.height,ctx=canvas.getContext('2d');
305
+ overlay.classList.add('show');
306
+ const agentColors={manager:'#00e5a0',developer:'#4a9eff',analyst:'#ffcc44',writer:'#cc88ff',image_agent:'#ff6688'};
307
+ const rooms=agentDefs.filter(a=>a.key!=='manager').slice(0,5);
308
+ const rW=Math.min(120,Math.floor((W-60)/Math.max(rooms.length,1)));
309
+ const rH=130,rY=H/2-rH/2;
310
+ const rPos=rooms.map((r,i)=>({x:30+i*(rW+14),y:rY,w:rW,h:rH,key:r.key,name:r.name,color:agentColors[r.key]||'#4a9eff'}));
311
+ const delSet=new Set(delegates);
312
+ const manColor='#00e5a0';
313
+ let mx=12,my=H/2+8,frame=0,stepI=0,phase='walking',talkF=0;
314
+ const TALK=85,SPD=2.8;
315
+ const msgs={writer:`"Write full report:\n${task.substring(0,26)}..."`,analyst:`"Review the\ndocument."`,developer:`"Build solution:\n${task.substring(0,24)}..."`,image_agent:`"Find images for:\n${task.substring(0,24)}..."`};
316
+ const steps=[{key:'__start',x:W*.07,y:H/2,msg:'Planning...'},...rPos.filter(r=>delSet.has(r.key)).map(r=>({key:r.key,x:r.x+r.w/2-8,y:r.y+r.h-26,msg:msgs[r.key]||`"Delegating task..."`})),{key:'__return',x:W*.07,y:H/2,msg:'Briefing complete!'}];
317
+ const visited=new Set();
318
+ let curMsg='';
319
+
320
+ function drawBg(){ctx.fillStyle='#080f1a';ctx.fillRect(0,0,W,H);ctx.strokeStyle='rgba(255,255,255,.025)';ctx.lineWidth=1;for(let x=0;x<W;x+=32){ctx.beginPath();ctx.moveTo(x,0);ctx.lineTo(x,H);ctx.stroke();}for(let y=0;y<H;y+=32){ctx.beginPath();ctx.moveTo(0,y);ctx.lineTo(W,y);ctx.stroke();}}
321
+
322
+ function drawRoom(r,vis){
323
+ ctx.fillStyle=vis?'#0d1a2a':'#090f1a';ctx.strokeStyle=vis?r.color:'#1e2d42';ctx.lineWidth=vis?2:1;
324
+ ctx.fillRect(r.x,r.y,r.w,r.h);ctx.strokeRect(r.x,r.y,r.w,r.h);
325
+ ctx.fillStyle=vis?r.color+'28':'#0d1828';ctx.fillRect(r.x,r.y,r.w,20);
326
+ ctx.fillStyle=vis?r.color:'#3a5a7a';ctx.font=`9px "Share Tech Mono",monospace`;ctx.textAlign='center';
327
+ ctx.fillText(r.name.toUpperCase(),r.x+r.w/2,r.y+14);
328
+ ctx.fillStyle='#7a5c0a';ctx.fillRect(r.x+8,r.y+r.h-36,r.w-16,5);
329
+ ctx.fillStyle='#030608';ctx.strokeStyle='#1a2535';ctx.lineWidth=1;
330
+ ctx.fillRect(r.x+14,r.y+r.h-58,r.w-28,22);ctx.strokeRect(r.x+14,r.y+r.h-58,r.w-28,22);
331
+ if(vis){ctx.fillStyle=r.color;ctx.globalAlpha=.5+.4*Math.sin(frame*.12);ctx.fillRect(r.x+17,r.y+r.h-54,(r.w-34)*.8,3);ctx.globalAlpha=.3;ctx.fillRect(r.x+17,r.y+r.h-49,(r.w-34)*.55,3);ctx.globalAlpha=1;}
332
+ if(!delSet.has(r.key))return;
333
+ const px=r.x+r.w/2-5,py=r.y+r.h-26;
334
+ ctx.fillStyle='#f0c8a0';ctx.fillRect(px+2,py,7,7);
335
+ ctx.fillStyle=r.color;ctx.fillRect(px,py+7,11,7);
336
+ ctx.fillStyle='#1e2535';ctx.fillRect(px+2,py+14,4,6);ctx.fillRect(px+6,py+14,4,6);
337
+ // LEDs
338
+ ['#00e5a0','#4a9eff','#ffcc44'].forEach((c,i)=>{ctx.beginPath();ctx.arc(r.x+r.w-14+i*6,r.y+r.h-8,2,0,Math.PI*2);ctx.fillStyle=c;ctx.globalAlpha=.3+.7*((Math.sin(frame*.09+i*1.4)+1)/2);ctx.fill();});ctx.globalAlpha=1;
339
  }
340
 
341
+ function drawManager(x,y,walking){
342
+ const bob=walking?Math.sin(frame*.45)*2:0;
343
+ ctx.fillStyle='#1a1a2e';ctx.fillRect(x+4,y,8,3);
344
+ ctx.fillStyle='#f0c8a0';ctx.fillRect(x+3,y+3,10,8);
345
+ ctx.fillStyle=manColor+'bb';ctx.fillRect(x,y+3,2,4);ctx.fillRect(x+13,y+3,2,4);
346
+ ctx.fillStyle=manColor;ctx.fillRect(x+2,y+11,12,9);
347
+ ctx.fillStyle='#1e2535';
348
+ ctx.fillRect(x+3,y+20,5,6+bob);ctx.fillRect(x+8,y+20,5,6-bob);
349
+ ctx.fillStyle='#111';ctx.fillRect(x+2,y+26+bob,5,3);ctx.fillRect(x+9,y+26-bob,5,3);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
350
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
 
352
+ function drawBubble(x,y,lines){
353
+ const mxW=Math.max(...lines.map(l=>l.length))*6.2+16,bh=lines.length*13+14;
354
+ const bx=Math.min(Math.max(x+14,4),W-mxW-4),by=Math.max(y-bh-8,4);
355
+ ctx.fillStyle='#0d1828';ctx.strokeStyle='#2a3d58';ctx.lineWidth=1;
356
+ ctx.fillRect(bx,by,mxW,bh);ctx.strokeRect(bx,by,mxW,bh);
357
+ ctx.fillStyle='#c8d8f0';ctx.font='10px "Share Tech Mono",monospace';ctx.textAlign='left';
358
+ lines.forEach((l,i)=>ctx.fillText(l,bx+8,by+12+i*13+2));
 
 
 
 
 
359
  }
 
 
 
 
 
 
360
 
361
+ function tick(){
362
+ drawBg();rPos.forEach(r=>drawRoom(r,visited.has(r.key)));
363
+ if(stepI<steps.length){
364
+ const step=steps[stepI];
365
+ const dx=step.x-mx,dy=step.y-my,dist=Math.sqrt(dx*dx+dy*dy);
366
+ if(dist>3&&phase==='walking'){mx+=dx/dist*SPD;my+=dy/dist*SPD;}
367
+ else if(phase==='walking'){
368
+ phase='talking';talkF=0;curMsg=step.msg;
369
+ if(step.key!=='__start'&&step.key!=='__return'){visited.add(step.key);logEl.textContent=`Manager briefing ${step.key}...`;addAct(`Manager briefing ${step.key}`,'#4a9eff');}
370
+ else if(step.key==='__return')logEl.textContent='Team briefed! Processing...';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
  }
372
+ if(phase==='talking'){talkF++;if(talkF>TALK){phase='walking';stepI++;curMsg='';}}
373
+ } else {setTimeout(()=>{overlay.classList.remove('show');onDone();},300);return;}
374
+ drawManager(Math.round(mx),Math.round(my),phase==='walking');
375
+ if(phase==='talking'&&curMsg)drawBubble(Math.round(mx),Math.round(my),curMsg.split('\n'));
376
+ frame++;requestAnimationFrame(tick);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
377
  }
378
+ tick();
379
+ }
380
+
381
+ function openModal(){document.getElementById('modal-bg').classList.add('show');}
382
+ function closeModal(){document.getElementById('modal-bg').classList.remove('show');}
383
+ async function submitAgent(){
384
+ const key=document.getElementById('m-key').value.trim();
385
+ const name=document.getElementById('m-name').value.trim()||key;
386
+ const role=document.getElementById('m-role').value.trim()||'General purpose agent.';
387
+ const prov=document.getElementById('m-prov').value;
388
+ if(!key){alert('Key required');return;}
389
+ const r=await fetch('/api/agents/add',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({key,name,role,provider:prov,models:PROV_MODELS[prov]})});
390
+ const d=await r.json();
391
+ if(d.success){agentDefs.push({key:d.agent.key,name:d.agent.name,role:d.agent.role});states[d.agent.key]={status:'idle',message:'',model:''};renderOffice();addAct(`Agent "${name}" added`,'#cc88ff');closeModal();['m-key','m-name','m-role'].forEach(id=>document.getElementById(id).value='');}
392
+ }
393
+
394
+ async function launchMission(){
395
+ const task=document.getElementById('task-input').value.trim();
396
+ if(!task)return;
397
+ const btn=document.getElementById('launch-btn');
398
+ btn.disabled=true;
399
+ document.getElementById('prog').classList.add('on');
400
+ document.getElementById('top-status').textContent='Running';
401
+ document.getElementById('top-dot').className='sdot da';
402
+ document.getElementById('floor-sub').textContent=`MISSION: "${task.substring(0,40).toUpperCase()}${task.length>40?'...':''}"`;
403
+ agentDefs.forEach(a=>{states[a.key]={status:'working',message:'',model:''};});
404
+ renderOffice();
405
+ agentDefs.forEach((a,i)=>{setTimeout(()=>{const ph=SPEECH[a.key]||['Working...'];showBubble(a.key,ph[Math.floor(Math.random()*ph.length)],2800);},i*500);});
406
+ addAct(`Mission: "${task.substring(0,40)}"`,'#4a9eff');
407
+ let apiData=null;
408
+ const fetchP=fetch('/api/mission',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({task})}).then(r=>r.json()).then(d=>{apiData=d;}).catch(e=>{apiData={error:e.message};});
409
+ const lower=task.toLowerCase();
410
+ let qd=[];
411
+ if(/informe|report|documento|word|escrib|redact/.test(lower))qd.push('writer');
412
+ if(/imagen|image|foto|visual|picture/.test(lower))qd.push('image_agent');
413
+ if(/analiz|review|evalua|revisar/.test(lower))qd.push('analyst');
414
+ if(!qd.length)qd=['developer','analyst'];
415
+ runWalkAnimation(task,qd,async()=>{
416
+ await fetchP;
417
+ if(!apiData||apiData.error){agentDefs.forEach(a=>{states[a.key]={status:'resting',message:apiData?.error||'Error',model:''};});renderOffice();addAct(`Error: ${apiData?.error}`,'#ff4455');notify('Mission Failed',apiData?.error||'Error','#ff4455');document.getElementById('top-dot').className='sdot dr';document.getElementById('top-status').textContent='Error';btn.disabled=false;document.getElementById('prog').classList.remove('on');return;}
418
+ let anyResting=false,hasDoc=false;
419
+ agentDefs.forEach(a=>{const r=apiData.results[a.key];if(!r){states[a.key]={status:'idle',message:'',model:''};return;}if(r.status==='resting')anyResting=true;const docFile=(a.key==='manager'&&apiData.doc_file)?apiData.doc_file:null;if(docFile)hasDoc=true;states[a.key]={status:r.status==='resting'?'resting':r.status==='idle'?'idle':'active',message:r.message||'',model:r.model||'',doc_file:docFile,_tw:true};});
420
+ renderOffice(apiData.events);
421
+ agentDefs.forEach((a,i)=>{if((states[a.key]||{}).status==='active'){setTimeout(()=>{showBubble(a.key,'Done ✓',2000);addAct(`${a.name}: complete`,'#00e5a0');},i*400);}});
422
+ if(hasDoc)addAct('Word document ready','#cc88ff');
423
+ mCount++;document.getElementById('mc-count').textContent=`MISSIONS: ${mCount}`;
424
+ history.push({task,time:new Date().toLocaleTimeString(),ok:!anyResting,doc_file:apiData.doc_file});updateHistory();
425
+ setTimeout(()=>notify(anyResting?'Mission Partial':hasDoc?'Document Ready':'Mission Complete',anyResting?'Some agents rate-limited.':hasDoc?'Download .docx below!':'All agents responded.',anyResting?'#ffcc44':hasDoc?'#cc88ff':'#00e5a0'),agentDefs.length*400+300);
426
+ document.getElementById('top-status').textContent=anyResting?'Partial':'Done';
427
+ document.getElementById('top-dot').className=`sdot ${anyResting?'da':'dg'}`;
428
+ document.getElementById('floor-sub').textContent=anyResting?'PARTIAL RESULTS':'MISSION COMPLETE';
429
+ btn.disabled=false;document.getElementById('prog').classList.remove('on');
430
+ });
431
  }
432
+ document.getElementById('task-input').addEventListener('keydown',e=>{if(e.key==='Enter')launchMission();});
433
+ loadAgents();addAct('System online','#00e5a0');addAct('Server rack nominal','#4a9eff');
 
 
 
 
 
 
 
434
  </script>
435
  </body>
436
  </html>