Pepguy commited on
Commit
b6b6593
·
verified ·
1 Parent(s): 119757f

Update public/index.html

Browse files
Files changed (1) hide show
  1. public/index.html +485 -371
public/index.html CHANGED
@@ -124,403 +124,517 @@
124
  </div>
125
  </main>
126
 
127
- <script>
128
- // NEW FEATURE: Toggle high-cost rendering during stream to prevent memory crashes
129
- let enableHighlighting = true;
130
-
131
- marked.setOptions({
132
- highlight: function(code, lang) {
133
- if (!enableHighlighting) return code; // Skip highlighting while streaming!
134
- const language = hljs.getLanguage(lang) ? lang : 'plaintext';
135
- try { return hljs.highlight(code, { language }).value; } catch(e) { return code; }
136
- }
137
- });
138
-
139
- let currentChatId = null;
140
- let attachedImages =[];
141
- let pollingInterval = null;
142
- let currentTokens = { total: 0, in: 0, out: 0 };
143
-
144
- function toggleInputState(isGenerating) {
145
- const sendBtn = document.getElementById('send-btn');
146
- const stopBtn = document.getElementById('stop-btn');
147
- if (isGenerating) {
148
- sendBtn.classList.add('hidden');
149
- stopBtn.classList.remove('hidden');
150
- } else {
151
- sendBtn.classList.remove('hidden');
152
- stopBtn.classList.add('hidden');
153
- }
154
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
 
156
- async function stopGeneration() {
157
- if (!currentChatId) return;
158
- toggleInputState(false);
159
- try {
160
- await fetch(`/api/chats/${currentChatId}/stop`, { method: 'POST' });
161
- } catch (err) {
162
- console.error("Failed to send stop signal", err);
163
- }
164
- }
165
 
166
- const chatWindow = document.getElementById('chat-window');
167
- const scrollBtn = document.getElementById('scroll-bottom-btn');
 
 
 
 
168
 
169
- chatWindow.addEventListener('scroll', () => {
170
- const distanceToBottom = chatWindow.scrollHeight - chatWindow.scrollTop - chatWindow.clientHeight;
171
- if (distanceToBottom > 100) scrollBtn.classList.remove('hidden');
172
- else scrollBtn.classList.add('hidden');
173
- });
174
 
175
- function scrollToBottom() {
176
- chatWindow.scrollTo({ top: chatWindow.scrollHeight, behavior: 'smooth' });
177
- }
 
 
 
 
 
178
 
179
- function enableTitleEdit() {
180
- if (!currentChatId) return;
181
- const currentTitle = document.getElementById('current-chat-title').innerText;
182
- document.getElementById('title-display').classList.add('hidden');
183
- document.getElementById('title-edit').classList.remove('hidden');
184
- document.getElementById('title-edit').classList.add('flex');
185
- const input = document.getElementById('title-input');
186
- input.value = currentTitle;
187
- input.focus();
188
- input.select();
189
- }
190
 
191
- function cancelTitleEdit() {
192
- document.getElementById('title-edit').classList.add('hidden');
193
- document.getElementById('title-edit').classList.remove('flex');
194
- document.getElementById('title-display').classList.remove('hidden');
195
- }
196
 
197
- async function saveTitle() {
198
- if (!currentChatId) return cancelTitleEdit();
199
- const newTitle = document.getElementById('title-input').value.trim();
200
- if (!newTitle) return cancelTitleEdit();
201
 
202
- document.getElementById('current-chat-title').innerText = newTitle;
203
- cancelTitleEdit();
204
 
205
- await fetch(`/api/chats/${currentChatId}/title`, {
206
- method: 'PUT',
207
- headers: { 'Content-Type': 'application/json' },
208
- body: JSON.stringify({ title: newTitle })
209
- });
210
- loadSidebar();
211
- }
 
 
212
 
213
- document.getElementById('title-input').addEventListener('keydown', (e) => {
214
- if (e.key === 'Enter') saveTitle();
215
- if (e.key === 'Escape') cancelTitleEdit();
216
- });
217
 
218
- function toggleSidebar() {
219
- const sidebar = document.getElementById('sidebar');
220
- const overlay = document.getElementById('mobile-overlay');
221
- sidebar.classList.toggle('-translate-x-full');
222
- overlay.classList.toggle('hidden');
223
- }
224
 
225
- const textarea = document.getElementById('message-input');
226
- textarea.addEventListener('input', function() {
227
- this.style.height = 'auto';
228
- this.style.height = (this.scrollHeight < 128 ? this.scrollHeight : 128) + 'px';
229
- });
230
-
231
- function updateTokenDisplay(total, input, output) {
232
- currentTokens = { total, in: input, out: output };
233
- document.getElementById('token-total').innerText = (total || 0).toLocaleString();
234
- document.getElementById('token-in').innerText = (input || 0).toLocaleString();
235
- document.getElementById('token-out').innerText = (output || 0).toLocaleString();
236
- }
237
 
238
- async function loadSidebar() {
239
- const res = await fetch('/api/chats');
240
- const chats = await res.json();
241
- const list = document.getElementById('chat-list');
242
- list.innerHTML = chats.map(c => `
243
- <div onclick="selectChat('${c.id}')" class="p-3 rounded-xl cursor-pointer transition border border-transparent ${c.id === currentChatId ? 'bg-gray-800 border-gray-700' : 'hover:bg-gray-800/50'}">
244
- <div class="text-sm font-medium truncate text-gray-200">${c.title}</div>
245
- <div class="text-[10px] text-gray-500 mt-1 flex justify-between">
246
- <span>Total: ${c.totalTokens.toLocaleString()}</span>
247
- <span class="opacity-70">↑${(c.outputTokens || 0).toLocaleString()}</span>
248
- </div>
249
- </div>
250
- `).join('');
251
- }
252
 
253
- async function createNewChat() {
254
- const res = await fetch('/api/chats', { method: 'POST' });
255
- const chat = await res.json();
256
- selectChat(chat.id);
257
- loadSidebar();
258
- if(window.innerWidth < 1024) toggleSidebar();
259
- }
260
 
261
- async function deleteCurrentChat() {
262
- if (!currentChatId) return;
263
- if (confirm("Permanently delete this chat?")) {
264
- await fetch(`/api/chats/${currentChatId}`, { method: 'DELETE' });
265
- currentChatId = null;
266
- document.getElementById('chat-window').innerHTML = '';
267
- document.getElementById('current-chat-title').innerText = 'Select or create a chat';
268
- document.getElementById('edit-title-btn').classList.add('hidden');
269
- updateTokenDisplay(0, 0, 0);
270
- toggleInputState(false);
271
- loadSidebar();
272
- }
273
- }
274
 
275
- async function selectChat(id) {
276
- currentChatId = id;
277
- if (pollingInterval) clearInterval(pollingInterval);
278
-
279
- const res = await fetch(`/api/chats/${id}`);
280
- const chat = await res.json();
281
-
282
- document.getElementById('current-chat-title').innerText = chat.title;
283
- document.getElementById('edit-title-btn').classList.remove('hidden');
284
-
285
- updateTokenDisplay(chat.totalTokens, chat.inputTokens, chat.outputTokens);
286
- cancelTitleEdit();
287
- renderMessages(chat.messages);
288
- loadSidebar();
289
-
290
- if(window.innerWidth < 1024 && !document.getElementById('sidebar').classList.contains('-translate-x-full')) {
291
- toggleSidebar();
292
- }
293
-
294
- toggleInputState(chat.isGenerating);
295
- if (chat.isGenerating) pollGeneratingChat(id);
296
- }
297
 
298
- function renderMessages(messages) {
299
- const container = document.getElementById('chat-window');
300
- const isScrolledToBottom = container.scrollHeight - container.clientHeight <= container.scrollTop + 50;
301
-
302
- container.innerHTML = messages.map(m => {
303
- let html = '';
304
- if (m.reasoning) {
305
- html += `<div class="reasoning-block"><i>Thinking Process</i><br/>${marked.parse(m.reasoning)}</div>`;
306
- }
307
- if (m.content) {
308
- html += DOMPurify.sanitize(marked.parse(m.content));
309
- }
310
-
311
- return `
312
- <div class="flex ${m.role === 'user' ? 'justify-end' : 'justify-start'} w-full">
313
- <div class="max-w-[95%] lg:max-w-[85%] rounded-2xl p-4 shadow-sm ${m.role === 'user' ? 'bg-blue-600 text-white rounded-br-sm' : 'bg-gray-850 border border-gray-800 text-gray-200 markdown-body rounded-bl-sm'}">
314
- ${m.role === 'user' ? `<div class="whitespace-pre-wrap">${m.content}</div>` : html}
315
- </div>
316
- </div>
317
- `;
318
- }).join('');
319
-
320
- document.querySelectorAll('.markdown-body pre').forEach(pre => {
321
- if (!pre.querySelector('.copy-btn')) {
322
- const btn = document.createElement('button');
323
- btn.className = 'copy-btn';
324
- btn.innerText = 'Copy';
325
- btn.onclick = () => {
326
- const codeText = pre.querySelector('code')?.innerText || pre.innerText.replace('Copy', '');
327
- navigator.clipboard.writeText(codeText.trim());
328
- btn.innerText = 'Copied!';
329
- setTimeout(() => btn.innerText = 'Copy', 2000);
330
- };
331
- pre.appendChild(btn);
332
- }
333
- });
334
-
335
- // Prevent unwanted jump to bottom if chat is empty or user scrolled up
336
- if (isScrolledToBottom && messages.length > 0) scrollToBottom();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
  }
 
338
 
339
- function pollGeneratingChat(id) {
340
- if (pollingInterval) clearInterval(pollingInterval);
341
-
342
- pollingInterval = setInterval(async () => {
343
- const res = await fetch(`/api/chats/${id}`);
344
- const chat = await res.json();
345
-
346
- renderMessages(chat.messages);
347
- updateTokenDisplay(chat.totalTokens, chat.inputTokens, chat.outputTokens);
348
-
349
- if (chat.title !== document.getElementById('current-chat-title').innerText && document.getElementById('title-display').classList.contains('hidden') === false) {
350
- document.getElementById('current-chat-title').innerText = chat.title;
351
- loadSidebar();
352
- }
353
-
354
- if (!chat.isGenerating) {
355
- clearInterval(pollingInterval);
356
- if (currentChatId === id) toggleInputState(false);
357
- }
358
- }, 1500);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
359
  }
360
 
361
- function handleImageSelect(event) {
362
- const files = event.target.files;
363
- const container = document.getElementById('image-preview-container');
364
- container.classList.remove('hidden');
365
-
366
- Array.from(files).forEach(file => {
367
- const reader = new FileReader();
368
- reader.onload = (e) => {
369
- attachedImages.push(e.target.result);
370
- container.innerHTML += `
371
- <div class="relative group">
372
- <img src="${e.target.result}" class="h-14 w-14 object-cover rounded-lg border border-gray-600 shadow-sm">
373
- </div>`;
374
- };
375
- reader.readAsDataURL(file);
376
- });
 
 
 
 
377
  }
378
 
379
- async function sendMessage() {
380
- const input = document.getElementById('message-input');
381
- const text = input.value.trim();
382
- if (!text || !currentChatId) return;
383
-
384
- const payload = {
385
- model: document.getElementById('model-select').value,
386
- prompt: text,
387
- images: attachedImages
388
- };
389
-
390
- const container = document.getElementById('chat-window');
391
- container.innerHTML += `
392
- <div class="flex justify-end w-full">
393
- <div class="max-w-[95%] lg:max-w-[85%] rounded-2xl rounded-br-sm p-4 bg-blue-600 text-white shadow-sm whitespace-pre-wrap">${text}</div>
394
- </div>
395
- <div class="flex justify-start w-full" id="temp-ai-wrapper">
396
- <div class="max-w-[95%] lg:max-w-[85%] rounded-2xl rounded-bl-sm p-4 bg-gray-850 border border-gray-800 text-gray-200 markdown-body shadow-sm" id="temp-ai-msg">
397
- <span class="flex items-center gap-2 text-gray-400">
398
- <svg class="animate-spin h-4 w-4" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
399
- Thinking...
400
- </span>
401
- </div>
402
- </div>
403
- `;
404
- scrollToBottom();
405
-
406
- input.value = '';
407
- input.style.height = 'auto';
408
- attachedImages =[];
409
- document.getElementById('image-preview-container').innerHTML = '';
410
- document.getElementById('image-preview-container').classList.add('hidden');
411
-
412
- toggleInputState(true);
413
-
414
- try {
415
- const response = await fetch(`/api/chats/${currentChatId}/stream`, {
416
- method: 'POST',
417
- headers: { 'Content-Type': 'application/json' },
418
- body: JSON.stringify(payload)
419
- });
420
-
421
- if (!response.ok) throw new Error("API stream rejected. Chat might be busy.");
422
-
423
- const reader = response.body.getReader();
424
- const decoder = new TextDecoder("utf-8");
425
- let aiContent = "";
426
- let aiReasoning = "";
427
-
428
- enableHighlighting = false; // SPEED UP: Disable expensive highlighting during stream
429
- let lastRenderTime = Date.now();
430
 
431
- while (true) {
432
- const { value, done } = await reader.read();
433
- if (done) break;
434
-
435
- const chunkText = decoder.decode(value, { stream: true });
436
- const chunks = chunkText.split(/(__THINK__|__USAGE__)/);
437
- let i = 0;
438
-
439
- while (i < chunks.length) {
440
- if (chunks[i] === '__THINK__') {
441
- aiReasoning += chunks[i+1] || '';
442
- i += 2;
443
- } else if (chunks[i] === '__USAGE__') {
444
- try {
445
- if(chunks[i+1]) {
446
- const usageData = JSON.parse(chunks[i+1]);
447
- updateTokenDisplay(
448
- currentTokens.total + usageData.totalTokens,
449
- currentTokens.in + usageData.inputTokens,
450
- currentTokens.out + usageData.outputTokens
451
- );
452
- }
453
- } catch(e) {}
454
- i += 2;
455
- } else {
456
- if (chunks[i]) aiContent += chunks[i];
457
- i++;
458
- }
459
- }
460
-
461
- // THROTTLE RENDER: Only update DOM max 5 times a second to prevent memory crashes
462
- if (Date.now() - lastRenderTime > 200) {
463
- let tempHtml = "";
464
- if (aiReasoning) tempHtml += `<div class="reasoning-block"><i>Thinking Process</i><br/>${marked.parse(aiReasoning)}</div>`;
465
- if (aiContent) tempHtml += DOMPurify.sanitize(marked.parse(aiContent));
466
-
467
- document.getElementById('temp-ai-msg').innerHTML = tempHtml || "<span class='animate-pulse text-gray-400'>Thinking...</span>";
468
-
469
- const distanceToBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
470
- if (distanceToBottom < 100) scrollToBottom();
471
-
472
- lastRenderTime = Date.now();
473
- }
474
- }
475
-
476
- // FINAL RENDER: Stream is complete, turn highlighting back on and do a final pristine render
477
- enableHighlighting = true;
478
-
479
- let finalHtml = "";
480
- if (aiReasoning) finalHtml += `<div class="reasoning-block"><i>Thinking Process</i><br/>${marked.parse(aiReasoning)}</div>`;
481
- if (aiContent) finalHtml += DOMPurify.sanitize(marked.parse(aiContent));
482
-
483
- const finalMsgElement = document.getElementById('temp-ai-msg');
484
- finalMsgElement.innerHTML = finalHtml || "Done.";
485
- finalMsgElement.removeAttribute('id'); // Remove ID so we don't accidentally overwrite it
486
-
487
- // Attach copy buttons explicitly since we aren't calling renderMessages()
488
- finalMsgElement.querySelectorAll('pre').forEach(pre => {
489
- if (!pre.querySelector('.copy-btn')) {
490
- const btn = document.createElement('button');
491
- btn.className = 'copy-btn';
492
- btn.innerText = 'Copy';
493
- btn.onclick = () => {
494
- const codeText = pre.querySelector('code')?.innerText || pre.innerText.replace('Copy', '');
495
- navigator.clipboard.writeText(codeText.trim());
496
- btn.innerText = 'Copied!';
497
- setTimeout(() => btn.innerText = 'Copy', 2000);
498
- };
499
- pre.appendChild(btn);
500
- }
501
- });
502
-
503
- scrollToBottom();
504
- loadSidebar(); // Sync sidebar updates but PREVENT wiping the main chat DOM
505
-
506
- } catch (error) {
507
- console.error(error);
508
- document.getElementById('temp-ai-msg').innerHTML = "<span class='text-red-400'>Connection lost or chat busy. Reload to sync memory.</span>";
509
- } finally {
510
- if (!pollingInterval) toggleInputState(false);
511
- }
512
  }
513
 
514
- document.getElementById('message-input').addEventListener('keydown', (e) => {
515
- if (e.key === 'Enter' && !e.shiftKey) {
516
- e.preventDefault();
517
- if (!document.getElementById('send-btn').classList.contains('hidden')) {
518
- sendMessage();
519
- }
520
- }
521
- });
522
 
523
- loadSidebar();
524
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
525
  </body>
526
  </html>
 
124
  </div>
125
  </main>
126
 
127
+ <!-- ================================= -->
128
+ <!-- CLIENT SCRIPT (REPLACE SCRIPT TAG) -->
129
+ <!-- ================================= -->
130
+
131
+ <script>
132
+
133
+ marked.setOptions({
134
+ highlight(code, lang) {
135
+ const language =
136
+ hljs.getLanguage(lang)
137
+ ? lang
138
+ : 'plaintext';
139
+
140
+ return hljs.highlight(code, {
141
+ language
142
+ }).value;
143
+ }
144
+ });
145
+
146
+ let currentChatId = null;
147
+ let attachedImages = [];
148
+
149
+ const chatWindow =
150
+ document.getElementById('chat-window');
151
+
152
+ const scrollBtn =
153
+ document.getElementById('scroll-bottom-btn');
154
+
155
+ // =================================
156
+ // SAFE HTML
157
+ // =================================
158
+
159
+ function escapeHtml(str = "") {
160
+ return str
161
+ .replaceAll("&", "&amp;")
162
+ .replaceAll("<", "&lt;")
163
+ .replaceAll(">", "&gt;")
164
+ .replaceAll('"', "&quot;")
165
+ .replaceAll("'", "&#39;");
166
+ }
167
+
168
+ // =================================
169
+ // TOKEN DISPLAY
170
+ // =================================
171
+
172
+ function updateTokenDisplay(
173
+ total,
174
+ input,
175
+ output
176
+ ) {
177
+
178
+ document.getElementById('token-total')
179
+ .innerText =
180
+ (total || 0).toLocaleString();
181
+
182
+ document.getElementById('token-in')
183
+ .innerText =
184
+ (input || 0).toLocaleString();
185
+
186
+ document.getElementById('token-out')
187
+ .innerText =
188
+ (output || 0).toLocaleString();
189
+ }
190
+
191
+ // =================================
192
+ // RENDER SINGLE MESSAGE
193
+ // =================================
194
+
195
+ function createMessageElement(
196
+ role,
197
+ content = "",
198
+ reasoning = ""
199
+ ) {
200
 
201
+ const wrapper =
202
+ document.createElement('div');
 
 
 
 
 
 
 
203
 
204
+ wrapper.className =
205
+ `flex ${
206
+ role === 'user'
207
+ ? 'justify-end'
208
+ : 'justify-start'
209
+ } w-full`;
210
 
211
+ const bubble =
212
+ document.createElement('div');
 
 
 
213
 
214
+ bubble.className =
215
+ `max-w-[95%] lg:max-w-[85%]
216
+ rounded-2xl p-4 shadow-sm
217
+ ${
218
+ role === 'user'
219
+ ? 'bg-blue-600 text-white rounded-br-sm'
220
+ : 'bg-gray-850 border border-gray-800 text-gray-200 markdown-body rounded-bl-sm'
221
+ }`;
222
 
223
+ if (role === 'user') {
 
 
 
 
 
 
 
 
 
 
224
 
225
+ bubble.innerHTML =
226
+ `<div class="whitespace-pre-wrap">${escapeHtml(content)}</div>`;
 
 
 
227
 
228
+ } else {
 
 
 
229
 
230
+ let html = "";
 
231
 
232
+ if (reasoning) {
233
+ html += `
234
+ <div class="reasoning-block">
235
+ <i>Thinking Process</i><br/>
236
+ ${DOMPurify.sanitize(
237
+ marked.parse(reasoning)
238
+ )}
239
+ </div>`;
240
+ }
241
 
242
+ html += DOMPurify.sanitize(
243
+ marked.parse(content)
244
+ );
 
245
 
246
+ bubble.innerHTML = html;
247
+ }
 
 
 
 
248
 
249
+ wrapper.appendChild(bubble);
 
 
 
 
 
 
 
 
 
 
 
250
 
251
+ return wrapper;
252
+ }
 
 
 
 
 
 
 
 
 
 
 
 
253
 
254
+ // =================================
255
+ // RENDER CHAT
256
+ // =================================
 
 
 
 
257
 
258
+ function renderMessages(messages) {
 
 
 
 
 
 
 
 
 
 
 
 
259
 
260
+ chatWindow.innerHTML = '';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
 
262
+ messages.forEach(m => {
263
+
264
+ chatWindow.appendChild(
265
+ createMessageElement(
266
+ m.role,
267
+ m.content,
268
+ m.reasoning
269
+ )
270
+ );
271
+ });
272
+
273
+ addCopyButtons();
274
+
275
+ scrollToBottom();
276
+ }
277
+
278
+ // =================================
279
+ // COPY BUTTONS
280
+ // =================================
281
+
282
+ function addCopyButtons() {
283
+
284
+ document
285
+ .querySelectorAll('.markdown-body pre')
286
+ .forEach(pre => {
287
+
288
+ if (
289
+ pre.querySelector('.copy-btn')
290
+ ) return;
291
+
292
+ const btn =
293
+ document.createElement('button');
294
+
295
+ btn.className = 'copy-btn';
296
+
297
+ btn.innerText = 'Copy';
298
+
299
+ btn.onclick = () => {
300
+
301
+ const codeText =
302
+ pre.querySelector('code')
303
+ ?.innerText || '';
304
+
305
+ navigator.clipboard.writeText(
306
+ codeText.trim()
307
+ );
308
+
309
+ btn.innerText = 'Copied';
310
+
311
+ setTimeout(() => {
312
+ btn.innerText = 'Copy';
313
+ }, 2000);
314
+ };
315
+
316
+ pre.appendChild(btn);
317
+ });
318
+ }
319
+
320
+ // =================================
321
+ // SCROLL
322
+ // =================================
323
+
324
+ function scrollToBottom() {
325
+
326
+ chatWindow.scrollTo({
327
+ top: chatWindow.scrollHeight,
328
+ behavior: 'smooth'
329
+ });
330
+ }
331
+
332
+ // =================================
333
+ // SIDEBAR
334
+ // =================================
335
+
336
+ async function loadSidebar() {
337
+
338
+ const res =
339
+ await fetch('/api/chats');
340
+
341
+ const chats =
342
+ await res.json();
343
+
344
+ document.getElementById('chat-list')
345
+ .innerHTML = chats.map(c => `
346
+
347
+ <div
348
+ onclick="selectChat('${c.id}')"
349
+ class="p-3 rounded-xl cursor-pointer
350
+ ${
351
+ c.id === currentChatId
352
+ ? 'bg-gray-800'
353
+ : 'hover:bg-gray-800/50'
354
+ }">
355
+
356
+ <div class="text-sm font-medium truncate text-gray-200">
357
+ ${escapeHtml(c.title)}
358
+ </div>
359
+
360
+ </div>
361
+
362
+ `).join('');
363
+ }
364
+
365
+ // =================================
366
+ // SELECT CHAT
367
+ // =================================
368
+
369
+ async function selectChat(id) {
370
+
371
+ currentChatId = id;
372
+
373
+ const res =
374
+ await fetch(`/api/chats/${id}`);
375
+
376
+ const chat =
377
+ await res.json();
378
+
379
+ document.getElementById(
380
+ 'current-chat-title'
381
+ ).innerText = chat.title;
382
+
383
+ renderMessages(chat.messages);
384
+
385
+ updateTokenDisplay(
386
+ chat.totalTokens,
387
+ chat.inputTokens,
388
+ chat.outputTokens
389
+ );
390
+
391
+ loadSidebar();
392
+ }
393
+
394
+ // =================================
395
+ // CREATE CHAT
396
+ // =================================
397
+
398
+ async function createNewChat() {
399
+
400
+ const res =
401
+ await fetch('/api/chats', {
402
+ method: 'POST'
403
+ });
404
+
405
+ const chat =
406
+ await res.json();
407
+
408
+ await selectChat(chat.id);
409
+ }
410
+
411
+ // =================================
412
+ // SEND MESSAGE
413
+ // =================================
414
+
415
+ async function sendMessage() {
416
+
417
+ const input =
418
+ document.getElementById(
419
+ 'message-input'
420
+ );
421
+
422
+ const text =
423
+ input.value.trim();
424
+
425
+ if (!text || !currentChatId)
426
+ return;
427
+
428
+ // ADD USER MESSAGE
429
+ const userElement =
430
+ createMessageElement(
431
+ 'user',
432
+ text
433
+ );
434
+
435
+ chatWindow.appendChild(userElement);
436
+
437
+ // ADD AI PLACEHOLDER
438
+ const aiWrapper =
439
+ document.createElement('div');
440
+
441
+ aiWrapper.className =
442
+ 'flex justify-start w-full';
443
+
444
+ aiWrapper.innerHTML = `
445
+ <div
446
+ class="max-w-[95%] lg:max-w-[85%]
447
+ rounded-2xl rounded-bl-sm p-4
448
+ bg-gray-850 border border-gray-800
449
+ text-gray-200 markdown-body"
450
+ id="streaming-ai-msg">
451
+
452
+ <span class="animate-pulse text-gray-400">
453
+ Thinking...
454
+ </span>
455
+
456
+ </div>
457
+ `;
458
+
459
+ chatWindow.appendChild(aiWrapper);
460
+
461
+ scrollToBottom();
462
+
463
+ input.value = '';
464
+
465
+ let aiContent = "";
466
+ let aiReasoning = "";
467
+
468
+ try {
469
+
470
+ const response =
471
+ await fetch(
472
+ `/api/chats/${currentChatId}/stream`,
473
+ {
474
+ method: 'POST',
475
+
476
+ headers: {
477
+ 'Content-Type':
478
+ 'application/json'
479
+ },
480
+
481
+ body: JSON.stringify({
482
+ model:
483
+ document.getElementById(
484
+ 'model-select'
485
+ ).value,
486
+
487
+ prompt: text,
488
+
489
+ images: attachedImages
490
+ })
491
  }
492
+ );
493
 
494
+ const reader =
495
+ response.body
496
+ .pipeThrough(
497
+ new TextDecoderStream()
498
+ )
499
+ .getReader();
500
+
501
+ let buffer = "";
502
+
503
+ while (true) {
504
+
505
+ const {
506
+ value,
507
+ done
508
+ } = await reader.read();
509
+
510
+ if (done) break;
511
+
512
+ buffer += value;
513
+
514
+ const events =
515
+ buffer.split("\n\n");
516
+
517
+ buffer = events.pop();
518
+
519
+ for (const event of events) {
520
+
521
+ const lines =
522
+ event.split("\n");
523
+
524
+ let type = "";
525
+ let data = "";
526
+
527
+ for (const line of lines) {
528
+
529
+ if (
530
+ line.startsWith("event:")
531
+ ) {
532
+ type =
533
+ line.replace(
534
+ "event:",
535
+ ""
536
+ ).trim();
537
+ }
538
+
539
+ if (
540
+ line.startsWith("data:")
541
+ ) {
542
+ data =
543
+ line.replace(
544
+ "data:",
545
+ ""
546
+ ).trim();
547
+ }
548
  }
549
 
550
+ if (!data) continue;
551
+
552
+ const parsed =
553
+ JSON.parse(data);
554
+
555
+ if (type === 'thinking') {
556
+
557
+ aiReasoning += parsed.text;
558
+
559
+ } else if (type === 'text') {
560
+
561
+ aiContent += parsed.text;
562
+
563
+ } else if (type === 'usage') {
564
+
565
+ updateTokenDisplay(
566
+ parsed.totalTokens,
567
+ parsed.inputTokens,
568
+ parsed.outputTokens
569
+ );
570
  }
571
 
572
+ let html = "";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
573
 
574
+ if (aiReasoning) {
575
+ html += `
576
+ <div class="reasoning-block">
577
+ <i>Thinking Process</i><br/>
578
+ ${DOMPurify.sanitize(
579
+ marked.parse(aiReasoning)
580
+ )}
581
+ </div>
582
+ `;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
583
  }
584
 
585
+ html += DOMPurify.sanitize(
586
+ marked.parse(aiContent)
587
+ );
 
 
 
 
 
588
 
589
+ document.getElementById(
590
+ 'streaming-ai-msg'
591
+ ).innerHTML = html;
592
+
593
+ addCopyButtons();
594
+
595
+ scrollToBottom();
596
+ }
597
+ }
598
+
599
+ } catch (err) {
600
+
601
+ console.error(err);
602
+
603
+ document.getElementById(
604
+ 'streaming-ai-msg'
605
+ ).innerHTML = `
606
+ <span class="text-red-400">
607
+ Connection lost.
608
+ </span>
609
+ `;
610
+ }
611
+ }
612
+
613
+ // =================================
614
+ // ENTER SEND
615
+ // =================================
616
+
617
+ document.getElementById(
618
+ 'message-input'
619
+ ).addEventListener('keydown', e => {
620
+
621
+ if (
622
+ e.key === 'Enter' &&
623
+ !e.shiftKey
624
+ ) {
625
+
626
+ e.preventDefault();
627
+
628
+ sendMessage();
629
+ }
630
+ });
631
+
632
+ // =================================
633
+ // INIT
634
+ // =================================
635
+
636
+ loadSidebar();
637
+
638
+ </script>
639
  </body>
640
  </html>