Spaces:
Paused
Paused
Upload 6 files
Browse files- main.py +22 -8
- main_backup.py +591 -0
- 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"
|
| 479 |
-
"
|
| 480 |
-
"
|
| 481 |
-
"
|
| 482 |
-
"
|
| 483 |
-
"
|
| 484 |
-
"
|
| 485 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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=
|
| 9 |
-
|
| 10 |
-
:
|
| 11 |
-
--
|
| 12 |
-
--
|
| 13 |
-
--
|
| 14 |
-
--
|
| 15 |
-
--
|
| 16 |
-
--
|
| 17 |
-
--
|
| 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 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 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 |
-
.
|
| 53 |
-
.
|
| 54 |
-
.
|
| 55 |
-
.
|
| 56 |
-
@keyframes
|
| 57 |
-
@keyframes
|
| 58 |
-
.clock{font-family:'VT323',monospace;font-size:
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
.
|
| 62 |
-
|
| 63 |
-
.
|
| 64 |
-
|
| 65 |
-
|
| 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 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
}
|
| 90 |
-
.
|
| 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 |
-
|
| 108 |
-
.
|
| 109 |
-
|
| 110 |
-
|
| 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 |
-
|
| 134 |
-
.room{
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 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 |
-
|
| 155 |
-
|
| 156 |
-
}
|
| 157 |
-
.
|
| 158 |
-
.
|
| 159 |
-
.
|
| 160 |
-
.
|
| 161 |
-
.
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
.
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 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 |
-
|
| 202 |
-
.
|
| 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 |
-
|
| 217 |
-
|
| 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 |
-
.
|
| 231 |
-
.
|
| 232 |
-
@keyframes
|
| 233 |
-
|
| 234 |
-
.
|
| 235 |
-
.
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
}
|
| 239 |
-
.
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
.
|
| 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 |
-
|
| 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:
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
.
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 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:#
|
| 324 |
-
.big-unit{height:
|
| 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 |
-
|
| 329 |
-
.
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
}
|
| 333 |
.modal-bg.show{display:flex}
|
| 334 |
-
.modal{
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
}
|
| 339 |
-
.
|
| 340 |
-
.field{
|
| 341 |
-
.field
|
| 342 |
-
.
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
}
|
| 348 |
-
.
|
| 349 |
-
.
|
| 350 |
-
.
|
| 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-
|
| 366 |
.act-scroll{flex:1;overflow-y:auto;max-height:260px}
|
| 367 |
-
.act-item{padding:7px 14px;border-bottom:1px solid rgba(
|
| 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(
|
| 373 |
-
.hist-item:hover{background:var(--
|
| 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 |
-
|
| 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
|
| 398 |
-
<
|
| 399 |
-
<span class="tb-
|
| 400 |
</div>
|
| 401 |
<div class="tb-right">
|
| 402 |
-
<div class="tb-stat"><div class="sdot
|
| 403 |
-
<div class="tb-stat"><div class="sdot
|
| 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="
|
| 412 |
<button class="launch" id="launch-btn" onclick="launchMission()">► LAUNCH</button>
|
| 413 |
-
<button class="add-
|
| 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-
|
| 420 |
-
<span>■
|
| 421 |
-
<span
|
| 422 |
</div>
|
| 423 |
<div class="ogrid" id="ogrid"></div>
|
| 424 |
</div>
|
| 425 |
-
|
| 426 |
<aside class="sidebar">
|
| 427 |
-
<div class="sb-sec">
|
| 428 |
-
|
| 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 |
-
<
|
|
|
|
|
|
|
|
|
|
| 441 |
<div class="modal-bg" id="modal-bg" onclick="if(event.target===this)closeModal()">
|
| 442 |
<div class="modal">
|
| 443 |
-
<div class="modal-
|
| 444 |
-
<div class="field">
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
</div>
|
| 448 |
-
<div class="
|
| 449 |
-
<
|
| 450 |
-
<
|
| 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
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
const
|
| 485 |
-
|
| 486 |
-
|
| 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 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
<
|
| 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">${
|
| 640 |
-
<div class="deskarea">
|
| 641 |
-
|
| 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
|
| 651 |
-
${empty
|
| 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
|
| 659 |
-
const racks
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
}
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 690 |
}
|
| 691 |
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 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 |
-
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
| 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 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
|
| 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
|
| 840 |
-
|
| 841 |
-
|
| 842 |
-
|
| 843 |
-
|
| 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 |
-
|
| 897 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 898 |
}
|
| 899 |
-
|
| 900 |
-
|
| 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()">► 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">■ 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>↓ 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">☕ 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>
|