| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width,initial-scale=1"> |
| <title>NEXUS Builder β AI Full-Stack App Maker</title> |
| <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>π</text></svg>"> |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet"> |
|
|
| <style> |
| /* βββββββββββββββ RESET & BASE βββββββββββββββ */ |
| *{margin:0;padding:0;box-sizing:border-box} |
| html,body{height:100%;overflow:hidden} |
| body{font-family:'Inter',system-ui,sans-serif;-webkit-font-smoothing:antialiased} |
| pre,code,.mono{font-family:'JetBrains Mono','Fira Code',monospace} |
| ::selection{background:var(--accent);color:#fff} |
| :focus-visible{outline:2px solid var(--accent);outline-offset:2px} |
|
|
| /* βββββββββββββββ SCROLLBAR βββββββββββββββ */ |
| ::-webkit-scrollbar{width:5px;height:5px} |
| ::-webkit-scrollbar-track{background:transparent} |
| ::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px} |
| ::-webkit-scrollbar-thumb:hover{background:var(--muted)} |
|
|
| /* βββββββββββββββ THEME VARS βββββββββββββββ */ |
| :root{ |
| --bg:#0A0A0F;--surface:#111118;--border:#1E1E2E; |
| --accent:#6C63FF;--accent2:#00D9FF; |
| --text:#F0F0FF;--muted:#8888AA; |
| --success:#22D3A8;--error:#FF4D6D;--warn:#FFB547; |
| --glass:rgba(17,17,24,.75); |
| } |
| [data-theme="light"]{ |
| --bg:#F8F8FC;--surface:#FFFFFF;--border:#E0E0EF; |
| --accent:#5B53E8;--accent2:#0099CC; |
| --text:#0A0A1A;--muted:#666688; |
| --success:#16A085;--error:#E74C6F;--warn:#E6A030; |
| --glass:rgba(255,255,255,.8); |
| } |
|
|
| body{background:var(--bg);color:var(--text)} |
|
|
| /* βββββββββββββββ LAYOUT βββββββββββββββ */ |
| #app{display:flex;flex-direction:column;height:100vh;width:100vw} |
|
|
| /* ββ HEADER ββ */ |
| .hdr{display:flex;align-items:center;justify-content:space-between; |
| padding:0 16px;height:48px;border-bottom:1px solid var(--border); |
| background:var(--glass);backdrop-filter:blur(12px);z-index:50;flex-shrink:0} |
| .hdr-logo{display:flex;align-items:center;gap:8px} |
| .hdr-logo svg{width:22px;height:22px;color:var(--accent)} |
| .hdr-logo span{font-size:18px;font-weight:700;letter-spacing:-.5px; |
| background:linear-gradient(135deg,var(--accent),var(--accent2)); |
| -webkit-background-clip:text;-webkit-text-fill-color:transparent} |
| .hdr-badge{font-size:10px;padding:2px 8px;border-radius:20px;font-weight:600; |
| background:color-mix(in srgb,var(--accent) 15%,transparent);color:var(--accent)} |
| .hdr-agents{display:flex;align-items:center;gap:14px} |
| .hdr-agent{display:flex;align-items:center;gap:5px;font-size:11px;color:var(--muted)} |
| .dot{width:7px;height:7px;border-radius:50%;flex-shrink:0} |
| .dot-idle{background:#555}.dot-active{background:var(--accent2);animation:pulse 1.4s infinite} |
| .dot-done{background:var(--success)}.dot-error{background:var(--error)} |
| @keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.5;transform:scale(1.4)}} |
| .hdr-right{display:flex;align-items:center;gap:8px} |
| .hdr-sid{font-size:10px;padding:3px 8px;border-radius:6px;border:1px solid var(--border); |
| background:var(--surface);color:var(--muted)} |
| .btn-icon{background:none;border:none;cursor:pointer;padding:6px;border-radius:8px; |
| color:var(--muted);transition:.2s} |
| .btn-icon:hover{color:var(--text);background:var(--surface)} |
|
|
| /* ββ MAIN ββ */ |
| .main{display:flex;flex:1;overflow:hidden} |
|
|
| /* ββ LEFT PANEL ββ */ |
| .left{width:420px;min-width:320px;display:flex;flex-direction:column; |
| border-right:1px solid var(--border);background:var(--bg);flex-shrink:0} |
| .tabs{display:flex;border-bottom:1px solid var(--border);flex-shrink:0} |
| .tab{flex:1;display:flex;align-items:center;justify-content:center;gap:5px; |
| padding:9px 0;font-size:11px;font-weight:500;cursor:pointer;border:none; |
| background:none;color:var(--muted);position:relative;transition:.2s} |
| .tab.on{color:var(--accent);background:var(--surface)} |
| .tab.on::after{content:'';position:absolute;bottom:0;left:0;right:0;height:2px; |
| background:var(--accent)} |
| .tab-badge{margin-left:3px;width:16px;height:16px;font-size:9px;display:flex; |
| align-items:center;justify-content:center;border-radius:50%; |
| background:color-mix(in srgb,var(--accent) 20%,transparent);color:var(--accent)} |
| .panel{flex:1;overflow:hidden;display:none;flex-direction:column} |
| .panel.on{display:flex} |
|
|
| /* ββ CHAT ββ */ |
| .chat-msgs{flex:1;overflow-y:auto;padding:14px} |
| .msg{margin-bottom:12px;display:flex;animation:fadeIn .3s} |
| .msg-user{justify-content:flex-end} |
| .msg-ai{justify-content:flex-start} |
| .msg-bubble{max-width:85%;padding:10px 14px;border-radius:16px;font-size:13px; |
| line-height:1.55;white-space:pre-wrap;word-break:break-word} |
| .msg-user .msg-bubble{background:var(--accent);color:#fff;border-bottom-right-radius:4px} |
| .msg-ai .msg-bubble{background:var(--surface);border:1px solid var(--border); |
| border-bottom-left-radius:4px} |
| .chat-loading{display:flex;align-items:center;gap:8px;padding:10px 14px; |
| background:var(--surface);border:1px solid var(--border);border-radius:16px; |
| font-size:12px;color:var(--muted);width:fit-content;animation:fadeIn .3s} |
| .typing{display:inline-block;width:2px;height:14px;background:var(--accent); |
| margin-left:3px;animation:blink 1s infinite;vertical-align:text-bottom} |
| @keyframes blink{0%,50%{opacity:1}51%,100%{opacity:0}} |
|
|
| .welcome{display:flex;flex-direction:column;align-items:center;justify-content:center; |
| height:100%;text-align:center;padding:20px} |
| .welcome h2{font-size:19px;font-weight:700;margin:10px 0 6px; |
| background:linear-gradient(135deg,var(--accent),var(--accent2)); |
| -webkit-background-clip:text;-webkit-text-fill-color:transparent} |
| .welcome p{font-size:12px;color:var(--muted);max-width:380px;margin-bottom:18px;line-height:1.5} |
| .type-btn{width:100%;text-align:left;padding:7px 12px;border-radius:8px;border:none; |
| cursor:pointer;font-size:11px;transition:.2s;background:transparent;color:var(--muted)} |
| .type-btn:hover,.type-btn.on{background:color-mix(in srgb,var(--accent) 12%,transparent); |
| color:var(--accent)} |
| .type-grid{display:grid;grid-template-columns:1fr 1fr;gap:3px;width:100%; |
| margin-bottom:14px;padding:6px;border-radius:10px;border:1px solid var(--border); |
| background:var(--surface)} |
| .example{width:100%;text-align:left;padding:10px 12px;border-radius:10px; |
| border:1px solid var(--border);background:var(--surface);color:var(--muted); |
| font-size:11px;cursor:pointer;transition:.2s;margin-bottom:6px;line-height:1.4} |
| .example:hover{border-color:color-mix(in srgb,var(--accent) 40%,transparent); |
| transform:translateY(-1px)} |
| .examples-label{font-size:10px;font-weight:600;color:var(--muted); |
| margin-bottom:6px;text-transform:uppercase;letter-spacing:.5px} |
|
|
| .chat-input{padding:10px;border-top:1px solid var(--border);flex-shrink:0;display:flex;gap:8px;align-items:flex-end} |
| .chat-input textarea{flex:1;resize:none;border-radius:12px;padding:10px 14px; |
| font-size:13px;border:1px solid var(--border);background:var(--surface); |
| color:var(--text);outline:none;font-family:inherit;line-height:1.5;min-height:44px;max-height:120px} |
| .chat-input textarea:focus{border-color:var(--accent)} |
| .btn-send{width:42px;height:42px;border-radius:12px;border:none;cursor:pointer; |
| background:var(--accent);color:#fff;display:flex;align-items:center;justify-content:center; |
| transition:.2s;flex-shrink:0} |
| .btn-send:hover{transform:scale(1.05)}.btn-send:disabled{opacity:.35;transform:none} |
| .btn-send svg{width:18px;height:18px} |
|
|
| /* ββ AGENT FEED ββ */ |
| .feed{flex:1;overflow-y:auto;padding:10px} |
| .feed-item{display:flex;gap:8px;padding:8px 10px;border-radius:10px;margin-bottom:6px; |
| font-size:11px;border:1px solid var(--border);background:var(--surface);animation:fadeIn .3s} |
| .feed-icon{font-size:15px;flex-shrink:0;margin-top:1px} |
| .feed-body{flex:1;min-width:0} |
| .feed-head{display:flex;justify-content:space-between;margin-bottom:3px} |
| .feed-agent{font-weight:600;text-transform:capitalize} |
| .feed-time{font-size:9px;color:var(--muted)} |
| .feed-text{color:var(--muted);word-break:break-word} |
| .feed-text pre{white-space:pre-wrap;max-height:80px;overflow-y:auto; |
| font-size:10px;line-height:1.5;margin-top:2px} |
| .feed-file{display:flex;align-items:center;gap:5px;color:var(--success)} |
| .feed-done{display:flex;align-items:center;gap:5px} |
| .empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center; |
| height:100%;color:var(--muted);font-size:13px;text-align:center;padding:20px;opacity:.6} |
| .empty-state svg{width:40px;height:40px;margin-bottom:10px;opacity:.4} |
|
|
| /* ββ FILE TREE ββ */ |
| .tree{flex:1;overflow-y:auto;padding:6px 0} |
| .tree-header{padding:4px 12px;font-size:9px;font-weight:700;text-transform:uppercase; |
| letter-spacing:.6px;color:var(--muted)} |
| .tree-dir-name,.tree-file{display:flex;align-items:center;gap:6px;padding:3px 8px; |
| cursor:pointer;font-size:11px;border-radius:4px;transition:.15s;user-select:none} |
| .tree-dir-name{font-weight:500;color:var(--text)} |
| .tree-dir-name:hover{background:color-mix(in srgb,var(--accent) 8%,transparent)} |
| .tree-file{color:var(--muted)} |
| .tree-file:hover{background:color-mix(in srgb,var(--accent) 8%,transparent);color:var(--text)} |
| .tree-file.sel{background:color-mix(in srgb,var(--accent) 15%,transparent);color:var(--accent)} |
| .tree-children{overflow:hidden} |
| .tree-icon{flex-shrink:0;font-size:13px} |
|
|
| /* ββ RIGHT PANEL ββ */ |
| .right{flex:1;display:flex;flex-direction:column;overflow:hidden;background:var(--bg)} |
| .sys-tabs{display:flex;gap:2px;padding:4px 8px;border-bottom:1px solid var(--border); |
| overflow-x:auto;flex-shrink:0} |
| .sys-tab{display:flex;align-items:center;gap:5px;padding:6px 12px;border-radius:8px; |
| font-size:11px;font-weight:500;border:none;cursor:pointer;background:none; |
| color:var(--muted);white-space:nowrap;transition:.2s} |
| .sys-tab.on{background:color-mix(in srgb,var(--accent) 12%,transparent);color:var(--accent)} |
| .toolbar{display:flex;justify-content:space-between;align-items:center; |
| padding:6px 12px;border-bottom:1px solid var(--border);flex-shrink:0} |
| .toolbar-group{display:flex;align-items:center;gap:3px} |
| .tb{display:flex;align-items:center;gap:5px;padding:5px 10px;border-radius:7px; |
| font-size:11px;font-weight:500;border:none;cursor:pointer;background:none; |
| color:var(--muted);transition:.2s} |
| .tb.on{background:color-mix(in srgb,var(--accent) 12%,transparent);color:var(--accent)} |
| .tb-sm{padding:5px;border-radius:6px} |
| .btn-export{display:flex;align-items:center;gap:5px;padding:5px 12px;border-radius:7px; |
| font-size:11px;font-weight:500;border:none;cursor:pointer;background:var(--accent); |
| color:#fff;transition:.2s} |
| .btn-export:hover{transform:scale(1.05)} |
|
|
| .preview-area{flex:1;overflow:hidden;display:flex;align-items:center; |
| justify-content:center;padding:16px;background:#0D0D12} |
| [data-theme="light"] .preview-area{background:#f0f0f5} |
| .preview-frame{border:1px solid var(--border);border-radius:10px;overflow:hidden; |
| height:100%;transition:width .3s;background:#fff} |
| .preview-frame iframe{width:100%;height:100%;border:none} |
| .preview-empty{text-align:center;color:var(--muted)} |
| .preview-empty .big{font-size:48px;margin-bottom:12px;opacity:.2} |
| .preview-empty h3{font-size:16px;font-weight:600;color:var(--text);margin-bottom:6px} |
| .preview-empty p{font-size:12px;line-height:1.5} |
|
|
| /* ββ CODE VIEW ββ */ |
| .code-view{width:100%;height:100%;display:flex;flex-direction:column;border-radius:10px; |
| overflow:hidden;border:1px solid var(--border)} |
| .code-head{display:flex;justify-content:space-between;align-items:center; |
| padding:6px 14px;background:var(--bg);border-bottom:1px solid var(--border)} |
| .code-name{font-size:11px;color:var(--muted)} |
| .code-meta{display:flex;align-items:center;gap:8px} |
| .code-lines{font-size:9px;color:var(--muted)} |
| .code-body{flex:1;overflow:auto;display:flex;background:#0D0D14;font-size:12px;line-height:1.65} |
| [data-theme="light"] .code-body{background:#fafafe} |
| .code-gutter{flex-shrink:0;text-align:right;padding:10px 12px 10px 14px; |
| color:#333355;user-select:none} |
| [data-theme="light"] .code-gutter{color:#bbbbcc} |
| .code-content{flex:1;padding:10px 14px 10px 0;overflow-x:auto;white-space:pre;tab-size:2} |
|
|
| /* ββ STATUS BAR ββ */ |
| .status{display:flex;justify-content:space-between;align-items:center; |
| padding:0 14px;height:26px;border-top:1px solid var(--border); |
| background:var(--surface);font-size:10px;color:var(--muted);flex-shrink:0} |
| .status-left,.status-right{display:flex;align-items:center;gap:12px} |
| .status-dot{width:6px;height:6px;border-radius:50%;margin-right:4px;display:inline-block} |
| .progress-dots{display:flex;align-items:center;gap:3px} |
| .pdot{width:5px;height:5px;border-radius:50%;background:var(--border)} |
| .pdot.on{background:var(--accent)}.pdot.past{background:var(--success)} |
| .pline{width:12px;height:1px;background:var(--border)} |
|
|
| /* βββββββββββββββ ANIMATIONS βββββββββββββββ */ |
| @keyframes fadeIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}} |
| @keyframes spin{to{transform:rotate(360deg)}} |
| .spin{animation:spin .8s linear infinite} |
|
|
| /* βββββββββββββββ RESPONSIVE βββββββββββββββ */ |
| @media(max-width:768px){ |
| .left{width:100%;border-right:none;border-bottom:1px solid var(--border)} |
| .main{flex-direction:column} |
| .hdr-agents{display:none} |
| } |
| </style> |
| </head> |
|
|
| <body> |
| <div id="app"> |
|
|
| |
| <header class="hdr"> |
| <div class="hdr-logo"> |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg> |
| <span class="mono">NEXUS</span> |
| <span class="hdr-badge">BUILDER</span> |
| </div> |
| <div class="hdr-agents" id="hdrAgents"></div> |
| <div class="hdr-right"> |
| <span class="hdr-sid" id="hdrSid" style="display:none"></span> |
| <button class="btn-icon" onclick="toggleTheme()" title="Toggle theme" id="themeBtn">π</button> |
| </div> |
| </header> |
|
|
| |
| <div class="main"> |
|
|
| |
| <div class="left"> |
| <div class="tabs"> |
| <button class="tab on" data-tab="chat" onclick="switchTab('chat')"> |
| <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg> |
| Chat |
| </button> |
| <button class="tab" data-tab="agents" onclick="switchTab('agents')"> |
| <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg> |
| Agents <span class="tab-badge" id="agentBadge" style="display:none">0</span> |
| </button> |
| <button class="tab" data-tab="files" onclick="switchTab('files')"> |
| <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg> |
| Files <span class="tab-badge" id="fileBadge" style="display:none">0</span> |
| </button> |
| </div> |
|
|
| |
| <div class="panel on" id="panelChat"> |
| <div class="chat-msgs" id="chatMsgs"></div> |
| <div class="chat-input"> |
| <textarea id="chatInput" rows="2" placeholder="Describe the app you want to buildβ¦" |
| onkeydown="handleChatKey(event)"></textarea> |
| <button class="btn-send" id="btnSend" onclick="handleSend()"> |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg> |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div class="panel" id="panelAgents"> |
| <div class="feed" id="agentFeed"></div> |
| </div> |
|
|
| |
| <div class="panel" id="panelFiles"> |
| <div class="tree" id="fileTree"></div> |
| </div> |
| </div> |
|
|
| |
| <div class="right"> |
| <div class="sys-tabs" id="sysTabs"></div> |
| <div class="toolbar"> |
| <div class="toolbar-group"> |
| <button class="tb on" id="tbPreview" onclick="setView('preview')"> |
| <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg> |
| Preview |
| </button> |
| <button class="tb" id="tbCode" onclick="setView('code')"> |
| <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg> |
| Code |
| </button> |
| </div> |
| <div class="toolbar-group"> |
| <button class="tb tb-sm" id="vpDesk" onclick="setVP('100%')" title="Desktop" style="color:var(--accent)"> |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg> |
| </button> |
| <button class="tb tb-sm" id="vpTab" onclick="setVP('768px')" title="Tablet"> |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="2" width="16" height="20" rx="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg> |
| </button> |
| <button class="tb tb-sm" id="vpMob" onclick="setVP('375px')" title="Mobile"> |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="5" y="2" width="14" height="20" rx="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg> |
| </button> |
| <button class="tb tb-sm" onclick="refreshPreview()" title="Refresh"> |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg> |
| </button> |
| <button class="btn-export" id="btnExport" onclick="doExport()" style="display:none"> |
| <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> |
| Export ZIP |
| </button> |
| </div> |
| </div> |
| <div class="preview-area" id="previewArea"></div> |
| </div> |
|
|
| </div> |
|
|
| |
| <footer class="status"> |
| <div class="status-left"> |
| <span id="statusLabel"><span class="status-dot" style="background:var(--muted)"></span> Ready</span> |
| <div class="progress-dots" id="progressDots" style="display:none"></div> |
| </div> |
| <div class="status-right"> |
| <span id="statusFiles" style="display:none">π 0 files</span> |
| <span id="statusErrors" style="display:none;color:var(--error)">β 0 errors</span> |
| <span>β‘ HuggingFace CPU</span> |
| </div> |
| </footer> |
|
|
| </div> |
|
|
| <script> |
| |
| |
| |
| |
| |
| const S = { |
| sid: null, |
| status: 'idle', |
| theme: localStorage.getItem('nexus-theme') || 'dark', |
| messages: [], |
| agentFeed: [], |
| agents: { |
| research: { name: 'GLM 4.5 Air', status: 'idle', icon: 'π', color: '#00D9FF' }, |
| orchestrator: { name: 'Trinity Large', status: 'idle', icon: 'π§ ', color: '#6C63FF' }, |
| frontend: { name: 'Qwen3 Coder', status: 'idle', icon: 'π¨', color: '#22D3A8' }, |
| backend: { name: 'MiniMax M2.5', status: 'idle', icon: 'π', color: '#FFB547' }, |
| }, |
| files: {}, |
| fileTree: [], |
| selectedFile: null, |
| activeTab: 'chat', |
| viewMode: 'preview', |
| previewSystem: 'preview', |
| vpWidth: '100%', |
| errors: [], |
| appType: 'saas', |
| evtSource: null, |
| }; |
| |
| const APP_TYPES = [ |
| { id:'saas', label:'SaaS Platform', e:'πΌ' }, |
| { id:'ecommerce', label:'E-Commerce', e:'π' }, |
| { id:'marketplace',label:'Marketplace', e:'πͺ' }, |
| { id:'social', label:'Social Network', e:'π₯' }, |
| { id:'education', label:'EdTech / LMS', e:'π' }, |
| { id:'health', label:'HealthTech', e:'π₯' }, |
| { id:'finance', label:'FinTech', e:'π°' }, |
| { id:'custom', label:'Custom', e:'β‘' }, |
| ]; |
| const EXAMPLES = [ |
| "Build a project management SaaS like Linear with team workspaces, sprint boards, and issue tracking", |
| "Create an online course marketplace where instructors sell video courses with progress tracking", |
| "Build a subscription fitness app with workout plans, progress photos, and meal tracking", |
| ]; |
| const SYSTEMS = [ |
| { id:'preview', label:'Overview', e:'π' }, |
| { id:'client_portal', label:'Portal', e:'π ' }, |
| { id:'public_landing', label:'Landing', e:'π' }, |
| { id:'marketing_cms', label:'Marketing', e:'π£' }, |
| { id:'analytics_dashboard', label:'Analytics', e:'π' }, |
| { id:'admin_panel', label:'Admin', e:'π‘οΈ' }, |
| ]; |
| |
| |
| function applyTheme() { |
| document.documentElement.setAttribute('data-theme', S.theme); |
| const btn = document.getElementById('themeBtn'); |
| btn.textContent = S.theme === 'dark' ? 'βοΈ' : 'π'; |
| } |
| function toggleTheme() { |
| S.theme = S.theme === 'dark' ? 'light' : 'dark'; |
| localStorage.setItem('nexus-theme', S.theme); |
| applyTheme(); |
| } |
| |
| |
| function switchTab(id) { |
| S.activeTab = id; |
| document.querySelectorAll('.tab').forEach(t => t.classList.toggle('on', t.dataset.tab === id)); |
| document.querySelectorAll('.panel').forEach(p => p.classList.toggle('on', p.id === 'panel' + id.charAt(0).toUpperCase() + id.slice(1))); |
| if (id === 'chat') renderChat(); |
| if (id === 'agents') renderFeed(); |
| if (id === 'files') renderTree(); |
| } |
| |
| |
| async function apiGenerate(prompt, appType) { |
| const r = await fetch('/api/generate', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ prompt, app_type: appType }), |
| }); |
| if (!r.ok) throw new Error(await r.text()); |
| return r.json(); |
| } |
| async function apiFiles(sid) { |
| const r = await fetch('/api/files/' + sid); |
| if (!r.ok) throw new Error('Failed'); |
| return r.json(); |
| } |
| async function apiFix(sid, err, path) { |
| const r = await fetch('/api/fix/' + sid, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ error_message: err, file_path: path }), |
| }); |
| return r.json(); |
| } |
| async function doExport() { |
| if (!S.sid) return; |
| const r = await fetch('/api/export/' + S.sid); |
| const b = await r.blob(); |
| const u = URL.createObjectURL(b); |
| const a = document.createElement('a'); |
| a.href = u; a.download = 'nexus-' + S.sid + '.zip'; |
| document.body.appendChild(a); a.click(); document.body.removeChild(a); |
| URL.revokeObjectURL(u); |
| } |
| |
| |
| function connectSSE(sid) { |
| if (S.evtSource) S.evtSource.close(); |
| const es = new EventSource('/api/stream/' + sid); |
| S.evtSource = es; |
| |
| ['agent_start','token','file_created','agent_done','error','done'].forEach(evt => { |
| es.addEventListener(evt, (e) => handleSSE(evt, JSON.parse(e.data))); |
| }); |
| es.onerror = () => { |
| if (es.readyState === EventSource.CLOSED) { |
| updateStatus('error'); |
| } |
| }; |
| } |
| function handleSSE(type, d) { |
| switch (type) { |
| case 'agent_start': |
| if (S.agents[d.agent]) S.agents[d.agent].status = 'active'; |
| addFeedItem(d.agent, d.content, 'start'); |
| updateAgents(); updateStatus(d.content || 'Workingβ¦'); |
| break; |
| case 'token': |
| addFeedToken(d.agent, d.content); |
| break; |
| case 'file_created': |
| addFeedItem(d.agent, d.content, 'file', d.file_path); |
| if (d.file_path) { |
| S.fileTree = [...new Set([...S.fileTree, d.file_path])].sort(); |
| updateFileBadge(); |
| } |
| break; |
| case 'agent_done': |
| if (S.agents[d.agent]) S.agents[d.agent].status = 'done'; |
| addFeedItem(d.agent, d.content, 'done'); |
| updateAgents(); |
| break; |
| case 'error': |
| S.errors.push(d.content); |
| if (S.agents[d.agent]) S.agents[d.agent].status = 'error'; |
| addFeedItem(d.agent, d.content, 'error'); |
| updateAgents(); updateStatusBar(); |
| break; |
| case 'done': |
| updateStatus(d.status === 'completed' ? 'completed' : 'error'); |
| if (d.session_id) { |
| apiFiles(d.session_id).then(r => { |
| S.files = r.files || {}; |
| S.fileTree = Object.keys(S.files).sort(); |
| renderTree(); updateFileBadge(); |
| document.getElementById('btnExport').style.display = 'flex'; |
| refreshPreview(); |
| }); |
| } |
| if (S.evtSource) { S.evtSource.close(); S.evtSource = null; } |
| break; |
| } |
| if (S.activeTab === 'agents') renderFeed(); |
| } |
| |
| |
| function addFeedItem(agent, content, type, filePath) { |
| S.agentFeed.push({ agent, content, type, filePath, time: new Date().toLocaleTimeString() }); |
| updateAgentBadge(); |
| } |
| function addFeedToken(agent, token) { |
| const last = S.agentFeed[S.agentFeed.length - 1]; |
| if (last && last.agent === agent && last.type === 'stream') { |
| last.content += token; |
| } else { |
| S.agentFeed.push({ agent, content: token, type: 'stream', time: new Date().toLocaleTimeString() }); |
| } |
| updateAgentBadge(); |
| if (S.activeTab === 'agents') renderFeed(); |
| } |
| |
| |
| function renderChat() { |
| const c = document.getElementById('chatMsgs'); |
| if (S.messages.length === 0) { |
| const typeGrid = APP_TYPES.map(t => |
| `<button class="type-btn ${S.appType===t.id?'on':''}" onclick="setAppType('${t.id}')">${t.e} ${t.label}</button>` |
| ).join(''); |
| const exList = EXAMPLES.map((ex,i) => |
| `<button class="example" onclick="useExample(${i})">β¨ ${ex}</button>` |
| ).join(''); |
| c.innerHTML = `<div class="welcome"> |
| <div style="font-size:42px;margin-bottom:4px">π</div> |
| <h2>Welcome to Nexus Builder</h2> |
| <p>Describe your app idea and 4 AI agents will build it β complete with auth, payments, analytics, and admin panel.</p> |
| <div class="type-grid">${typeGrid}</div> |
| <div class="examples-label">Try an example:</div> |
| ${exList} |
| </div>`; |
| return; |
| } |
| let html = S.messages.map(m => |
| `<div class="msg msg-${m.role==='user'?'user':'ai'}"><div class="msg-bubble">${esc(m.content)}</div></div>` |
| ).join(''); |
| if (isActive()) { |
| html += `<div class="chat-loading"> |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2" class="spin"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg> |
| Agents workingβ¦<span class="typing"></span> |
| </div>`; |
| } |
| c.innerHTML = html; |
| c.scrollTop = c.scrollHeight; |
| } |
| function setAppType(id) { |
| S.appType = id; |
| renderChat(); |
| } |
| function useExample(i) { |
| document.getElementById('chatInput').value = EXAMPLES[i]; |
| document.getElementById('chatInput').focus(); |
| } |
| function handleChatKey(e) { |
| if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } |
| } |
| async function handleSend() { |
| const input = document.getElementById('chatInput'); |
| const text = input.value.trim(); |
| if (!text || isActive()) return; |
| input.value = ''; |
| |
| |
| S.agents = Object.fromEntries(Object.entries(S.agents).map(([k,v])=>[k,{...v,status:'idle'}])); |
| S.agentFeed = []; S.files = {}; S.fileTree = []; S.errors = []; |
| S.selectedFile = null; |
| updateAgents(); updateAgentBadge(); updateFileBadge(); |
| |
| S.messages.push({ role: 'user', content: text }); |
| renderChat(); |
| |
| try { |
| const res = await apiGenerate(text, S.appType); |
| S.sid = res.session_id; |
| document.getElementById('hdrSid').textContent = S.sid; |
| document.getElementById('hdrSid').style.display = ''; |
| updateStatus('connected'); |
| S.messages.push({ role: 'assistant', content: `π Generation started! Session: ${S.sid}\n\nCoordinating 4 AI agents to build your applicationβ¦` }); |
| renderChat(); |
| connectSSE(S.sid); |
| } catch (err) { |
| updateStatus('error'); |
| S.errors.push(err.message); |
| S.messages.push({ role: 'assistant', content: `β Error: ${err.message}` }); |
| renderChat(); |
| } |
| } |
| |
| |
| function renderFeed() { |
| const el = document.getElementById('agentFeed'); |
| if (S.agentFeed.length === 0) { |
| el.innerHTML = `<div class="empty-state"> |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg> |
| Agent activity will appear here |
| </div>`; |
| return; |
| } |
| const items = S.agentFeed.slice(-80).map(item => { |
| const ag = S.agents[item.agent] || { icon:'βοΈ', color:'#888' }; |
| let body = ''; |
| if (item.type === 'file') body = `<div class="feed-file">π ${esc(item.content)}</div>`; |
| else if (item.type === 'done') body = `<div class="feed-done">β
${esc(item.content)}</div>`; |
| else if (item.type === 'error') body = `<div style="color:var(--error)">β ${esc(item.content)}</div>`; |
| else if (item.type === 'stream') body = `<pre>${esc(item.content.slice(-250))}<span class="typing"></span></pre>`; |
| else body = esc(item.content); |
| return `<div class="feed-item"> |
| <span class="feed-icon">${ag.icon}</span> |
| <div class="feed-body"> |
| <div class="feed-head"> |
| <span class="feed-agent" style="color:${ag.color}">${item.agent}</span> |
| <span class="feed-time">${item.time}</span> |
| </div> |
| <div class="feed-text">${body}</div> |
| </div> |
| </div>`; |
| }); |
| el.innerHTML = items.join(''); |
| el.scrollTop = el.scrollHeight; |
| } |
| |
| |
| function renderTree() { |
| const el = document.getElementById('fileTree'); |
| if (S.fileTree.length === 0) { |
| el.innerHTML = `<div class="empty-state"> |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg> |
| Generated files will appear here |
| </div>`; |
| return; |
| } |
| const tree = buildTree(S.fileTree); |
| el.innerHTML = `<div class="tree-header">Project Files (${S.fileTree.length})</div>` + renderNode(tree, 0); |
| el.querySelectorAll('.tree-file').forEach(f => { |
| f.addEventListener('click', () => selectFile(f.dataset.path)); |
| }); |
| } |
| function buildTree(paths) { |
| const root = {}; |
| paths.forEach(p => { |
| const parts = p.split('/'); |
| let node = root; |
| parts.forEach((part, i) => { |
| if (i === parts.length - 1) node[part] = { _path: p }; |
| else { if (!node[part] || node[part]._path) node[part] = {}; node = node[part]; } |
| }); |
| }); |
| return root; |
| } |
| function renderNode(node, depth) { |
| const entries = Object.entries(node).sort(([a,av],[b,bv]) => { |
| const af = !!av._path, bf = !!bv._path; |
| if (af !== bf) return af ? 1 : -1; |
| return a.localeCompare(b); |
| }); |
| return entries.map(([name, val]) => { |
| if (val._path) { |
| const ext = name.split('.').pop() || ''; |
| const icon = fileIcon(ext); |
| const sel = S.selectedFile === val._path ? ' sel' : ''; |
| return `<div class="tree-file${sel}" data-path="${val._path}" style="padding-left:${depth*16+12}px"> |
| <span class="tree-icon">${icon}</span>${esc(name)} |
| </div>`; |
| } |
| const open = depth < 2; |
| return `<div> |
| <div class="tree-dir-name" style="padding-left:${depth*16+12}px" onclick="this.parentElement.classList.toggle('collapsed')"> |
| <span class="tree-icon">${open?'π':'π'}</span>${esc(name)} |
| </div> |
| <div class="tree-children">${renderNode(val, depth+1)}</div> |
| </div>`; |
| }).join(''); |
| } |
| function fileIcon(ext) { |
| const m = { jsx:'βοΈ', tsx:'βοΈ', js:'π', ts:'π', py:'π', css:'π¨', |
| html:'π', json:'π', sql:'ποΈ', md:'π', yml:'βοΈ', yaml:'βοΈ', txt:'π' }; |
| return m[ext] || 'π'; |
| } |
| function selectFile(path) { |
| S.selectedFile = path; |
| setView('code'); |
| renderTree(); |
| renderCodeView(); |
| } |
| |
| |
| function renderCodeView() { |
| const area = document.getElementById('previewArea'); |
| const file = S.selectedFile; |
| const content = file ? (S.files[file] || '') : ''; |
| if (!content) { |
| area.innerHTML = `<div class="preview-empty"><div class="big">π</div> |
| <h3>Code View</h3><p>Select a file from the tree to view its code</p></div>`; |
| return; |
| } |
| const lines = content.split('\n'); |
| const ext = (file.split('.').pop() || '').toLowerCase(); |
| const gutter = lines.map((_, i) => i + 1).join('\n'); |
| const highlighted = highlight(content, ext); |
| area.innerHTML = `<div class="code-view"> |
| <div class="code-head"> |
| <span class="code-name mono">${esc(file)}</span> |
| <div class="code-meta"> |
| <span class="code-lines">${lines.length} lines</span> |
| <button class="btn-icon" onclick="copyCode()" title="Copy">π</button> |
| </div> |
| </div> |
| <div class="code-body"> |
| <pre class="code-gutter mono">${gutter}</pre> |
| <pre class="code-content mono">${highlighted}</pre> |
| </div> |
| </div>`; |
| } |
| function copyCode() { |
| if (S.selectedFile && S.files[S.selectedFile]) { |
| navigator.clipboard.writeText(S.files[S.selectedFile]); |
| } |
| } |
| |
| |
| function highlight(code, ext) { |
| let h = esc(code); |
| |
| h = h.replace(/(["'`])(?:(?=(\\?))\2[\s\S])*?\1/g, '<span style="color:#22D3A8">$&</span>'); |
| |
| h = h.replace(/(\/\/.*$|#(?!.*{).*$)/gm, '<span style="color:#555577">$&</span>'); |
| h = h.replace(/(\/\*[\s\S]*?\*\/|--\s.*$)/gm, '<span style="color:#555577">$&</span>'); |
| |
| h = h.replace(/\b(const|let|var|function|return|if|else|for|while|class|import|export|from|default|async|await|try|catch|throw|new|this|def|elif|except|raise|with|yield|lambda|True|False|None|SELECT|FROM|WHERE|INSERT|INTO|VALUES|UPDATE|SET|DELETE|CREATE|TABLE|ALTER|DROP|INDEX|JOIN|LEFT|RIGHT|INNER|ON|AND|OR|NOT|NULL|PRIMARY|KEY|FOREIGN|REFERENCES|UNIQUE|DEFAULT|ENABLE|ROW|LEVEL|SECURITY|POLICY|USING|GRANT|BEGIN|COMMIT|TRIGGER|FUNCTION)\b/g, |
| '<span style="color:#6C63FF;font-weight:600">$&</span>'); |
| |
| h = h.replace(/\b(\d+\.?\d*)\b/g, '<span style="color:#FFB547">$&</span>'); |
| return h; |
| } |
| |
| |
| function renderPreview() { |
| const area = document.getElementById('previewArea'); |
| if (S.viewMode === 'code') { renderCodeView(); return; } |
| if (!S.sid) { |
| area.innerHTML = `<div class="preview-empty"><div class="big">π₯οΈ</div> |
| <h3>Live Preview</h3><p>Your generated app will be previewed here.<br>Start by describing your app in the chat.</p></div>`; |
| return; |
| } |
| const url = S.previewSystem === 'preview' |
| ? '/api/preview/' + S.sid |
| : '/api/preview/' + S.sid + '/' + S.previewSystem; |
| area.innerHTML = `<div class="preview-frame" style="width:${S.vpWidth}"> |
| <iframe src="${url}" id="previewIframe"></iframe> |
| </div>`; |
| } |
| function refreshPreview() { renderPreview(); } |
| function setVP(w) { |
| S.vpWidth = w; |
| document.querySelectorAll('#vpDesk,#vpTab,#vpMob').forEach(b => b.style.color = 'var(--muted)'); |
| if (w === '100%') document.getElementById('vpDesk').style.color = 'var(--accent)'; |
| else if (w === '768px') document.getElementById('vpTab').style.color = 'var(--accent)'; |
| else document.getElementById('vpMob').style.color = 'var(--accent)'; |
| const f = document.querySelector('.preview-frame'); |
| if (f) f.style.width = w; |
| } |
| function setView(mode) { |
| S.viewMode = mode; |
| document.getElementById('tbPreview').classList.toggle('on', mode === 'preview'); |
| document.getElementById('tbCode').classList.toggle('on', mode === 'code'); |
| if (mode === 'preview') renderPreview(); else renderCodeView(); |
| } |
| |
| |
| function renderSysTabs() { |
| document.getElementById('sysTabs').innerHTML = SYSTEMS.map(s => |
| `<button class="sys-tab ${S.previewSystem===s.id?'on':''}" onclick="setSys('${s.id}')">${s.e} ${s.label}</button>` |
| ).join(''); |
| } |
| function setSys(id) { |
| S.previewSystem = id; |
| renderSysTabs(); |
| if (S.viewMode === 'preview') renderPreview(); |
| } |
| |
| |
| function updateAgents() { |
| const el = document.getElementById('hdrAgents'); |
| el.innerHTML = Object.entries(S.agents).map(([k,a]) => { |
| const dc = a.status === 'active' ? 'dot-active' : a.status === 'done' ? 'dot-done' : a.status === 'error' ? 'dot-error' : 'dot-idle'; |
| return `<div class="hdr-agent">${a.icon}<div class="dot ${dc}"></div>${k.charAt(0).toUpperCase()+k.slice(1,4)}</div>`; |
| }).join(''); |
| } |
| |
| |
| const STATUS_MAP = { |
| idle: { label:'Ready', color:'var(--muted)' }, |
| starting: { label:'Startingβ¦', color:'var(--warn)' }, |
| connected: { label:'Connected', color:'var(--success)' }, |
| researching: { label:'π Researchingβ¦',color:'var(--accent2)' }, |
| orchestrating:{label:'π§ Blueprintingβ¦',color:'var(--accent)' }, |
| building: { label:'π Buildingβ¦', color:'var(--accent)' }, |
| merging: { label:'π¦ Mergingβ¦', color:'var(--accent2)' }, |
| fixing: { label:'π§ Fixingβ¦', color:'var(--warn)' }, |
| completed: { label:'β
Complete', color:'var(--success)' }, |
| error: { label:'β Error', color:'var(--error)' }, |
| }; |
| function updateStatus(s) { |
| S.status = s; |
| const info = STATUS_MAP[s] || { label: s, color: 'var(--muted)' }; |
| const dotColor = isActive() ? 'var(--success)' : s==='error' ? 'var(--error)' : 'var(--muted)'; |
| document.getElementById('statusLabel').innerHTML = |
| `<span class="status-dot" style="background:${dotColor}"></span> <span style="color:${info.color};font-weight:500">${info.label}</span>`; |
| updateStatusBar(); |
| const inp = document.getElementById('chatInput'); |
| const btn = document.getElementById('btnSend'); |
| inp.disabled = isActive(); |
| btn.disabled = isActive(); |
| } |
| function updateStatusBar() { |
| const fc = S.fileTree.length; |
| const ec = S.errors.length; |
| const fe = document.getElementById('statusFiles'); |
| const ee = document.getElementById('statusErrors'); |
| if (fc > 0) { fe.style.display = ''; fe.textContent = `π ${fc} files`; } |
| else fe.style.display = 'none'; |
| if (ec > 0) { ee.style.display = ''; ee.textContent = `β ${ec} errors`; } |
| else ee.style.display = 'none'; |
| } |
| function isActive() { |
| return !['idle','completed','error'].includes(S.status); |
| } |
| |
| |
| function updateAgentBadge() { |
| const b = document.getElementById('agentBadge'); |
| const n = S.agentFeed.length; |
| if (n > 0) { b.style.display = ''; b.textContent = n > 99 ? '99+' : n; } |
| else b.style.display = 'none'; |
| } |
| function updateFileBadge() { |
| const b = document.getElementById('fileBadge'); |
| const n = S.fileTree.length; |
| if (n > 0) { b.style.display = ''; b.textContent = n; } |
| else b.style.display = 'none'; |
| } |
| |
| |
| function esc(s) { |
| if (!s) return ''; |
| return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); |
| } |
| |
| |
| setInterval(() => { fetch('/api/health').catch(()=>{}); }, 30000); |
| |
| |
| applyTheme(); |
| updateAgents(); |
| renderChat(); |
| renderSysTabs(); |
| renderPreview(); |
| |
| |
| document.addEventListener('click', (e) => { |
| |
| }); |
| </script> |
| </body> |
| </html> |