Pepguy commited on
Commit
b66135b
·
verified ·
1 Parent(s): 51d39d2

Update public/index.html

Browse files
Files changed (1) hide show
  1. public/index.html +25 -201
public/index.html CHANGED
@@ -60,7 +60,7 @@
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-[50%]">
64
  <button onclick="toggleSidebar()" class="lg:hidden p-1 -ml-1 text-gray-400 hover:text-white shrink-0">
65
  <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path></svg>
66
  </button>
@@ -85,10 +85,13 @@
85
  </div>
86
  </div>
87
 
88
- <div class="flex items-center gap-2 lg:gap-4 shrink-0 pl-2">
89
- <span class="hidden sm:inline-block text-xs text-blue-300 bg-blue-900/30 border border-blue-800/50 px-2 py-1 rounded-md font-mono">
90
- Tokens: <span id="token-counter">0</span>
91
- </span>
 
 
 
92
  <select id="model-select" class="bg-gray-800 text-xs lg:text-sm rounded-md border border-gray-700 px-2 py-1.5 outline-none focus:border-blue-500 transition-colors cursor-pointer text-gray-200">
93
  <option value="claude">Claude Sonnet 4.6</option>
94
  <option value="haiku">Claude Haiku</option>
@@ -133,6 +136,9 @@
133
  let currentChatId = null;
134
  let attachedImages =[];
135
  let pollingInterval = null;
 
 
 
136
 
137
  // --- TITLE EDITING LOGIC ---
138
  function enableTitleEdit() {
@@ -167,16 +173,14 @@
167
  headers: { 'Content-Type': 'application/json' },
168
  body: JSON.stringify({ title: newTitle })
169
  });
170
- loadSidebar(); // Refresh sidebar to show new name
171
  }
172
 
173
- // Handle Enter and Escape keys for title input
174
  document.getElementById('title-input').addEventListener('keydown', (e) => {
175
  if (e.key === 'Enter') saveTitle();
176
  if (e.key === 'Escape') cancelTitleEdit();
177
  });
178
 
179
-
180
  // --- UI CORE LOGIC ---
181
  function toggleSidebar() {
182
  const sidebar = document.getElementById('sidebar');
@@ -191,6 +195,13 @@
191
  this.style.height = (this.scrollHeight < 128 ? this.scrollHeight : 128) + 'px';
192
  });
193
 
 
 
 
 
 
 
 
194
  async function loadSidebar() {
195
  const res = await fetch('/api/chats');
196
  const chats = await res.json();
@@ -198,7 +209,10 @@
198
  list.innerHTML = chats.map(c => `
199
  <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'}">
200
  <div class="text-sm font-medium truncate text-gray-200">${c.title}</div>
201
- <div class="text-xs text-gray-500 mt-1">${c.totalTokens.toLocaleString()} tokens</div>
 
 
 
202
  </div>
203
  `).join('');
204
  }
@@ -219,195 +233,5 @@
219
  document.getElementById('chat-window').innerHTML = '';
220
  document.getElementById('current-chat-title').innerText = 'Select or create a chat';
221
  document.getElementById('edit-title-btn').classList.add('hidden');
222
- document.getElementById('token-counter').innerText = '0';
223
- loadSidebar();
224
- }
225
- }
226
-
227
- async function selectChat(id) {
228
- currentChatId = id;
229
- clearInterval(pollingInterval);
230
- const res = await fetch(`/api/chats/${id}`);
231
- const chat = await res.json();
232
-
233
- document.getElementById('current-chat-title').innerText = chat.title;
234
- document.getElementById('edit-title-btn').classList.remove('hidden'); // Show edit icon for active chat
235
- document.getElementById('token-counter').innerText = chat.totalTokens.toLocaleString();
236
-
237
- cancelTitleEdit(); // Ensure we reset title UI state if switching
238
- renderMessages(chat.messages);
239
- loadSidebar();
240
-
241
- if(window.innerWidth < 1024 && !document.getElementById('sidebar').classList.contains('-translate-x-full')) {
242
- toggleSidebar();
243
- }
244
- if (chat.isGenerating) pollGeneratingChat(id);
245
- }
246
-
247
- function renderMessages(messages) {
248
- const container = document.getElementById('chat-window');
249
- container.innerHTML = messages.map(m => {
250
- let html = '';
251
- if (m.reasoning) {
252
- html += `<div class="reasoning-block"><i>Thinking Process</i><br/>${marked.parse(m.reasoning)}</div>`;
253
- }
254
- if (m.content) {
255
- html += DOMPurify.sanitize(marked.parse(m.content));
256
- }
257
-
258
- return `
259
- <div class="flex ${m.role === 'user' ? 'justify-end' : 'justify-start'} w-full">
260
- <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'}">
261
- ${m.role === 'user' ? `<div class="whitespace-pre-wrap">${m.content}</div>` : html}
262
- </div>
263
- </div>
264
- `;
265
- }).join('');
266
-
267
- document.querySelectorAll('.markdown-body pre').forEach(pre => {
268
- if (!pre.querySelector('.copy-btn')) {
269
- const btn = document.createElement('button');
270
- btn.className = 'copy-btn';
271
- btn.innerText = 'Copy';
272
- btn.onclick = () => {
273
- const codeText = pre.querySelector('code')?.innerText || pre.innerText.replace('Copy', '');
274
- navigator.clipboard.writeText(codeText.trim());
275
- btn.innerText = 'Copied!';
276
- setTimeout(() => btn.innerText = 'Copy', 2000);
277
- };
278
- pre.appendChild(btn);
279
- }
280
- });
281
- container.scrollTop = container.scrollHeight;
282
- }
283
-
284
- function pollGeneratingChat(id) {
285
- pollingInterval = setInterval(async () => {
286
- const res = await fetch(`/api/chats/${id}`);
287
- const chat = await res.json();
288
- renderMessages(chat.messages);
289
- document.getElementById('token-counter').innerText = chat.totalTokens.toLocaleString();
290
- if (chat.title !== document.getElementById('current-chat-title').innerText && document.getElementById('title-display').classList.contains('hidden') === false) {
291
- document.getElementById('current-chat-title').innerText = chat.title;
292
- loadSidebar();
293
- }
294
- if (!chat.isGenerating) clearInterval(pollingInterval);
295
- }, 1500);
296
- }
297
-
298
- function handleImageSelect(event) {
299
- const files = event.target.files;
300
- const container = document.getElementById('image-preview-container');
301
- container.classList.remove('hidden');
302
-
303
- Array.from(files).forEach(file => {
304
- const reader = new FileReader();
305
- reader.onload = (e) => {
306
- attachedImages.push(e.target.result);
307
- container.innerHTML += `
308
- <div class="relative group">
309
- <img src="${e.target.result}" class="h-14 w-14 object-cover rounded-lg border border-gray-600 shadow-sm">
310
- </div>`;
311
- };
312
- reader.readAsDataURL(file);
313
- });
314
- }
315
-
316
- async function sendMessage() {
317
- const input = document.getElementById('message-input');
318
- const text = input.value.trim();
319
- if (!text || !currentChatId) return;
320
-
321
- const payload = {
322
- model: document.getElementById('model-select').value,
323
- prompt: text,
324
- images: attachedImages
325
- };
326
-
327
- const container = document.getElementById('chat-window');
328
- container.innerHTML += `
329
- <div class="flex justify-end w-full">
330
- <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>
331
- </div>
332
- <div class="flex justify-start w-full" id="temp-ai-wrapper">
333
- <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">
334
- <span class="flex items-center gap-2 text-gray-400">
335
- <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>
336
- Thinking...
337
- </span>
338
- </div>
339
- </div>
340
- `;
341
- container.scrollTop = container.scrollHeight;
342
-
343
- input.value = '';
344
- input.style.height = 'auto';
345
- attachedImages =[];
346
- document.getElementById('image-preview-container').innerHTML = '';
347
- document.getElementById('image-preview-container').classList.add('hidden');
348
- document.getElementById('send-btn').disabled = true;
349
-
350
- try {
351
- const response = await fetch(`/api/chats/${currentChatId}/stream`, {
352
- method: 'POST',
353
- headers: { 'Content-Type': 'application/json' },
354
- body: JSON.stringify(payload)
355
- });
356
-
357
- const reader = response.body.getReader();
358
- const decoder = new TextDecoder("utf-8");
359
- let aiContent = "";
360
- let aiReasoning = "";
361
-
362
- while (true) {
363
- const { value, done } = await reader.read();
364
- if (done) break;
365
-
366
- const chunkText = decoder.decode(value, { stream: true });
367
-
368
- const chunks = chunkText.split(/(__THINK__|__USAGE__)/);
369
- let i = 0;
370
- while (i < chunks.length) {
371
- if (chunks[i] === '__THINK__') {
372
- aiReasoning += chunks[i+1];
373
- i += 2;
374
- } else if (chunks[i] === '__USAGE__') {
375
- const usageData = JSON.parse(chunks[i+1]);
376
- document.getElementById('token-counter').innerText =
377
- (parseInt(document.getElementById('token-counter').innerText.replace(/,/g, '')) + usageData.totalTokenCount).toLocaleString();
378
- i += 2;
379
- } else {
380
- aiContent += chunks[i];
381
- i++;
382
- }
383
- }
384
-
385
- let tempHtml = "";
386
- if (aiReasoning) tempHtml += `<div class="reasoning-block"><i>Thinking Process</i><br/>${marked.parse(aiReasoning)}</div>`;
387
- if (aiContent) tempHtml += DOMPurify.sanitize(marked.parse(aiContent));
388
-
389
- document.getElementById('temp-ai-msg').innerHTML = tempHtml || "<span class='animate-pulse text-gray-400'>Thinking...</span>";
390
- container.scrollTop = container.scrollHeight;
391
- }
392
-
393
- selectChat(currentChatId);
394
-
395
- } catch (error) {
396
- console.error(error);
397
- document.getElementById('temp-ai-msg').innerHTML = "<span class='text-red-400'>Error connecting to API.</span>";
398
- } finally {
399
- document.getElementById('send-btn').disabled = false;
400
- }
401
- }
402
-
403
- document.getElementById('message-input').addEventListener('keydown', (e) => {
404
- if (e.key === 'Enter' && !e.shiftKey) {
405
- e.preventDefault();
406
- sendMessage();
407
- }
408
- });
409
-
410
- loadSidebar();
411
- </script>
412
- </body>
413
- </html>
 
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">
65
  <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path></svg>
66
  </button>
 
85
  </div>
86
  </div>
87
 
88
+ <div class="flex items-center gap-3 lg:gap-4 shrink-0 pl-2">
89
+ <!-- Detailed Token Display -->
90
+ <div class="hidden sm:flex flex-col items-end text-[10px] text-blue-300 bg-blue-900/30 border border-blue-800/50 px-2 py-1 rounded-md font-mono leading-tight">
91
+ <span class="font-semibold text-[11px]">Total: <span id="token-total">0</span></span>
92
+ <span class="text-blue-400/70">In: <span id="token-in">0</span> | Out: <span id="token-out">0</span></span>
93
+ </div>
94
+
95
  <select id="model-select" class="bg-gray-800 text-xs lg:text-sm rounded-md border border-gray-700 px-2 py-1.5 outline-none focus:border-blue-500 transition-colors cursor-pointer text-gray-200">
96
  <option value="claude">Claude Sonnet 4.6</option>
97
  <option value="haiku">Claude Haiku</option>
 
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() {
 
173
  headers: { 'Content-Type': 'application/json' },
174
  body: JSON.stringify({ title: newTitle })
175
  });
176
+ loadSidebar();
177
  }
178
 
 
179
  document.getElementById('title-input').addEventListener('keydown', (e) => {
180
  if (e.key === 'Enter') saveTitle();
181
  if (e.key === 'Escape') cancelTitleEdit();
182
  });
183
 
 
184
  // --- UI CORE LOGIC ---
185
  function toggleSidebar() {
186
  const sidebar = document.getElementById('sidebar');
 
195
  this.style.height = (this.scrollHeight < 128 ? this.scrollHeight : 128) + 'px';
196
  });
197
 
198
+ function updateTokenDisplay(total, input, output) {
199
+ currentTokens = { total, in: input, out: output };
200
+ document.getElementById('token-total').innerText = (total || 0).toLocaleString();
201
+ document.getElementById('token-in').innerText = (input || 0).toLocaleString();
202
+ document.getElementById('token-out').innerText = (output || 0).toLocaleString();
203
+ }
204
+
205
  async function loadSidebar() {
206
  const res = await fetch('/api/chats');
207
  const chats = await res.json();
 
209
  list.innerHTML = chats.map(c => `
210
  <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'}">
211
  <div class="text-sm font-medium truncate text-gray-200">${c.title}</div>
212
+ <div class="text-[10px] text-gray-500 mt-1 flex justify-between">
213
+ <span>Total: ${c.totalTokens.toLocaleString()}</span>
214
+ <span class="opacity-70">↑${(c.outputTokens || 0).toLocaleString()}</span>
215
+ </div>
216
  </div>
217
  `).join('');
218
  }
 
233
  document.getElementById('chat-window').innerHTML = '';
234
  document.getElementById('current-chat-title').innerText = 'Select or create a chat';
235
  document.getElementById('edit-title-btn').classList.add('hidden');
236
+ updateTokenDisplay(0, 0, 0);
237
+ loadSid