Elysia-Suite commited on
Commit
b97e847
·
verified ·
1 Parent(s): 70e1ff0

Upload 15 files

Browse files
Files changed (4) hide show
  1. assets/css/styles.css +143 -2
  2. assets/js/script.js +141 -14
  3. assets/js/utils.js +19 -2
  4. index.html +39 -10
assets/css/styles.css CHANGED
@@ -1075,6 +1075,7 @@ body {
1075
  .input-container {
1076
  border-top: 1px solid var(--color-border);
1077
  padding: 16px 24px;
 
1078
  background-color: var(--color-surface);
1079
  }
1080
 
@@ -1561,6 +1562,16 @@ body {
1561
  background: var(--color-border-hover);
1562
  }
1563
 
 
 
 
 
 
 
 
 
 
 
1564
  /* Collapsible Sidebar Sections */
1565
  .section-collapsible {
1566
  border: 1px solid var(--color-border);
@@ -1626,7 +1637,7 @@ body {
1626
  text-align: center;
1627
  font-size: 12px;
1628
  color: var(--color-text-tertiary);
1629
- z-index: 100;
1630
  }
1631
 
1632
  .app-footer a {
@@ -1647,7 +1658,7 @@ body {
1647
 
1648
  /* Adjust main content to account for footer */
1649
  .main-content {
1650
- padding-bottom: 40px;
1651
  }
1652
 
1653
  /* ═══════════════════════════════════════════════════════════════════════════
@@ -1802,3 +1813,133 @@ body {
1802
  .reset-cost-btn:active {
1803
  transform: translateY(0);
1804
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1075
  .input-container {
1076
  border-top: 1px solid var(--color-border);
1077
  padding: 16px 24px;
1078
+ padding-bottom: 56px; /* Account for fixed footer */
1079
  background-color: var(--color-surface);
1080
  }
1081
 
 
1562
  background: var(--color-border-hover);
1563
  }
1564
 
1565
+ /* Firefox scrollbar styling */
1566
+ * {
1567
+ scrollbar-width: thin;
1568
+ scrollbar-color: var(--color-border) transparent;
1569
+ }
1570
+
1571
+ *:hover {
1572
+ scrollbar-color: var(--color-border-hover) transparent;
1573
+ }
1574
+
1575
  /* Collapsible Sidebar Sections */
1576
  .section-collapsible {
1577
  border: 1px solid var(--color-border);
 
1637
  text-align: center;
1638
  font-size: 12px;
1639
  color: var(--color-text-tertiary);
1640
+ z-index: 50;
1641
  }
1642
 
1643
  .app-footer a {
 
1658
 
1659
  /* Adjust main content to account for footer */
1660
  .main-content {
1661
+ /* Footer space handled by input-container padding-bottom */
1662
  }
1663
 
1664
  /* ═══════════════════════════════════════════════════════════════════════════
 
1813
  .reset-cost-btn:active {
1814
  transform: translateY(0);
1815
  }
1816
+ /* ========================================
1817
+ Scroll to Bottom Button
1818
+ ======================================== */
1819
+ .scroll-to-bottom-btn {
1820
+ position: absolute;
1821
+ bottom: 100px;
1822
+ right: 24px;
1823
+ width: 40px;
1824
+ height: 40px;
1825
+ border-radius: 50%;
1826
+ background: var(--color-surface);
1827
+ border: 1px solid var(--color-border);
1828
+ color: var(--color-text-secondary);
1829
+ cursor: pointer;
1830
+ display: flex;
1831
+ align-items: center;
1832
+ justify-content: center;
1833
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
1834
+ transition: all 0.2s ease;
1835
+ z-index: 20;
1836
+ opacity: 1;
1837
+ transform: translateY(0);
1838
+ }
1839
+
1840
+ .scroll-to-bottom-btn:hover {
1841
+ background: var(--color-primary);
1842
+ border-color: var(--color-primary);
1843
+ color: white;
1844
+ transform: translateY(-2px);
1845
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
1846
+ }
1847
+
1848
+ .scroll-to-bottom-btn.hidden {
1849
+ opacity: 0;
1850
+ transform: translateY(20px);
1851
+ pointer-events: none;
1852
+ }
1853
+
1854
+ /* Midnight theme */
1855
+ [data-theme="midnight"] .scroll-to-bottom-btn {
1856
+ background: #1a0f28;
1857
+ border-color: #2a1a3d;
1858
+ box-shadow: 0 4px 15px rgba(168, 85, 247, 0.2);
1859
+ }
1860
+
1861
+ [data-theme="midnight"] .scroll-to-bottom-btn:hover {
1862
+ background: linear-gradient(135deg, #a855f7 0%, #7c3aed 100%);
1863
+ border-color: #a855f7;
1864
+ }
1865
+
1866
+ /* ========================================
1867
+ Focus Mode — Hide Sidebar on Desktop
1868
+ ======================================== */
1869
+ .focus-mode-btn {
1870
+ position: absolute;
1871
+ top: 16px;
1872
+ right: 16px;
1873
+ z-index: 10;
1874
+ display: none; /* Hidden on mobile by default */
1875
+ align-items: center;
1876
+ justify-content: center;
1877
+ width: 36px;
1878
+ height: 36px;
1879
+ border: 1px solid var(--color-border);
1880
+ border-radius: 8px;
1881
+ background-color: var(--color-surface);
1882
+ color: var(--color-text-secondary);
1883
+ cursor: pointer;
1884
+ transition: all 0.2s;
1885
+ }
1886
+
1887
+ .focus-mode-btn:hover {
1888
+ background-color: var(--color-surface-secondary);
1889
+ color: var(--color-text);
1890
+ }
1891
+
1892
+ .focus-mode-btn .icon-collapse {
1893
+ display: none;
1894
+ }
1895
+
1896
+ .focus-mode-btn .icon-expand {
1897
+ display: block;
1898
+ }
1899
+
1900
+ /* When focus mode is active */
1901
+ body.focus-mode .focus-mode-btn .icon-expand {
1902
+ display: none;
1903
+ }
1904
+
1905
+ body.focus-mode .focus-mode-btn .icon-collapse {
1906
+ display: block;
1907
+ }
1908
+
1909
+ body.focus-mode .focus-mode-btn {
1910
+ background-color: var(--color-primary);
1911
+ border-color: var(--color-primary);
1912
+ color: white;
1913
+ }
1914
+
1915
+ /* Desktop: Show focus button and handle sidebar hiding */
1916
+ @media (min-width: 1025px) {
1917
+ .focus-mode-btn {
1918
+ display: flex;
1919
+ }
1920
+
1921
+ body.focus-mode .sidebar {
1922
+ transform: translateX(-100%);
1923
+ position: absolute;
1924
+ }
1925
+
1926
+ body.focus-mode .main-content {
1927
+ margin-left: 0;
1928
+ }
1929
+
1930
+ /* Smooth transition for sidebar */
1931
+ .sidebar {
1932
+ transition: transform 0.3s var(--transition-timing);
1933
+ }
1934
+ }
1935
+
1936
+ /* Midnight theme focus button */
1937
+ [data-theme="midnight"] .focus-mode-btn:hover {
1938
+ background: linear-gradient(135deg, #1a0f28 0%, #2a1a3d 100%);
1939
+ border-color: #a855f7;
1940
+ }
1941
+
1942
+ [data-theme="midnight"] body.focus-mode .focus-mode-btn {
1943
+ background: linear-gradient(135deg, #a855f7 0%, #7c3aed 100%);
1944
+ border-color: #a855f7;
1945
+ }
assets/js/script.js CHANGED
@@ -70,7 +70,10 @@ const elements = {
70
  presValue: document.getElementById("presValue"),
71
  autoShowReasoningInput: document.getElementById("autoShowReasoning"),
72
  contextLimitInput: document.getElementById("contextLimit"),
73
- messageCounter: document.getElementById("messageCounter")
 
 
 
74
  };
75
 
76
  // NOTE: escapeHtml is defined in utils.js - use that global function
@@ -133,6 +136,11 @@ async function importConversation() {
133
 
134
  // Load a conversation into the UI (called by onConversationChange callback)
135
  function loadConversationIntoUI(conversation, messages) {
 
 
 
 
 
136
  // Clear and load messages
137
  elements.chatMessages.innerHTML = "";
138
 
@@ -160,7 +168,7 @@ function loadConversationIntoUI(conversation, messages) {
160
 
161
  updateMessageCounter();
162
 
163
- // ONLY place that refreshes conversation list
164
  conversationsUI.refresh();
165
 
166
  // Show notification when switching conversations (but not on initial load)
@@ -203,6 +211,7 @@ async function init() {
203
  updateModelSelection();
204
  updateReasoningSection();
205
  checkApiKey();
 
206
  conversationsUI.init();
207
  await initializeConversations();
208
  }
@@ -448,18 +457,102 @@ function setupEventListeners() {
448
  sendMessage();
449
  }
450
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
451
  });
452
  }
453
 
 
 
 
454
  // Handle input change
455
  function handleInputChange() {
456
- // Auto-resize textarea
457
- elements.messageInput.style.height = "auto";
458
- elements.messageInput.style.height = elements.messageInput.scrollHeight + "px";
 
 
 
459
 
460
  updateSendButton();
461
  }
462
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
463
  // Check API key
464
  function checkApiKey() {
465
  if (!state.apiKey) {
@@ -790,10 +883,19 @@ function addMessage(role, content, reasoning = null, timestamp = null, messageId
790
 
791
  // Delete a message from UI, state, and database
792
  async function deleteMessage(messageDiv, content, role) {
793
- // Find and remove from state.messages
794
- const msgIndex = state.messages.findIndex(m => m.role === role && m.content === content);
795
- if (msgIndex !== -1) {
796
- state.messages.splice(msgIndex, 1);
 
 
 
 
 
 
 
 
 
797
  }
798
 
799
  // Remove from database if we have a messageId
@@ -985,6 +1087,11 @@ async function regenerateResponse(messageDiv) {
985
  );
986
  state.messages.push({ role: "assistant", content: fullContent });
987
 
 
 
 
 
 
988
  try {
989
  await saveMessageToDB("assistant", fullContent, reasoning);
990
  } catch (error) {}
@@ -1179,6 +1286,12 @@ function finalizeStreamingMessage(messageDiv, textDiv, contentDiv, content, reas
1179
  footerDiv.innerHTML = `
1180
  <span class="message-time">${timeStr}</span>
1181
  <div class="message-actions">
 
 
 
 
 
 
1182
  <button class="msg-action-btn" data-action="copy" title="Copy">
1183
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1184
  <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
@@ -1194,6 +1307,11 @@ function finalizeStreamingMessage(messageDiv, textDiv, contentDiv, content, reas
1194
  </div>
1195
  `;
1196
 
 
 
 
 
 
1197
  footerDiv.querySelector('[data-action="copy"]').addEventListener("click", () => {
1198
  navigator.clipboard
1199
  .writeText(content)
@@ -1267,10 +1385,10 @@ async function sendMessage() {
1267
  if (chunk.reasoning) {
1268
  reasoning = chunk.reasoning;
1269
  }
1270
- // Handle usage stats
1271
  if (chunk.usage) {
1272
  state.lastUsage = chunk.usage;
1273
- updateTokenCounter();
1274
  }
1275
  });
1276
 
@@ -1279,6 +1397,11 @@ async function sendMessage() {
1279
  finalizeStreamingMessage(messageDiv, textDiv, contentDiv, fullContent, reasoning, new Date().toISOString());
1280
  state.messages.push({ role: "assistant", content: fullContent });
1281
 
 
 
 
 
 
1282
  // Save to database
1283
  try {
1284
  await saveMessageToDB("assistant", fullContent, reasoning);
@@ -1347,15 +1470,19 @@ function updateSendButton() {
1347
  }
1348
 
1349
  // Update token counter display
1350
- function updateTokenCounter() {
1351
  const sessionCostEl = document.getElementById("sessionCostDisplay");
1352
  const tokenUsageEl = document.getElementById("tokenUsageDisplay");
1353
 
1354
  if (state.lastUsage) {
1355
  const input = state.lastUsage.prompt_tokens || 0;
1356
  const output = state.lastUsage.completion_tokens || 0;
1357
- const lastCost = calculateCost(state.lastUsage, state.selectedModel);
1358
- state.totalCost += lastCost;
 
 
 
 
1359
 
1360
  const costStr =
1361
  state.totalCost < 0.01 ? `${(state.totalCost * 100).toFixed(2)}¢` : `$${state.totalCost.toFixed(4)}`;
 
70
  presValue: document.getElementById("presValue"),
71
  autoShowReasoningInput: document.getElementById("autoShowReasoning"),
72
  contextLimitInput: document.getElementById("contextLimit"),
73
+ messageCounter: document.getElementById("messageCounter"),
74
+ // New UI elements
75
+ scrollToBottomBtn: document.getElementById("scrollToBottomBtn"),
76
+ focusModeBtn: document.getElementById("focusModeBtn")
77
  };
78
 
79
  // NOTE: escapeHtml is defined in utils.js - use that global function
 
136
 
137
  // Load a conversation into the UI (called by onConversationChange callback)
138
  function loadConversationIntoUI(conversation, messages) {
139
+ // Reset session cost when switching conversations
140
+ state.totalCost = 0;
141
+ state.lastUsage = null;
142
+ updateTokenCounter(false);
143
+
144
  // Clear and load messages
145
  elements.chatMessages.innerHTML = "";
146
 
 
168
 
169
  updateMessageCounter();
170
 
171
+ // Refresh conversation list to update active state (single call)
172
  conversationsUI.refresh();
173
 
174
  // Show notification when switching conversations (but not on initial load)
 
211
  updateModelSelection();
212
  updateReasoningSection();
213
  checkApiKey();
214
+ restoreFocusMode(); // Restore focus mode preference
215
  conversationsUI.init();
216
  await initializeConversations();
217
  }
 
457
  sendMessage();
458
  }
459
  }
460
+ // Ctrl/Cmd + B: Toggle focus mode
461
+ if ((e.ctrlKey || e.metaKey) && e.key === "b") {
462
+ e.preventDefault();
463
+ toggleFocusMode();
464
+ }
465
+ });
466
+
467
+ // Focus mode button
468
+ if (elements.focusModeBtn) {
469
+ elements.focusModeBtn.addEventListener("click", toggleFocusMode);
470
+ }
471
+
472
+ // Scroll to bottom button
473
+ if (elements.scrollToBottomBtn) {
474
+ elements.scrollToBottomBtn.addEventListener("click", scrollToBottom);
475
+ }
476
+
477
+ // Show/hide scroll-to-bottom button based on scroll position
478
+ elements.chatMessages.addEventListener("scroll", handleChatScroll);
479
+
480
+ // Warn user before closing tab if streaming is in progress
481
+ window.addEventListener("beforeunload", e => {
482
+ if (state.isStreaming) {
483
+ e.preventDefault();
484
+ e.returnValue = "A response is currently being generated. Are you sure you want to leave?";
485
+ return e.returnValue;
486
+ }
487
  });
488
  }
489
 
490
+ // Debounce helper for performance
491
+ let resizeTimeout = null;
492
+
493
  // Handle input change
494
  function handleInputChange() {
495
+ // Debounced auto-resize textarea
496
+ if (resizeTimeout) clearTimeout(resizeTimeout);
497
+ resizeTimeout = setTimeout(() => {
498
+ elements.messageInput.style.height = "auto";
499
+ elements.messageInput.style.height = elements.messageInput.scrollHeight + "px";
500
+ }, 16); // ~60fps
501
 
502
  updateSendButton();
503
  }
504
 
505
+ // ========================================
506
+ // Scroll to Bottom & Focus Mode
507
+ // ========================================
508
+
509
+ // Scroll to bottom of chat
510
+ function scrollToBottom() {
511
+ elements.chatMessages.scrollTo({
512
+ top: elements.chatMessages.scrollHeight,
513
+ behavior: "smooth"
514
+ });
515
+ }
516
+
517
+ // Handle chat scroll - show/hide scroll-to-bottom button
518
+ let scrollTimeout = null;
519
+ function handleChatScroll() {
520
+ if (scrollTimeout) clearTimeout(scrollTimeout);
521
+ scrollTimeout = setTimeout(() => {
522
+ const { scrollTop, scrollHeight, clientHeight } = elements.chatMessages;
523
+ const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
524
+
525
+ // Show button if user has scrolled up more than 200px from bottom
526
+ if (elements.scrollToBottomBtn) {
527
+ if (distanceFromBottom > 200) {
528
+ elements.scrollToBottomBtn.classList.remove("hidden");
529
+ } else {
530
+ elements.scrollToBottomBtn.classList.add("hidden");
531
+ }
532
+ }
533
+ }, 100);
534
+ }
535
+
536
+ // Toggle focus mode (hide/show sidebar on desktop)
537
+ function toggleFocusMode() {
538
+ document.body.classList.toggle("focus-mode");
539
+ const isFocusMode = document.body.classList.contains("focus-mode");
540
+
541
+ // Save preference
542
+ localStorage.setItem("focusMode", isFocusMode ? "true" : "false");
543
+
544
+ // Show notification
545
+ showNotification(isFocusMode ? "Focus mode enabled (Ctrl+B to exit)" : "Focus mode disabled", "info");
546
+ }
547
+
548
+ // Restore focus mode on load
549
+ function restoreFocusMode() {
550
+ const savedFocusMode = localStorage.getItem("focusMode");
551
+ if (savedFocusMode === "true") {
552
+ document.body.classList.add("focus-mode");
553
+ }
554
+ }
555
+
556
  // Check API key
557
  function checkApiKey() {
558
  if (!state.apiKey) {
 
883
 
884
  // Delete a message from UI, state, and database
885
  async function deleteMessage(messageDiv, content, role) {
886
+ // Get the index of this message in the DOM to find correct state index
887
+ const allMessages = Array.from(elements.chatMessages.querySelectorAll(".message"));
888
+ const domIndex = allMessages.indexOf(messageDiv);
889
+
890
+ // Find and remove from state.messages using index (more reliable than content matching)
891
+ if (domIndex !== -1 && domIndex < state.messages.length) {
892
+ state.messages.splice(domIndex, 1);
893
+ } else {
894
+ // Fallback to content matching if index fails
895
+ const msgIndex = state.messages.findIndex(m => m.role === role && m.content === content);
896
+ if (msgIndex !== -1) {
897
+ state.messages.splice(msgIndex, 1);
898
+ }
899
  }
900
 
901
  // Remove from database if we have a messageId
 
1087
  );
1088
  state.messages.push({ role: "assistant", content: fullContent });
1089
 
1090
+ // Add cost for this regenerated response
1091
+ if (state.lastUsage) {
1092
+ updateTokenCounter(true);
1093
+ }
1094
+
1095
  try {
1096
  await saveMessageToDB("assistant", fullContent, reasoning);
1097
  } catch (error) {}
 
1286
  footerDiv.innerHTML = `
1287
  <span class="message-time">${timeStr}</span>
1288
  <div class="message-actions">
1289
+ <button class="msg-action-btn" data-action="regenerate" title="Regenerate">
1290
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1291
+ <polyline points="23 4 23 10 17 10"></polyline>
1292
+ <path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
1293
+ </svg>
1294
+ </button>
1295
  <button class="msg-action-btn" data-action="copy" title="Copy">
1296
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1297
  <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
 
1307
  </div>
1308
  `;
1309
 
1310
+ // Regenerate button handler
1311
+ footerDiv.querySelector('[data-action="regenerate"]').addEventListener("click", async () => {
1312
+ await regenerateResponse(messageDiv);
1313
+ });
1314
+
1315
  footerDiv.querySelector('[data-action="copy"]').addEventListener("click", () => {
1316
  navigator.clipboard
1317
  .writeText(content)
 
1385
  if (chunk.reasoning) {
1386
  reasoning = chunk.reasoning;
1387
  }
1388
+ // Handle usage stats (display only, don't add cost yet)
1389
  if (chunk.usage) {
1390
  state.lastUsage = chunk.usage;
1391
+ updateTokenCounter(false);
1392
  }
1393
  });
1394
 
 
1397
  finalizeStreamingMessage(messageDiv, textDiv, contentDiv, fullContent, reasoning, new Date().toISOString());
1398
  state.messages.push({ role: "assistant", content: fullContent });
1399
 
1400
+ // Now add the cost for this complete response
1401
+ if (state.lastUsage) {
1402
+ updateTokenCounter(true);
1403
+ }
1404
+
1405
  // Save to database
1406
  try {
1407
  await saveMessageToDB("assistant", fullContent, reasoning);
 
1470
  }
1471
 
1472
  // Update token counter display
1473
+ function updateTokenCounter(addCost = false) {
1474
  const sessionCostEl = document.getElementById("sessionCostDisplay");
1475
  const tokenUsageEl = document.getElementById("tokenUsageDisplay");
1476
 
1477
  if (state.lastUsage) {
1478
  const input = state.lastUsage.prompt_tokens || 0;
1479
  const output = state.lastUsage.completion_tokens || 0;
1480
+
1481
+ // Only add cost when explicitly requested (after a complete response)
1482
+ if (addCost) {
1483
+ const lastCost = calculateCost(state.lastUsage, state.selectedModel);
1484
+ state.totalCost += lastCost;
1485
+ }
1486
 
1487
  const costStr =
1488
  state.totalCost < 0.01 ? `${(state.totalCost * 100).toFixed(2)}¢` : `$${state.totalCost.toFixed(4)}`;
assets/js/utils.js CHANGED
@@ -147,9 +147,25 @@ function showImportFilePicker() {
147
  const text = await file.text();
148
  const data = JSON.parse(text);
149
 
150
- // Validate structure
151
  if (!data.conversation || !data.messages || !Array.isArray(data.messages)) {
152
- throw new Error("Invalid file format");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  }
154
 
155
  resolve(data);
@@ -166,6 +182,7 @@ function showImportFilePicker() {
166
  // Export for use in other scripts
167
  if (typeof window !== "undefined") {
168
  window.escapeHtml = escapeHtml;
 
169
  window.downloadFile = downloadFile;
170
  window.sanitizeFilename = sanitizeFilename;
171
  window.showExportFormatModal = showExportFormatModal;
 
147
  const text = await file.text();
148
  const data = JSON.parse(text);
149
 
150
+ // Validate basic structure
151
  if (!data.conversation || !data.messages || !Array.isArray(data.messages)) {
152
+ throw new Error("Invalid file format: missing conversation or messages");
153
+ }
154
+
155
+ // Validate conversation object
156
+ if (!data.conversation.title || typeof data.conversation.title !== "string") {
157
+ throw new Error("Invalid file format: missing or invalid conversation title");
158
+ }
159
+
160
+ // Validate messages structure
161
+ for (let i = 0; i < data.messages.length; i++) {
162
+ const msg = data.messages[i];
163
+ if (!msg.role || !msg.content) {
164
+ throw new Error(`Invalid message at index ${i}: missing role or content`);
165
+ }
166
+ if (msg.role !== "user" && msg.role !== "assistant") {
167
+ throw new Error(`Invalid message role at index ${i}: must be 'user' or 'assistant'`);
168
+ }
169
  }
170
 
171
  resolve(data);
 
182
  // Export for use in other scripts
183
  if (typeof window !== "undefined") {
184
  window.escapeHtml = escapeHtml;
185
+ window.wrapTablesForScroll = wrapTablesForScroll;
186
  window.downloadFile = downloadFile;
187
  window.sanitizeFilename = sanitizeFilename;
188
  window.showExportFormatModal = showExportFormatModal;
index.html CHANGED
@@ -63,17 +63,18 @@
63
 
64
  <!-- New Chat Button -->
65
  <div class="section">
66
- <button id="newChatBtn" class="btn-primary w-full">
67
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
68
- stroke-width="2">
69
  <line x1="12" y1="5" x2="12" y2="19"></line>
70
  <line x1="5" y1="12" x2="19" y2="12"></line>
71
  </svg>
72
  <span>New Chat</span>
73
  </button>
74
- <button id="importConversationBtn" class="btn-secondary w-full" style="margin-top: 8px;">
 
75
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
76
- stroke-width="2">
77
  <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
78
  <polyline points="17 8 12 3 7 8"></polyline>
79
  <line x1="12" y1="3" x2="12" y2="15"></line>
@@ -251,14 +252,34 @@
251
  <!-- Main Content -->
252
  <main class="main-content">
253
  <!-- Toggle Sidebar Button (Mobile) -->
254
- <button id="toggleSidebar" class="toggle-sidebar-btn">
255
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
 
256
  <line x1="3" y1="12" x2="21" y2="12"></line>
257
  <line x1="3" y1="6" x2="21" y2="6"></line>
258
  <line x1="3" y1="18" x2="21" y2="18"></line>
259
  </svg>
260
  </button>
261
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  <!-- Chat Container -->
263
  <div class="chat-container">
264
  <div id="chatMessages" class="chat-messages">
@@ -269,14 +290,22 @@
269
  </div>
270
  </div>
271
 
 
 
 
 
 
 
 
 
272
  <!-- Input Area -->
273
  <div class="input-container">
274
  <div class="input-wrapper">
275
- <textarea id="messageInput" class="message-input" placeholder="Type your message..."
276
- rows="2"></textarea>
277
- <button id="sendBtn" class="send-btn">
278
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
279
- stroke-width="2">
280
  <line x1="22" y1="2" x2="11" y2="13"></line>
281
  <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
282
  </svg>
 
63
 
64
  <!-- New Chat Button -->
65
  <div class="section">
66
+ <button id="newChatBtn" class="btn-primary w-full" aria-label="Start a new chat">
67
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
68
+ stroke-width="2" aria-hidden="true">
69
  <line x1="12" y1="5" x2="12" y2="19"></line>
70
  <line x1="5" y1="12" x2="19" y2="12"></line>
71
  </svg>
72
  <span>New Chat</span>
73
  </button>
74
+ <button id="importConversationBtn" class="btn-secondary w-full" style="margin-top: 8px;"
75
+ aria-label="Import a chat from JSON file">
76
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
77
+ stroke-width="2" aria-hidden="true">
78
  <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
79
  <polyline points="17 8 12 3 7 8"></polyline>
80
  <line x1="12" y1="3" x2="12" y2="15"></line>
 
252
  <!-- Main Content -->
253
  <main class="main-content">
254
  <!-- Toggle Sidebar Button (Mobile) -->
255
+ <button id="toggleSidebar" class="toggle-sidebar-btn" aria-label="Toggle sidebar">
256
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
257
+ aria-hidden="true">
258
  <line x1="3" y1="12" x2="21" y2="12"></line>
259
  <line x1="3" y1="6" x2="21" y2="6"></line>
260
  <line x1="3" y1="18" x2="21" y2="18"></line>
261
  </svg>
262
  </button>
263
 
264
+ <!-- Focus Mode Toggle (Desktop) -->
265
+ <button id="focusModeBtn" class="focus-mode-btn" aria-label="Toggle focus mode"
266
+ title="Toggle Focus Mode (Ctrl+B)">
267
+ <svg class="icon-expand" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor"
268
+ stroke-width="2" aria-hidden="true">
269
+ <polyline points="15 3 21 3 21 9"></polyline>
270
+ <polyline points="9 21 3 21 3 15"></polyline>
271
+ <line x1="21" y1="3" x2="14" y2="10"></line>
272
+ <line x1="3" y1="21" x2="10" y2="14"></line>
273
+ </svg>
274
+ <svg class="icon-collapse" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor"
275
+ stroke-width="2" aria-hidden="true">
276
+ <polyline points="4 14 10 14 10 20"></polyline>
277
+ <polyline points="20 10 14 10 14 4"></polyline>
278
+ <line x1="14" y1="10" x2="21" y2="3"></line>
279
+ <line x1="3" y1="21" x2="10" y2="14"></line>
280
+ </svg>
281
+ </button>
282
+
283
  <!-- Chat Container -->
284
  <div class="chat-container">
285
  <div id="chatMessages" class="chat-messages">
 
290
  </div>
291
  </div>
292
 
293
+ <!-- Scroll to Bottom Button -->
294
+ <button id="scrollToBottomBtn" class="scroll-to-bottom-btn hidden" aria-label="Scroll to bottom">
295
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
296
+ aria-hidden="true">
297
+ <polyline points="6 9 12 15 18 9"></polyline>
298
+ </svg>
299
+ </button>
300
+
301
  <!-- Input Area -->
302
  <div class="input-container">
303
  <div class="input-wrapper">
304
+ <textarea id="messageInput" class="message-input" placeholder="Type your message..." rows="2"
305
+ aria-label="Message input"></textarea>
306
+ <button id="sendBtn" class="send-btn" aria-label="Send message">
307
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
308
+ stroke-width="2" aria-hidden="true">
309
  <line x1="22" y1="2" x2="11" y2="13"></line>
310
  <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
311
  </svg>