mission-control-v2 / templates /index_old.html
vfven's picture
Rename templates/index.html to templates/index_old.html
5797a4f verified
Raw
History Blame
60 kB
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Mission Control AI</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=IBM+Plex+Sans:wght@400;500;600&family=VT323&display=swap" rel="stylesheet">
<style>
:root{
--wall:#ede8dc;--wall2:#e0dace;--floor:#cdc8b2;
--desk:#c8a86a;--desk-hi:#e0bf82;--desk-sh:#a88848;
--skin:#f5c9a0;
--green:#16a34a;--green-l:#22c55e;--green-bg:#f0fdf4;
--amber:#d97706;--red:#dc2626;--blue:#2563eb;--purple:#7c3aed;--cyan:#0891b2;
--text:#111827;--text2:#374151;--text3:#9ca3af;
--panel:#111827;--panel2:#1f2937;--border-dark:#374151;
--white:#ffffff;--gray-50:#f9fafb;--gray-100:#f3f4f6;--gray-200:#e5e7eb;
--radius:6px;--shadow:0 4px 20px rgba(0,0,0,.12);--shadow-lg:0 10px 40px rgba(0,0,0,.18);
}
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%;overflow:hidden}
body{font-family:'IBM Plex Sans',system-ui,sans-serif;background:var(--floor);color:var(--text);display:flex;flex-direction:column;font-size:13px;height:100vh;overflow:hidden}
/* ── TOPBAR ── */
.topbar{height:44px;background:var(--panel);border-bottom:1px solid var(--border-dark);display:flex;align-items:center;justify-content:space-between;padding:0 16px;flex-shrink:0;z-index:100}
.tb-logo{font-family:'VT323',monospace;font-size:21px;color:var(--green-l);letter-spacing:3px}
.tb-ver{font-size:10px;color:#4b5563;font-family:'DM Mono',monospace}
.tb-right{display:flex;align-items:center;gap:14px}
.tb-stat{font-size:11px;color:#6b7280;display:flex;align-items:center;gap:5px;font-family:'DM Mono',monospace}
.sdot{width:6px;height:6px;border-radius:50%}
.dg{background:var(--green-l);animation:pulse-g 2s ease infinite}
.da{background:#f59e0b;animation:pulse-g 1.2s ease infinite}
.dr{background:var(--red);animation:blink .5s step-end infinite}
.dd{background:#4b5563}
@keyframes pulse-g{0%,100%{box-shadow:0 0 0 0 currentColor}50%{box-shadow:0 0 0 3px transparent}}
@keyframes blink{0%,100%{opacity:1}50%{opacity:0}}
.clock{font-family:'DM Mono',monospace;font-size:14px;color:var(--green-l);letter-spacing:.5px}
/* ── PROGRESS ── */
.prog{height:2px;background:var(--panel);overflow:hidden;display:none;flex-shrink:0}
.prog.on{display:block}
.prog::after{content:'';display:block;height:100%;width:28%;background:var(--green-l);animation:slide 1.4s ease-in-out infinite}
@keyframes slide{0%{margin-left:-28%}100%{margin-left:110%}}
/* ── MISSION BAR ── */
.mbar{background:var(--panel2);border-bottom:1px solid var(--border-dark);padding:7px 16px;display:flex;gap:8px;flex-shrink:0;flex-wrap:wrap;align-items:center}
.minput{flex:1;min-width:200px;height:32px;background:#0f172a;border:1px solid var(--border-dark);color:#e2e8f0;font-family:'DM Mono',monospace;font-size:11px;padding:0 12px;outline:none;border-radius:var(--radius);transition:border-color .15s}
.minput:focus{border-color:var(--green-l)}
.minput::placeholder{color:#4b5563}
.btn-launch{height:32px;padding:0 16px;background:var(--green-l);border:none;color:#052e16;font-family:'IBM Plex Sans',sans-serif;font-size:12px;font-weight:600;letter-spacing:.5px;cursor:pointer;border-radius:var(--radius);transition:background .15s;white-space:nowrap}
.btn-launch:hover{background:#4ade80}
.btn-launch:disabled{background:#166534;color:#15803d;cursor:not-allowed}
.btn-sec{height:32px;padding:0 12px;background:transparent;border:1px solid var(--border-dark);color:#9ca3af;font-family:'IBM Plex Sans',sans-serif;font-size:11px;cursor:pointer;border-radius:var(--radius);transition:all .15s;white-space:nowrap}
.btn-sec:hover{border-color:var(--blue);color:#93c5fd}
.mc-lbl{font-family:'DM Mono',monospace;font-size:11px;color:#4b5563;white-space:nowrap;align-self:center}
/* ── MAIN GRID ── */
.main{display:grid;grid-template-columns:1fr 420px;overflow:hidden;flex:1;min-height:0;height:0}
@media(max-width:900px){.main{grid-template-columns:1fr}.right-col{display:none}}
/* ── OFFICE ── */
.office{position:relative;overflow:hidden;background:var(--floor);min-height:0;height:100%}
/* Wall */
.wall{position:absolute;top:0;left:0;right:0;height:40%;background:linear-gradient(180deg,var(--wall) 0%,var(--wall2) 100%)}
.wall::after{content:'';position:absolute;bottom:0;left:0;right:0;height:6px;background:#c4b898;box-shadow:0 2px 4px rgba(0,0,0,.08)}
/* Floor */
.floor-area{position:absolute;top:40%;bottom:0;left:0;right:0;background:var(--floor)}
.floor-area::before{content:'';position:absolute;inset:0;
background-image:linear-gradient(90deg,rgba(0,0,0,.03) 1px,transparent 1px),linear-gradient(rgba(0,0,0,.03) 1px,transparent 1px);
background-size:48px 32px}
/* Windows β€” inset on wall, away from corners */
.window{position:absolute;top:4%;width:10%;height:28%;background:linear-gradient(180deg,#bfdbfe 0%,#93c5fd 50%,#dbeafe 100%);border:3px solid #d1c4a8;border-radius:2px;box-shadow:inset 0 0 16px rgba(191,219,254,.4)}
.window::before{content:'';position:absolute;top:0;left:50%;transform:translateX(-50%);width:2px;height:100%;background:rgba(209,196,168,.7)}
.window::after{content:'';position:absolute;left:0;top:50%;transform:translateY(-50%);height:2px;width:100%;background:rgba(209,196,168,.7)}
.window.wl{left:20%}
.window.wr{right:20%}
/* Big screen β€” center of wall */
.bigscreen{position:absolute;left:50%;top:2%;transform:translateX(-50%);width:min(34%,300px);aspect-ratio:16/9;background:#1e293b;border:5px solid #94a3b8;border-radius:4px;box-shadow:var(--shadow-lg);overflow:hidden;z-index:5}
.bsi{width:100%;height:100%;background:#0f2418;position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;padding:6px}
.bsi::before{content:'';position:absolute;left:0;right:0;height:1px;background:rgba(34,197,94,.25);animation:scan 3s linear infinite;top:0}
@keyframes scan{0%{top:0}100%{top:100%}}
.bs-bracket{position:absolute;width:7px;height:7px;border-color:var(--green-l);border-style:solid}
.bs-bracket.tl{top:3px;left:3px;border-width:1.5px 0 0 1.5px}
.bs-bracket.tr{top:3px;right:3px;border-width:1.5px 1.5px 0 0}
.bs-bracket.bl{bottom:3px;left:3px;border-width:0 0 1.5px 1.5px}
.bs-bracket.br{bottom:3px;right:3px;border-width:0 1.5px 1.5px 0}
.bs-title{font-family:'DM Mono',monospace;font-size:9px;color:var(--green-l);letter-spacing:3px;text-align:center;text-transform:uppercase}
.bs-text{font-family:'DM Mono',monospace;font-size:8px;color:#86efac;text-align:center;line-height:1.5;padding:0 4px;max-height:60%;overflow:hidden}
.bs-stand{position:absolute;left:50%;transform:translateX(-50%);width:52px;height:7px;background:#94a3b8;border-radius:0 0 3px 3px;z-index:4}
.bs-base{position:absolute;left:50%;transform:translateX(-50%);width:88px;height:4px;background:#7c8fa0;border-radius:3px;z-index:4}
/* Bookshelf β€” below left window on wall */
.bookshelf{position:absolute;left:3%;top:33%;width:38px;background:#b8905a;border:1.5px solid #9a7840;border-radius:2px;padding:3px;display:flex;flex-direction:column;gap:2px;z-index:6;box-shadow:var(--shadow)}
.brow{display:flex;gap:1px}
.bk{width:5px;height:17px;border-radius:1px 2px 2px 1px}
/* Server rack β€” below right window on wall */
.server-rack{position:absolute;right:3%;top:33%;width:44px;background:#0f172a;border:1px solid #1e3a5f;border-radius:2px;padding:4px 3px;display:flex;flex-direction:column;gap:2px;z-index:6;box-shadow:var(--shadow)}
.rack-u{height:7px;background:#0a1628;border:1px solid #1a3050;display:flex;align-items:center;gap:2px;padding:0 2px}
.rack-led{width:3.5px;height:3.5px;border-radius:50%}
.rack-bar{flex:1;height:1.5px;background:#1a3050}
.rack-lbl{font-family:'DM Mono',monospace;font-size:7px;color:#1e3a5f;text-align:center;letter-spacing:1px;margin-top:2px}
/* Plant */
.plant{position:absolute;z-index:6}
.plant-pot{width:20px;height:14px;background:#92400e;border-radius:0 0 3px 3px;margin:0 auto}
.plant-soil{width:24px;height:4px;background:#451a03;border-radius:2px;margin:0 auto}
.plant-leaves{display:flex;justify-content:center;gap:2px;margin-bottom:1px}
.leaf{width:11px;height:16px;background:#15803d;border-radius:50% 50% 40% 40%}
.leaf:first-child{transform:rotate(-24deg);background:#166534}
.leaf:last-child{transform:rotate(24deg);background:#14532d}
/* ── DESK GRID β€” 3 top + 3 bottom ── */
.desk-grid{position:absolute;left:0;right:0;top:0;bottom:0;pointer-events:none}
.desk{position:absolute;width:110px;height:62px;background:var(--desk);border-radius:5px;pointer-events:all;
box-shadow:0 8px 20px rgba(0,0,0,.14),inset 0 2px 6px rgba(255,255,255,.18)}
.desk::before{content:'';position:absolute;top:0;left:0;right:0;height:2.5px;background:rgba(255,255,255,.22);border-radius:5px 5px 0 0}
.d-monitor{position:absolute;top:5px;left:7px;width:74px;height:43px;background:#1e293b;border:1.5px solid #0f172a;border-radius:2px;overflow:hidden}
.d-screen{width:100%;height:100%;background:#071014;display:flex;align-items:center;justify-content:center;font-family:'DM Mono',monospace;font-size:7px;letter-spacing:.5px}
.d-mug{position:absolute;top:7px;right:6px;width:14px;height:16px;border-radius:2px 2px 4px 4px;box-shadow:inset 0 -3px 5px rgba(0,0,0,.2)}
.d-kbd{position:absolute;bottom:5px;left:10px;width:58px;height:6px;background:#e2d9c8;border-radius:1.5px}
.d-paper{position:absolute;bottom:14px;right:7px;width:20px;height:15px;background:#fafaf5;border:1px solid #e5e7eb;transform:rotate(-2deg)}
/* Agent character */
.agent{position:absolute;width:32px;height:72px;z-index:20;cursor:pointer;transform:translateX(-50%);transition:left .9s cubic-bezier(.4,0,.2,1),bottom .9s cubic-bezier(.4,0,.2,1)}
.a-hair{width:22px;height:13px;position:absolute;top:-3px;left:5px;border-radius:50% 50% 20% 20%}
.a-head{width:20px;height:20px;background:var(--skin);border-radius:50%;position:absolute;top:0;left:6px;box-shadow:0 2px 5px rgba(0,0,0,.1)}
.a-eyes{position:absolute;top:7px;left:9px;display:flex;gap:5px}
.a-eye{width:2.5px;height:2.5px;background:#1f2937;border-radius:50%}
.a-body{width:24px;height:30px;position:absolute;top:18px;left:4px;border-radius:6px 6px 4px 4px;box-shadow:0 2px 8px rgba(0,0,0,.1)}
.a-badge{position:absolute;top:22px;left:50%;transform:translateX(-50%);width:8px;height:2px;border-radius:1px;opacity:.55}
.a-arms{position:absolute;top:21px;left:0;right:0}
.a-arm{position:absolute;width:6px;height:18px;border-radius:3px;top:0}
.a-arm.al{left:0;transform-origin:top center}.a-arm.ar{right:0;transform-origin:top center}
.a-legs{position:absolute;top:47px;left:4px;right:4px;height:16px;display:flex;gap:2px}
.a-leg{flex:1;background:#374151;border-radius:2px 2px 3px 3px}
.a-feet{position:absolute;top:62px;left:2px;right:2px;display:flex;gap:2px}
.a-foot{flex:1;height:4px;background:#111827;border-radius:0 0 2px 2px}
.a-tag{position:absolute;bottom:-15px;left:50%;transform:translateX(-50%);white-space:nowrap;font-family:'DM Mono',monospace;font-size:8px;padding:1px 5px;border-radius:3px;letter-spacing:.3px}
.bubble{position:absolute;bottom:80px;left:50%;transform:translateX(-50%) scale(.9);background:var(--white);color:var(--text);padding:5px 9px;border-radius:8px;font-size:10px;white-space:nowrap;box-shadow:var(--shadow);border:1px solid var(--gray-200);opacity:0;transition:all .3s;z-index:30;pointer-events:none;max-width:140px;white-space:normal;text-align:center;line-height:1.3;font-family:'IBM Plex Sans',sans-serif}
.bubble::after{content:'';position:absolute;top:100%;left:50%;transform:translateX(-50%);border:5px solid transparent;border-top-color:var(--white)}
.bubble.show{opacity:1;transform:translateX(-50%) scale(1)}
/* Animations */
@keyframes bob{0%,100%{transform:translateX(-50%) translateY(0)}50%{transform:translateX(-50%) translateY(-3px)}}
@keyframes swL{0%,100%{transform:rotate(-13deg)}50%{transform:rotate(13deg)}}
@keyframes swR{0%,100%{transform:rotate(13deg)}50%{transform:rotate(-13deg)}}
@keyframes type{0%,100%{transform:rotate(-7deg)}50%{transform:rotate(7deg)}}
@keyframes breathe{0%,100%{transform:scaleY(1)}50%{transform:scaleY(1.015)}}
.agent.walking{animation:bob .5s ease-in-out infinite}
.agent.walking .al{animation:swL .5s ease-in-out infinite}
.agent.walking .ar{animation:swR .5s ease-in-out infinite}
.agent.working .al{animation:type .28s ease-in-out infinite}
.agent.working .ar{animation:type .28s ease-in-out infinite reverse}
.agent.idle .a-body{animation:breathe 3s ease-in-out infinite}
/* ── RIGHT COLUMN ── */
.right-col{background:var(--white);border-left:1px solid var(--gray-200);display:flex;flex-direction:column;overflow:hidden;min-height:0;height:100%}
/* Tabs */
.tabs{display:flex;border-bottom:1px solid var(--gray-200);flex-shrink:0;background:var(--gray-50)}
.tab{flex:1;padding:8px 4px;font-size:11px;font-weight:500;text-align:center;cursor:pointer;border-bottom:2px solid transparent;color:var(--text3);transition:all .15s;letter-spacing:.3px;font-family:'IBM Plex Sans',sans-serif}
.tab.on{color:var(--text);border-bottom-color:var(--green-l);background:var(--white)}
.tab:hover:not(.on){color:var(--text2);background:var(--gray-100)}
.pane{flex:1;overflow:hidden;display:none;min-height:0;flex-direction:column}
.pane.on{display:flex;flex-direction:column;flex:1;min-height:0;overflow:hidden}
/* Results */
.results-wrap{padding:10px}
.r-empty{font-size:11px;color:var(--text3);text-align:center;padding:24px 12px;font-style:italic}
.r-card{border:1px solid var(--gray-200);border-radius:var(--radius);padding:10px 12px;margin-bottom:8px;border-left:3px solid}
.r-name{font-size:12px;font-weight:600;margin-bottom:5px;display:flex;align-items:center;gap:6px;font-family:'IBM Plex Sans',sans-serif}
.r-dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}
.r-text{font-size:11px;color:var(--text2);line-height:1.6;word-break:break-word;user-select:text;cursor:text;font-family:'IBM Plex Sans',sans-serif}
.r-model{font-size:9px;color:var(--text3);margin-top:4px;font-family:'DM Mono',monospace}
.dl-btn{display:inline-flex;align-items:center;gap:4px;margin-top:5px;padding:4px 10px;background:var(--green-bg);border:1px solid var(--green-l);color:var(--green);font-size:10px;cursor:pointer;text-decoration:none;border-radius:4px;font-family:'IBM Plex Sans',sans-serif;font-weight:500;transition:background .15s}
.dl-btn:hover{background:#dcfce7}
.img-row{display:flex;gap:5px;flex-wrap:wrap;margin-top:6px}
.img-th{width:90px;height:66px;object-fit:cover;border:1px solid var(--gray-200);border-radius:6px;cursor:pointer;transition:all .15s;box-shadow:var(--shadow)}
.img-th:hover{border-color:var(--green-l);transform:scale(1.03)}
/* Activity */
.act-wrap{padding:0 10px 10px}
.sect-title{font-size:10px;font-weight:600;color:var(--text3);letter-spacing:.8px;text-transform:uppercase;padding:8px 0 5px;font-family:'IBM Plex Sans',sans-serif}
.act-item{padding:4px 0;border-bottom:1px solid var(--gray-100);display:flex;gap:6px;align-items:flex-start}
.act-dot{width:4px;height:4px;border-radius:50%;margin-top:4.5px;flex-shrink:0}
.act-msg{font-size:10px;color:var(--text);line-height:1.4;font-family:'IBM Plex Sans',sans-serif}
.act-time{font-size:9px;color:var(--text3);margin-top:1px;font-family:'DM Mono',monospace}
/* ── CHAT ── */
.chat-col{display:flex;flex-direction:column;flex:1;min-height:0;overflow:hidden}
.chat-chips{padding:8px 10px;border-bottom:1px solid var(--gray-100);display:flex;gap:5px;flex-wrap:wrap;flex-shrink:0;background:var(--gray-50)}
.chip{font-size:10px;font-weight:500;padding:3px 9px;border:1px solid;border-radius:20px;cursor:pointer;transition:all .15s;font-family:'IBM Plex Sans',sans-serif;white-space:nowrap}
.chip.on{color:var(--white)}
.chat-hdr{padding:5px 12px;border-bottom:1px solid var(--gray-100);display:flex;justify-content:space-between;align-items:center;min-height:26px;flex-shrink:0;background:var(--white)}
.chat-hdr-info{font-size:11px;color:var(--text3);font-family:'DM Mono',monospace}
.chat-hdr-clear{font-size:10px;color:var(--text3);cursor:pointer;padding:2px 7px;border:1px solid var(--gray-200);border-radius:4px;transition:all .15s;font-family:'IBM Plex Sans',sans-serif}
.chat-hdr-clear:hover{color:var(--red);border-color:#fca5a5}
.chat-msgs{flex:1;overflow-y:auto;padding:10px 12px;display:flex;flex-direction:column;gap:8px;min-height:0;background:var(--gray-50)}
.msg{padding:9px 11px;border-radius:8px;font-size:11px;line-height:1.6;word-break:break-word;font-family:'IBM Plex Sans',sans-serif;box-sizing:border-box}
.msg.user{background:var(--gray-100);border:1px solid var(--gray-200);align-self:flex-end;color:var(--text);max-width:88%}
.msg.agent{background:var(--white);border:1px solid var(--gray-200);color:var(--text);box-shadow:var(--shadow);align-self:stretch}
.msg.user .msg-who{font-size:9px;color:var(--text3);text-align:right;margin-bottom:2px;font-family:'DM Mono',monospace}
.msg.agent .msg-who{font-size:9px;margin-bottom:3px;font-family:'DM Mono',monospace;font-weight:500}
.msg-text{user-select:text;cursor:text;word-break:break-word;overflow-wrap:anywhere;white-space:normal}
.msg-model{font-size:8px;color:var(--text3);margin-top:4px;font-family:'DM Mono',monospace}
.msg-imgs{display:flex;gap:6px;flex-wrap:wrap;margin-top:8px;padding:6px;background:var(--gray-50);border-radius:6px;border:1px solid var(--gray-200)}
.chat-typing{padding:2px 12px 0;font-size:10px;color:var(--text3);height:16px;font-style:italic;font-family:'IBM Plex Sans',sans-serif;flex-shrink:0}
.chat-input-row{padding:8px 10px;border-top:1px solid var(--gray-200);display:flex;gap:7px;flex-shrink:0;background:var(--white)}
.chat-inp{flex:1;height:32px;background:var(--gray-50);border:1px solid var(--gray-200);color:var(--text);font-family:'IBM Plex Sans',sans-serif;font-size:11px;padding:0 10px;outline:none;border-radius:var(--radius);transition:border-color .15s}
.chat-inp:focus{border-color:var(--green-l);background:var(--white)}
.chat-btn{height:32px;padding:0 14px;background:var(--green-l);border:none;color:#052e16;font-family:'IBM Plex Sans',sans-serif;font-size:11px;font-weight:600;cursor:pointer;border-radius:var(--radius);transition:background .15s}
.chat-btn:hover{background:#4ade80}
.chat-btn:disabled{background:#d1fae5;color:#6ee7b7;cursor:not-allowed}
/* ── ARCHIVE ── */
.arch-wrap{padding:10px}
.arch-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px}
.arch-title{font-size:12px;font-weight:600;color:var(--text2);letter-spacing:.3px}
.arch-refresh{font-size:10px;color:var(--blue);cursor:pointer;padding:3px 8px;border:1px solid var(--gray-200);border-radius:4px;font-family:'IBM Plex Sans',sans-serif;transition:background .15s}
.arch-refresh:hover{background:var(--gray-100)}
.arch-item{display:flex;align-items:center;gap:8px;padding:6px 0;border-bottom:1px solid var(--gray-100)}
.arch-icon{font-size:16px;flex-shrink:0;width:22px;text-align:center}
.arch-info{flex:1;min-width:0}
.arch-name{font-size:11px;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:500}
.arch-meta{font-size:9px;color:var(--text3);margin-top:1px;font-family:'DM Mono',monospace}
.arch-dl{color:var(--green);cursor:pointer;font-size:18px;text-decoration:none;flex-shrink:0;line-height:1}
.arch-del{color:var(--red);cursor:pointer;font-size:13px;flex-shrink:0;padding:0 4px;opacity:.4;line-height:1}
.arch-del:hover{opacity:1}
.arch-empty{font-size:11px;color:var(--text3);text-align:center;padding:24px;font-style:italic}
/* ── LIGHTBOX ── */
.lbox{position:fixed;inset:0;background:rgba(0,0,0,.9);z-index:900;display:none;align-items:center;justify-content:center;flex-direction:column;gap:12px}
.lbox.show{display:flex}
.lbox img{max-width:85vw;max-height:72vh;border-radius:6px;box-shadow:var(--shadow-lg)}
.lbox-btns{display:flex;gap:10px}
.lbox-dl{display:inline-flex;align-items:center;gap:5px;padding:7px 16px;background:rgba(34,197,94,.12);border:1px solid var(--green-l);color:var(--green-l);font-size:11px;cursor:pointer;text-decoration:none;border-radius:var(--radius);font-family:'IBM Plex Sans',sans-serif;font-weight:500}
.lbox-cl{font-size:11px;color:#9ca3af;cursor:pointer;padding:7px 16px;border:1px solid #374151;border-radius:var(--radius);font-family:'IBM Plex Sans',sans-serif}
.lbox-cl:hover{color:var(--white)}
/* ── MODAL ── */
.modal-bg{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:300;display:none;align-items:center;justify-content:center;backdrop-filter:blur(2px)}
.modal-bg.show{display:flex}
.modal{background:var(--white);border-radius:10px;padding:22px;width:360px;box-shadow:var(--shadow-lg)}
.modal-t{font-size:16px;font-weight:600;color:var(--text);margin-bottom:14px;letter-spacing:.2px}
.field{margin-bottom:10px}
.field label{display:block;font-size:10px;font-weight:500;color:var(--text2);letter-spacing:.5px;text-transform:uppercase;margin-bottom:3px}
.field input,.field select,.field textarea{width:100%;background:var(--gray-50);border:1px solid var(--gray-200);color:var(--text);font-family:'IBM Plex Sans',sans-serif;font-size:11px;padding:7px 10px;outline:none;border-radius:var(--radius);transition:border-color .15s}
.field input:focus,.field select:focus,.field textarea:focus{border-color:var(--green-l);background:var(--white)}
.field textarea{resize:vertical;min-height:52px}
.modal-acts{display:flex;gap:8px;justify-content:flex-end;margin-top:14px}
.mbtn{padding:7px 16px;font-size:11px;font-weight:500;cursor:pointer;border:1px solid;background:transparent;border-radius:var(--radius);transition:all .15s;font-family:'IBM Plex Sans',sans-serif}
.mbtn-ok{border-color:var(--green-l);color:var(--green)}.mbtn-ok:hover{background:var(--green-bg)}
.mbtn-no{border-color:var(--gray-200);color:var(--text2)}.mbtn-no:hover{background:var(--gray-100)}
/* ── NOTIFICATION ── */
.notif{position:fixed;top:52px;right:12px;background:var(--white);border:1px solid var(--gray-200);border-left:3px solid var(--green-l);padding:10px 14px;max-width:250px;z-index:600;transform:translateX(270px);transition:transform .3s ease;border-radius:var(--radius);box-shadow:var(--shadow-lg)}
.notif.show{transform:translateX(0)}
.notif-t{font-size:13px;font-weight:600;letter-spacing:.2px;margin-bottom:2px}
.notif-m{color:var(--text2);font-size:10px;line-height:1.4}
</style>
</head>
<body>
<header class="topbar">
<div style="display:flex;align-items:center;gap:8px">
<span class="tb-logo">MISSION CONTROL AI</span>
<span class="tb-ver">v8</span>
</div>
<div class="tb-right">
<div class="tb-stat"><div class="sdot dg"></div><span id="top-active">0 active</span></div>
<div class="tb-stat"><div class="sdot dd" id="top-dot"></div><span id="top-status">Idle</span></div>
<div class="clock" id="clock">00:00:00</div>
</div>
</header>
<div class="prog" id="prog"></div>
<div class="mbar">
<input class="minput" id="task-input" placeholder="Assign mission to agents..." autocomplete="off"/>
<button class="btn-launch" id="launch-btn" onclick="launchMission()">β–Ά Launch</button>
<button class="btn-sec" onclick="openModal()">+ Agent</button>
<span class="mc-lbl" id="mc-count">0 missions</span>
</div>
<div class="main">
<div class="office" id="office">
<div class="wall"></div>
<div class="window wl"></div>
<div class="window wr"></div>
<div class="floor-area"></div>
<!-- Big screen center -->
<div class="bigscreen" id="bigscreen">
<div class="bsi">
<div class="bs-bracket tl"></div><div class="bs-bracket tr"></div>
<div class="bs-bracket bl"></div><div class="bs-bracket br"></div>
<div class="bs-title" id="bs-title">MISSION CONTROL AI</div>
<div class="bs-text" id="bs-text">Awaiting mission...</div>
</div>
</div>
<div class="bs-stand" id="bs-stand"></div>
<div class="bs-base" id="bs-base"></div>
<!-- Bookshelf left β€” clear of window -->
<div class="bookshelf" id="bookshelf"></div>
<!-- Server rack right β€” clear of window -->
<div class="server-rack" id="server-rack"></div>
<!-- Plants on floor -->
<div class="plant" style="left:5%;bottom:25%">
<div class="plant-leaves"><div class="leaf"></div><div class="leaf"></div><div class="leaf"></div></div>
<div class="plant-soil"></div><div class="plant-pot"></div>
</div>
<div class="plant" style="right:5%;bottom:22%">
<div class="plant-leaves"><div class="leaf"></div><div class="leaf"></div><div class="leaf"></div></div>
<div class="plant-soil"></div><div class="plant-pot"></div>
</div>
<div id="desks-layer"></div>
<div id="agents-layer"></div>
</div>
<!-- RIGHT COLUMN -->
<div class="right-col">
<div class="tabs">
<div class="tab on" onclick="switchTab('results')">Results</div>
<div class="tab" onclick="switchTab('chat')">Chat</div>
<div class="tab" onclick="switchTab('archive')">Archive</div>
</div>
<div class="pane on" id="pane-results" style="overflow-y:auto">
<div class="results-wrap" id="results-wrap">
<div class="r-empty">Launch a mission to see results.</div>
</div>
<div class="act-wrap">
<div class="sect-title">Activity</div>
<div id="act-log"></div>
</div>
</div>
<div class="pane" id="pane-chat">
<div class="chat-col">
<div class="chat-chips" id="chat-chips"></div>
<div class="chat-hdr">
<div id="chat-hdr-info" class="chat-hdr-info">Select an agent</div>
<div class="chat-hdr-clear" onclick="clearChat()">πŸ—‘ Clear</div>
</div>
<div class="chat-msgs" id="chat-msgs">
<div style="text-align:center;padding:24px;font-size:11px;color:var(--text3);font-style:italic">Select an agent above to chat directly</div>
</div>
<div class="chat-typing" id="chat-typing"></div>
<div class="chat-input-row">
<input class="chat-inp" id="chat-input" placeholder="Message agent..." autocomplete="off"/>
<button class="chat-btn" id="chat-btn" onclick="sendChat()">Send</button>
<button class="chat-btn" id="collab-btn" onclick="sendCollab()" title="Team mission: Manager coordinates all agents with shared context" style="background:#1d4ed8;margin-left:4px;font-size:10px;padding:0 10px;white-space:nowrap">&#9881; Team</button>
</div>
</div>
</div>
<div class="pane" id="pane-archive">
<div class="arch-wrap">
<div class="arch-head">
<div class="arch-title">File Archive</div>
<div class="arch-refresh" onclick="loadArchive()">↻ Refresh</div>
</div>
<div id="arch-list"><div class="arch-empty">No files yet</div></div>
</div>
</div>
</div>
</div>
<!-- Lightbox -->
<div class="lbox" id="lbox" onclick="if(event.target===this)closeLbox()">
<img id="lbox-img" src="" alt=""/>
<div class="lbox-btns">
<a class="lbox-dl" id="lbox-dl" href="#" download>↓ Download</a>
<div class="lbox-cl" onclick="closeLbox()">βœ• Close</div>
</div>
</div>
<!-- Modal -->
<div class="modal-bg" id="modal-bg" onclick="if(event.target===this)closeModal()">
<div class="modal">
<div class="modal-t">Add New Agent</div>
<div class="field"><label>Key (ID)</label><input id="m-key" placeholder="e.g. researcher"/></div>
<div class="field"><label>Display Name</label><input id="m-name" placeholder="e.g. Researcher"/></div>
<div class="field"><label>Role / Specialty</label><textarea id="m-role" placeholder="Especialista en..."></textarea></div>
<div class="field"><label>Provider</label>
<select id="m-prov">
<option value="openrouter">OpenRouter (free models)</option>
<option value="gemini">Google Gemini</option>
<option value="groq">Groq</option>
</select>
</div>
<div class="modal-acts">
<button class="mbtn mbtn-no" onclick="closeModal()">Cancel</button>
<button class="mbtn mbtn-ok" onclick="submitAgent()">Add Agent</button>
</div>
</div>
</div>
<!-- Notification -->
<div class="notif" id="notif">
<div class="notif-t" id="notif-t">Done</div>
<div class="notif-m" id="notif-m"></div>
</div>
<script>
const AC = {manager:'#16a34a',backend_dev:'#2563eb',frontend_dev:'#7c3aed',analyst:'#d97706',writer:'#0891b2',image_agent:'#be185d',developer:'#2563eb'};
const SC = {manager:'#14532d',backend_dev:'#1e3a8a',frontend_dev:'#4c1d95',analyst:'#78350f',writer:'#164e63',image_agent:'#831843',developer:'#1e3a8a'};
const HC = {manager:'#78350f',backend_dev:'#111827',frontend_dev:'#1e1b4b',analyst:'#451a03',writer:'#0c1a2e',image_agent:'#1f0a14',developer:'#111827'};
const IDLE_SAY = {manager:['Planning…','KPIs look good','Need coffee?'],backend_dev:['Debugging…','PR approved!','Ship it!'],frontend_dev:['CSS is fine','Pixel perfect','Dark mode ❀'],analyst:['ROI looks good','Risk: low','Numbers ok'],writer:['Draft ready','Editing…','Word count: ∞'],image_agent:['Searching…','HD quality','Found it!'],developer:['Compiling…','LGTM','Bug fixed!']};
const WORK_SAY = {manager:['On it!','Delegating…','Team ready'],backend_dev:['Building…','Almost done','Tests pass'],frontend_dev:['Styling…','Responsive βœ“','Deployed!'],analyst:['Analyzing…','Insight!','Report ready'],writer:['Writing…','First draft!','Publishing…'],image_agent:['Fetching…','Generating…','Done!'],developer:['Coding…','Feature done','Bug squashed']};
const PROV_M = {openrouter:['google/gemma-3-12b-it:free','meta-llama/llama-3.3-70b-instruct:free'],gemini:['gemini-2.0-flash','gemini-1.5-flash'],groq:['llama-3.1-8b-instant','gemma2-9b-it']};
const BOOKS = ['#ef4444','#3b82f6','#22c55e','#f59e0b','#8b5cf6','#06b6d4','#ec4899','#14b8a6','#f97316','#6366f1'];
const MUGS = ['#fde68a','#fca5a5','#93c5fd','#86efac','#c4b5fd','#fcd34d','#fb923c'];
let agents=[], states={}, positions={}, targets={}, modes={};
let activity=[], mCount=0, bubT={};
let chatAgent=null, chatHist={};
// ── INIT ─────────────────────────────────────────────────────────────────
async function init(){
positionScreen();
buildBookshelf();
buildServerRack();
const d = await fetch('/api/agents').then(r=>r.json());
agents = d.agents;
agents.forEach(a=>{
states[a.key] = {status:'idle',message:'',model:'',doc_file:null,img_base:null,img_count:0};
modes[a.key] = 'idle';
chatHist[a.key] = [];
});
renderDesks(); renderAgents(); renderChips();
startLife();
addAct('Office ready','#16a34a');
}
function positionScreen(){
const off = document.getElementById('office');
const bs = document.getElementById('bigscreen');
const W = off.clientWidth, H = off.clientHeight;
const sh = bs.offsetHeight;
const sy = H * 0.02 + sh;
document.getElementById('bs-stand').style.cssText = `position:absolute;left:${W/2-26}px;top:${sy}px;width:52px;height:7px;background:#94a3b8;border-radius:0 0 3px 3px;z-index:4`;
document.getElementById('bs-base').style.cssText = `position:absolute;left:${W/2-44}px;top:${sy+7}px;width:88px;height:4px;background:#7c8fa0;border-radius:3px;z-index:4`;
}
function buildBookshelf(){
const el = document.getElementById('bookshelf');
let h = '';
for(let r=0;r<3;r++){h+='<div class="brow">';for(let b=0;b<5;b++)h+=`<div class="bk" style="background:${BOOKS[(r*5+b)%BOOKS.length]}"></div>`;h+='</div>';}
el.innerHTML = h;
}
function buildServerRack(){
const el = document.getElementById('server-rack');
const leds = ['#22c55e','#3b82f6','#f59e0b','#22c55e','#ec4899','#22c55e'];
el.innerHTML = leds.map((c,i)=>`<div class="rack-u"><div class="rack-led" style="background:${c};animation:led-blink ${.7+i*.25}s ease-in-out infinite alternate"></div><div class="rack-bar"></div></div>`).join('') + '<div class="rack-lbl">SRV</div>';
}
// ── DESK LAYOUT β€” 3 top row + 3 bottom row ───────────────────────────────
function getDeskLayout(count){
// 3 desks top row, 3 desks bottom row
const row0cnt = Math.min(count, 3);
const row1cnt = Math.max(0, count - 3);
const result = [];
const off = document.getElementById('office');
const W = off.clientWidth, H = off.clientHeight;
const floorH = H * 0.60; // floor = bottom 60%
const deskW = 110, deskH = 62;
// Row 0 (top desks): sits at 58% from bottom of floor β€” agents appear above them near wall
// Row 1 (bottom desks): sits at 10% from bottom β€” agents appear above them mid-floor
const row0bottom = Math.round(floorH * 0.56);
const row1bottom = Math.round(floorH * 0.10);
[[row0cnt, row0bottom],[row1cnt, row1bottom]].forEach(([cnt, bottomPx], row)=>{
if(!cnt) return;
const usable = W * 0.78;
const gap = cnt > 1 ? (usable - cnt * deskW) / (cnt - 1) : 0;
const startX = (W - (cnt * deskW + Math.max(0,cnt-1) * gap)) / 2;
for(let i=0;i<cnt;i++){
result.push({ x: startX + i*(deskW+gap), y: bottomPx, row, deskW, deskH });
}
});
return result;
}
function renderDesks(){
const layer = document.getElementById('desks-layer');
const layout = getDeskLayout(agents.length);
layer.innerHTML = agents.map((a,i)=>{
const p = layout[i]; if(!p) return '';
const st = states[a.key]?.status || 'idle';
const sc = st==='working'?'#22c55e': st==='active'?'#16a34a':'#374151';
const stxt = st==='working'?'● BUSY': st==='active'?'βœ“ DONE':'β€” IDLE';
return `<div id="desk-${a.key}" class="desk" style="left:${p.x}px;bottom:${p.y}px">
<div class="d-monitor"><div class="d-screen" id="dscr-${a.key}" style="color:${sc}">${stxt}</div></div>
<div class="d-mug" style="background:${MUGS[i%MUGS.length]}"></div>
<div class="d-kbd"></div><div class="d-paper"></div>
</div>`;
}).join('');
}
function seatOf(key){
const i = agents.findIndex(a=>a.key===key); if(i<0) return{x:40,y:20};
const layout = getDeskLayout(agents.length);
const p = layout[i]; if(!p) return{x:40,y:20};
// Agent centered above desk (pixel coords, not percent)
return { x: p.x + (p.deskW||110)/2 - 15, y: p.y + (p.deskH||62) + 6 };
}
function renderAgents(){
const layer = document.getElementById('agents-layer');
layer.innerHTML = agents.map(a=>{
const color = AC[a.key]||'#2563eb';
const shirt = SC[a.key]||'#1e3a8a';
const hair = HC[a.key]||'#111827';
const st = states[a.key]||{status:'idle'};
const seat = seatOf(a.key);
if(!positions[a.key]) positions[a.key] = seat;
const pos = positions[a.key];
const mode = modes[a.key]||'idle';
const isEmpty = st.status==='resting';
const tagColor = st.status==='working'?color:st.status==='active'?'#16a34a':st.status==='resting'?'#d97706':'#9ca3af';
const tagBg = st.status==='working'?`${color}18`:'rgba(255,255,255,.85)';
const tagTxt = ({idle:'IDLE',working:'●',active:'βœ“',resting:'ZZZ'}[st.status]||'IDLE')+' '+a.name;
return `<div class="agent ${mode}" id="ag-${a.key}" style="left:${pos.x}px;bottom:${pos.y}px;opacity:${isEmpty?.3:1}" onclick="onAgentClick('${a.key}')">
<div class="a-hair" style="background:${hair}"></div>
<div class="a-head"><div class="a-eyes"><div class="a-eye"></div><div class="a-eye"></div></div></div>
<div class="a-body" style="background:${shirt}"><div class="a-badge" style="background:${color}"></div></div>
<div class="a-arms"><div class="a-arm al" style="background:${shirt}"></div><div class="a-arm ar" style="background:${shirt}"></div></div>
<div class="a-legs"><div class="a-leg"></div><div class="a-leg"></div></div>
<div class="a-feet"><div class="a-foot"></div><div class="a-foot"></div></div>
<div class="a-tag" style="color:${tagColor};background:${tagBg}">${tagTxt}</div>
<div class="bubble" id="bub-${a.key}"></div>
${isEmpty?'<div style="position:absolute;top:-20px;left:50%;transform:translateX(-50%);font-size:13px">πŸ’€</div>':''}
</div>`;
}).join('');
}
function updateAgent(key){
const el = document.getElementById(`ag-${key}`); if(!el) return;
const st = states[key]||{status:'idle'};
const mode = modes[key]||'idle';
const color = AC[key]||'#2563eb';
const pos = positions[key]||seatOf(key);
el.style.left = pos.x + 'px';
el.style.bottom = pos.y + 'px';
el.className = `agent ${mode}`;
el.style.opacity = st.status==='resting'?.3:1;
const tag = el.querySelector('.a-tag');
if(tag){
const tc = st.status==='working'?color:st.status==='active'?'#16a34a':st.status==='resting'?'#d97706':'#9ca3af';
tag.style.color = tc;
tag.style.background = st.status==='working'?`${color}18`:'rgba(255,255,255,.85)';
const name = agents.find(a=>a.key===key)?.name||key;
tag.textContent = ({idle:'IDLE',working:'●',active:'βœ“',resting:'ZZZ'}[st.status]||'IDLE')+' '+name;
}
const dscr = document.getElementById(`dscr-${key}`);
if(dscr){
dscr.textContent = st.status==='working'?'● BUSY': st.status==='active'?'βœ“ DONE':'β€” IDLE';
dscr.style.color = st.status==='working'?'#22c55e':st.status==='active'?'#16a34a':'#374151';
}
}
function onAgentClick(key){ switchTab('chat'); selectAgent(key); }
// ── IDLE LIFE ─────────────────────────────────────────────────────────────
function startLife(){
const off = document.getElementById('office');
setInterval(()=>{
agents.forEach(a=>{
if(states[a.key]?.status==='working') return;
if(Math.random()<.18){
modes[a.key]='walking';
const W=off.clientWidth, H=off.clientHeight;
targets[a.key]={x:W*.08+Math.random()*W*.78, y:H*.02+Math.random()*H*.50};
}
if(Math.random()<.05){
const ph=IDLE_SAY[a.key]||['...'];
showBubble(a.key,ph[~~(Math.random()*ph.length)],2200);
}
});
},3500);
setInterval(()=>{
agents.forEach(a=>{
if(states[a.key]?.status==='working'){
const s=seatOf(a.key); positions[a.key]=s; modes[a.key]='working'; targets[a.key]=null; updateAgent(a.key); return;
}
const t=targets[a.key]; if(!t) return;
const p=positions[a.key]||seatOf(a.key);
const dx=t.x-p.x, dy=t.y-p.y, dist=Math.sqrt(dx*dx+dy*dy);
if(dist<1.5){positions[a.key]={x:t.x,y:t.y};targets[a.key]=null;modes[a.key]='idle';}
else{positions[a.key]={x:p.x+dx/dist*.45,y:p.y+dy/dist*.45};modes[a.key]='walking';}
updateAgent(a.key);
});
},80);
}
// ── TABS ──────────────────────────────────────────────────────────────────
function switchTab(name){
['results','chat','archive'].forEach((t,i)=>{
document.querySelectorAll('.tab')[i].classList.toggle('on',t===name);
const p=document.getElementById(`pane-${t}`);
p.classList.toggle('on',t===name);
});
if(name==='archive') loadArchive();
}
// ── CHAT ──────────────────────────────────────────────────────────────────
function renderChips(){
const c=document.getElementById('chat-chips');
c.innerHTML=agents.map(a=>{
const color=AC[a.key]||'#2563eb', sel=chatAgent===a.key;
return `<div class="chip ${sel?'on':''}" id="chip-${a.key}" style="border-color:${color};color:${sel?'#fff':color};background:${sel?color:'transparent'}" onclick="selectAgent('${a.key}')">${a.name}</div>`;
}).join('');
}
function selectAgent(key){
chatAgent=key; renderChips(); renderChat();
updateChatHdr(); document.getElementById('chat-input').focus();
}
function updateChatHdr(){
const el=document.getElementById('chat-hdr-info'); if(!el) return;
if(!chatAgent){el.textContent='Select an agent';return;}
const color=AC[chatAgent]||'#2563eb';
const name=agents.find(a=>a.key===chatAgent)?.name||chatAgent;
const turns=(chatHist[chatAgent]||[]).filter(m=>m.role==='user').length;
el.innerHTML=`<span style="color:${color};font-weight:600">${name}</span> <span style="color:var(--text3)">${turns} turn${turns!==1?'s':''}</span>`;
}
function renderChat(){
const c=document.getElementById('chat-msgs');
const msgs=chatHist[chatAgent]||[];
if(!chatAgent){c.innerHTML='<div style="text-align:center;padding:24px;font-size:11px;color:var(--text3);font-style:italic">Select an agent above to chat directly</div>';return;}
if(!msgs.length){
const name=agents.find(a=>a.key===chatAgent)?.name||chatAgent;
const color=AC[chatAgent]||'#2563eb';
const emoji={manager:'πŸ‘”',backend_dev:'πŸ’»',frontend_dev:'🎨',analyst:'πŸ“Š',writer:'✍️',image_agent:'πŸ–ΌοΈ'}[chatAgent]||'πŸ€–';
c.innerHTML=`<div style="text-align:center;padding:24px"><div style="font-size:28px;margin-bottom:8px">${emoji}</div><div style="font-size:13px;font-weight:600;color:${color}">${name}</div><div style="font-size:11px;color:var(--text3);margin-top:5px">Chat directly β€” no manager needed.<br>Remembers last 20 turns.</div></div>`;
return;
}
c.innerHTML=msgs.map(m=>{
if(m.role==='user') return `<div class="msg user"><div class="msg-who">You</div><div class="msg-text">${m.text}</div></div>`;
const color=AC[chatAgent]||'#2563eb';
const name=agents.find(a=>a.key===chatAgent)?.name||chatAgent;
let imgHtml='';
if(m.imgs&&m.imgs.count>0){
let tw='<div style="display:flex;flex-wrap:wrap;gap:8px;margin-top:10px;padding:10px;background:#f9fafb;border-radius:10px;border:1px solid #e5e7eb">';
for(let i=0;i<m.imgs.count;i++){
const src='/api/docs/'+m.imgs.base+'_img'+(i+1)+'.jpg';
tw+='<div class="chat-thumb" data-src="'+src+'" data-fn="image_'+(i+1)+'.jpg">';
tw+='<img src="'+src+'" style="width:120px;height:90px;object-fit:cover;display:block;border-radius:6px" onerror="this.parentElement.style.display=\'none\'">';
tw+='</div>';
}
tw+='</div>';
let dw='<div style="display:flex;gap:5px;flex-wrap:wrap;margin-top:7px">';
for(let i=0;i<m.imgs.count;i++){
const src='/api/docs/'+m.imgs.base+'_img'+(i+1)+'.jpg';
dw+='<a href="'+src+'" download="image_'+(i+1)+'.jpg" class="dl-btn">↓ img '+(i+1)+'</a>';
}
dw+='</div>';
imgHtml=tw+dw;
}
const errSt=m.error?'color:var(--red);':'';
// Collab message: different agent than selected
const msgColor = m.agentColor || color;
const msgName = m.agentName || name;
const collabTag = m.collab ? '<span style="font-size:8px;background:#dbeafe;color:#1d4ed8;padding:1px 5px;border-radius:3px;margin-left:5px;font-weight:500">TEAM</span>' : '';
// Download button for generated files
let fileHtml = '';
if(m.doc_file){
const ext = m.doc_file.split('.').pop().toLowerCase();
const lbl = {py:'Python',html:'HTML',xlsx:'Excel',docx:'Word'}[ext]||'File';
fileHtml = `<a class="dl-btn" href="/api/docs/${m.doc_file}" download style="margin-top:5px">↓ ${lbl}</a>`;
}
return `<div class="msg agent" style="${m.collab?'border-left:3px solid '+msgColor+';background:#fafafa':''}">`+
`<div class="msg-who" style="color:${msgColor}">${msgName}${collabTag}${m.turn?` <span style="font-size:8px;color:var(--text3);font-weight:400">#${m.turn}</span>`:''}</div>`+
`<div class="msg-text" style="${errSt}">${m.text}</div>`+
imgHtml+fileHtml+
`${m.model?`<div class="msg-model">via ${m.model}</div>`:''}</div>`;
}).join('');
c.scrollTop=c.scrollHeight;
// click handler for image thumbs
c.querySelectorAll('.chat-thumb').forEach(el=>{
el.style.cursor='pointer'; el.style.borderRadius='8px'; el.style.overflow='hidden';
el.style.border='2px solid #e5e7eb'; el.style.transition='border-color .2s,transform .2s';
el.style.boxShadow='0 2px 8px rgba(0,0,0,.1)';
el.onmouseover=()=>{el.style.borderColor='#22c55e';el.style.transform='scale(1.03)'};
el.onmouseout=()=>{el.style.borderColor='#e5e7eb';el.style.transform='scale(1)'};
el.onclick=()=>openLbox(el.dataset.src,el.dataset.fn);
});
}
async function sendChat(){
const inp=document.getElementById('chat-input');
const msg=inp.value.trim();
if(!msg||!chatAgent) return;
const btn=document.getElementById('chat-btn');
btn.disabled=true; inp.value='';
if(!chatHist[chatAgent]) chatHist[chatAgent]=[];
chatHist[chatAgent].push({role:'user',text:msg});
renderChat();
const name=agents.find(a=>a.key===chatAgent)?.name||chatAgent;
document.getElementById('chat-typing').textContent=`${name} is typing…`;
try{
const r=await fetch('/api/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({agent:chatAgent,message:msg,session_id:chatAgent})});
const d=await r.json();
if(d.error) throw new Error(d.error);
// Main agent response
const imgs=d.img_base&&d.img_count>0?{base:d.img_base,count:d.img_count}:null;
chatHist[chatAgent].push({role:'agent',text:d.response,model:d.model,turn:d.turn,imgs});
// If manager delegated β†’ show each sub-agent as separate message
if(d.delegate_result&&d.delegate_result.sub_results){
Object.entries(d.delegate_result.sub_results).forEach(([key,r])=>{
if(!r||r.status==='idle') return;
const sc=AC[key]||'#2563eb';
const sn=agents.find(a=>a.key===key)?.name||key;
const si=(key==='image_agent'&&r.img_base&&r.img_count>0)?{base:r.img_base,count:r.img_count}:null;
chatHist[chatAgent].push({role:'agent',agentKey:key,agentName:sn,agentColor:sc,
text:r.message||'Done.',model:r.model||'',doc_file:r.doc_file||null,imgs:si,collab:true});
});
}
showBubble(chatAgent,d.response.substring(0,50)+(d.response.length>50?'…':''),3000);
updateChatHdr();
}catch(e){
chatHist[chatAgent].push({role:'agent',text:`⚠ Error: ${e.message}`,model:'',error:true});
}
document.getElementById('chat-typing').textContent='';
renderChat(); btn.disabled=false; inp.focus();
}
async function clearChat(){
if(!chatAgent) return;
await fetch(`/api/chat/${chatAgent}`,{method:'DELETE'});
chatHist[chatAgent]=[]; updateChatHdr(); renderChat();
}
// ── COLLABORATIVE MISSION FROM CHAT ────────────────────────────────────────
async function sendCollab(){
const inp = document.getElementById('chat-input');
const task = inp.value.trim();
if(!task) return;
const btn = document.getElementById('collab-btn');
const sbtn = document.getElementById('chat-btn');
btn.disabled=true; sbtn.disabled=true; inp.value='';
// Add user message to current chat (shown as "team mission" request)
if(!chatAgent) {
// No specific agent selected β€” show in a neutral way
if(!chatHist['__collab']) chatHist['__collab']=[];
}
const target = chatAgent || '__collab';
if(!chatHist[target]) chatHist[target]=[];
chatHist[target].push({role:'user', text:'[Team Mission] '+task});
renderChat();
document.getElementById('chat-typing').textContent='Manager coordinating team...';
try{
// Send to /api/collab β€” Manager coordinates all agents
const resp = await fetch('/api/collab',{
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({task, agents: chatAgent && chatAgent!=='manager' ? [] : []})
});
const d = await resp.json();
if(d.error) throw new Error(d.error);
// Show each agent's response as a separate message
const agentOrder = d.delegates || Object.keys(d.results||{});
agentOrder.forEach(key=>{
const r = d.results[key];
if(!r || r.status==='idle') return;
const color = AC[key]||'#2563eb';
const name = agents.find(a=>a.key===key)?.name || key;
chatHist[target].push({
role: 'agent',
agentKey: key,
agentName: name,
agentColor: color,
text: r.message || r.output || 'Done.',
model: r.model || '',
doc_file: r.doc_file || null,
turn: null,
collab: true,
});
});
showBubble(chatAgent||'manager','Team done! βœ“', 2500);
addAct('Team mission: '+task.substring(0,40),'#1d4ed8');
// Refresh results panel too
if(d.results) showResults(d.results);
}catch(e){
chatHist[target].push({role:'agent',text:'⚠ Error: '+e.message,model:'',error:true});
}
document.getElementById('chat-typing').textContent='';
renderChat();
btn.disabled=false; sbtn.disabled=false;
}
document.addEventListener('DOMContentLoaded',()=>{
document.getElementById('chat-input')?.addEventListener('keydown',e=>{if(e.key==='Enter')sendChat();});
// Image click via delegation (backup for inline onclick)
document.getElementById('chat-msgs')?.addEventListener('click',e=>{
const img=e.target.closest('.chat-img-preview');
if(img) openLbox(img.src, img.dataset.fname||'image.jpg');
});
});
// ── ARCHIVE ───────────────────────────────────────────────────────────────
const FILE_ICONS={docx:'πŸ“„',xlsx:'πŸ“Š',html:'🌐',py:'🐍',groovy:'βš™οΈ',jpg:'πŸ–ΌοΈ',png:'πŸ–ΌοΈ',txt:'πŸ“'};
function fmtBytes(b){if(b<1024)return b+'B';if(b<1048576)return (b/1024).toFixed(1)+'KB';return (b/1048576).toFixed(1)+'MB';}
async function loadArchive(){
const list=document.getElementById('arch-list');
list.innerHTML='<div class="arch-empty">Loading…</div>';
try{
const d=await fetch('/api/archive').then(r=>r.json());
if(!d.files?.length){list.innerHTML='<div class="arch-empty">No files yet</div>';return;}
list.innerHTML=d.files.map(f=>`
<div class="arch-item">
<div class="arch-icon">${FILE_ICONS[f.ext]||'πŸ“Ž'}</div>
<div class="arch-info"><div class="arch-name" title="${f.name}">${f.name}</div><div class="arch-meta">${f.modified} Β· ${fmtBytes(f.size)}</div></div>
<a class="arch-dl" href="/api/docs/${f.name}" download title="Download">⬇</a>
<div class="arch-del" onclick="delArchive('${f.name}')" title="Delete">Γ—</div>
</div>`).join('');
}catch(e){list.innerHTML=`<div class="arch-empty">Error: ${e.message}</div>`;}
}
async function delArchive(name){
if(!confirm(`Delete ${name}?`)) return;
await fetch(`/api/archive/${name}`,{method:'DELETE'});
loadArchive();
}
// ── HELPERS ───────────────────────────────────────────────────────────────
function showBubble(key,text,ms=2500){
const b=document.getElementById(`bub-${key}`); if(!b) return;
if(bubT[key]) clearTimeout(bubT[key]);
b.textContent=text; b.classList.add('show');
bubT[key]=setTimeout(()=>b.classList.remove('show'),ms);
}
function updateScreen(title,text){
document.getElementById('bs-title').textContent=title;
document.getElementById('bs-text').textContent=text;
}
function addAct(msg,color='#2563eb'){
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,20).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('');
}
function notify(title,msg,color='#16a34a'){
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;
document.getElementById('notif').classList.add('show');
setTimeout(()=>document.getElementById('notif').classList.remove('show'),5500);
}
// ── RESULTS ───────────────────────────────────────────────────────────────
function showResults(results){
const wrap=document.getElementById('results-wrap');
const cards=agents.map(a=>{
const r=results[a.key]; if(!r||r.status==='idle') return '';
const color=AC[a.key]||'#2563eb';
const msg=(r.message||'').substring(0,700)+(r.message?.length>700?'…':'');
let extra='';
if(r.doc_file){
const ext=r.doc_file.split('.').pop().toLowerCase();
const label={xlsx:'Excel',docx:'Word',html:'HTML',py:'Python',groovy:'Groovy',jpg:'Image'}[ext]||'File';
extra+=`<a class="dl-btn" href="/api/docs/${r.doc_file}" download>↓ ${label}</a>`;
}
if(r.img_base&&r.img_count>0){
extra+='<div class="img-row">'+Array.from({length:r.img_count},(_,i)=>`<img class="img-th" src="/api/docs/${r.img_base}_img${i+1}.jpg" onclick="openLbox('/api/docs/${r.img_base}_img${i+1}.jpg','img${i+1}.jpg')" onerror="this.style.display='none'"/>`).join('')+'</div>';
}
return `<div class="r-card" style="border-left-color:${color}"><div class="r-name"><div class="r-dot" style="background:${color}"></div><span style="color:${color}">${a.name}</span></div><div class="r-text">${msg||'Completed.'}</div>${r.model?`<div class="r-model">via ${r.model}</div>`:''}${extra}</div>`;
}).filter(Boolean);
wrap.innerHTML = cards.length ? cards.join('') : '<div class="r-empty">No agents responded.</div>';
switchTab('results');
}
// ── LIGHTBOX ──────────────────────────────────────────────────────────────
function openLbox(src,fn){document.getElementById('lbox-img').src=src;document.getElementById('lbox-dl').href=src;document.getElementById('lbox-dl').download=fn;document.getElementById('lbox').classList.add('show');}
function closeLbox(){document.getElementById('lbox').classList.remove('show');}
// ── MODAL ─────────────────────────────────────────────────────────────────
function openModal(){document.getElementById('modal-bg').classList.add('show');}
function closeModal(){document.getElementById('modal-bg').classList.remove('show');}
async function submitAgent(){
const key=document.getElementById('m-key').value.trim();
const name=document.getElementById('m-name').value.trim()||key;
const role=document.getElementById('m-role').value.trim()||'General purpose agent.';
const prov=document.getElementById('m-prov').value;
if(!key){alert('Key required');return;}
const d=await fetch('/api/agents/add',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({key,name,role,provider:prov,models:PROV_M[prov]})}).then(r=>r.json());
if(d.success){
agents.push({key:d.agent.key,name:d.agent.name,role:d.agent.role});
states[d.agent.key]={status:'idle',message:'',model:'',doc_file:null,img_base:null,img_count:0};
modes[d.agent.key]='idle'; chatHist[d.agent.key]=[];
renderDesks(); renderAgents(); renderChips();
addAct(`Agent "${name}" added`,'#7c3aed'); closeModal();
['m-key','m-name','m-role'].forEach(id=>document.getElementById(id).value='');
}
}
// ── CLOCK ─────────────────────────────────────────────────────────────────
setInterval(()=>{
const n=new Date();
document.getElementById('clock').textContent=[n.getHours(),n.getMinutes(),n.getSeconds()].map(x=>String(x).padStart(2,'0')).join(':');
},1000);
// ── LAUNCH ────────────────────────────────────────────────────────────────
async function launchMission(){
const task=document.getElementById('task-input').value.trim(); if(!task) return;
const btn=document.getElementById('launch-btn'); btn.disabled=true;
document.getElementById('prog').classList.add('on');
document.getElementById('top-status').textContent='Running';
document.getElementById('top-dot').className='sdot da';
updateScreen('PROCESSING', task.substring(0,70));
agents.forEach(a=>{
states[a.key]={status:'working',message:'',model:'',doc_file:null,img_base:null,img_count:0};
modes[a.key]='working';
const s=seatOf(a.key); positions[a.key]=s; targets[a.key]=null; updateAgent(a.key);
});
addAct(`Mission: "${task.substring(0,38)}"`,'#2563eb');
agents.forEach((a,i)=>setTimeout(()=>{const ph=WORK_SAY[a.key]||['Working…'];showBubble(a.key,ph[~~(Math.random()*ph.length)],3000);},i*450));
let data=null;
try{
const r=await fetch('/api/mission',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({task})});
data=await r.json(); if(!r.ok&&!data.error) data.error=`HTTP ${r.status}`;
}catch(e){data={error:e.message};}
if(!data||data.error){
agents.forEach(a=>{states[a.key]={status:'resting',message:data?.error||'Error',model:'',doc_file:null,img_base:null,img_count:0};modes[a.key]='idle';updateAgent(a.key);});
updateScreen('ERROR',data?.error||'Mission failed');
addAct(`Error: ${data?.error}`,'#dc2626'); notify('Mission Failed',data?.error||'Error','#dc2626');
document.getElementById('top-dot').className='sdot dr'; document.getElementById('top-status').textContent='Error';
btn.disabled=false; document.getElementById('prog').classList.remove('on'); return;
}
let anyResting=false,hasFile=false,screenText='';
agents.forEach(a=>{
const r=data.results[a.key];
if(!r){states[a.key]={status:'idle',message:'',model:'',doc_file:null,img_base:null,img_count:0};return;}
if(r.status==='resting') anyResting=true;
if(r.doc_file) hasFile=true;
states[a.key]={status:r.status==='resting'?'resting':r.status==='idle'?'idle':'active',message:r.message||'',model:r.model||'',doc_file:r.doc_file||null,img_base:r.img_base||null,img_count:r.img_count||0};
modes[a.key]='idle'; updateAgent(a.key);
if(r.message&&r.status!=='resting') screenText+=(agents.find(x=>x.key===a.key)?.name||a.key)+': '+r.message.substring(0,50)+' ';
if(r.status!=='resting'&&r.status!=='idle') setTimeout(()=>showBubble(a.key,'Done βœ“',2000),~~(Math.random()*700));
});
updateScreen('COMPLETE',screenText||'All agents done.');
showResults(data.results);
if(hasFile) loadArchive();
mCount++;
document.getElementById('mc-count').textContent=`${mCount} mission${mCount!==1?'s':''}`;
addAct('Mission complete','#16a34a');
setTimeout(()=>notify(anyResting?'Partial results':hasFile?'Files ready':'Mission complete',anyResting?'Some agents rate-limited.':hasFile?'Check Archive tab.':'All agents responded.',anyResting?'#d97706':hasFile?'#7c3aed':'#16a34a'),350);
document.getElementById('top-status').textContent=anyResting?'Partial':'Done';
document.getElementById('top-dot').className=`sdot ${anyResting?'da':'dg'}`;
btn.disabled=false; document.getElementById('prog').classList.remove('on');
}
document.addEventListener('DOMContentLoaded',()=>{
document.getElementById('task-input')?.addEventListener('keydown',e=>{if(e.key==='Enter')launchMission();});
});
// LED animation + resize
const s=document.createElement('style');
s.textContent='@keyframes led-blink{0%{opacity:.2}100%{opacity:1;box-shadow:0 0 4px currentColor}}';
document.head.appendChild(s);
window.addEventListener('resize',()=>{positionScreen();renderDesks();renderAgents();});
init();
</script>
</body>
</html>