Bluestrikeai commited on
Commit
45c8e24
Β·
verified Β·
1 Parent(s): 409ebb3

Create index.html

Browse files
Files changed (1) hide show
  1. index.html +892 -0
index.html ADDED
@@ -0,0 +1,892 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <title>NEXUS Builder β€” AI Full-Stack App Maker</title>
7
+ <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>">
8
+ <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">
9
+
10
+ <style>
11
+ /* ═══════════════ RESET & BASE ═══════════════ */
12
+ *{margin:0;padding:0;box-sizing:border-box}
13
+ html,body{height:100%;overflow:hidden}
14
+ body{font-family:'Inter',system-ui,sans-serif;-webkit-font-smoothing:antialiased}
15
+ pre,code,.mono{font-family:'JetBrains Mono','Fira Code',monospace}
16
+ ::selection{background:var(--accent);color:#fff}
17
+ :focus-visible{outline:2px solid var(--accent);outline-offset:2px}
18
+
19
+ /* ═══════════════ SCROLLBAR ═══════════════ */
20
+ ::-webkit-scrollbar{width:5px;height:5px}
21
+ ::-webkit-scrollbar-track{background:transparent}
22
+ ::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
23
+ ::-webkit-scrollbar-thumb:hover{background:var(--muted)}
24
+
25
+ /* ═══════════════ THEME VARS ═══════════════ */
26
+ :root{
27
+ --bg:#0A0A0F;--surface:#111118;--border:#1E1E2E;
28
+ --accent:#6C63FF;--accent2:#00D9FF;
29
+ --text:#F0F0FF;--muted:#8888AA;
30
+ --success:#22D3A8;--error:#FF4D6D;--warn:#FFB547;
31
+ --glass:rgba(17,17,24,.75);
32
+ }
33
+ [data-theme="light"]{
34
+ --bg:#F8F8FC;--surface:#FFFFFF;--border:#E0E0EF;
35
+ --accent:#5B53E8;--accent2:#0099CC;
36
+ --text:#0A0A1A;--muted:#666688;
37
+ --success:#16A085;--error:#E74C6F;--warn:#E6A030;
38
+ --glass:rgba(255,255,255,.8);
39
+ }
40
+
41
+ body{background:var(--bg);color:var(--text)}
42
+
43
+ /* ═══════════════ LAYOUT ═══════════════ */
44
+ #app{display:flex;flex-direction:column;height:100vh;width:100vw}
45
+
46
+ /* ── HEADER ── */
47
+ .hdr{display:flex;align-items:center;justify-content:space-between;
48
+ padding:0 16px;height:48px;border-bottom:1px solid var(--border);
49
+ background:var(--glass);backdrop-filter:blur(12px);z-index:50;flex-shrink:0}
50
+ .hdr-logo{display:flex;align-items:center;gap:8px}
51
+ .hdr-logo svg{width:22px;height:22px;color:var(--accent)}
52
+ .hdr-logo span{font-size:18px;font-weight:700;letter-spacing:-.5px;
53
+ background:linear-gradient(135deg,var(--accent),var(--accent2));
54
+ -webkit-background-clip:text;-webkit-text-fill-color:transparent}
55
+ .hdr-badge{font-size:10px;padding:2px 8px;border-radius:20px;font-weight:600;
56
+ background:color-mix(in srgb,var(--accent) 15%,transparent);color:var(--accent)}
57
+ .hdr-agents{display:flex;align-items:center;gap:14px}
58
+ .hdr-agent{display:flex;align-items:center;gap:5px;font-size:11px;color:var(--muted)}
59
+ .dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}
60
+ .dot-idle{background:#555}.dot-active{background:var(--accent2);animation:pulse 1.4s infinite}
61
+ .dot-done{background:var(--success)}.dot-error{background:var(--error)}
62
+ @keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.5;transform:scale(1.4)}}
63
+ .hdr-right{display:flex;align-items:center;gap:8px}
64
+ .hdr-sid{font-size:10px;padding:3px 8px;border-radius:6px;border:1px solid var(--border);
65
+ background:var(--surface);color:var(--muted)}
66
+ .btn-icon{background:none;border:none;cursor:pointer;padding:6px;border-radius:8px;
67
+ color:var(--muted);transition:.2s}
68
+ .btn-icon:hover{color:var(--text);background:var(--surface)}
69
+
70
+ /* ── MAIN ── */
71
+ .main{display:flex;flex:1;overflow:hidden}
72
+
73
+ /* ── LEFT PANEL ── */
74
+ .left{width:420px;min-width:320px;display:flex;flex-direction:column;
75
+ border-right:1px solid var(--border);background:var(--bg);flex-shrink:0}
76
+ .tabs{display:flex;border-bottom:1px solid var(--border);flex-shrink:0}
77
+ .tab{flex:1;display:flex;align-items:center;justify-content:center;gap:5px;
78
+ padding:9px 0;font-size:11px;font-weight:500;cursor:pointer;border:none;
79
+ background:none;color:var(--muted);position:relative;transition:.2s}
80
+ .tab.on{color:var(--accent);background:var(--surface)}
81
+ .tab.on::after{content:'';position:absolute;bottom:0;left:0;right:0;height:2px;
82
+ background:var(--accent)}
83
+ .tab-badge{margin-left:3px;width:16px;height:16px;font-size:9px;display:flex;
84
+ align-items:center;justify-content:center;border-radius:50%;
85
+ background:color-mix(in srgb,var(--accent) 20%,transparent);color:var(--accent)}
86
+ .panel{flex:1;overflow:hidden;display:none;flex-direction:column}
87
+ .panel.on{display:flex}
88
+
89
+ /* ── CHAT ── */
90
+ .chat-msgs{flex:1;overflow-y:auto;padding:14px}
91
+ .msg{margin-bottom:12px;display:flex;animation:fadeIn .3s}
92
+ .msg-user{justify-content:flex-end}
93
+ .msg-ai{justify-content:flex-start}
94
+ .msg-bubble{max-width:85%;padding:10px 14px;border-radius:16px;font-size:13px;
95
+ line-height:1.55;white-space:pre-wrap;word-break:break-word}
96
+ .msg-user .msg-bubble{background:var(--accent);color:#fff;border-bottom-right-radius:4px}
97
+ .msg-ai .msg-bubble{background:var(--surface);border:1px solid var(--border);
98
+ border-bottom-left-radius:4px}
99
+ .chat-loading{display:flex;align-items:center;gap:8px;padding:10px 14px;
100
+ background:var(--surface);border:1px solid var(--border);border-radius:16px;
101
+ font-size:12px;color:var(--muted);width:fit-content;animation:fadeIn .3s}
102
+ .typing{display:inline-block;width:2px;height:14px;background:var(--accent);
103
+ margin-left:3px;animation:blink 1s infinite;vertical-align:text-bottom}
104
+ @keyframes blink{0%,50%{opacity:1}51%,100%{opacity:0}}
105
+
106
+ .welcome{display:flex;flex-direction:column;align-items:center;justify-content:center;
107
+ height:100%;text-align:center;padding:20px}
108
+ .welcome h2{font-size:19px;font-weight:700;margin:10px 0 6px;
109
+ background:linear-gradient(135deg,var(--accent),var(--accent2));
110
+ -webkit-background-clip:text;-webkit-text-fill-color:transparent}
111
+ .welcome p{font-size:12px;color:var(--muted);max-width:380px;margin-bottom:18px;line-height:1.5}
112
+ .type-btn{width:100%;text-align:left;padding:7px 12px;border-radius:8px;border:none;
113
+ cursor:pointer;font-size:11px;transition:.2s;background:transparent;color:var(--muted)}
114
+ .type-btn:hover,.type-btn.on{background:color-mix(in srgb,var(--accent) 12%,transparent);
115
+ color:var(--accent)}
116
+ .type-grid{display:grid;grid-template-columns:1fr 1fr;gap:3px;width:100%;
117
+ margin-bottom:14px;padding:6px;border-radius:10px;border:1px solid var(--border);
118
+ background:var(--surface)}
119
+ .example{width:100%;text-align:left;padding:10px 12px;border-radius:10px;
120
+ border:1px solid var(--border);background:var(--surface);color:var(--muted);
121
+ font-size:11px;cursor:pointer;transition:.2s;margin-bottom:6px;line-height:1.4}
122
+ .example:hover{border-color:color-mix(in srgb,var(--accent) 40%,transparent);
123
+ transform:translateY(-1px)}
124
+ .examples-label{font-size:10px;font-weight:600;color:var(--muted);
125
+ margin-bottom:6px;text-transform:uppercase;letter-spacing:.5px}
126
+
127
+ .chat-input{padding:10px;border-top:1px solid var(--border);flex-shrink:0;display:flex;gap:8px;align-items:flex-end}
128
+ .chat-input textarea{flex:1;resize:none;border-radius:12px;padding:10px 14px;
129
+ font-size:13px;border:1px solid var(--border);background:var(--surface);
130
+ color:var(--text);outline:none;font-family:inherit;line-height:1.5;min-height:44px;max-height:120px}
131
+ .chat-input textarea:focus{border-color:var(--accent)}
132
+ .btn-send{width:42px;height:42px;border-radius:12px;border:none;cursor:pointer;
133
+ background:var(--accent);color:#fff;display:flex;align-items:center;justify-content:center;
134
+ transition:.2s;flex-shrink:0}
135
+ .btn-send:hover{transform:scale(1.05)}.btn-send:disabled{opacity:.35;transform:none}
136
+ .btn-send svg{width:18px;height:18px}
137
+
138
+ /* ── AGENT FEED ── */
139
+ .feed{flex:1;overflow-y:auto;padding:10px}
140
+ .feed-item{display:flex;gap:8px;padding:8px 10px;border-radius:10px;margin-bottom:6px;
141
+ font-size:11px;border:1px solid var(--border);background:var(--surface);animation:fadeIn .3s}
142
+ .feed-icon{font-size:15px;flex-shrink:0;margin-top:1px}
143
+ .feed-body{flex:1;min-width:0}
144
+ .feed-head{display:flex;justify-content:space-between;margin-bottom:3px}
145
+ .feed-agent{font-weight:600;text-transform:capitalize}
146
+ .feed-time{font-size:9px;color:var(--muted)}
147
+ .feed-text{color:var(--muted);word-break:break-word}
148
+ .feed-text pre{white-space:pre-wrap;max-height:80px;overflow-y:auto;
149
+ font-size:10px;line-height:1.5;margin-top:2px}
150
+ .feed-file{display:flex;align-items:center;gap:5px;color:var(--success)}
151
+ .feed-done{display:flex;align-items:center;gap:5px}
152
+ .empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;
153
+ height:100%;color:var(--muted);font-size:13px;text-align:center;padding:20px;opacity:.6}
154
+ .empty-state svg{width:40px;height:40px;margin-bottom:10px;opacity:.4}
155
+
156
+ /* ── FILE TREE ── */
157
+ .tree{flex:1;overflow-y:auto;padding:6px 0}
158
+ .tree-header{padding:4px 12px;font-size:9px;font-weight:700;text-transform:uppercase;
159
+ letter-spacing:.6px;color:var(--muted)}
160
+ .tree-dir-name,.tree-file{display:flex;align-items:center;gap:6px;padding:3px 8px;
161
+ cursor:pointer;font-size:11px;border-radius:4px;transition:.15s;user-select:none}
162
+ .tree-dir-name{font-weight:500;color:var(--text)}
163
+ .tree-dir-name:hover{background:color-mix(in srgb,var(--accent) 8%,transparent)}
164
+ .tree-file{color:var(--muted)}
165
+ .tree-file:hover{background:color-mix(in srgb,var(--accent) 8%,transparent);color:var(--text)}
166
+ .tree-file.sel{background:color-mix(in srgb,var(--accent) 15%,transparent);color:var(--accent)}
167
+ .tree-children{overflow:hidden}
168
+ .tree-icon{flex-shrink:0;font-size:13px}
169
+
170
+ /* ── RIGHT PANEL ── */
171
+ .right{flex:1;display:flex;flex-direction:column;overflow:hidden;background:var(--bg)}
172
+ .sys-tabs{display:flex;gap:2px;padding:4px 8px;border-bottom:1px solid var(--border);
173
+ overflow-x:auto;flex-shrink:0}
174
+ .sys-tab{display:flex;align-items:center;gap:5px;padding:6px 12px;border-radius:8px;
175
+ font-size:11px;font-weight:500;border:none;cursor:pointer;background:none;
176
+ color:var(--muted);white-space:nowrap;transition:.2s}
177
+ .sys-tab.on{background:color-mix(in srgb,var(--accent) 12%,transparent);color:var(--accent)}
178
+ .toolbar{display:flex;justify-content:space-between;align-items:center;
179
+ padding:6px 12px;border-bottom:1px solid var(--border);flex-shrink:0}
180
+ .toolbar-group{display:flex;align-items:center;gap:3px}
181
+ .tb{display:flex;align-items:center;gap:5px;padding:5px 10px;border-radius:7px;
182
+ font-size:11px;font-weight:500;border:none;cursor:pointer;background:none;
183
+ color:var(--muted);transition:.2s}
184
+ .tb.on{background:color-mix(in srgb,var(--accent) 12%,transparent);color:var(--accent)}
185
+ .tb-sm{padding:5px;border-radius:6px}
186
+ .btn-export{display:flex;align-items:center;gap:5px;padding:5px 12px;border-radius:7px;
187
+ font-size:11px;font-weight:500;border:none;cursor:pointer;background:var(--accent);
188
+ color:#fff;transition:.2s}
189
+ .btn-export:hover{transform:scale(1.05)}
190
+
191
+ .preview-area{flex:1;overflow:hidden;display:flex;align-items:center;
192
+ justify-content:center;padding:16px;background:#0D0D12}
193
+ [data-theme="light"] .preview-area{background:#f0f0f5}
194
+ .preview-frame{border:1px solid var(--border);border-radius:10px;overflow:hidden;
195
+ height:100%;transition:width .3s;background:#fff}
196
+ .preview-frame iframe{width:100%;height:100%;border:none}
197
+ .preview-empty{text-align:center;color:var(--muted)}
198
+ .preview-empty .big{font-size:48px;margin-bottom:12px;opacity:.2}
199
+ .preview-empty h3{font-size:16px;font-weight:600;color:var(--text);margin-bottom:6px}
200
+ .preview-empty p{font-size:12px;line-height:1.5}
201
+
202
+ /* ── CODE VIEW ── */
203
+ .code-view{width:100%;height:100%;display:flex;flex-direction:column;border-radius:10px;
204
+ overflow:hidden;border:1px solid var(--border)}
205
+ .code-head{display:flex;justify-content:space-between;align-items:center;
206
+ padding:6px 14px;background:var(--bg);border-bottom:1px solid var(--border)}
207
+ .code-name{font-size:11px;color:var(--muted)}
208
+ .code-meta{display:flex;align-items:center;gap:8px}
209
+ .code-lines{font-size:9px;color:var(--muted)}
210
+ .code-body{flex:1;overflow:auto;display:flex;background:#0D0D14;font-size:12px;line-height:1.65}
211
+ [data-theme="light"] .code-body{background:#fafafe}
212
+ .code-gutter{flex-shrink:0;text-align:right;padding:10px 12px 10px 14px;
213
+ color:#333355;user-select:none}
214
+ [data-theme="light"] .code-gutter{color:#bbbbcc}
215
+ .code-content{flex:1;padding:10px 14px 10px 0;overflow-x:auto;white-space:pre;tab-size:2}
216
+
217
+ /* ── STATUS BAR ── */
218
+ .status{display:flex;justify-content:space-between;align-items:center;
219
+ padding:0 14px;height:26px;border-top:1px solid var(--border);
220
+ background:var(--surface);font-size:10px;color:var(--muted);flex-shrink:0}
221
+ .status-left,.status-right{display:flex;align-items:center;gap:12px}
222
+ .status-dot{width:6px;height:6px;border-radius:50%;margin-right:4px;display:inline-block}
223
+ .progress-dots{display:flex;align-items:center;gap:3px}
224
+ .pdot{width:5px;height:5px;border-radius:50%;background:var(--border)}
225
+ .pdot.on{background:var(--accent)}.pdot.past{background:var(--success)}
226
+ .pline{width:12px;height:1px;background:var(--border)}
227
+
228
+ /* ═══════════════ ANIMATIONS ═══════════════ */
229
+ @keyframes fadeIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
230
+ @keyframes spin{to{transform:rotate(360deg)}}
231
+ .spin{animation:spin .8s linear infinite}
232
+
233
+ /* ═══════════════ RESPONSIVE ═══════════════ */
234
+ @media(max-width:768px){
235
+ .left{width:100%;border-right:none;border-bottom:1px solid var(--border)}
236
+ .main{flex-direction:column}
237
+ .hdr-agents{display:none}
238
+ }
239
+ </style>
240
+ </head>
241
+
242
+ <body>
243
+ <div id="app">
244
+
245
+ <!-- ═══════════ HEADER ═══════════ -->
246
+ <header class="hdr">
247
+ <div class="hdr-logo">
248
+ <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>
249
+ <span class="mono">NEXUS</span>
250
+ <span class="hdr-badge">BUILDER</span>
251
+ </div>
252
+ <div class="hdr-agents" id="hdrAgents"></div>
253
+ <div class="hdr-right">
254
+ <span class="hdr-sid" id="hdrSid" style="display:none"></span>
255
+ <button class="btn-icon" onclick="toggleTheme()" title="Toggle theme" id="themeBtn">πŸŒ™</button>
256
+ </div>
257
+ </header>
258
+
259
+ <!-- ═══════════ MAIN ═══════════ -->
260
+ <div class="main">
261
+
262
+ <!-- LEFT PANEL -->
263
+ <div class="left">
264
+ <div class="tabs">
265
+ <button class="tab on" data-tab="chat" onclick="switchTab('chat')">
266
+ <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>
267
+ Chat
268
+ </button>
269
+ <button class="tab" data-tab="agents" onclick="switchTab('agents')">
270
+ <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>
271
+ Agents <span class="tab-badge" id="agentBadge" style="display:none">0</span>
272
+ </button>
273
+ <button class="tab" data-tab="files" onclick="switchTab('files')">
274
+ <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>
275
+ Files <span class="tab-badge" id="fileBadge" style="display:none">0</span>
276
+ </button>
277
+ </div>
278
+
279
+ <!-- Chat Panel -->
280
+ <div class="panel on" id="panelChat">
281
+ <div class="chat-msgs" id="chatMsgs"></div>
282
+ <div class="chat-input">
283
+ <textarea id="chatInput" rows="2" placeholder="Describe the app you want to build…"
284
+ onkeydown="handleChatKey(event)"></textarea>
285
+ <button class="btn-send" id="btnSend" onclick="handleSend()">
286
+ <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>
287
+ </button>
288
+ </div>
289
+ </div>
290
+
291
+ <!-- Agent Feed Panel -->
292
+ <div class="panel" id="panelAgents">
293
+ <div class="feed" id="agentFeed"></div>
294
+ </div>
295
+
296
+ <!-- File Tree Panel -->
297
+ <div class="panel" id="panelFiles">
298
+ <div class="tree" id="fileTree"></div>
299
+ </div>
300
+ </div>
301
+
302
+ <!-- RIGHT PANEL -->
303
+ <div class="right">
304
+ <div class="sys-tabs" id="sysTabs"></div>
305
+ <div class="toolbar">
306
+ <div class="toolbar-group">
307
+ <button class="tb on" id="tbPreview" onclick="setView('preview')">
308
+ <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>
309
+ Preview
310
+ </button>
311
+ <button class="tb" id="tbCode" onclick="setView('code')">
312
+ <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>
313
+ Code
314
+ </button>
315
+ </div>
316
+ <div class="toolbar-group">
317
+ <button class="tb tb-sm" id="vpDesk" onclick="setVP('100%')" title="Desktop" style="color:var(--accent)">
318
+ <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>
319
+ </button>
320
+ <button class="tb tb-sm" id="vpTab" onclick="setVP('768px')" title="Tablet">
321
+ <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>
322
+ </button>
323
+ <button class="tb tb-sm" id="vpMob" onclick="setVP('375px')" title="Mobile">
324
+ <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>
325
+ </button>
326
+ <button class="tb tb-sm" onclick="refreshPreview()" title="Refresh">
327
+ <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>
328
+ </button>
329
+ <button class="btn-export" id="btnExport" onclick="doExport()" style="display:none">
330
+ <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>
331
+ Export ZIP
332
+ </button>
333
+ </div>
334
+ </div>
335
+ <div class="preview-area" id="previewArea"></div>
336
+ </div>
337
+
338
+ </div><!-- /main -->
339
+
340
+ <!-- ═══════════ STATUS BAR ═══════════ -->
341
+ <footer class="status">
342
+ <div class="status-left">
343
+ <span id="statusLabel"><span class="status-dot" style="background:var(--muted)"></span> Ready</span>
344
+ <div class="progress-dots" id="progressDots" style="display:none"></div>
345
+ </div>
346
+ <div class="status-right">
347
+ <span id="statusFiles" style="display:none">πŸ“ 0 files</span>
348
+ <span id="statusErrors" style="display:none;color:var(--error)">⚠ 0 errors</span>
349
+ <span>⚑ HuggingFace CPU</span>
350
+ </div>
351
+ </footer>
352
+
353
+ </div><!-- /app -->
354
+
355
+ <script>
356
+ /* ═══════════════════════════════════════════════════════════
357
+ NEXUS BUILDER β€” FRONTEND (Vanilla JS)
358
+ ═══════════════════════════════════════════════════════════ */
359
+
360
+ // ─── STATE ───────────────────────────────────────────────
361
+ const S = {
362
+ sid: null,
363
+ status: 'idle',
364
+ theme: localStorage.getItem('nexus-theme') || 'dark',
365
+ messages: [],
366
+ agentFeed: [],
367
+ agents: {
368
+ research: { name: 'GLM 4.5 Air', status: 'idle', icon: '🌐', color: '#00D9FF' },
369
+ orchestrator: { name: 'Trinity Large', status: 'idle', icon: '🧠', color: '#6C63FF' },
370
+ frontend: { name: 'Qwen3 Coder', status: 'idle', icon: '🎨', color: '#22D3A8' },
371
+ backend: { name: 'MiniMax M2.5', status: 'idle', icon: 'πŸ”', color: '#FFB547' },
372
+ },
373
+ files: {},
374
+ fileTree: [],
375
+ selectedFile: null,
376
+ activeTab: 'chat',
377
+ viewMode: 'preview',
378
+ previewSystem: 'preview',
379
+ vpWidth: '100%',
380
+ errors: [],
381
+ appType: 'saas',
382
+ evtSource: null,
383
+ };
384
+
385
+ const APP_TYPES = [
386
+ { id:'saas', label:'SaaS Platform', e:'πŸ’Ό' },
387
+ { id:'ecommerce', label:'E-Commerce', e:'πŸ›’' },
388
+ { id:'marketplace',label:'Marketplace', e:'πŸͺ' },
389
+ { id:'social', label:'Social Network', e:'πŸ‘₯' },
390
+ { id:'education', label:'EdTech / LMS', e:'πŸ“š' },
391
+ { id:'health', label:'HealthTech', e:'πŸ₯' },
392
+ { id:'finance', label:'FinTech', e:'πŸ’°' },
393
+ { id:'custom', label:'Custom', e:'⚑' },
394
+ ];
395
+ const EXAMPLES = [
396
+ "Build a project management SaaS like Linear with team workspaces, sprint boards, and issue tracking",
397
+ "Create an online course marketplace where instructors sell video courses with progress tracking",
398
+ "Build a subscription fitness app with workout plans, progress photos, and meal tracking",
399
+ ];
400
+ const SYSTEMS = [
401
+ { id:'preview', label:'Overview', e:'πŸ“‹' },
402
+ { id:'client_portal', label:'Portal', e:'🏠' },
403
+ { id:'public_landing', label:'Landing', e:'🌐' },
404
+ { id:'marketing_cms', label:'Marketing', e:'πŸ“£' },
405
+ { id:'analytics_dashboard', label:'Analytics', e:'πŸ“Š' },
406
+ { id:'admin_panel', label:'Admin', e:'πŸ›‘οΈ' },
407
+ ];
408
+
409
+ // ─── THEME ───────────────────────────────────────────────
410
+ function applyTheme() {
411
+ document.documentElement.setAttribute('data-theme', S.theme);
412
+ const btn = document.getElementById('themeBtn');
413
+ btn.textContent = S.theme === 'dark' ? 'β˜€οΈ' : 'πŸŒ™';
414
+ }
415
+ function toggleTheme() {
416
+ S.theme = S.theme === 'dark' ? 'light' : 'dark';
417
+ localStorage.setItem('nexus-theme', S.theme);
418
+ applyTheme();
419
+ }
420
+
421
+ // ─── TABS ────────────────────────────────────────────────
422
+ function switchTab(id) {
423
+ S.activeTab = id;
424
+ document.querySelectorAll('.tab').forEach(t => t.classList.toggle('on', t.dataset.tab === id));
425
+ document.querySelectorAll('.panel').forEach(p => p.classList.toggle('on', p.id === 'panel' + id.charAt(0).toUpperCase() + id.slice(1)));
426
+ if (id === 'chat') renderChat();
427
+ if (id === 'agents') renderFeed();
428
+ if (id === 'files') renderTree();
429
+ }
430
+
431
+ // ─── API ─────────────────────────────────────────────────
432
+ async function apiGenerate(prompt, appType) {
433
+ const r = await fetch('/api/generate', {
434
+ method: 'POST',
435
+ headers: { 'Content-Type': 'application/json' },
436
+ body: JSON.stringify({ prompt, app_type: appType }),
437
+ });
438
+ if (!r.ok) throw new Error(await r.text());
439
+ return r.json();
440
+ }
441
+ async function apiFiles(sid) {
442
+ const r = await fetch('/api/files/' + sid);
443
+ if (!r.ok) throw new Error('Failed');
444
+ return r.json();
445
+ }
446
+ async function apiFix(sid, err, path) {
447
+ const r = await fetch('/api/fix/' + sid, {
448
+ method: 'POST',
449
+ headers: { 'Content-Type': 'application/json' },
450
+ body: JSON.stringify({ error_message: err, file_path: path }),
451
+ });
452
+ return r.json();
453
+ }
454
+ async function doExport() {
455
+ if (!S.sid) return;
456
+ const r = await fetch('/api/export/' + S.sid);
457
+ const b = await r.blob();
458
+ const u = URL.createObjectURL(b);
459
+ const a = document.createElement('a');
460
+ a.href = u; a.download = 'nexus-' + S.sid + '.zip';
461
+ document.body.appendChild(a); a.click(); document.body.removeChild(a);
462
+ URL.revokeObjectURL(u);
463
+ }
464
+
465
+ // ─── SSE ─────────────────────────────────────────────────
466
+ function connectSSE(sid) {
467
+ if (S.evtSource) S.evtSource.close();
468
+ const es = new EventSource('/api/stream/' + sid);
469
+ S.evtSource = es;
470
+
471
+ ['agent_start','token','file_created','agent_done','error','done'].forEach(evt => {
472
+ es.addEventListener(evt, (e) => handleSSE(evt, JSON.parse(e.data)));
473
+ });
474
+ es.onerror = () => {
475
+ if (es.readyState === EventSource.CLOSED) {
476
+ updateStatus('error');
477
+ }
478
+ };
479
+ }
480
+ function handleSSE(type, d) {
481
+ switch (type) {
482
+ case 'agent_start':
483
+ if (S.agents[d.agent]) S.agents[d.agent].status = 'active';
484
+ addFeedItem(d.agent, d.content, 'start');
485
+ updateAgents(); updateStatus(d.content || 'Working…');
486
+ break;
487
+ case 'token':
488
+ addFeedToken(d.agent, d.content);
489
+ break;
490
+ case 'file_created':
491
+ addFeedItem(d.agent, d.content, 'file', d.file_path);
492
+ if (d.file_path) {
493
+ S.fileTree = [...new Set([...S.fileTree, d.file_path])].sort();
494
+ updateFileBadge();
495
+ }
496
+ break;
497
+ case 'agent_done':
498
+ if (S.agents[d.agent]) S.agents[d.agent].status = 'done';
499
+ addFeedItem(d.agent, d.content, 'done');
500
+ updateAgents();
501
+ break;
502
+ case 'error':
503
+ S.errors.push(d.content);
504
+ if (S.agents[d.agent]) S.agents[d.agent].status = 'error';
505
+ addFeedItem(d.agent, d.content, 'error');
506
+ updateAgents(); updateStatusBar();
507
+ break;
508
+ case 'done':
509
+ updateStatus(d.status === 'completed' ? 'completed' : 'error');
510
+ if (d.session_id) {
511
+ apiFiles(d.session_id).then(r => {
512
+ S.files = r.files || {};
513
+ S.fileTree = Object.keys(S.files).sort();
514
+ renderTree(); updateFileBadge();
515
+ document.getElementById('btnExport').style.display = 'flex';
516
+ refreshPreview();
517
+ });
518
+ }
519
+ if (S.evtSource) { S.evtSource.close(); S.evtSource = null; }
520
+ break;
521
+ }
522
+ if (S.activeTab === 'agents') renderFeed();
523
+ }
524
+
525
+ // ─── FEED HELPERS ────────────────────────────────────────
526
+ function addFeedItem(agent, content, type, filePath) {
527
+ S.agentFeed.push({ agent, content, type, filePath, time: new Date().toLocaleTimeString() });
528
+ updateAgentBadge();
529
+ }
530
+ function addFeedToken(agent, token) {
531
+ const last = S.agentFeed[S.agentFeed.length - 1];
532
+ if (last && last.agent === agent && last.type === 'stream') {
533
+ last.content += token;
534
+ } else {
535
+ S.agentFeed.push({ agent, content: token, type: 'stream', time: new Date().toLocaleTimeString() });
536
+ }
537
+ updateAgentBadge();
538
+ if (S.activeTab === 'agents') renderFeed();
539
+ }
540
+
541
+ // ─── CHAT ────────────────────────────────────────────────
542
+ function renderChat() {
543
+ const c = document.getElementById('chatMsgs');
544
+ if (S.messages.length === 0) {
545
+ const typeGrid = APP_TYPES.map(t =>
546
+ `<button class="type-btn ${S.appType===t.id?'on':''}" onclick="setAppType('${t.id}')">${t.e} ${t.label}</button>`
547
+ ).join('');
548
+ const exList = EXAMPLES.map((ex,i) =>
549
+ `<button class="example" onclick="useExample(${i})">✨ ${ex}</button>`
550
+ ).join('');
551
+ c.innerHTML = `<div class="welcome">
552
+ <div style="font-size:42px;margin-bottom:4px">πŸš€</div>
553
+ <h2>Welcome to Nexus Builder</h2>
554
+ <p>Describe your app idea and 4 AI agents will build it β€” complete with auth, payments, analytics, and admin panel.</p>
555
+ <div class="type-grid">${typeGrid}</div>
556
+ <div class="examples-label">Try an example:</div>
557
+ ${exList}
558
+ </div>`;
559
+ return;
560
+ }
561
+ let html = S.messages.map(m =>
562
+ `<div class="msg msg-${m.role==='user'?'user':'ai'}"><div class="msg-bubble">${esc(m.content)}</div></div>`
563
+ ).join('');
564
+ if (isActive()) {
565
+ html += `<div class="chat-loading">
566
+ <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>
567
+ Agents working…<span class="typing"></span>
568
+ </div>`;
569
+ }
570
+ c.innerHTML = html;
571
+ c.scrollTop = c.scrollHeight;
572
+ }
573
+ function setAppType(id) {
574
+ S.appType = id;
575
+ renderChat();
576
+ }
577
+ function useExample(i) {
578
+ document.getElementById('chatInput').value = EXAMPLES[i];
579
+ document.getElementById('chatInput').focus();
580
+ }
581
+ function handleChatKey(e) {
582
+ if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); }
583
+ }
584
+ async function handleSend() {
585
+ const input = document.getElementById('chatInput');
586
+ const text = input.value.trim();
587
+ if (!text || isActive()) return;
588
+ input.value = '';
589
+
590
+ // Reset
591
+ S.agents = Object.fromEntries(Object.entries(S.agents).map(([k,v])=>[k,{...v,status:'idle'}]));
592
+ S.agentFeed = []; S.files = {}; S.fileTree = []; S.errors = [];
593
+ S.selectedFile = null;
594
+ updateAgents(); updateAgentBadge(); updateFileBadge();
595
+
596
+ S.messages.push({ role: 'user', content: text });
597
+ renderChat();
598
+
599
+ try {
600
+ const res = await apiGenerate(text, S.appType);
601
+ S.sid = res.session_id;
602
+ document.getElementById('hdrSid').textContent = S.sid;
603
+ document.getElementById('hdrSid').style.display = '';
604
+ updateStatus('connected');
605
+ S.messages.push({ role: 'assistant', content: `πŸš€ Generation started! Session: ${S.sid}\n\nCoordinating 4 AI agents to build your application…` });
606
+ renderChat();
607
+ connectSSE(S.sid);
608
+ } catch (err) {
609
+ updateStatus('error');
610
+ S.errors.push(err.message);
611
+ S.messages.push({ role: 'assistant', content: `❌ Error: ${err.message}` });
612
+ renderChat();
613
+ }
614
+ }
615
+
616
+ // ─── AGENT FEED RENDER ───────────────────────���──────────
617
+ function renderFeed() {
618
+ const el = document.getElementById('agentFeed');
619
+ if (S.agentFeed.length === 0) {
620
+ el.innerHTML = `<div class="empty-state">
621
+ <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>
622
+ Agent activity will appear here
623
+ </div>`;
624
+ return;
625
+ }
626
+ const items = S.agentFeed.slice(-80).map(item => {
627
+ const ag = S.agents[item.agent] || { icon:'βš™οΈ', color:'#888' };
628
+ let body = '';
629
+ if (item.type === 'file') body = `<div class="feed-file">πŸ“„ ${esc(item.content)}</div>`;
630
+ else if (item.type === 'done') body = `<div class="feed-done">βœ… ${esc(item.content)}</div>`;
631
+ else if (item.type === 'error') body = `<div style="color:var(--error)">❌ ${esc(item.content)}</div>`;
632
+ else if (item.type === 'stream') body = `<pre>${esc(item.content.slice(-250))}<span class="typing"></span></pre>`;
633
+ else body = esc(item.content);
634
+ return `<div class="feed-item">
635
+ <span class="feed-icon">${ag.icon}</span>
636
+ <div class="feed-body">
637
+ <div class="feed-head">
638
+ <span class="feed-agent" style="color:${ag.color}">${item.agent}</span>
639
+ <span class="feed-time">${item.time}</span>
640
+ </div>
641
+ <div class="feed-text">${body}</div>
642
+ </div>
643
+ </div>`;
644
+ });
645
+ el.innerHTML = items.join('');
646
+ el.scrollTop = el.scrollHeight;
647
+ }
648
+
649
+ // ─── FILE TREE RENDER ───────────────────────────────────
650
+ function renderTree() {
651
+ const el = document.getElementById('fileTree');
652
+ if (S.fileTree.length === 0) {
653
+ el.innerHTML = `<div class="empty-state">
654
+ <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>
655
+ Generated files will appear here
656
+ </div>`;
657
+ return;
658
+ }
659
+ const tree = buildTree(S.fileTree);
660
+ el.innerHTML = `<div class="tree-header">Project Files (${S.fileTree.length})</div>` + renderNode(tree, 0);
661
+ el.querySelectorAll('.tree-file').forEach(f => {
662
+ f.addEventListener('click', () => selectFile(f.dataset.path));
663
+ });
664
+ }
665
+ function buildTree(paths) {
666
+ const root = {};
667
+ paths.forEach(p => {
668
+ const parts = p.split('/');
669
+ let node = root;
670
+ parts.forEach((part, i) => {
671
+ if (i === parts.length - 1) node[part] = { _path: p };
672
+ else { if (!node[part] || node[part]._path) node[part] = {}; node = node[part]; }
673
+ });
674
+ });
675
+ return root;
676
+ }
677
+ function renderNode(node, depth) {
678
+ const entries = Object.entries(node).sort(([a,av],[b,bv]) => {
679
+ const af = !!av._path, bf = !!bv._path;
680
+ if (af !== bf) return af ? 1 : -1;
681
+ return a.localeCompare(b);
682
+ });
683
+ return entries.map(([name, val]) => {
684
+ if (val._path) {
685
+ const ext = name.split('.').pop() || '';
686
+ const icon = fileIcon(ext);
687
+ const sel = S.selectedFile === val._path ? ' sel' : '';
688
+ return `<div class="tree-file${sel}" data-path="${val._path}" style="padding-left:${depth*16+12}px">
689
+ <span class="tree-icon">${icon}</span>${esc(name)}
690
+ </div>`;
691
+ }
692
+ const open = depth < 2;
693
+ return `<div>
694
+ <div class="tree-dir-name" style="padding-left:${depth*16+12}px" onclick="this.parentElement.classList.toggle('collapsed')">
695
+ <span class="tree-icon">${open?'πŸ“‚':'πŸ“'}</span>${esc(name)}
696
+ </div>
697
+ <div class="tree-children">${renderNode(val, depth+1)}</div>
698
+ </div>`;
699
+ }).join('');
700
+ }
701
+ function fileIcon(ext) {
702
+ const m = { jsx:'βš›οΈ', tsx:'βš›οΈ', js:'πŸ“œ', ts:'πŸ“˜', py:'🐍', css:'🎨',
703
+ html:'🌐', json:'πŸ“‹', sql:'πŸ—„οΈ', md:'πŸ“', yml:'βš™οΈ', yaml:'βš™οΈ', txt:'πŸ“„' };
704
+ return m[ext] || 'πŸ“„';
705
+ }
706
+ function selectFile(path) {
707
+ S.selectedFile = path;
708
+ setView('code');
709
+ renderTree();
710
+ renderCodeView();
711
+ }
712
+
713
+ // ─── CODE VIEW ──────────────────────────────────────────
714
+ function renderCodeView() {
715
+ const area = document.getElementById('previewArea');
716
+ const file = S.selectedFile;
717
+ const content = file ? (S.files[file] || '') : '';
718
+ if (!content) {
719
+ area.innerHTML = `<div class="preview-empty"><div class="big">πŸ“</div>
720
+ <h3>Code View</h3><p>Select a file from the tree to view its code</p></div>`;
721
+ return;
722
+ }
723
+ const lines = content.split('\n');
724
+ const ext = (file.split('.').pop() || '').toLowerCase();
725
+ const gutter = lines.map((_, i) => i + 1).join('\n');
726
+ const highlighted = highlight(content, ext);
727
+ area.innerHTML = `<div class="code-view">
728
+ <div class="code-head">
729
+ <span class="code-name mono">${esc(file)}</span>
730
+ <div class="code-meta">
731
+ <span class="code-lines">${lines.length} lines</span>
732
+ <button class="btn-icon" onclick="copyCode()" title="Copy">πŸ“‹</button>
733
+ </div>
734
+ </div>
735
+ <div class="code-body">
736
+ <pre class="code-gutter mono">${gutter}</pre>
737
+ <pre class="code-content mono">${highlighted}</pre>
738
+ </div>
739
+ </div>`;
740
+ }
741
+ function copyCode() {
742
+ if (S.selectedFile && S.files[S.selectedFile]) {
743
+ navigator.clipboard.writeText(S.files[S.selectedFile]);
744
+ }
745
+ }
746
+
747
+ // ─── SYNTAX HIGHLIGHTING ────────────────────────────────
748
+ function highlight(code, ext) {
749
+ let h = esc(code);
750
+ // strings
751
+ h = h.replace(/(["'`])(?:(?=(\\?))\2[\s\S])*?\1/g, '<span style="color:#22D3A8">$&</span>');
752
+ // comments
753
+ h = h.replace(/(\/\/.*$|#(?!.*{).*$)/gm, '<span style="color:#555577">$&</span>');
754
+ h = h.replace(/(\/\*[\s\S]*?\*\/|--\s.*$)/gm, '<span style="color:#555577">$&</span>');
755
+ // keywords (JS/Python/SQL combined)
756
+ 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,
757
+ '<span style="color:#6C63FF;font-weight:600">$&</span>');
758
+ // numbers
759
+ h = h.replace(/\b(\d+\.?\d*)\b/g, '<span style="color:#FFB547">$&</span>');
760
+ return h;
761
+ }
762
+
763
+ // ─── PREVIEW ────────────────────────────────────────────
764
+ function renderPreview() {
765
+ const area = document.getElementById('previewArea');
766
+ if (S.viewMode === 'code') { renderCodeView(); return; }
767
+ if (!S.sid) {
768
+ area.innerHTML = `<div class="preview-empty"><div class="big">πŸ–₯️</div>
769
+ <h3>Live Preview</h3><p>Your generated app will be previewed here.<br>Start by describing your app in the chat.</p></div>`;
770
+ return;
771
+ }
772
+ const url = S.previewSystem === 'preview'
773
+ ? '/api/preview/' + S.sid
774
+ : '/api/preview/' + S.sid + '/' + S.previewSystem;
775
+ area.innerHTML = `<div class="preview-frame" style="width:${S.vpWidth}">
776
+ <iframe src="${url}" id="previewIframe"></iframe>
777
+ </div>`;
778
+ }
779
+ function refreshPreview() { renderPreview(); }
780
+ function setVP(w) {
781
+ S.vpWidth = w;
782
+ document.querySelectorAll('#vpDesk,#vpTab,#vpMob').forEach(b => b.style.color = 'var(--muted)');
783
+ if (w === '100%') document.getElementById('vpDesk').style.color = 'var(--accent)';
784
+ else if (w === '768px') document.getElementById('vpTab').style.color = 'var(--accent)';
785
+ else document.getElementById('vpMob').style.color = 'var(--accent)';
786
+ const f = document.querySelector('.preview-frame');
787
+ if (f) f.style.width = w;
788
+ }
789
+ function setView(mode) {
790
+ S.viewMode = mode;
791
+ document.getElementById('tbPreview').classList.toggle('on', mode === 'preview');
792
+ document.getElementById('tbCode').classList.toggle('on', mode === 'code');
793
+ if (mode === 'preview') renderPreview(); else renderCodeView();
794
+ }
795
+
796
+ // ─── SYSTEM TABS ────────────────────────────────────────
797
+ function renderSysTabs() {
798
+ document.getElementById('sysTabs').innerHTML = SYSTEMS.map(s =>
799
+ `<button class="sys-tab ${S.previewSystem===s.id?'on':''}" onclick="setSys('${s.id}')">${s.e} ${s.label}</button>`
800
+ ).join('');
801
+ }
802
+ function setSys(id) {
803
+ S.previewSystem = id;
804
+ renderSysTabs();
805
+ if (S.viewMode === 'preview') renderPreview();
806
+ }
807
+
808
+ // ─── HEADER AGENTS ──────────────────────────────────────
809
+ function updateAgents() {
810
+ const el = document.getElementById('hdrAgents');
811
+ el.innerHTML = Object.entries(S.agents).map(([k,a]) => {
812
+ const dc = a.status === 'active' ? 'dot-active' : a.status === 'done' ? 'dot-done' : a.status === 'error' ? 'dot-error' : 'dot-idle';
813
+ return `<div class="hdr-agent">${a.icon}<div class="dot ${dc}"></div>${k.charAt(0).toUpperCase()+k.slice(1,4)}</div>`;
814
+ }).join('');
815
+ }
816
+
817
+ // ─── STATUS ─────────────────────────────────────────────
818
+ const STATUS_MAP = {
819
+ idle: { label:'Ready', color:'var(--muted)' },
820
+ starting: { label:'Starting…', color:'var(--warn)' },
821
+ connected: { label:'Connected', color:'var(--success)' },
822
+ researching: { label:'πŸ” Researching…',color:'var(--accent2)' },
823
+ orchestrating:{label:'🧠 Blueprinting…',color:'var(--accent)' },
824
+ building: { label:'πŸš€ Building…', color:'var(--accent)' },
825
+ merging: { label:'πŸ“¦ Merging…', color:'var(--accent2)' },
826
+ fixing: { label:'πŸ”§ Fixing…', color:'var(--warn)' },
827
+ completed: { label:'βœ… Complete', color:'var(--success)' },
828
+ error: { label:'❌ Error', color:'var(--error)' },
829
+ };
830
+ function updateStatus(s) {
831
+ S.status = s;
832
+ const info = STATUS_MAP[s] || { label: s, color: 'var(--muted)' };
833
+ const dotColor = isActive() ? 'var(--success)' : s==='error' ? 'var(--error)' : 'var(--muted)';
834
+ document.getElementById('statusLabel').innerHTML =
835
+ `<span class="status-dot" style="background:${dotColor}"></span> <span style="color:${info.color};font-weight:500">${info.label}</span>`;
836
+ updateStatusBar();
837
+ const inp = document.getElementById('chatInput');
838
+ const btn = document.getElementById('btnSend');
839
+ inp.disabled = isActive();
840
+ btn.disabled = isActive();
841
+ }
842
+ function updateStatusBar() {
843
+ const fc = S.fileTree.length;
844
+ const ec = S.errors.length;
845
+ const fe = document.getElementById('statusFiles');
846
+ const ee = document.getElementById('statusErrors');
847
+ if (fc > 0) { fe.style.display = ''; fe.textContent = `πŸ“ ${fc} files`; }
848
+ else fe.style.display = 'none';
849
+ if (ec > 0) { ee.style.display = ''; ee.textContent = `⚠ ${ec} errors`; }
850
+ else ee.style.display = 'none';
851
+ }
852
+ function isActive() {
853
+ return !['idle','completed','error'].includes(S.status);
854
+ }
855
+
856
+ // ─── BADGES ─────────────────────────────────────────────
857
+ function updateAgentBadge() {
858
+ const b = document.getElementById('agentBadge');
859
+ const n = S.agentFeed.length;
860
+ if (n > 0) { b.style.display = ''; b.textContent = n > 99 ? '99+' : n; }
861
+ else b.style.display = 'none';
862
+ }
863
+ function updateFileBadge() {
864
+ const b = document.getElementById('fileBadge');
865
+ const n = S.fileTree.length;
866
+ if (n > 0) { b.style.display = ''; b.textContent = n; }
867
+ else b.style.display = 'none';
868
+ }
869
+
870
+ // ─── ESCAPE HTML ────────────────────────────────────────
871
+ function esc(s) {
872
+ if (!s) return '';
873
+ return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
874
+ }
875
+
876
+ // ─── KEEPALIVE ──────────────────────────────────────────
877
+ setInterval(() => { fetch('/api/health').catch(()=>{}); }, 30000);
878
+
879
+ // ─── INIT ───────────────────────────────────────────────
880
+ applyTheme();
881
+ updateAgents();
882
+ renderChat();
883
+ renderSysTabs();
884
+ renderPreview();
885
+
886
+ // collapsed tree dirs
887
+ document.addEventListener('click', (e) => {
888
+ // Handle collapsed class toggle (delegated)
889
+ });
890
+ </script>
891
+ </body>
892
+ </html>