Spaces:
Sleeping
Sleeping
Update public/index.html
Browse files- 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-[
|
| 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-
|
| 89 |
-
<
|
| 90 |
-
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
| 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();
|
| 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-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 223 |
-
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|