Pepguy commited on
Commit
d69031c
·
verified ·
1 Parent(s): 8bcce92

Update public/index.html

Browse files
Files changed (1) hide show
  1. public/index.html +69 -19
public/index.html CHANGED
@@ -56,9 +56,9 @@
56
  </aside>
57
 
58
  <!-- Main Chat Area -->
59
- <main class="flex-1 flex flex-col h-full w-full bg-gray-950 min-w-0">
60
  <!-- Header -->
61
- <header class="h-14 min-h-[3.5rem] border-b border-gray-800 flex items-center px-4 lg:px-6 justify-between bg-gray-900/80 backdrop-blur-sm z-10">
62
 
63
  <div class="flex items-center gap-3 overflow-hidden group w-full max-w-[45%]">
64
  <button onclick="toggleSidebar()" class="lg:hidden p-1 -ml-1 text-gray-400 hover:text-white shrink-0">
@@ -103,11 +103,16 @@
103
  </div>
104
  </header>
105
 
106
- <!-- Messages -->
107
  <div id="chat-window" class="flex-1 overflow-y-auto p-4 lg:p-6 space-y-6 scroll-smooth"></div>
108
 
 
 
 
 
 
109
  <!-- Input Area -->
110
- <div class="p-3 lg:p-4 bg-gray-900 border-t border-gray-800 shrink-0">
111
  <div class="max-w-4xl mx-auto">
112
  <div id="image-preview-container" class="flex flex-wrap gap-2 mb-2 hidden px-1"></div>
113
  <div class="flex items-end gap-2 bg-gray-850 rounded-xl border border-gray-700 p-2 focus-within:border-blue-500 focus-within:ring-1 focus-within:ring-blue-500 transition-all shadow-sm">
@@ -136,10 +141,27 @@
136
  let currentChatId = null;
137
  let attachedImages =[];
138
  let pollingInterval = null;
139
-
140
- // Local token state variables
141
  let currentTokens = { total: 0, in: 0, out: 0 };
142
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  // --- TITLE EDITING LOGIC ---
144
  function enableTitleEdit() {
145
  if (!currentChatId) return;
@@ -181,6 +203,7 @@
181
  if (e.key === 'Escape') cancelTitleEdit();
182
  });
183
 
 
184
  // --- UI CORE LOGIC ---
185
  function toggleSidebar() {
186
  const sidebar = document.getElementById('sidebar');
@@ -240,7 +263,8 @@
240
 
241
  async function selectChat(id) {
242
  currentChatId = id;
243
- clearInterval(pollingInterval);
 
244
  const res = await fetch(`/api/chats/${id}`);
245
  const chat = await res.json();
246
 
@@ -248,7 +272,6 @@
248
  document.getElementById('edit-title-btn').classList.remove('hidden');
249
 
250
  updateTokenDisplay(chat.totalTokens, chat.inputTokens, chat.outputTokens);
251
-
252
  cancelTitleEdit();
253
  renderMessages(chat.messages);
254
  loadSidebar();
@@ -256,11 +279,21 @@
256
  if(window.innerWidth < 1024 && !document.getElementById('sidebar').classList.contains('-translate-x-full')) {
257
  toggleSidebar();
258
  }
259
- if (chat.isGenerating) pollGeneratingChat(id);
 
 
 
 
 
 
 
260
  }
261
 
262
  function renderMessages(messages) {
263
  const container = document.getElementById('chat-window');
 
 
 
264
  container.innerHTML = messages.map(m => {
265
  let html = '';
266
  if (m.reasoning) {
@@ -293,13 +326,18 @@
293
  pre.appendChild(btn);
294
  }
295
  });
296
- container.scrollTop = container.scrollHeight;
 
 
297
  }
298
 
299
  function pollGeneratingChat(id) {
 
 
300
  pollingInterval = setInterval(async () => {
301
  const res = await fetch(`/api/chats/${id}`);
302
  const chat = await res.json();
 
303
  renderMessages(chat.messages);
304
  updateTokenDisplay(chat.totalTokens, chat.inputTokens, chat.outputTokens);
305
 
@@ -307,8 +345,14 @@
307
  document.getElementById('current-chat-title').innerText = chat.title;
308
  loadSidebar();
309
  }
310
- if (!chat.isGenerating) clearInterval(pollingInterval);
311
- }, 1500);
 
 
 
 
 
 
312
  }
313
 
314
  function handleImageSelect(event) {
@@ -354,7 +398,7 @@
354
  </div>
355
  </div>
356
  `;
357
- container.scrollTop = container.scrollHeight;
358
 
359
  input.value = '';
360
  input.style.height = 'auto';
@@ -370,6 +414,8 @@
370
  body: JSON.stringify(payload)
371
  });
372
 
 
 
373
  const reader = response.body.getReader();
374
  const decoder = new TextDecoder("utf-8");
375
  let aiContent = "";
@@ -389,7 +435,6 @@
389
  i += 2;
390
  } else if (chunks[i] === '__USAGE__') {
391
  const usageData = JSON.parse(chunks[i+1]);
392
- // Update the live token state globally
393
  updateTokenDisplay(
394
  currentTokens.total + usageData.totalTokens,
395
  currentTokens.in + usageData.inputTokens,
@@ -407,24 +452,29 @@
407
  if (aiContent) tempHtml += DOMPurify.sanitize(marked.parse(aiContent));
408
 
409
  document.getElementById('temp-ai-msg').innerHTML = tempHtml || "<span class='animate-pulse text-gray-400'>Thinking...</span>";
410
- container.scrollTop = container.scrollHeight;
 
 
 
411
  }
412
 
413
- // Force final refresh of states from db
414
  selectChat(currentChatId);
415
 
416
  } catch (error) {
417
  console.error(error);
418
- document.getElementById('temp-ai-msg').innerHTML = "<span class='text-red-400'>Error connecting to API.</span>";
419
  } finally {
420
- document.getElementById('send-btn').disabled = false;
 
 
 
421
  }
422
  }
423
 
424
  document.getElementById('message-input').addEventListener('keydown', (e) => {
425
  if (e.key === 'Enter' && !e.shiftKey) {
426
  e.preventDefault();
427
- sendMessage();
428
  }
429
  });
430
 
 
56
  </aside>
57
 
58
  <!-- Main Chat Area -->
59
+ <main class="flex-1 flex flex-col h-full w-full bg-gray-950 min-w-0 relative">
60
  <!-- Header -->
61
+ <header class="h-14 min-h-[3.5rem] border-b border-gray-800 flex items-center px-4 lg:px-6 justify-between bg-gray-900/80 backdrop-blur-sm z-10 shrink-0">
62
 
63
  <div class="flex items-center gap-3 overflow-hidden group w-full max-w-[45%]">
64
  <button onclick="toggleSidebar()" class="lg:hidden p-1 -ml-1 text-gray-400 hover:text-white shrink-0">
 
103
  </div>
104
  </header>
105
 
106
+ <!-- Messages Area -->
107
  <div id="chat-window" class="flex-1 overflow-y-auto p-4 lg:p-6 space-y-6 scroll-smooth"></div>
108
 
109
+ <!-- Scroll to Bottom Button -->
110
+ <button id="scroll-bottom-btn" onclick="scrollToBottom()" class="hidden absolute bottom-28 right-8 p-2 bg-gray-800/80 hover:bg-gray-700 text-gray-300 hover:text-white rounded-full shadow-lg border border-gray-700 backdrop-blur-sm transition-all z-20">
111
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"></path></svg>
112
+ </button>
113
+
114
  <!-- Input Area -->
115
+ <div class="p-3 lg:p-4 bg-gray-900 border-t border-gray-800 shrink-0 relative z-30">
116
  <div class="max-w-4xl mx-auto">
117
  <div id="image-preview-container" class="flex flex-wrap gap-2 mb-2 hidden px-1"></div>
118
  <div class="flex items-end gap-2 bg-gray-850 rounded-xl border border-gray-700 p-2 focus-within:border-blue-500 focus-within:ring-1 focus-within:ring-blue-500 transition-all shadow-sm">
 
141
  let currentChatId = null;
142
  let attachedImages =[];
143
  let pollingInterval = null;
 
 
144
  let currentTokens = { total: 0, in: 0, out: 0 };
145
 
146
+ // --- SCROLL TO BOTTOM LOGIC ---
147
+ const chatWindow = document.getElementById('chat-window');
148
+ const scrollBtn = document.getElementById('scroll-bottom-btn');
149
+
150
+ chatWindow.addEventListener('scroll', () => {
151
+ // Show the button if we scroll up more than 100 pixels from the very bottom
152
+ const distanceToBottom = chatWindow.scrollHeight - chatWindow.scrollTop - chatWindow.clientHeight;
153
+ if (distanceToBottom > 100) {
154
+ scrollBtn.classList.remove('hidden');
155
+ } else {
156
+ scrollBtn.classList.add('hidden');
157
+ }
158
+ });
159
+
160
+ function scrollToBottom() {
161
+ chatWindow.scrollTo({ top: chatWindow.scrollHeight, behavior: 'smooth' });
162
+ }
163
+
164
+
165
  // --- TITLE EDITING LOGIC ---
166
  function enableTitleEdit() {
167
  if (!currentChatId) return;
 
203
  if (e.key === 'Escape') cancelTitleEdit();
204
  });
205
 
206
+
207
  // --- UI CORE LOGIC ---
208
  function toggleSidebar() {
209
  const sidebar = document.getElementById('sidebar');
 
263
 
264
  async function selectChat(id) {
265
  currentChatId = id;
266
+ if (pollingInterval) clearInterval(pollingInterval);
267
+
268
  const res = await fetch(`/api/chats/${id}`);
269
  const chat = await res.json();
270
 
 
272
  document.getElementById('edit-title-btn').classList.remove('hidden');
273
 
274
  updateTokenDisplay(chat.totalTokens, chat.inputTokens, chat.outputTokens);
 
275
  cancelTitleEdit();
276
  renderMessages(chat.messages);
277
  loadSidebar();
 
279
  if(window.innerWidth < 1024 && !document.getElementById('sidebar').classList.contains('-translate-x-full')) {
280
  toggleSidebar();
281
  }
282
+
283
+ // Lock UI and start polling if background generation is active
284
+ if (chat.isGenerating) {
285
+ document.getElementById('send-btn').disabled = true;
286
+ pollGeneratingChat(id);
287
+ } else {
288
+ document.getElementById('send-btn').disabled = false;
289
+ }
290
  }
291
 
292
  function renderMessages(messages) {
293
  const container = document.getElementById('chat-window');
294
+ // Track if user is already at the bottom before adding new HTML
295
+ const isScrolledToBottom = container.scrollHeight - container.clientHeight <= container.scrollTop + 20;
296
+
297
  container.innerHTML = messages.map(m => {
298
  let html = '';
299
  if (m.reasoning) {
 
326
  pre.appendChild(btn);
327
  }
328
  });
329
+
330
+ // Auto-scroll logic if user was already at the bottom
331
+ if (isScrolledToBottom) scrollToBottom();
332
  }
333
 
334
  function pollGeneratingChat(id) {
335
+ if (pollingInterval) clearInterval(pollingInterval);
336
+
337
  pollingInterval = setInterval(async () => {
338
  const res = await fetch(`/api/chats/${id}`);
339
  const chat = await res.json();
340
+
341
  renderMessages(chat.messages);
342
  updateTokenDisplay(chat.totalTokens, chat.inputTokens, chat.outputTokens);
343
 
 
345
  document.getElementById('current-chat-title').innerText = chat.title;
346
  loadSidebar();
347
  }
348
+
349
+ if (!chat.isGenerating) {
350
+ clearInterval(pollingInterval);
351
+ if (currentChatId === id) {
352
+ document.getElementById('send-btn').disabled = false;
353
+ }
354
+ }
355
+ }, 1500);
356
  }
357
 
358
  function handleImageSelect(event) {
 
398
  </div>
399
  </div>
400
  `;
401
+ scrollToBottom();
402
 
403
  input.value = '';
404
  input.style.height = 'auto';
 
414
  body: JSON.stringify(payload)
415
  });
416
 
417
+ if (!response.ok) throw new Error("API stream rejected. Chat might be busy.");
418
+
419
  const reader = response.body.getReader();
420
  const decoder = new TextDecoder("utf-8");
421
  let aiContent = "";
 
435
  i += 2;
436
  } else if (chunks[i] === '__USAGE__') {
437
  const usageData = JSON.parse(chunks[i+1]);
 
438
  updateTokenDisplay(
439
  currentTokens.total + usageData.totalTokens,
440
  currentTokens.in + usageData.inputTokens,
 
452
  if (aiContent) tempHtml += DOMPurify.sanitize(marked.parse(aiContent));
453
 
454
  document.getElementById('temp-ai-msg').innerHTML = tempHtml || "<span class='animate-pulse text-gray-400'>Thinking...</span>";
455
+
456
+ // Only auto-scroll while streaming if the user is currently at the bottom.
457
+ const distanceToBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
458
+ if (distanceToBottom < 100) scrollToBottom();
459
  }
460
 
 
461
  selectChat(currentChatId);
462
 
463
  } catch (error) {
464
  console.error(error);
465
+ document.getElementById('temp-ai-msg').innerHTML = "<span class='text-red-400'>Connection lost or chat busy. Reload to sync memory.</span>";
466
  } finally {
467
+ // Do not blindly re-enable if a background generation was picked up by a polling loop
468
+ if (!pollingInterval) {
469
+ document.getElementById('send-btn').disabled = false;
470
+ }
471
  }
472
  }
473
 
474
  document.getElementById('message-input').addEventListener('keydown', (e) => {
475
  if (e.key === 'Enter' && !e.shiftKey) {
476
  e.preventDefault();
477
+ if (!document.getElementById('send-btn').disabled) sendMessage();
478
  }
479
  });
480