R-Kentaren commited on
Commit
2417726
Β·
verified Β·
1 Parent(s): 0235f63

rewrite: use gradio.Server + @app .api() + custom HTML (proper Gradio 6.x pattern)

Browse files
Files changed (1) hide show
  1. index.html +675 -356
index.html CHANGED
@@ -4,520 +4,839 @@
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>SoniCoder v2 β€” AI Code Agent</title>
 
 
 
 
7
  <style>
8
- :root {
9
- --bg-primary: #0f0f13;
10
- --bg-secondary: #16161d;
11
- --bg-tertiary: #1e1e2a;
12
- --bg-hover: #252535;
13
- --text-primary: #e4e4f0;
14
- --text-secondary: #9393a8;
15
- --text-muted: #6b6b80;
16
- --accent: #7c6ef0;
17
- --accent-hover: #6b5de0;
18
- --accent-light: rgba(124,110,240,0.12);
19
- --success: #4ade80;
20
- --warning: #fbbf24;
21
- --error: #f87171;
22
- --info: #60a5fa;
23
- --border: #2a2a3a;
24
- --code-bg: #1a1a28;
25
- --radius: 12px;
26
- --radius-sm: 8px;
27
- --shadow: 0 4px 24px rgba(0,0,0,0.4);
28
- }
29
- * { margin:0; padding:0; box-sizing:border-box; }
30
- body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background:var(--bg-primary); color:var(--text-primary); height:100vh; overflow:hidden; }
31
-
32
- .app { display:flex; height:100vh; }
33
- .sidebar { width:280px; background:var(--bg-secondary); border-right:1px solid var(--border); display:flex; flex-direction:column; flex-shrink:0; }
34
- .main { flex:1; display:flex; flex-direction:column; min-width:0; }
35
- .workspace-panel { width:300px; background:var(--bg-secondary); border-left:1px solid var(--border); display:flex; flex-direction:column; flex-shrink:0; transition:width 0.3s; overflow:hidden; }
36
- .workspace-panel.collapsed { width:0; border-left:none; }
37
-
38
- .sidebar-header { padding:20px; border-bottom:1px solid var(--border); }
39
- .logo { font-size:20px; font-weight:700; background:linear-gradient(135deg, #7c6ef0, #a78bfa, #60a5fa); -webkit-background-clip:text; -webkit-text-fill-color:transparent; }
40
- .logo-sub { font-size:11px; color:var(--text-muted); margin-top:2px; letter-spacing:1px; text-transform:uppercase; }
41
- .model-selector { padding:16px 20px; border-bottom:1px solid var(--border); }
42
- .model-selector label { font-size:11px; color:var(--text-muted); text-transform:uppercase; letter-spacing:0.5px; display:block; margin-bottom:8px; }
43
- .model-selector select { width:100%; padding:8px 12px; background:var(--bg-tertiary); color:var(--text-primary); border:1px solid var(--border); border-radius:var(--radius-sm); font-size:13px; cursor:pointer; outline:none; }
44
- .model-selector select:focus { border-color:var(--accent); }
45
- .model-status { padding:8px 20px; border-bottom:1px solid var(--border); }
46
- .status-dot { display:inline-block; width:8px; height:8px; border-radius:50%; margin-right:6px; }
47
- .status-dot.ready { background:var(--success); box-shadow:0 0 6px var(--success); }
48
- .status-dot.loading { background:var(--warning); animation:pulse 1.5s infinite; }
49
- .status-dot.error { background:var(--error); }
50
- .status-dot.unknown { background:var(--text-muted); }
51
- @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
52
- .status-text { font-size:12px; color:var(--text-secondary); }
53
-
54
- .sidebar-section { padding:16px 20px; }
55
- .sidebar-section h3 { font-size:11px; color:var(--text-muted); text-transform:uppercase; letter-spacing:0.5px; margin-bottom:12px; }
56
- .sidebar-info { font-size:12px; color:var(--text-secondary); line-height:1.6; }
57
- .sidebar-info code { background:var(--bg-tertiary); padding:2px 6px; border-radius:4px; font-size:11px; color:var(--accent); }
58
- .new-chat-btn { margin:16px 20px; padding:10px; background:var(--accent); color:white; border:none; border-radius:var(--radius-sm); font-size:13px; font-weight:600; cursor:pointer; transition:all 0.2s; }
59
- .new-chat-btn:hover { background:var(--accent-hover); transform:translateY(-1px); }
60
-
61
- .chat-header { padding:16px 24px; border-bottom:1px solid var(--border); display:flex; align-items:center; justify-content:space-between; }
62
- .chat-header h2 { font-size:15px; font-weight:600; }
63
- .header-actions { display:flex; gap:8px; }
64
- .header-btn { padding:6px 12px; background:var(--bg-tertiary); color:var(--text-secondary); border:1px solid var(--border); border-radius:var(--radius-sm); font-size:12px; cursor:pointer; transition:all 0.2s; }
65
- .header-btn:hover { background:var(--bg-hover); color:var(--text-primary); }
66
-
67
- .chat-messages { flex:1; overflow-y:auto; padding:24px; scroll-behavior:smooth; }
68
- .chat-messages::-webkit-scrollbar { width:6px; }
69
- .chat-messages::-webkit-scrollbar-track { background:transparent; }
70
- .chat-messages::-webkit-scrollbar-thumb { background:var(--border); border-radius:3px; }
71
-
72
- .message { margin-bottom:24px; animation:fadeIn 0.3s ease; }
73
- @keyframes fadeIn { from{opacity:0;transform:translateY(8px)} to{opacity:1;transform:translateY(0)} }
74
- .message-header { display:flex; align-items:center; gap:8px; margin-bottom:8px; }
75
- .message-avatar { width:28px; height:28px; border-radius:8px; display:flex; align-items:center; justify-content:center; font-size:13px; font-weight:700; }
76
- .message-avatar.user { background:var(--accent); color:white; }
77
- .message-avatar.agent { background:var(--bg-tertiary); color:var(--accent); border:1px solid var(--border); }
78
- .message-role { font-size:13px; font-weight:600; }
79
- .message-time { font-size:11px; color:var(--text-muted); }
80
-
81
- .message-body { padding-left:36px; line-height:1.7; font-size:14px; color:var(--text-primary); }
82
- .message-body p { margin-bottom:8px; }
83
- .message-body code { background:var(--code-bg); padding:2px 6px; border-radius:4px; font-size:13px; font-family:'JetBrains Mono','Fira Code',monospace; }
84
- .message-body pre { background:var(--code-bg); padding:16px; border-radius:var(--radius-sm); overflow-x:auto; margin:12px 0; border:1px solid var(--border); }
85
- .message-body pre code { background:none; padding:0; font-size:13px; line-height:1.5; }
86
- .message-body ul, .message-body ol { padding-left:20px; margin:8px 0; }
87
- .message-body li { margin:4px 0; }
88
-
89
- .tool-call { background:var(--bg-tertiary); border:1px solid var(--border); border-radius:var(--radius-sm); margin:12px 0; padding:12px 16px; }
90
- .tool-call-header { display:flex; align-items:center; gap:8px; margin-bottom:8px; }
91
- .tool-icon { font-size:14px; }
92
- .tool-name { font-size:13px; font-weight:600; color:var(--accent); }
93
- .tool-status { font-size:11px; padding:2px 8px; border-radius:10px; font-weight:600; }
94
- .tool-status.success { background:rgba(74,222,128,0.12); color:var(--success); }
95
- .tool-status.error { background:rgba(248,113,113,0.12); color:var(--error); }
96
- .tool-status.running { background:rgba(251,191,36,0.12); color:var(--warning); }
97
- .tool-args { font-size:12px; color:var(--text-muted); margin-bottom:8px; }
98
- .tool-result { font-size:12px; background:var(--bg-primary); border-radius:6px; padding:8px 12px; max-height:300px; overflow-y:auto; white-space:pre-wrap; word-break:break-all; font-family:'JetBrains Mono',monospace; line-height:1.5; color:var(--text-secondary); }
99
- .tool-result::-webkit-scrollbar { width:4px; }
100
- .tool-result::-webkit-scrollbar-thumb { background:var(--border); border-radius:2px; }
101
- .iteration-badge { display:inline-flex; align-items:center; gap:4px; font-size:11px; color:var(--text-muted); background:var(--bg-tertiary); padding:2px 10px; border-radius:10px; margin-bottom:8px; }
102
-
103
- .chat-input { padding:16px 24px 24px; border-top:1px solid var(--border); }
104
- .input-wrapper { display:flex; align-items:flex-end; gap:12px; background:var(--bg-tertiary); border:1px solid var(--border); border-radius:var(--radius); padding:12px 16px; transition:border-color 0.2s; }
105
- .input-wrapper:focus-within { border-color:var(--accent); }
106
- .input-wrapper textarea { flex:1; background:none; border:none; outline:none; color:var(--text-primary); font-size:14px; font-family:inherit; resize:none; max-height:200px; line-height:1.5; }
107
- .input-wrapper textarea::placeholder { color:var(--text-muted); }
108
- .send-btn { padding:8px 20px; background:var(--accent); color:white; border:none; border-radius:var(--radius-sm); font-size:13px; font-weight:600; cursor:pointer; transition:all 0.2s; white-space:nowrap; }
109
- .send-btn:hover { background:var(--accent-hover); }
110
- .send-btn:disabled { opacity:0.5; cursor:not-allowed; }
111
- .stop-btn { padding:8px 20px; background:var(--error); color:white; border:none; border-radius:var(--radius-sm); font-size:13px; font-weight:600; cursor:pointer; }
112
- .input-hint { font-size:11px; color:var(--text-muted); margin-top:8px; text-align:center; }
113
-
114
- .ws-header { padding:16px; border-bottom:1px solid var(--border); display:flex; align-items:center; justify-content:space-between; }
115
- .ws-header h3 { font-size:13px; font-weight:600; }
116
- .ws-tree { flex:1; overflow-y:auto; padding:12px 16px; font-family:'JetBrains Mono',monospace; font-size:12px; line-height:1.8; color:var(--text-secondary); white-space:pre; }
117
- .ws-tree::-webkit-scrollbar { width:4px; }
118
- .ws-tree::-webkit-scrollbar-thumb { background:var(--border); border-radius:2px; }
119
-
120
- .typing-indicator { display:flex; gap:4px; padding:8px 0; }
121
- .typing-dot { width:6px; height:6px; border-radius:50%; background:var(--accent); animation:typingBounce 1.4s infinite; }
122
- .typing-dot:nth-child(2) { animation-delay:0.2s; }
123
- .typing-dot:nth-child(3) { animation-delay:0.4s; }
124
- @keyframes typingBounce { 0%,60%,100%{transform:translateY(0)} 30%{transform:translateY(-6px)} }
125
-
126
- .examples { padding:16px 20px; }
127
- .example-btn { display:block; width:100%; text-align:left; padding:10px 12px; background:var(--bg-tertiary); color:var(--text-secondary); border:1px solid var(--border); border-radius:var(--radius-sm); font-size:12px; margin-bottom:8px; cursor:pointer; transition:all 0.2s; line-height:1.4; }
128
- .example-btn:hover { background:var(--bg-hover); color:var(--text-primary); border-color:var(--accent); }
129
-
130
- @media (max-width:900px) {
131
- .sidebar { display:none; }
132
- .workspace-panel { display:none; }
133
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  </style>
135
  </head>
136
  <body>
137
- <div class="app">
138
- <aside class="sidebar">
139
- <div class="sidebar-header">
140
- <div class="logo">SoniCoder v2</div>
141
- <div class="logo-sub">AI Code Agent</div>
142
- </div>
143
- <div class="model-selector">
144
- <label>Model</label>
145
- <select id="modelSelect"></select>
146
  </div>
147
- <div class="model-status">
148
- <span class="status-dot unknown" id="statusDot"></span>
149
- <span class="status-text" id="statusText">Loading...</span>
150
- </div>
151
- <button class="new-chat-btn" id="newChatBtn">+ New Chat</button>
152
- <div class="sidebar-section">
153
- <h3>Quick Start</h3>
154
- <div class="sidebar-info">
155
- Ask me to build anything &mdash; a web app, API, script, or full-stack project. I'll create the files and verify they work.
156
  </div>
157
- </div>
158
- <div class="examples" id="examplesPanel">
159
- <div class="example-btn" data-example="Build a Flask REST API with user CRUD endpoints">Build a Flask REST API with user CRUD endpoints</div>
160
- <div class="example-btn" data-example="Create a React todo app with localStorage persistence">Create a React todo app with localStorage persistence</div>
161
- <div class="example-btn" data-example="Write a Python web scraper with error handling">Write a Python web scraper with error handling</div>
162
- <div class="example-btn" data-example="Build a CLI tool that converts Markdown to HTML">Build a CLI tool that converts Markdown to HTML</div>
163
- <div class="example-btn" data-example="Create a FastAPI backend with JWT authentication">Create a FastAPI backend with JWT authentication</div>
164
- </div>
165
- </aside>
166
-
167
- <div class="main">
168
- <div class="chat-header">
169
- <h2>Chat</h2>
170
- <div class="header-actions">
171
- <button class="header-btn" id="toggleWsBtn">Workspace</button>
172
- <button class="header-btn" id="refreshWsBtn">Refresh</button>
173
  </div>
 
174
  </div>
175
- <div class="chat-messages" id="chatMessages">
176
- <div class="message" id="welcomeMsg" style="text-align:center; padding:60px 20px;">
177
- <div style="font-size:28px; margin-bottom:12px; opacity:0.6;">SoniCoder v2</div>
178
- <div style="color:var(--text-secondary); font-size:14px; max-width:500px; margin:0 auto; line-height:1.7;">
179
- AI-powered code agent that writes, runs, and debugs code. Built with patterns from Gemini CLI and Claude Code. Supports MCP tools and multiple 1B models.
 
 
 
 
 
 
 
 
 
 
 
180
  </div>
 
181
  </div>
182
  </div>
183
- <div class="chat-input">
184
- <div class="input-wrapper">
185
- <textarea id="chatInput" placeholder="Describe what you want to build..." rows="1"></textarea>
186
- <button class="send-btn" id="sendBtn">Send</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  </div>
188
- <div class="input-hint">Press Enter to send, Shift+Enter for new line</div>
189
  </div>
190
  </div>
191
 
192
- <div class="workspace-panel" id="workspacePanel">
193
- <div class="ws-header"><h3>Workspace</h3></div>
194
- <div class="ws-tree" id="wsTree">Loading...</div>
 
 
195
  </div>
196
  </div>
197
 
198
- <script>
199
- // ── State ──────────────────────────────────────────────────────────────
200
- var sessionId = '';
201
- var isStreaming = false;
202
- var currentToolCalls = {};
203
- var currentStreamDiv = null;
 
204
 
205
- // ── Init ──────────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  document.addEventListener('DOMContentLoaded', function() {
207
  loadConfig();
208
  pollModelStatus();
209
  refreshWorkspace();
 
 
 
 
 
 
 
210
  });
211
 
212
- // ── API calls (plain fetch to FastAPI routes on gradio.Server) ────────
 
 
 
 
 
 
 
 
 
 
 
 
213
 
214
  async function loadConfig() {
215
  try {
216
  var resp = await fetch('/api/get_config');
217
  var config = await resp.json();
218
-
219
- var select = document.getElementById('modelSelect');
220
- select.innerHTML = '';
221
  (config.models || []).forEach(function(m) {
222
  var opt = document.createElement('option');
223
  opt.value = m.key;
224
  opt.textContent = m.name + ' (' + m.size_gb + 'GB)';
225
  if (m.key === config.default_model) opt.selected = true;
226
- select.appendChild(opt);
227
  });
228
- select.addEventListener('change', function() { switchModel(select.value); });
229
- } catch (e) { console.error('Config load failed:', e); }
 
230
  }
231
 
232
  async function pollModelStatus() {
233
  try {
234
  var resp = await fetch('/api/model_status');
235
- var status = await resp.json();
236
- var dot = document.getElementById('statusDot');
237
- var text = document.getElementById('statusText');
238
- dot.className = 'status-dot ' + (status.status || 'unknown');
239
- var names = { ready:'Ready', loading:'Loading...', error:'Error', unknown:'Unknown' };
240
- text.textContent = status.model_name || names[status.status] || 'Unknown';
241
- if (status.status === 'loading') setTimeout(pollModelStatus, 3000);
242
- } catch (e) { setTimeout(pollModelStatus, 5000); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  }
244
 
245
  async function switchModel(key) {
246
  try {
 
 
 
247
  await fetch('/api/switch_model', {
248
- method: 'POST',
249
- headers: { 'Content-Type': 'application/json' },
250
  body: JSON.stringify({ model_key: key }),
251
  });
252
- document.getElementById('statusDot').className = 'status-dot loading';
253
- document.getElementById('statusText').textContent = 'Switching...';
254
  setTimeout(pollModelStatus, 2000);
255
- } catch (e) { console.error(e); }
256
  }
257
 
258
  async function refreshWorkspace() {
259
  try {
260
- var resp = await fetch('/api/workspace_tree?max_depth=3');
261
  var data = await resp.json();
262
- document.getElementById('wsTree').textContent = data.tree || '(empty)';
263
- } catch (e) { console.error(e); }
 
 
 
264
  }
265
 
266
- async function resetSession() {
267
- if (sessionId) {
268
- try {
269
- await fetch('/api/reset_session', {
270
- method: 'POST',
271
- headers: { 'Content-Type': 'application/json' },
272
- body: JSON.stringify({ session_id: sessionId }),
273
- });
274
- } catch (e) { /* ignore */ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
275
  }
276
- sessionId = '';
277
- document.getElementById('chatMessages').innerHTML =
278
- '<div class="message" id="welcomeMsg" style="text-align:center; padding:60px 20px;">' +
279
- '<div style="font-size:28px; margin-bottom:12px; opacity:0.6;">SoniCoder v2</div>' +
280
- '<div style="color:var(--text-secondary); font-size:14px;">New session started. What would you like to build?</div>' +
281
- '</div>';
282
  }
283
 
284
- // ── Message Rendering ────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
 
286
- function appendMessage(role, content, extra) {
287
- extra = extra || {};
288
- var container = document.getElementById('chatMessages');
289
- var div = document.createElement('div');
290
- div.className = 'message';
291
- div.id = extra.msgId || '';
292
 
293
- var time = new Date().toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'});
294
- var avatarClass = role === 'user' ? 'user' : 'agent';
295
- var avatarText = role === 'user' ? 'Y' : 'SC';
296
- var iterationHtml = extra.iteration ? '<span class="iteration-badge">Iteration ' + extra.iteration + '</span>' : '';
 
 
 
 
 
297
 
298
- div.innerHTML =
299
- '<div class="message-header">' +
300
- '<div class="message-avatar ' + avatarClass + '">' + avatarText + '</div>' +
301
- '<span class="message-role">' + (role === 'user' ? 'You' : 'SoniCoder') + '</span>' +
302
- '<span class="message-time">' + time + '</span>' +
303
- iterationHtml +
304
- '</div>' +
305
- '<div class="message-body">' + formatContent(content) + '</div>';
 
 
 
 
 
 
 
 
 
 
 
306
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  container.appendChild(div);
308
  container.scrollTop = container.scrollHeight;
309
  return div;
310
  }
311
 
312
- function formatContent(text) {
313
  if (!text) return '';
 
 
314
  text = text.replace(/```(\w*)\n([\s\S]*?)```/g, function(_, lang, code) {
315
- return '<pre><code class="language-' + lang + '">' + escapeHtml(code.trim()) + '</code></pre>';
 
316
  });
317
  text = text.replace(/`([^`]+)`/g, '<code>$1</code>');
318
  text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
319
- text = text.replace(/^[-β€’] (.+)$/gm, '<li>$1</li>');
320
- text = text.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>');
 
 
 
321
  text = text.replace(/\n/g, '<br>');
322
  return text;
323
  }
324
 
325
- function escapeHtml(text) {
326
- var d = document.createElement('div');
327
- d.textContent = text;
328
- return d.innerHTML;
 
 
 
 
 
329
  }
330
 
331
- // ── Tool Call UI ─────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
332
 
333
  function addToolCall(toolName, args) {
334
- var container = document.getElementById('chatMessages');
335
  var id = 'tc_' + Date.now();
336
  var div = document.createElement('div');
337
- div.className = 'tool-call';
338
  div.id = id;
 
 
 
 
 
 
 
339
  div.innerHTML =
340
- '<div class="tool-call-header">' +
341
  '<span class="tool-icon">&#9889;</span>' +
342
- '<span class="tool-name">' + escapeHtml(toolName) + '</span>' +
343
- '<span class="tool-status running">Running</span>' +
344
  '</div>' +
345
- '<div class="tool-args">' + formatToolArgs(args) + '</div>' +
346
- '<div class="tool-result" style="display:none;"></div>';
347
  container.appendChild(div);
348
  container.scrollTop = container.scrollHeight;
349
- currentToolCalls[id] = div;
350
  return id;
351
  }
352
 
353
  function updateToolCall(id, success, output) {
354
- var div = currentToolCalls[id];
355
  if (!div) return;
356
- var status = div.querySelector('.tool-status');
357
  var result = div.querySelector('.tool-result');
358
- status.className = 'tool-status ' + (success ? 'success' : 'error');
359
- status.textContent = success ? 'Done' : 'Error';
 
 
 
 
 
360
  if (output) {
361
  result.textContent = output;
362
- result.style.display = 'block';
363
  }
364
- document.getElementById('chatMessages').scrollTop =
365
- document.getElementById('chatMessages').scrollHeight;
366
- }
367
-
368
- function formatToolArgs(args) {
369
- if (!args) return '';
370
- var lines = Object.entries(args).map(function(entry) {
371
- var k = entry[0], v = entry[1];
372
- var val = typeof v === 'string' ? (v.length > 60 ? v.slice(0, 60) + '...' : v) : JSON.stringify(v);
373
- return k + ': ' + val;
374
- });
375
- return escapeHtml(lines.join('\n'));
376
  }
377
 
378
- // ── Streaming UI ─────────────────────────────────────────────────────
379
 
380
  function startStreaming() {
381
- var welcome = document.getElementById('welcomeMsg');
382
- if (welcome) welcome.remove();
383
- currentStreamDiv = appendMessage('agent', '', {});
384
- var body = currentStreamDiv.querySelector('.message-body');
385
- body.innerHTML = '<div class="typing-indicator"><div class="typing-dot"></div><div class="typing-dot"></div><div class="typing-dot"></div></div>';
386
  }
387
 
388
  function updateStreaming(text) {
389
- if (!currentStreamDiv) return;
390
- var body = currentStreamDiv.querySelector('.message-body');
391
- var formatted = formatContent(text);
392
- body.innerHTML = formatted || '<div class="typing-indicator"><div class="typing-dot"></div><div class="typing-dot"></div><div class="typing-dot"></div></div>';
393
- document.getElementById('chatMessages').scrollTop =
394
- document.getElementById('chatMessages').scrollHeight;
395
  }
396
 
397
  function finalizeStream(text) {
398
- if (!currentStreamDiv) return;
399
- var body = currentStreamDiv.querySelector('.message-body');
400
- body.innerHTML = formatContent(text);
401
- currentStreamDiv = null;
 
402
  }
403
 
404
- // ── Send Message (POST /chat β†’ StreamingResponse) ────────────────────
405
 
406
- async function sendMessage() {
407
- var input = document.getElementById('chatInput');
408
  var message = input.value.trim();
409
- if (!message || isStreaming) return;
410
-
411
  input.value = '';
412
  input.style.height = 'auto';
413
- isStreaming = true;
414
-
415
- var sendBtn = document.getElementById('sendBtn');
416
- sendBtn.textContent = 'Stop';
417
- sendBtn.className = 'stop-btn';
418
-
419
- appendMessage('user', message);
420
  startStreaming();
 
421
 
422
  try {
423
  var resp = await fetch('/chat', {
424
  method: 'POST',
425
- headers: { 'Content-Type': 'application/json' },
426
- body: JSON.stringify({ message: message, session_id: sessionId }),
427
  });
428
-
429
  if (!resp.ok) throw new Error('HTTP ' + resp.status);
430
-
431
  var reader = resp.body.getReader();
432
  var decoder = new TextDecoder();
433
  var buffer = '';
434
-
435
  while (true) {
436
  var result = await reader.read();
437
  if (result.done) break;
438
- buffer += decoder.decode(result.value, { stream: true });
439
  var lines = buffer.split('\n');
440
  buffer = lines.pop() || '';
441
-
442
  for (var i = 0; i < lines.length; i++) {
443
  var line = lines[i];
444
  if (!line.trim()) continue;
445
  try {
446
- var event = JSON.parse(line);
447
- switch (event.type) {
448
  case 'streaming':
449
- var display = (event.content || '').replace(/```tool[\s\S]*?```/g, '').trim();
450
- updateStreaming(display);
451
  break;
452
  case 'tool_call':
453
- addToolCall(event.tool, event.args);
 
454
  break;
455
  case 'tool_result':
456
- var tcIds = Object.keys(currentToolCalls).reverse();
457
  for (var j = 0; j < tcIds.length; j++) {
458
- var tcDiv = currentToolCalls[tcIds[j]];
459
- if (tcDiv && tcDiv.querySelector('.tool-status.running')) {
460
- updateToolCall(tcIds[j], event.result.success, event.result.output || event.result.error);
461
  break;
462
  }
463
  }
 
 
 
 
 
 
464
  break;
465
  case 'status':
466
- if (event.iteration && currentStreamDiv) {
467
- var header = currentStreamDiv.querySelector('.message-header');
468
- if (header) {
469
- var badge = header.querySelector('.iteration-badge');
470
- if (!badge) {
471
- badge = document.createElement('span');
472
- badge.className = 'iteration-badge';
473
- header.appendChild(badge);
474
- }
475
- badge.textContent = 'Iteration ' + event.iteration + '/' + event.max_iterations;
476
- }
477
  }
478
- if (event.session_id && !sessionId) sessionId = event.session_id;
479
  break;
480
  case 'complete':
481
- finalizeStream(event.content || 'Done.');
482
- if (event.iterations) setTimeout(refreshWorkspace, 500);
 
 
483
  break;
484
  case 'error':
485
- finalizeStream('Error: ' + event.error);
 
486
  break;
487
  }
488
- } catch (e) { /* skip malformed */ }
489
  }
490
  }
491
- } catch (e) {
492
- if (e.name !== 'AbortError') finalizeStream('Connection error: ' + e.message);
 
 
 
493
  }
 
 
 
 
494
 
495
- isStreaming = false;
496
- sendBtn.textContent = 'Send';
497
- sendBtn.className = 'send-btn';
 
 
 
498
  }
499
 
500
- // ── Event Listeners ──────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
501
 
502
- document.getElementById('sendBtn').addEventListener('click', sendMessage);
503
- document.getElementById('chatInput').addEventListener('keydown', function(e) {
504
- if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
505
- });
506
- document.getElementById('chatInput').addEventListener('input', function() {
507
- this.style.height = 'auto';
508
- this.style.height = Math.min(this.scrollHeight, 200) + 'px';
509
- });
510
- document.getElementById('newChatBtn').addEventListener('click', resetSession);
511
- document.getElementById('toggleWsBtn').addEventListener('click', function() {
512
- document.getElementById('workspacePanel').classList.toggle('collapsed');
513
- });
514
- document.getElementById('refreshWsBtn').addEventListener('click', refreshWorkspace);
515
- document.getElementById('examplesPanel').addEventListener('click', function(e) {
516
- var btn = e.target.closest('.example-btn');
517
- if (!btn) return;
518
- document.getElementById('chatInput').value = btn.dataset.example;
519
- sendMessage();
520
- });
521
  </script>
522
  </body>
523
  </html>
 
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>SoniCoder v2 β€” AI Code Agent</title>
7
+ <meta name="description" content="AI-powered code agent with MCP support, multiple 1B models, and Gemini CLI patterns">
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&display=swap" rel="stylesheet">
11
  <style>
12
+ /* ═══════════════════════════════════════════════════════
13
+ RESET & BASE
14
+ ═══════════════════════════════════════════════════════ */
15
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
16
+ :root{
17
+ --bg-deep:#0a0e14;--bg-panel:#0d1117;--bg-code:#161b22;
18
+ --border:#1e2a3a;--border-focus:#2d4a6a;
19
+ --green:#39ff14;--green-dim:#1a7a0a;--cyan:#00d4ff;--amber:#ffb300;
20
+ --purple:#a855f7;--gray-light:#e0e0e0;--gray-mid:#8b949e;--gray-dim:#484f58;
21
+ --red:#ff5555;--success:#50fa7b;--code-text:#f8f8f2;
22
+ --glow-green:0 0 8px rgba(57,255,20,0.3);--glow-cyan:0 0 8px rgba(0,212,255,0.3);
23
+ --glow-amber:0 0 8px rgba(255,179,0,0.2);--glow-purple:0 0 8px rgba(168,85,247,0.3);
24
+ --font-mono:'JetBrains Mono','Fira Code','Cascadia Code',monospace;
25
+ --radius:4px;--transition:0.2s ease;
26
+ }
27
+ html,body{height:100%;background:var(--bg-deep);color:var(--gray-light);font-family:var(--font-mono);font-size:13px;line-height:1.6;overflow:hidden}
28
+ body::after{content:'';position:fixed;inset:0;pointer-events:none;z-index:9999;background:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(0,0,0,0.03) 2px,rgba(0,0,0,0.03) 4px)}
29
+ ::selection{background:rgba(57,255,20,0.25);color:#fff}
30
+ ::-webkit-scrollbar{width:6px;height:6px}
31
+ ::-webkit-scrollbar-track{background:transparent}
32
+ ::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
33
+ ::-webkit-scrollbar-thumb:hover{background:var(--gray-dim)}
34
+ a{color:var(--cyan);text-decoration:none}
35
+ a:hover{text-decoration:underline;text-shadow:var(--glow-cyan)}
36
+
37
+ /* ═══ APP SHELL ═══ */
38
+ #app{display:flex;flex-direction:column;height:100vh;max-height:100vh}
39
+
40
+ /* ═══ HEADER ═══ */
41
+ #header{display:flex;align-items:center;justify-content:space-between;padding:10px 20px;background:var(--bg-panel);border-bottom:1px solid var(--border);flex-shrink:0;gap:16px}
42
+ .header-title{display:flex;flex-direction:column;gap:2px}
43
+ .header-ascii{color:var(--green);font-size:11px;line-height:1.3;text-shadow:var(--glow-green);white-space:pre;letter-spacing:.5px}
44
+ .header-subtitle{color:var(--gray-mid);font-size:11px;padding-left:3px}
45
+ .header-actions{display:flex;align-items:center;gap:10px;flex-shrink:0}
46
+ .pill{display:inline-flex;align-items:center;gap:5px;padding:4px 10px;border:1px solid var(--border);border-radius:12px;font-size:11px;color:var(--gray-mid);text-decoration:none;transition:all var(--transition);cursor:default}
47
+ .pill .dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}
48
+ .pill .dot.ready{background:var(--success);box-shadow:0 0 6px var(--success)}
49
+ .pill .dot.loading{background:var(--amber);box-shadow:0 0 6px var(--amber);animation:pulse 1.5s ease infinite}
50
+ .pill .dot.error{background:var(--red);box-shadow:0 0 6px var(--red)}
51
+ .pill .dot.unknown{background:var(--gray-dim)}
52
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
53
+
54
+ #model-select{background:var(--bg-deep);border:1px solid var(--border);color:var(--cyan);font-family:var(--font-mono);font-size:11px;padding:5px 8px;border-radius:var(--radius);outline:none;cursor:pointer;transition:border-color var(--transition)}
55
+ #model-select:focus{border-color:var(--border-focus)}
56
+ #model-select option{background:var(--bg-deep);color:var(--gray-light)}
57
+
58
+ .mcp-pill .dot.connected{background:var(--success);box-shadow:0 0 6px var(--success)}
59
+ .mcp-pill .dot.disconnected{background:var(--red);box-shadow:0 0 6px var(--red)}
60
+
61
+ .header-btn{background:transparent;border:1px solid var(--border);color:var(--amber);font-family:var(--font-mono);font-size:11px;padding:5px 12px;border-radius:var(--radius);cursor:pointer;transition:all var(--transition);letter-spacing:1px;white-space:nowrap}
62
+ .header-btn:hover{border-color:var(--amber);background:rgba(255,179,0,.08);text-shadow:var(--glow-amber)}
63
+
64
+ /* ═══ BANNER ═══ */
65
+ #banner{background:linear-gradient(90deg,rgba(168,85,247,.08),rgba(57,255,20,.05));border-bottom:1px solid var(--border);color:var(--gray-mid);font-size:12px;padding:7px 18px;text-align:center;flex-shrink:0}
66
+ #banner strong{color:var(--gray-light);font-weight:600}
67
+ #banner a{color:var(--purple);font-weight:600}
68
+
69
+ /* ═══ MAIN LAYOUT ═══ */
70
+ #main{display:flex;flex:1;min-height:0;overflow:hidden}
71
+
72
+ /* ═══ TERMINAL PANEL ═══ */
73
+ #terminal-panel{display:flex;flex-direction:column;flex:1;min-width:0;border-right:1px solid var(--border)}
74
+ .panel-label{padding:6px 16px;font-size:10px;letter-spacing:2px;color:var(--gray-dim);border-bottom:1px solid var(--border);background:rgba(13,17,23,.6);text-transform:uppercase;flex-shrink:0}
75
+ #chat-messages{flex:1;overflow-y:auto;padding:16px;scroll-behavior:smooth}
76
+
77
+ /* Messages */
78
+ .msg{margin-bottom:14px;line-height:1.65;animation:msgIn .25s ease}
79
+ @keyframes msgIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
80
+ .msg-prefix{font-weight:600;margin-right:4px}
81
+ .msg-user .msg-prefix{color:var(--green);text-shadow:var(--glow-green)}
82
+ .msg-user .msg-content{color:var(--green);text-shadow:var(--glow-green)}
83
+ .msg-assistant .msg-prefix{color:var(--cyan);text-shadow:var(--glow-cyan);float:left}
84
+ .msg-assistant .msg-body{overflow:hidden}
85
+ .msg-assistant .msg-content{color:var(--gray-light)}
86
+ .msg-system .msg-prefix{color:var(--amber);text-shadow:var(--glow-amber)}
87
+ .msg-system .msg-content{color:var(--amber);opacity:.85}
88
+
89
+ .msg-content strong{color:#fff;font-weight:600}
90
+ .msg-content em{font-style:italic;color:var(--gray-mid)}
91
+ .msg-content code:not(pre code){background:var(--bg-code);color:var(--code-text);padding:1px 5px;border-radius:3px;font-size:12px;border:1px solid var(--border)}
92
+ .msg-content a{color:var(--cyan)}
93
+ .msg-content ul,.msg-content ol{margin:6px 0 6px 20px}
94
+ .msg-content li{margin-bottom:3px}
95
+ .msg-content h1,.msg-content h2,.msg-content h3{color:var(--cyan);margin:10px 0 6px;font-size:14px;text-shadow:var(--glow-cyan)}
96
+ .msg-content p{margin:4px 0}
97
+
98
+ /* Code blocks */
99
+ .code-block-wrap{margin:8px 0;border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;background:var(--bg-code)}
100
+ .code-block-header{display:flex;align-items:center;justify-content:space-between;padding:4px 10px;background:rgba(30,42,58,.5);border-bottom:1px solid var(--border);font-size:11px}
101
+ .code-lang{color:var(--amber);text-transform:uppercase;letter-spacing:1px}
102
+ .btn-copy{background:transparent;border:1px solid var(--border);color:var(--gray-mid);font-family:var(--font-mono);font-size:10px;padding:2px 8px;border-radius:3px;cursor:pointer;transition:all var(--transition)}
103
+ .btn-copy:hover{border-color:var(--green);color:var(--green)}
104
+ .btn-copy.copied{border-color:var(--success);color:var(--success)}
105
+ .code-block-wrap pre{margin:0;padding:10px 12px;overflow-x:auto;font-size:12px;line-height:1.5;color:var(--code-text)}
106
+ .code-block-wrap pre code{font-family:var(--font-mono);background:none;border:none;padding:0}
107
+
108
+ /* Tool calls */
109
+ .tool-block{margin:8px 0;border:1px solid var(--border);border-radius:var(--radius);background:var(--bg-code);overflow:hidden}
110
+ .tool-header{display:flex;align-items:center;gap:8px;padding:6px 10px;background:rgba(30,42,58,.5);border-bottom:1px solid var(--border);font-size:11px;cursor:pointer}
111
+ .tool-icon{font-size:13px}
112
+ .tool-name{color:var(--purple);font-weight:600;flex:1}
113
+ .tool-badge{padding:1px 8px;border-radius:10px;font-size:10px;font-weight:600;letter-spacing:.5px}
114
+ .tool-badge.running{background:rgba(255,179,0,.15);color:var(--amber);border:1px solid rgba(255,179,0,.3)}
115
+ .tool-badge.success{background:rgba(80,250,123,.1);color:var(--success);border:1px solid rgba(80,250,123,.3)}
116
+ .tool-badge.error{background:rgba(255,85,85,.1);color:var(--red);border:1px solid rgba(255,85,85,.3)}
117
+ .tool-args{padding:6px 10px;font-size:11px;color:var(--gray-dim);border-bottom:1px solid rgba(30,42,58,.3);display:none}
118
+ .tool-block.open .tool-args{display:block}
119
+ .tool-result{padding:8px 10px;font-size:11px;color:var(--gray-mid);max-height:200px;overflow-y:auto;white-space:pre-wrap;word-break:break-all;display:none}
120
+ .tool-block.open .tool-result{display:block}
121
+ .tool-result.error-output{color:var(--red)}
122
+
123
+ /* Streaming cursor */
124
+ .streaming-cursor::after{content:'\2588';animation:blink .8s step-end infinite;color:var(--green);margin-left:2px}
125
+ @keyframes blink{50%{opacity:0}}
126
+
127
+ /* Iteration badge */
128
+ .iteration-badge{display:inline-flex;align-items:center;gap:4px;margin:4px 0 2px;padding:2px 8px;border-radius:10px;background:rgba(168,85,247,.1);border:1px solid rgba(168,85,247,.2);font-size:10px;color:var(--purple);letter-spacing:.5px}
129
+
130
+ /* ═══ INPUT AREA ═══ */
131
+ #input-area{flex-shrink:0;border-top:1px solid var(--border);background:var(--bg-panel);padding:10px 16px 8px}
132
+ #input-row{display:flex;gap:8px;align-items:flex-end}
133
+ .input-prompt{color:var(--green);font-weight:700;font-size:14px;line-height:36px;text-shadow:var(--glow-green);flex-shrink:0}
134
+ #chat-input{flex:1;background:var(--bg-deep);border:1px solid var(--border);border-radius:var(--radius);color:var(--green);font-family:var(--font-mono);font-size:13px;padding:8px 12px;resize:none;outline:none;min-height:36px;max-height:120px;line-height:1.5;transition:border-color var(--transition);caret-color:var(--green);text-shadow:var(--glow-green)}
135
+ #chat-input::placeholder{color:var(--gray-dim);text-shadow:none}
136
+ #chat-input:focus{border-color:var(--border-focus)}
137
+ #btn-send,#btn-stop{font-family:var(--font-mono);font-size:12px;padding:8px 14px;border-radius:var(--radius);cursor:pointer;transition:all var(--transition);letter-spacing:1px;flex-shrink:0;height:36px;display:flex;align-items:center;gap:4px}
138
+ #btn-send{background:transparent;border:1px solid var(--green);color:var(--green)}
139
+ #btn-send:hover:not(:disabled){background:var(--green);color:var(--bg-deep);box-shadow:0 0 12px rgba(57,255,20,.3)}
140
+ #btn-send:disabled{opacity:.3;cursor:not-allowed}
141
+ #btn-stop{background:transparent;border:1px solid var(--red);color:var(--red);display:none}
142
+ #btn-stop:hover{background:var(--red);color:var(--bg-deep);box-shadow:0 0 12px rgba(255,85,85,.3)}
143
+ #examples-row{display:flex;align-items:center;gap:8px;margin-top:8px;flex-wrap:wrap}
144
+ .examples-label{font-size:10px;color:var(--gray-dim);letter-spacing:1px;text-transform:uppercase;flex-shrink:0}
145
+ .example-chip{background:rgba(30,42,58,.4);border:1px solid var(--border);border-radius:12px;padding:3px 10px;font-family:var(--font-mono);font-size:11px;color:var(--gray-mid);cursor:pointer;transition:all var(--transition);white-space:nowrap}
146
+ .example-chip:hover{border-color:var(--purple);color:var(--purple);background:rgba(168,85,247,.05);text-shadow:var(--glow-purple)}
147
+
148
+ /* ═══ OUTPUT PANEL ═══ */
149
+ #output-panel{display:flex;flex-direction:column;width:45%;min-width:340px;max-width:55%;min-height:0;background:var(--bg-panel)}
150
+ #output-tabs{display:flex;border-bottom:1px solid var(--border);background:rgba(13,17,23,.6);flex-shrink:0}
151
+ .output-tab{flex:1;background:transparent;border:none;border-bottom:2px solid transparent;color:var(--gray-dim);font-family:var(--font-mono);font-size:11px;padding:8px 6px;cursor:pointer;transition:all var(--transition);letter-spacing:.5px;text-transform:uppercase}
152
+ .output-tab:hover{color:var(--gray-mid)}
153
+ .output-tab.active{color:var(--cyan);border-bottom-color:var(--cyan);text-shadow:var(--glow-cyan)}
154
+ #output-content{flex:1;min-height:0;overflow:hidden;position:relative}
155
+ .tab-pane{display:none;height:100%;min-height:0}
156
+ .tab-pane.active{display:flex;flex-direction:column}
157
+
158
+ /* Preview */
159
+ #pane-preview{align-items:stretch;justify-content:stretch;position:relative;min-height:0;overflow:hidden}
160
+ .preview-placeholder{align-self:center;margin:auto;text-align:center;color:var(--gray-dim);padding:40px 20px}
161
+ .preview-placeholder .ascii-art{font-size:11px;line-height:1.3;margin-bottom:16px;color:var(--border-focus)}
162
+ .preview-placeholder .placeholder-text{font-size:12px;letter-spacing:.5px}
163
+ #preview-iframe{display:none;position:absolute;inset:0;width:100%;height:100%;min-height:0;border:none;background:#fff}
164
+ #btn-fullscreen{display:none;position:absolute;top:8px;right:8px;background:rgba(13,17,23,.8);border:1px solid var(--border);color:var(--gray-mid);font-family:var(--font-mono);font-size:11px;padding:4px 10px;border-radius:var(--radius);cursor:pointer;z-index:5;transition:all var(--transition)}
165
+ #btn-fullscreen:hover{border-color:var(--cyan);color:var(--cyan)}
166
+
167
+ /* Console */
168
+ #pane-console{padding:12px 16px;gap:12px;overflow-y:auto}
169
+ .console-section{margin-bottom:8px}
170
+ .console-label{font-size:10px;letter-spacing:2px;color:var(--gray-dim);margin-bottom:4px;text-transform:uppercase}
171
+ .console-output{background:var(--bg-deep);border:1px solid var(--border);border-radius:var(--radius);padding:10px 12px;font-size:12px;line-height:1.5;white-space:pre-wrap;word-break:break-word;min-height:40px;max-height:280px;overflow-y:auto}
172
+ #console-stdout{color:var(--success)}
173
+ #console-stderr{color:var(--red)}
174
+
175
+ /* Code */
176
+ #pane-code{padding:0}
177
+ .code-tab-header{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-bottom:1px solid var(--border);background:rgba(30,42,58,.3);flex-shrink:0}
178
+ .code-tab-lang{font-size:11px;color:var(--amber);letter-spacing:1px;text-transform:uppercase}
179
+ .code-tab-actions{display:flex;gap:8px}
180
+ .code-tab-btn{background:transparent;border:1px solid var(--border);color:var(--gray-mid);font-family:var(--font-mono);font-size:10px;padding:3px 8px;border-radius:3px;cursor:pointer;transition:all var(--transition)}
181
+ .code-tab-btn:hover{border-color:var(--cyan);color:var(--cyan)}
182
+ #code-display{flex:1;overflow:auto;padding:12px;background:var(--bg-code)}
183
+ #code-display pre{margin:0;font-size:12px;line-height:1.5;color:var(--code-text)}
184
+ .code-placeholder{display:flex;align-items:center;justify-content:center;height:100%;color:var(--gray-dim);font-size:12px}
185
+
186
+ /* Files */
187
+ #pane-files{padding:0}
188
+ .files-header{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-bottom:1px solid var(--border);background:rgba(30,42,58,.3);flex-shrink:0}
189
+ .files-header span{font-size:11px;color:var(--amber);letter-spacing:1px;text-transform:uppercase}
190
+ .files-breadcrumb{padding:6px 12px;font-size:11px;color:var(--gray-dim);border-bottom:1px solid var(--border);background:rgba(30,42,58,.2)}
191
+ .files-breadcrumb span{color:var(--cyan);cursor:pointer}
192
+ .files-breadcrumb span:hover{text-shadow:var(--glow-cyan)}
193
+ #files-tree{flex:1;overflow-y:auto;padding:8px 12px;font-size:12px;line-height:1.8;color:var(--gray-mid);white-space:pre}
194
+ #file-viewer{flex:1;overflow:auto;padding:12px;background:var(--bg-code);display:none}
195
+ #file-viewer pre{margin:0;font-size:11px;line-height:1.5;color:var(--code-text)}
196
+ .file-line-num{color:var(--gray-dim);display:inline-block;width:50px;text-align:right;margin-right:12px;user-select:none;border-right:1px solid var(--border);padding-right:8px}
197
+
198
+ /* MCP */
199
+ #pane-mcp{padding:16px;gap:12px;overflow-y:auto}
200
+ .mcp-server{border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;margin-bottom:8px}
201
+ .mcp-server-header{display:flex;align-items:center;gap:8px;padding:8px 12px;background:rgba(30,42,58,.5);border-bottom:1px solid var(--border);font-size:12px}
202
+ .mcp-server-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
203
+ .mcp-server-dot.connected{background:var(--success);box-shadow:0 0 6px var(--success)}
204
+ .mcp-server-dot.disconnected{background:var(--red);box-shadow:0 0 6px var(--red)}
205
+ .mcp-server-name{font-weight:600;color:var(--gray-light);flex:1}
206
+ .mcp-tools{padding:8px 12px;display:flex;flex-wrap:wrap;gap:4px}
207
+ .mcp-tool-tag{padding:2px 8px;background:rgba(0,212,255,.08);border:1px solid rgba(0,212,255,.2);border-radius:10px;font-size:10px;color:var(--cyan)}
208
+ .mcp-empty{color:var(--gray-dim);font-size:12px;text-align:center;padding:40px 20px}
209
+
210
+ /* ═══ STATUS BAR ═══ */
211
+ #status-bar{display:flex;align-items:center;gap:8px;padding:5px 16px;border-top:1px solid var(--border);background:var(--bg-panel);font-size:11px;flex-shrink:0}
212
+ .status-indicator{display:inline-flex;align-items:center;gap:6px}
213
+ .status-dot{font-size:10px;line-height:1}
214
+ #status-text{letter-spacing:1px;text-transform:uppercase}
215
+ .status-idle{color:var(--gray-dim)}
216
+ .status-working{color:var(--amber);text-shadow:var(--glow-amber)}
217
+ .status-success{color:var(--success);text-shadow:0 0 8px rgba(80,250,123,.3)}
218
+ .status-error{color:var(--red);text-shadow:0 0 8px rgba(255,85,85,.3)}
219
+ .status-info{color:var(--cyan);text-shadow:var(--glow-cyan)}
220
+ @keyframes spin{to{transform:rotate(360deg)}}
221
+ .status-working .status-dot{display:inline-block;animation:spin 1s linear infinite}
222
+
223
+ /* ═══ FULLSCREEN ═══ */
224
+ #fullscreen-overlay{display:none;position:fixed;inset:0;z-index:1000;background:var(--bg-deep);flex-direction:column}
225
+ #fullscreen-overlay.active{display:flex}
226
+ #fullscreen-bar{display:flex;align-items:center;justify-content:space-between;padding:8px 16px;border-bottom:1px solid var(--border);background:var(--bg-panel)}
227
+ #fullscreen-bar span{color:var(--cyan);font-size:12px;letter-spacing:1px}
228
+ #btn-exit-fullscreen{background:transparent;border:1px solid var(--border);color:var(--gray-mid);font-family:var(--font-mono);font-size:11px;padding:4px 12px;border-radius:var(--radius);cursor:pointer;transition:all var(--transition)}
229
+ #btn-exit-fullscreen:hover{border-color:var(--red);color:var(--red)}
230
+ #fullscreen-iframe{flex:1;border:none;background:#fff}
231
+
232
+ /* ═══ RESPONSIVE ═══ */
233
+ @media(max-width:900px){
234
+ #main{flex-direction:column}
235
+ #terminal-panel{border-right:none;border-bottom:1px solid var(--border);max-height:55vh}
236
+ #output-panel{width:100%;max-width:100%;min-width:0;flex:1}
237
+ .header-ascii{font-size:10px}
238
+ #chat-input{font-size:12px}
239
+ }
240
+ @media(max-width:600px){
241
+ #header{padding:8px 12px;gap:8px}
242
+ .header-ascii{display:none}
243
+ .header-subtitle{display:none}
244
+ .pill{font-size:10px;padding:3px 8px}
245
+ #chat-messages{padding:10px}
246
+ #input-area{padding:8px 10px 6px}
247
+ #examples-row{display:none}
248
+ }
249
  </style>
250
  </head>
251
  <body>
252
+ <div id="app">
253
+ <header id="header">
254
+ <div class="header-title">
255
+ <div class="header-ascii">&#9556;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552; SONICODER v2 &#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9562;</div>
256
+ <div class="header-subtitle">AI Code Agent | MCP Support | <span id="header-model-name">Loading...</span></div>
 
 
 
 
257
  </div>
258
+ <div class="header-actions">
259
+ <div class="pill" id="model-pill">
260
+ <span class="dot loading" id="model-dot"></span>
261
+ <span id="model-pill-text">Loading...</span>
 
 
 
 
 
262
  </div>
263
+ <select id="model-select" title="Switch AI model"></select>
264
+ <div class="pill mcp-pill" id="mcp-pill">
265
+ <span class="dot disconnected" id="mcp-dot"></span>
266
+ <span id="mcp-pill-text">MCP</span>
 
 
 
 
 
 
 
 
 
 
 
 
267
  </div>
268
+ <button class="header-btn" onclick="newChat()" title="Start a new chat session">[NEW]</button>
269
  </div>
270
+ </header>
271
+
272
+ <div id="banner">
273
+ Powered by <a id="banner-model-link" href="https://huggingface.co/Qwen/Qwen2.5-Coder-1.5B-Instruct" target="_blank" rel="noopener"><strong id="banner-model-name">Qwen2.5-Coder-1.5B</strong></a> running locally &mdash; no external APIs. Supports MCP tools, multiple 1B models, and agentic code generation with Gemini CLI patterns.
274
+ </div>
275
+
276
+ <div id="main">
277
+ <div id="terminal-panel">
278
+ <div class="panel-label">Terminal</div>
279
+ <div id="chat-messages"></div>
280
+ <div id="input-area">
281
+ <div id="input-row">
282
+ <span class="input-prompt">&#10095;</span>
283
+ <textarea id="chat-input" rows="1" placeholder="Describe what you want to build..." spellcheck="false"></textarea>
284
+ <button id="btn-send" onclick="handleSend()" title="Send message">&#10148;</button>
285
+ <button id="btn-stop" onclick="stopGeneration()" title="Stop generation">&#9632; STOP</button>
286
  </div>
287
+ <div id="examples-row"></div>
288
  </div>
289
  </div>
290
+
291
+ <div id="output-panel">
292
+ <div id="output-tabs">
293
+ <button class="output-tab active" data-tab="preview" onclick="switchTab('preview')">Preview</button>
294
+ <button class="output-tab" data-tab="console" onclick="switchTab('console')">Console</button>
295
+ <button class="output-tab" data-tab="code" onclick="switchTab('code')">Code</button>
296
+ <button class="output-tab" data-tab="files" onclick="switchTab('files')">Files</button>
297
+ <button class="output-tab" data-tab="mcp" onclick="switchTab('mcp')">MCP</button>
298
+ </div>
299
+ <div id="output-content">
300
+ <div class="tab-pane active" id="pane-preview">
301
+ <div class="preview-placeholder" id="preview-placeholder">
302
+ <div class="ascii-art">
303
+ &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
304
+ &#9474; &#9585;&#9473;&#9473;&#9473;&#9586; &#9474;
305
+ &#9474; &#9474; &#9654; &#9474; OUTPUT &#9474;
306
+ &#9474; &#9589;&#9473;&#9473;&#9473;&#9588; &#9474;
307
+ &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;</div>
308
+ <div class="placeholder-text">Generated code preview will appear here</div>
309
+ </div>
310
+ <iframe id="preview-iframe" sandbox="allow-scripts"></iframe>
311
+ <button id="btn-fullscreen" onclick="openFullscreen()">&#10570; FULLSCREEN</button>
312
+ </div>
313
+ <div class="tab-pane" id="pane-console">
314
+ <div class="console-section">
315
+ <div class="console-label">stdout:</div>
316
+ <div class="console-output" id="console-stdout">No output yet.</div>
317
+ </div>
318
+ <div class="console-section">
319
+ <div class="console-label">stderr:</div>
320
+ <div class="console-output" id="console-stderr">No errors.</div>
321
+ </div>
322
+ </div>
323
+ <div class="tab-pane" id="pane-code">
324
+ <div class="code-tab-header">
325
+ <span class="code-tab-lang" id="code-tab-lang">&mdash;</span>
326
+ <div class="code-tab-actions">
327
+ <button class="code-tab-btn" onclick="copyCode()">&#128203; Copy</button>
328
+ </div>
329
+ </div>
330
+ <div id="code-display"><div class="code-placeholder">No code generated yet.</div></div>
331
+ </div>
332
+ <div class="tab-pane" id="pane-files">
333
+ <div class="files-header">
334
+ <span>Workspace</span>
335
+ <button class="code-tab-btn" onclick="refreshWorkspace()">&#8635; Refresh</button>
336
+ </div>
337
+ <div id="files-tree"></div>
338
+ <div id="file-viewer"><pre id="file-content"></pre></div>
339
+ </div>
340
+ <div class="tab-pane" id="pane-mcp">
341
+ <div id="mcp-content"><div class="mcp-empty">Loading MCP status...</div></div>
342
+ </div>
343
  </div>
 
344
  </div>
345
  </div>
346
 
347
+ <div id="status-bar">
348
+ <div class="status-indicator status-idle" id="status-indicator">
349
+ <span class="status-dot">&#9679;</span>
350
+ <span id="status-text">LOADING MODEL...</span>
351
+ </div>
352
  </div>
353
  </div>
354
 
355
+ <div id="fullscreen-overlay">
356
+ <div id="fullscreen-bar">
357
+ <span>PREVIEW</span>
358
+ <button id="btn-exit-fullscreen" onclick="closeFullscreen()">[&#10005; CLOSE]</button>
359
+ </div>
360
+ <iframe id="fullscreen-iframe" sandbox="allow-scripts"></iframe>
361
+ </div>
362
 
363
+ <script>
364
+ // ═══ STATE ═══
365
+ var state = {
366
+ sessionId: '',
367
+ isGenerating: false,
368
+ modelReady: false,
369
+ activeTab: 'preview',
370
+ toolCalls: {},
371
+ currentStreamDiv: null,
372
+ lastCode: '',
373
+ lastCodeLang: '',
374
+ consoleStdout: '',
375
+ consoleStderr: '',
376
+ generatedFiles: [],
377
+ };
378
+
379
+ // ═══ INIT ═══
380
  document.addEventListener('DOMContentLoaded', function() {
381
  loadConfig();
382
  pollModelStatus();
383
  refreshWorkspace();
384
+ loadMcpStatus();
385
+
386
+ var input = document.getElementById('chat-input');
387
+ input.addEventListener('input', autoResize);
388
+ input.addEventListener('keydown', function(e) {
389
+ if (e.key === 'Enter' && e.shiftKey) { e.preventDefault(); handleSend(); }
390
+ });
391
  });
392
 
393
+ function autoResize() {
394
+ var el = document.getElementById('chat-input');
395
+ el.style.height = 'auto';
396
+ el.style.height = Math.min(el.scrollHeight, 120) + 'px';
397
+ }
398
+
399
+ // ═══ API HELPERS ═══
400
+
401
+ function setStatus(cls, text) {
402
+ var ind = document.getElementById('status-indicator');
403
+ ind.className = 'status-indicator ' + cls;
404
+ document.getElementById('status-text').textContent = text;
405
+ }
406
 
407
  async function loadConfig() {
408
  try {
409
  var resp = await fetch('/api/get_config');
410
  var config = await resp.json();
411
+ var sel = document.getElementById('model-select');
412
+ sel.innerHTML = '';
 
413
  (config.models || []).forEach(function(m) {
414
  var opt = document.createElement('option');
415
  opt.value = m.key;
416
  opt.textContent = m.name + ' (' + m.size_gb + 'GB)';
417
  if (m.key === config.default_model) opt.selected = true;
418
+ sel.appendChild(opt);
419
  });
420
+ sel.addEventListener('change', function() { switchModel(sel.value); });
421
+ renderExamples(config);
422
+ } catch(e) { console.error('Config load failed:', e); }
423
  }
424
 
425
  async function pollModelStatus() {
426
  try {
427
  var resp = await fetch('/api/model_status');
428
+ var data = await resp.json();
429
+ var dot = document.getElementById('model-dot');
430
+ var pillText = document.getElementById('model-pill-text');
431
+
432
+ if (data.status === 'ready') {
433
+ state.modelReady = true;
434
+ dot.className = 'dot ready';
435
+ pillText.textContent = data.model_name || 'Ready';
436
+ document.getElementById('header-model-name').textContent = data.model_name || '';
437
+ document.getElementById('banner-model-name').textContent = data.model_name || '';
438
+ setStatus('status-success', 'MODEL READY');
439
+ setTimeout(function() {
440
+ if (!state.isGenerating) setStatus('status-idle', 'IDLE');
441
+ }, 3000);
442
+ return;
443
+ } else if (data.status === 'loading') {
444
+ dot.className = 'dot loading';
445
+ pillText.textContent = 'Loading...';
446
+ setStatus('status-working', 'LOADING MODEL...');
447
+ } else {
448
+ dot.className = 'dot error';
449
+ pillText.textContent = 'Error';
450
+ setStatus('status-error', 'MODEL ERROR');
451
+ }
452
+ setTimeout(pollModelStatus, 3000);
453
+ } catch(e) { setTimeout(pollModelStatus, 5000); }
454
  }
455
 
456
  async function switchModel(key) {
457
  try {
458
+ document.getElementById('model-dot').className = 'dot loading';
459
+ document.getElementById('model-pill-text').textContent = 'Switching...';
460
+ setStatus('status-working', 'SWITCHING MODEL...');
461
  await fetch('/api/switch_model', {
462
+ method: 'POST', headers: {'Content-Type': 'application/json'},
 
463
  body: JSON.stringify({ model_key: key }),
464
  });
 
 
465
  setTimeout(pollModelStatus, 2000);
466
+ } catch(e) { console.error(e); }
467
  }
468
 
469
  async function refreshWorkspace() {
470
  try {
471
+ var resp = await fetch('/api/workspace_tree?max_depth=4');
472
  var data = await resp.json();
473
+ var tree = document.getElementById('files-tree');
474
+ tree.textContent = data.tree || '(empty workspace)';
475
+ tree.style.display = 'block';
476
+ document.getElementById('file-viewer').style.display = 'none';
477
+ } catch(e) { console.error(e); }
478
  }
479
 
480
+ async function loadMcpStatus() {
481
+ try {
482
+ var resp = await fetch('/api/mcp_status');
483
+ var data = await resp.json();
484
+ var dot = document.getElementById('mcp-dot');
485
+ var text = document.getElementById('mcp-pill-text');
486
+ var content = document.getElementById('mcp-content');
487
+ var servers = data.servers || {};
488
+ var keys = Object.keys(servers);
489
+ if (keys.length === 0) {
490
+ dot.className = 'dot disconnected';
491
+ text.textContent = 'MCP';
492
+ content.innerHTML = '<div class="mcp-empty">No MCP servers configured.<br><br>Add MCP servers in your config to extend SoniCoder with external tools.</div>';
493
+ return;
494
+ }
495
+ var anyConnected = keys.some(function(k) { return servers[k].status === 'connected'; });
496
+ dot.className = anyConnected ? 'dot connected' : 'dot disconnected';
497
+ text.textContent = 'MCP (' + keys.length + ')';
498
+ var html = '';
499
+ keys.forEach(function(name) {
500
+ var s = servers[name];
501
+ var isConn = s.status === 'connected';
502
+ html += '<div class="mcp-server">';
503
+ html += '<div class="mcp-server-header">';
504
+ html += '<span class="mcp-server-dot ' + (isConn ? 'connected' : 'disconnected') + '"></span>';
505
+ html += '<span class="mcp-server-name">' + escHtml(name) + '</span>';
506
+ html += '<span class="tool-badge ' + (isConn ? 'success' : 'error') + '">' + s.status + '</span>';
507
+ html += '</div>';
508
+ if (s.tools && s.tools.length > 0) {
509
+ html += '<div class="mcp-tools">';
510
+ s.tools.forEach(function(t) { html += '<span class="mcp-tool-tag">' + escHtml(t) + '</span>'; });
511
+ html += '</div>';
512
+ }
513
+ html += '</div>';
514
+ });
515
+ content.innerHTML = html;
516
+ } catch(e) {
517
+ document.getElementById('mcp-dot').className = 'dot disconnected';
518
+ document.getElementById('mcp-content').innerHTML = '<div class="mcp-empty">MCP status unavailable.</div>';
519
  }
 
 
 
 
 
 
520
  }
521
 
522
+ function renderExamples(config) {
523
+ var examples = [
524
+ 'Build a Flask REST API with user CRUD',
525
+ 'Create a todo app with localStorage',
526
+ 'Write a Python web scraper',
527
+ 'Build a Markdown to HTML CLI tool',
528
+ 'Create FastAPI with JWT auth',
529
+ 'Make a React calculator component',
530
+ ];
531
+ var row = document.getElementById('examples-row');
532
+ row.innerHTML = '<span class="examples-label">Try:</span>';
533
+ examples.forEach(function(ex) {
534
+ var chip = document.createElement('span');
535
+ chip.className = 'example-chip';
536
+ chip.textContent = ex;
537
+ chip.onclick = function() {
538
+ document.getElementById('chat-input').value = ex;
539
+ handleSend();
540
+ };
541
+ row.appendChild(chip);
542
+ });
543
+ }
544
 
545
+ // ═══ TABS ═══
 
 
 
 
 
546
 
547
+ function switchTab(tab) {
548
+ state.activeTab = tab;
549
+ document.querySelectorAll('.output-tab').forEach(function(t) {
550
+ t.classList.toggle('active', t.dataset.tab === tab);
551
+ });
552
+ document.querySelectorAll('.tab-pane').forEach(function(p) {
553
+ p.classList.toggle('active', p.id === 'pane-' + tab);
554
+ });
555
+ }
556
 
557
+ // ═══ FULLSCREEN ═══
558
+
559
+ function openFullscreen() {
560
+ var iframe = document.getElementById('fullscreen-iframe');
561
+ var src = document.getElementById('preview-iframe').src;
562
+ if (src) iframe.src = src;
563
+ document.getElementById('fullscreen-overlay').classList.add('active');
564
+ }
565
+ function closeFullscreen() {
566
+ document.getElementById('fullscreen-overlay').classList.remove('active');
567
+ document.getElementById('fullscreen-iframe').src = 'about:blank';
568
+ }
569
+ document.addEventListener('keydown', function(e) {
570
+ if (e.key === 'Escape') closeFullscreen();
571
+ });
572
+
573
+ // ═══ MESSAGES ═══
574
+
575
+ function escHtml(t) { var d = document.createElement('div'); d.textContent = t; return d.innerHTML; }
576
 
577
+ function addMessage(role, content, extra) {
578
+ extra = extra || {};
579
+ var container = document.getElementById('chat-messages');
580
+ var div = document.createElement('div');
581
+ div.className = 'msg msg-' + role;
582
+ div.id = extra.msgId || '';
583
+ var prefix = '';
584
+ if (role === 'user') prefix = '<span class="msg-prefix">&gt; </span>';
585
+ else if (role === 'assistant') prefix = '<span class="msg-prefix">&#9670; </span>';
586
+ else if (role === 'system') prefix = '<span class="msg-prefix">&#9888; </span>';
587
+ var bodyClass = role === 'assistant' ? '<div class="msg-body">' : '';
588
+ var bodyClose = role === 'assistant' ? '</div>' : '';
589
+ var iterationHtml = extra.iteration ? '<div class="iteration-badge">Iteration ' + extra.iteration + '/' + extra.max_iterations + '</div>' : '';
590
+ div.innerHTML = iterationHtml + prefix + '<span class="msg-content">' + renderMd(content) + '</span>' + bodyClose + bodyClose;
591
  container.appendChild(div);
592
  container.scrollTop = container.scrollHeight;
593
  return div;
594
  }
595
 
596
+ function renderMd(text) {
597
  if (!text) return '';
598
+ text = escHtml(text);
599
+ // Code blocks
600
  text = text.replace(/```(\w*)\n([\s\S]*?)```/g, function(_, lang, code) {
601
+ var id = 'cb_' + Date.now() + Math.random().toString(36).substr(2,4);
602
+ return '<div class="code-block-wrap"><div class="code-block-header"><span class="code-lang">' + (lang||'code') + '</span><button class="btn-copy" onclick="copyBlock(\'' + id + '\')">&#128203; Copy</button></div><pre><code id="' + id + '">' + code.trim() + '</code></pre></div>';
603
  });
604
  text = text.replace(/`([^`]+)`/g, '<code>$1</code>');
605
  text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
606
+ text = text.replace(/\*([^*]+)\*/g, '<em>$1</em>');
607
+ text = text.replace(/^### (.+)$/gm, '<h3>$1</h3>');
608
+ text = text.replace(/^## (.+)$/gm, '<h2>$1</h2>');
609
+ text = text.replace(/^# (.+)$/gm, '<h1>$1</h1>');
610
+ text = text.replace(/^[-*] (.+)$/gm, '<li>$1</li>');
611
  text = text.replace(/\n/g, '<br>');
612
  return text;
613
  }
614
 
615
+ function copyBlock(id) {
616
+ var el = document.getElementById(id);
617
+ if (!el) return;
618
+ navigator.clipboard.writeText(el.textContent).then(function() {
619
+ var btn = el.closest('.code-block-wrap').querySelector('.btn-copy');
620
+ btn.textContent = 'Copied!';
621
+ btn.classList.add('copied');
622
+ setTimeout(function() { btn.innerHTML = '&#128203; Copy'; btn.classList.remove('copied'); }, 2000);
623
+ });
624
  }
625
 
626
+ function copyCode() {
627
+ if (!state.lastCode) return;
628
+ navigator.clipboard.writeText(state.lastCode).then(function() {
629
+ var btn = document.querySelector('#pane-code .code-tab-btn');
630
+ btn.textContent = 'Copied!';
631
+ setTimeout(function() { btn.innerHTML = '&#128203; Copy'; }, 2000);
632
+ });
633
+ }
634
+
635
+ // ═══ TOOL CALLS ═══
636
 
637
  function addToolCall(toolName, args) {
638
+ var container = document.getElementById('chat-messages');
639
  var id = 'tc_' + Date.now();
640
  var div = document.createElement('div');
641
+ div.className = 'tool-block open';
642
  div.id = id;
643
+ var argsStr = '';
644
+ if (args) {
645
+ Object.entries(args).forEach(function(e) {
646
+ var v = typeof e[1] === 'string' ? (e[1].length > 80 ? e[1].slice(0,80) + '...' : e[1]) : JSON.stringify(e[1]);
647
+ argsStr += escHtml(e[0] + ': ' + v) + '\n';
648
+ });
649
+ }
650
  div.innerHTML =
651
+ '<div class="tool-header">' +
652
  '<span class="tool-icon">&#9889;</span>' +
653
+ '<span class="tool-name">' + escHtml(toolName) + '</span>' +
654
+ '<span class="tool-badge running">RUNNING</span>' +
655
  '</div>' +
656
+ '<div class="tool-args">' + argsStr + '</div>' +
657
+ '<div class="tool-result"></div>';
658
  container.appendChild(div);
659
  container.scrollTop = container.scrollHeight;
660
+ state.toolCalls[id] = div;
661
  return id;
662
  }
663
 
664
  function updateToolCall(id, success, output) {
665
+ var div = state.toolCalls[id];
666
  if (!div) return;
667
+ var badge = div.querySelector('.tool-badge');
668
  var result = div.querySelector('.tool-result');
669
+ if (success) {
670
+ badge.className = 'tool-badge success';
671
+ badge.textContent = 'DONE';
672
+ } else {
673
+ badge.className = 'tool-badge error';
674
+ badge.textContent = 'ERROR';
675
+ }
676
  if (output) {
677
  result.textContent = output;
678
+ result.className = success ? 'tool-result' : 'tool-result error-output';
679
  }
680
+ document.getElementById('chat-messages').scrollTop = document.getElementById('chat-messages').scrollHeight;
 
 
 
 
 
 
 
 
 
 
 
681
  }
682
 
683
+ // ═══ STREAMING ═══
684
 
685
  function startStreaming() {
686
+ state.currentStreamDiv = addMessage('assistant', '', {msgId: 'streaming-msg'});
687
+ state.currentStreamDiv.querySelector('.msg-content').innerHTML = '<span class="streaming-cursor"></span>';
688
+ document.getElementById('chat-messages').scrollTop = document.getElementById('chat-messages').scrollHeight;
 
 
689
  }
690
 
691
  function updateStreaming(text) {
692
+ if (!state.currentStreamDiv) return;
693
+ var el = state.currentStreamDiv.querySelector('.msg-content');
694
+ var clean = text.replace(/```tool[\s\S]*?```/g, '').trim();
695
+ el.innerHTML = renderMd(clean) + '<span class="streaming-cursor"></span>';
696
+ document.getElementById('chat-messages').scrollTop = document.getElementById('chat-messages').scrollHeight;
 
697
  }
698
 
699
  function finalizeStream(text) {
700
+ if (!state.currentStreamDiv) return;
701
+ var el = state.currentStreamDiv.querySelector('.msg-content');
702
+ el.innerHTML = renderMd(text || 'Done.');
703
+ state.currentStreamDiv.removeAttribute('id');
704
+ state.currentStreamDiv = null;
705
  }
706
 
707
+ // ═══ SEND MESSAGE ═══
708
 
709
+ async function handleSend() {
710
+ var input = document.getElementById('chat-input');
711
  var message = input.value.trim();
712
+ if (!message || state.isGenerating) return;
 
713
  input.value = '';
714
  input.style.height = 'auto';
715
+ state.isGenerating = true;
716
+ document.getElementById('btn-send').style.display = 'none';
717
+ document.getElementById('btn-stop').style.display = 'flex';
718
+ addMessage('user', message);
 
 
 
719
  startStreaming();
720
+ setStatus('status-working', 'GENERATING...');
721
 
722
  try {
723
  var resp = await fetch('/chat', {
724
  method: 'POST',
725
+ headers: {'Content-Type': 'application/json'},
726
+ body: JSON.stringify({ message: message, session_id: state.sessionId }),
727
  });
 
728
  if (!resp.ok) throw new Error('HTTP ' + resp.status);
 
729
  var reader = resp.body.getReader();
730
  var decoder = new TextDecoder();
731
  var buffer = '';
 
732
  while (true) {
733
  var result = await reader.read();
734
  if (result.done) break;
735
+ buffer += decoder.decode(result.value, {stream: true});
736
  var lines = buffer.split('\n');
737
  buffer = lines.pop() || '';
 
738
  for (var i = 0; i < lines.length; i++) {
739
  var line = lines[i];
740
  if (!line.trim()) continue;
741
  try {
742
+ var ev = JSON.parse(line);
743
+ switch (ev.type) {
744
  case 'streaming':
745
+ updateStreaming(ev.content || '');
 
746
  break;
747
  case 'tool_call':
748
+ setStatus('status-info', 'TOOL: ' + ev.tool);
749
+ addToolCall(ev.tool, ev.args);
750
  break;
751
  case 'tool_result':
752
+ var tcIds = Object.keys(state.toolCalls).reverse();
753
  for (var j = 0; j < tcIds.length; j++) {
754
+ var tcDiv = state.toolCalls[tcIds[j]];
755
+ if (tcDiv && tcDiv.querySelector('.tool-badge.running')) {
756
+ updateToolCall(tcIds[j], ev.result.success, ev.result.output || ev.result.error);
757
  break;
758
  }
759
  }
760
+ // Capture code for Code tab
761
+ if (ev.result.output && (ev.result.output.includes('<!DOCTYPE') || ev.result.output.includes('<html'))) {
762
+ showPreview(ev.result.output);
763
+ }
764
+ if (ev.result.stdout) { state.consoleStdout += ev.result.stdout + '\n'; updateConsole(); }
765
+ if (ev.result.stderr) { state.consoleStderr += ev.result.stderr + '\n'; updateConsole(); }
766
  break;
767
  case 'status':
768
+ if (ev.iteration && state.currentStreamDiv) {
769
+ setStatus('status-working', 'ITERATION ' + ev.iteration + '/' + ev.max_iterations);
 
 
 
 
 
 
 
 
 
770
  }
771
+ if (ev.session_id && !state.sessionId) state.sessionId = ev.session_id;
772
  break;
773
  case 'complete':
774
+ finalizeStream(ev.content || 'Done.');
775
+ if (ev.iterations) setTimeout(refreshWorkspace, 500);
776
+ setStatus('status-success', 'COMPLETE');
777
+ setTimeout(function() { if (!state.isGenerating) setStatus('status-idle', 'IDLE'); }, 3000);
778
  break;
779
  case 'error':
780
+ finalizeStream('Error: ' + ev.error);
781
+ setStatus('status-error', 'ERROR');
782
  break;
783
  }
784
+ } catch(e) { /* skip */ }
785
  }
786
  }
787
+ } catch(e) {
788
+ if (e.name !== 'AbortError') {
789
+ finalizeStream('Connection error: ' + e.message);
790
+ setStatus('status-error', 'CONNECTION ERROR');
791
+ }
792
  }
793
+ state.isGenerating = false;
794
+ document.getElementById('btn-send').style.display = 'flex';
795
+ document.getElementById('btn-stop').style.display = 'none';
796
+ }
797
 
798
+ function stopGeneration() {
799
+ state.isGenerating = false;
800
+ if (state.currentStreamDiv) finalizeStream(state.currentStreamDiv.querySelector('.msg-content').textContent || '(stopped)');
801
+ document.getElementById('btn-send').style.display = 'flex';
802
+ document.getElementById('btn-stop').style.display = 'none';
803
+ setStatus('status-idle', 'IDLE');
804
  }
805
 
806
+ function newChat() {
807
+ if (state.sessionId) {
808
+ fetch('/api/reset_session', {
809
+ method: 'POST', headers: {'Content-Type': 'application/json'},
810
+ body: JSON.stringify({ session_id: state.sessionId }),
811
+ }).catch(function(){});
812
+ }
813
+ state.sessionId = '';
814
+ state.toolCalls = {};
815
+ state.consoleStdout = '';
816
+ state.consoleStderr = '';
817
+ document.getElementById('chat-messages').innerHTML = '';
818
+ addMessage('system', 'New session started. What would you like to build?');
819
+ updateConsole();
820
+ setStatus('status-idle', 'IDLE');
821
+ }
822
 
823
+ // ═══ PREVIEW & CONSOLE ═══
824
+
825
+ function showPreview(html) {
826
+ var iframe = document.getElementById('preview-iframe');
827
+ var placeholder = document.getElementById('preview-placeholder');
828
+ var btn = document.getElementById('btn-fullscreen');
829
+ placeholder.style.display = 'none';
830
+ iframe.style.display = 'block';
831
+ btn.style.display = 'block';
832
+ iframe.srcdoc = html;
833
+ switchTab('preview');
834
+ }
835
+
836
+ function updateConsole() {
837
+ document.getElementById('console-stdout').textContent = state.consoleStdout || 'No output yet.';
838
+ document.getElementById('console-stderr').textContent = state.consoleStderr || 'No errors.';
839
+ }
 
 
840
  </script>
841
  </body>
842
  </html>