Spaces:
Sleeping
Sleeping
| <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">⚙ 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> |