personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: major cleanup and bug fixes for chat app

- Fix duplicate form submission handler causing false error alerts
- Add showError() function and ensure input re-enables after errors
- Add null safety checks in initAppBarControls()
- Remove dead code: headerInfo, updateHeader, loadHistory, eventProcessingActive
- Fix memory leak in processedEventIds with auto-pruning at 500 entries
- Add overflow-x: hidden to fix horizontal scrollbar on messages
- Use facet theme colors for user messages and activity indicator
- Integrate action buttons into message bubble (hover-only, smaller)
- Remove header bar for cleaner UX
- Fix backend label consistency (GPT-4)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

+263 -269
+3 -59
apps/chat/app_bar.html
··· 88 88 89 89 <script> 90 90 (function() { 91 - const form = document.getElementById('chatInputForm'); 92 91 const input = document.getElementById('chatMessageInput'); 93 - const sendBtn = document.getElementById('chatSendBtn'); 94 - const backendToggle = document.getElementById('chatBackendToggle'); 95 92 96 - if (!form || !input || !sendBtn || !backendToggle) { 97 - console.error('Chat app bar elements not found'); 93 + if (!input) { 94 + console.error('Chat input not found'); 98 95 return; 99 96 } 100 97 101 - // Form submission 102 - form.addEventListener('submit', (e) => { 103 - e.preventDefault(); 104 - const message = input.value.trim(); 105 - if (!message) return; 106 - 107 - sendBtn.disabled = true; 108 - sendBtn.textContent = 'Sending...'; 109 - 110 - // Send message to chat endpoint 111 - fetch('/app/chat/send', { 112 - method: 'POST', 113 - headers: { 'Content-Type': 'application/json' }, 114 - body: JSON.stringify({ 115 - message: message, 116 - backend: backendToggle.dataset.backend 117 - }) 118 - }) 119 - .then(r => r.json()) 120 - .then(data => { 121 - if (data.success) { 122 - input.value = ''; 123 - input.style.height = 'auto'; 124 - } else { 125 - alert('Error: ' + (data.error || 'Unknown error')); 126 - } 127 - }) 128 - .catch(err => { 129 - console.error('Send error:', err); 130 - alert('Failed to send message'); 131 - }) 132 - .finally(() => { 133 - sendBtn.disabled = false; 134 - sendBtn.textContent = 'Send'; 135 - }); 136 - }); 137 - 138 - // Backend toggle 139 - const backends = [ 140 - { name: 'anthropic', label: 'Claude' }, 141 - { name: 'google', label: 'Gemini' }, 142 - { name: 'openai', label: 'GPT-4' } 143 - ]; 144 - let currentBackendIndex = 0; 145 - 146 - backendToggle.addEventListener('click', () => { 147 - currentBackendIndex = (currentBackendIndex + 1) % backends.length; 148 - const backend = backends[currentBackendIndex]; 149 - backendToggle.dataset.backend = backend.name; 150 - backendToggle.textContent = backend.label; 151 - }); 152 - 153 98 // Auto-resize textarea 154 99 input.addEventListener('input', () => { 155 100 input.style.height = 'auto'; 156 101 input.style.height = Math.min(input.scrollHeight, 120) + 'px'; 157 102 }); 158 103 159 - // FIXED: Only focus if nothing else is focused 160 - // Use setTimeout to avoid focus stealing during page load 104 + // Focus input if nothing else is focused 161 105 setTimeout(() => { 162 106 if (document.activeElement === document.body) { 163 107 input.focus();
+260 -210
apps/chat/workspace.html
··· 3 3 display: flex; 4 4 flex-direction: column; 5 5 overflow-y: auto; 6 + overflow-x: hidden; 6 7 } 7 - .message {margin-bottom:0.75em; padding:0.75em 1em; border-radius:12px; max-width:75%; box-shadow:0 1px 3px rgba(0,0,0,0.1); animation:fadeInUp 0.3s ease-out; position:relative;} 8 - .from-user {background:linear-gradient(135deg, #667eea 0%, #764ba2 100%); color:white; align-self:flex-end; margin-left:20%;} 9 - .from-bot {background:#fff; border:1px solid #e0e0e0; align-self:flex-start; margin-right:20%;} 10 - .copy-markdown-btn {position:absolute; top:0.5em; right:0.5em; background:rgba(0,0,0,0.05); border:1px solid rgba(0,0,0,0.1); border-radius:4px; padding:0.25em 0.5em; cursor:pointer; opacity:0; transition:opacity 0.2s; font-size:0.9em; color:#666;} 11 - .copy-markdown-btn:hover {background:rgba(0,0,0,0.1); color:#333;} 12 - .from-bot:hover .copy-markdown-btn {opacity:1;} 13 - .copy-markdown-btn.copied {background:#4caf50; color:white; border-color:#4caf50;} 14 - .copy-markdown-btn-bottom {position:relative; display:block; margin-bottom:0.75em; margin-top:-0.5em; margin-left:0; padding-left:0; text-align:left; align-self:flex-start; max-width:75%; margin-right:20%;} 15 - .copy-markdown-btn-bottom button {background:transparent; border:1px solid rgba(0,0,0,0.15); border-radius:6px; padding:0.4em 0.5em; cursor:pointer; font-size:1em; color:#666; transition:all 0.2s; line-height:1;} 16 - .copy-markdown-btn-bottom button:hover {background:rgba(0,0,0,0.05); border-color:rgba(0,0,0,0.25);} 17 - .copy-markdown-btn-bottom button.copied {background:#4caf50; color:white; border-color:#4caf50;} 18 - .copy-user-btn-bottom {position:relative; display:flex; gap:0.3em; justify-content:flex-end; margin-bottom:0.75em; margin-top:-0.5em; margin-right:0; padding-right:0; text-align:right; align-self:flex-end; max-width:75%; margin-left:20%;} 19 - .copy-user-btn-bottom button {background:transparent; border:1px solid rgba(255,255,255,0.3); border-radius:6px; padding:0.4em 0.5em; cursor:pointer; font-size:1em; color:rgba(255,255,255,0.7); transition:all 0.2s; line-height:1; display:inline-flex; align-items:center; justify-content:center;} 20 - .copy-user-btn-bottom button:hover {background:rgba(255,255,255,0.1); border-color:rgba(255,255,255,0.5); color:rgba(255,255,255,0.9);} 21 - .copy-user-btn-bottom button.copied {background:#4caf50; color:white; border-color:#4caf50;} 22 - @keyframes fadeInUp {from{opacity:0;transform:translateY(10px);}to{opacity:1;transform:translateY(0);}} 23 - #messages .event-card{background:#fff4e5;border:1px solid #f0ad4e;border-radius:6px;padding:0.5em 0.75em;margin-bottom:0.75em;font-size:0.85em;max-width:80%;align-self:flex-start;cursor:help;transition:all 0.2s;} 24 - #messages .event-card:hover{background:#fff0d4;border-color:#e09e3a;transform:translateY(-1px);box-shadow:0 2px 4px rgba(0,0,0,0.1);} 25 - #messages .event-card a{text-decoration:none;color:#007bff;} 26 - #messages .event-card a:hover{text-decoration:underline;} 27 - #messages .tool-placard{background:#e3f2fd;border:1px solid #2196f3;border-radius:6px;padding:0.4em 0.6em;margin-bottom:0.5em;font-size:0.8em;max-width:60%;align-self:flex-start;cursor:help;transition:all 0.2s;} 28 - #messages .tool-placard:hover{background:#d1e8fc;border-color:#1976d2;transform:translateY(-1px);} 29 - #messages .tool-placard .tool-name{font-weight:bold;color:#1976d2;} 30 - #messages .tool-placard .tool-status{font-size:0.9em;color:#666;margin-left:0.5em;} 31 - #messages .tool-placard.completed{background:#e8f5e9;border-color:#4caf50;} 32 - #messages .tool-placard.completed:hover{background:#d4f0d7;} 33 - #messages .tool-placard.completed .tool-name{color:#2e7d32;} 34 - #messages .agent-update{background:#fff3e0;border:1px solid #ff9800;border-radius:6px;padding:0.4em 0.6em;margin-bottom:0.5em;font-size:0.8em;max-width:60%;align-self:flex-start;color:#e65100;font-style:italic;transition:all 0.2s;} 35 - #messages .agent-update:hover{background:#ffe8cc;} 36 - #messages .thinking-card{background:#f8f4ff;border:1px solid #9c27b0;border-radius:6px;padding:0.5em 0.75em;margin-bottom:0.75em;font-size:0.85em;max-width:80%;align-self:flex-start;cursor:pointer;transition:all 0.2s;} 37 - #messages .thinking-card:hover{background:#f3e5f5;transform:translateY(-1px);box-shadow:0 2px 4px rgba(0,0,0,0.05);} 38 - #messages .thinking-card .thinking-label{font-weight:bold;color:#7b1fa2;margin-bottom:0.25em;} 39 - #messages .thinking-card .thinking-content{color:#333;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;} 40 - #messages .thinking-card.expanded .thinking-content{white-space:pre-wrap;overflow:visible;text-overflow:clip;} 41 - .message h1,.message h2,.message h3,.message h4,.message h5,.message h6 {margin:0.5em 0 0.3em 0;} 42 - .message p {margin:0.3em 0;} 43 - .message ul,.message ol {margin:0.3em 0; padding-left:1.5em;} 44 - .message code {background:rgba(0,0,0,0.1); padding:0.1em 0.3em; border-radius:3px; font-size:0.9em;} 45 - .from-user code {background:rgba(255,255,255,0.2);} 46 - .message pre {background:rgba(0,0,0,0.1); padding:0.5em; border-radius:4px; overflow-x:auto; margin:0.5em 0;} 47 - .from-user pre {background:rgba(255,255,255,0.15);} 48 - .message pre code {background:none; padding:0;} 49 - .message blockquote {margin:0.5em 0; padding-left:1em; border-left:3px solid rgba(0,0,0,0.2);} 50 - .from-user blockquote {border-left-color:rgba(255,255,255,0.4);} 51 - @keyframes typingBounce {0%,80%,100%{transform:scale(0.8);opacity:0.5;}40%{transform:scale(1);opacity:1;}} 52 - .activity-indicator {align-self:flex-start; padding:0.75em 1em; background:#f0f0f0; border-radius:12px; margin-bottom:0.5em; max-width:80px; display:none;} 53 - .activity-indicator.active {display:flex; align-items:center; gap:4px; animation:fadeInUp 0.3s ease-out;} 54 - .activity-indicator span {width:8px; height:8px; background:#999; border-radius:50%; animation:typingBounce 1.4s infinite ease-in-out;} 55 - .activity-indicator span:nth-child(1) {animation-delay:-0.32s;} 56 - .activity-indicator span:nth-child(2) {animation-delay:-0.16s;} 8 + 9 + /* Base message styles */ 10 + .message { 11 + margin-bottom: 0.75em; 12 + padding: 0.75em 1em; 13 + border-radius: 12px; 14 + max-width: 75%; 15 + box-shadow: 0 1px 3px rgba(0,0,0,0.08); 16 + animation: fadeInUp 0.3s ease-out; 17 + position: relative; 18 + } 19 + 20 + /* User messages - use facet theme colors */ 21 + .from-user { 22 + background: var(--facet-color, #667eea); 23 + color: white; 24 + align-self: flex-end; 25 + margin-left: 20%; 26 + } 27 + 28 + /* Bot messages */ 29 + .from-bot { 30 + background: #fff; 31 + border: 1px solid #e0e0e0; 32 + align-self: flex-start; 33 + margin-right: 20%; 34 + } 35 + 36 + /* Action buttons - integrated into message, hover-only, small */ 37 + .message-actions { 38 + position: absolute; 39 + bottom: 0.4em; 40 + display: flex; 41 + gap: 0.25em; 42 + opacity: 0; 43 + transition: opacity 0.15s; 44 + } 45 + .from-user .message-actions { left: 0.5em; } 46 + .from-bot .message-actions { right: 0.5em; } 47 + .message:hover .message-actions { opacity: 1; } 48 + 49 + .message-actions button { 50 + background: transparent; 51 + border: none; 52 + padding: 0.2em; 53 + cursor: pointer; 54 + font-size: 0.7em; 55 + opacity: 0.5; 56 + transition: opacity 0.15s; 57 + line-height: 1; 58 + } 59 + .message-actions button:hover { opacity: 1; } 60 + .from-user .message-actions button { color: rgba(255,255,255,0.9); } 61 + .from-bot .message-actions button { color: #666; } 62 + .message-actions button.copied { opacity: 1; color: #4caf50; } 63 + 64 + @keyframes fadeInUp { 65 + from { opacity: 0; transform: translateY(10px); } 66 + to { opacity: 1; transform: translateY(0); } 67 + } 68 + 69 + /* Event cards, tool placards, etc */ 70 + #messages .event-card { 71 + background: #fff4e5; 72 + border: 1px solid #f0ad4e; 73 + border-radius: 6px; 74 + padding: 0.5em 0.75em; 75 + margin-bottom: 0.75em; 76 + font-size: 0.85em; 77 + max-width: 80%; 78 + align-self: flex-start; 79 + cursor: help; 80 + transition: all 0.2s; 81 + } 82 + #messages .event-card:hover { 83 + background: #fff0d4; 84 + border-color: #e09e3a; 85 + transform: translateY(-1px); 86 + box-shadow: 0 2px 4px rgba(0,0,0,0.1); 87 + } 88 + #messages .event-card a { text-decoration: none; color: #007bff; } 89 + #messages .event-card a:hover { text-decoration: underline; } 90 + 91 + #messages .tool-placard { 92 + background: #e3f2fd; 93 + border: 1px solid #2196f3; 94 + border-radius: 6px; 95 + padding: 0.4em 0.6em; 96 + margin-bottom: 0.5em; 97 + font-size: 0.8em; 98 + max-width: 60%; 99 + align-self: flex-start; 100 + cursor: help; 101 + transition: all 0.2s; 102 + } 103 + #messages .tool-placard:hover { 104 + background: #d1e8fc; 105 + border-color: #1976d2; 106 + transform: translateY(-1px); 107 + } 108 + #messages .tool-placard .tool-name { font-weight: bold; color: #1976d2; } 109 + #messages .tool-placard .tool-status { font-size: 0.9em; color: #666; margin-left: 0.5em; } 110 + #messages .tool-placard.completed { background: #e8f5e9; border-color: #4caf50; } 111 + #messages .tool-placard.completed:hover { background: #d4f0d7; } 112 + #messages .tool-placard.completed .tool-name { color: #2e7d32; } 113 + 114 + #messages .agent-update { 115 + background: #fff3e0; 116 + border: 1px solid #ff9800; 117 + border-radius: 6px; 118 + padding: 0.4em 0.6em; 119 + margin-bottom: 0.5em; 120 + font-size: 0.8em; 121 + max-width: 60%; 122 + align-self: flex-start; 123 + color: #e65100; 124 + font-style: italic; 125 + transition: all 0.2s; 126 + } 127 + #messages .agent-update:hover { background: #ffe8cc; } 128 + 129 + #messages .thinking-card { 130 + background: #f8f4ff; 131 + border: 1px solid #9c27b0; 132 + border-radius: 6px; 133 + padding: 0.5em 0.75em; 134 + margin-bottom: 0.75em; 135 + font-size: 0.85em; 136 + max-width: 80%; 137 + align-self: flex-start; 138 + cursor: pointer; 139 + transition: all 0.2s; 140 + } 141 + #messages .thinking-card:hover { 142 + background: #f3e5f5; 143 + transform: translateY(-1px); 144 + box-shadow: 0 2px 4px rgba(0,0,0,0.05); 145 + } 146 + #messages .thinking-card .thinking-label { font-weight: bold; color: #7b1fa2; margin-bottom: 0.25em; } 147 + #messages .thinking-card .thinking-content { color: #333; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } 148 + #messages .thinking-card.expanded .thinking-content { white-space: pre-wrap; overflow: visible; text-overflow: clip; } 149 + 150 + /* Message content typography */ 151 + .message h1, .message h2, .message h3, .message h4, .message h5, .message h6 { margin: 0.5em 0 0.3em 0; } 152 + .message p { margin: 0.3em 0; } 153 + .message ul, .message ol { margin: 0.3em 0; padding-left: 1.5em; } 154 + .message code { background: rgba(0,0,0,0.1); padding: 0.1em 0.3em; border-radius: 3px; font-size: 0.9em; } 155 + .from-user code { background: rgba(255,255,255,0.2); } 156 + .message pre { background: rgba(0,0,0,0.1); padding: 0.5em; border-radius: 4px; overflow-x: auto; margin: 0.5em 0; } 157 + .from-user pre { background: rgba(255,255,255,0.15); } 158 + .message pre code { background: none; padding: 0; } 159 + .message blockquote { margin: 0.5em 0; padding-left: 1em; border-left: 3px solid rgba(0,0,0,0.2); } 160 + .from-user blockquote { border-left-color: rgba(255,255,255,0.4); } 161 + 162 + /* Activity indicator - uses facet color */ 163 + @keyframes typingBounce { 164 + 0%, 80%, 100% { transform: scale(0.8); opacity: 0.5; } 165 + 40% { transform: scale(1); opacity: 1; } 166 + } 167 + .activity-indicator { 168 + align-self: flex-start; 169 + padding: 0.5em 0.75em; 170 + background: var(--facet-bg, #f0f0f0); 171 + border-radius: 12px; 172 + margin-bottom: 0.5em; 173 + display: none; 174 + } 175 + .activity-indicator.active { 176 + display: flex; 177 + align-items: center; 178 + gap: 4px; 179 + animation: fadeInUp 0.3s ease-out; 180 + } 181 + .activity-indicator span { 182 + width: 6px; 183 + height: 6px; 184 + background: var(--facet-color, #999); 185 + border-radius: 50%; 186 + animation: typingBounce 1.4s infinite ease-in-out; 187 + } 188 + .activity-indicator span:nth-child(1) { animation-delay: -0.32s; } 189 + .activity-indicator span:nth-child(2) { animation-delay: -0.16s; } 57 190 </style> 58 191 59 192 <div class="workspace-content"> ··· 63 196 <script src="{{ vendor_lib('marked') }}"></script> 64 197 <script> 65 198 const sendUrl = '{{ url_for('app:chat.send_message') }}'; 66 - const historyUrl = '{{ url_for('app:chat.chat_history') }}'; 67 199 const clearUrl = '{{ url_for('app:chat.clear_history') }}'; 68 200 const agentUrl = '{{ url_for('app:chat.agent_events', agent_id='AGENT_ID') }}'; 69 201 const messagesDiv = document.getElementById('messages'); 70 202 const searchBase = '{{ url_for('app:search.index') }}'; 71 203 204 + // Simple error display - adds error message to chat 205 + function showError(message) { 206 + const div = document.createElement('div'); 207 + div.className = 'message from-bot'; 208 + div.innerHTML = `<strong style="color:#c62828">Error:</strong> ${message}`; 209 + messagesDiv.appendChild(div); 210 + messagesDiv.scrollTop = messagesDiv.scrollHeight; 211 + 212 + // Re-enable input so user can retry 213 + if (input) { 214 + input.disabled = false; 215 + input.placeholder = 'Send a message...'; 216 + input.focus(); 217 + } 218 + } 219 + 72 220 // Wait for app bar to load 73 221 let form, input, backendToggle, currentBackend; 74 222 ··· 76 224 form = document.getElementById('chatInputForm'); 77 225 input = document.getElementById('chatMessageInput'); 78 226 backendToggle = document.getElementById('chatBackendToggle'); 227 + 228 + if (!form || !input || !backendToggle) { 229 + console.error('Chat controls not found'); 230 + return; 231 + } 232 + 79 233 currentBackend = backendToggle.dataset.backend || 'anthropic'; 80 234 81 - // Backend toggle cycle: anthropic -> google -> openai -> anthropic 235 + // Backend toggle cycle 82 236 const backends = { 83 237 anthropic: { next: 'google', label: 'Claude' }, 84 238 google: { next: 'openai', label: 'Gemini' }, 85 - openai: { next: 'anthropic', label: 'ChatGPT' } 239 + openai: { next: 'anthropic', label: 'GPT-4' } 86 240 }; 87 241 88 242 backendToggle.addEventListener('click', () => { 89 - const next = backends[currentBackend].next; 90 - currentBackend = next; 91 - backendToggle.dataset.backend = next; 92 - backendToggle.textContent = backends[next].label; 93 - backendToggle.title = `Click to change backend (current: ${backends[next].label})`; 243 + currentBackend = backends[currentBackend].next; 244 + backendToggle.dataset.backend = currentBackend; 245 + backendToggle.textContent = backends[currentBackend].label; 94 246 }); 95 247 96 - // Setup form submission 97 248 setupFormHandlers(); 98 - 99 249 input.focus(); 100 250 } 101 251 ··· 108 258 109 259 // Deduplication using agent_id:ts as key 110 260 const processedEventIds = new Set(); 261 + const MAX_EVENT_IDS = 500; 111 262 112 263 function shouldSkipEvent(event) { 113 264 if (!event || !event.agent_id || !event.ts) { ··· 118 269 return true; 119 270 } 120 271 processedEventIds.add(eventKey); 272 + 273 + // Prevent memory leak - clear old entries if too many 274 + if (processedEventIds.size > MAX_EVENT_IDS) { 275 + const entries = Array.from(processedEventIds); 276 + entries.slice(0, 100).forEach(e => processedEventIds.delete(e)); 277 + } 121 278 return false; 122 279 } 123 280 ··· 157 314 // Parse URL parameters 158 315 const urlParams = new URLSearchParams(window.location.search); 159 316 160 - // Unified state management 317 + // State management 161 318 let currentAgentId = null; 162 319 let isComplete = false; 163 - let headerInfo = null; // {startTs, prompt} 164 320 165 321 // Create persistent activity indicator 166 322 const activityIndicator = document.createElement('div'); ··· 168 324 activityIndicator.id = 'activityIndicator'; 169 325 activityIndicator.innerHTML = '<span></span><span></span><span></span>'; 170 326 171 - // Activity indicator state 172 - let eventProcessingActive = false; 173 - let eventTimeout = null; 174 327 let chatControlsInitialized = false; 175 328 176 329 function showActivityIndicator() { ··· 178 331 messagesDiv.appendChild(activityIndicator); 179 332 } 180 333 activityIndicator.classList.add('active'); 181 - eventProcessingActive = true; 182 - 183 - // Auto-scroll to keep activity indicator in view 184 334 requestAnimationFrame(() => { 185 335 messagesDiv.scrollTop = messagesDiv.scrollHeight; 186 336 }); 187 - 188 - // Clear any existing timeout 189 - if (eventTimeout) { 190 - clearTimeout(eventTimeout); 191 - eventTimeout = null; 192 - } 193 337 } 194 338 195 339 function forceHideActivityIndicator() { 196 - // Force hide without delay 197 - if (eventTimeout) { 198 - clearTimeout(eventTimeout); 199 - eventTimeout = null; 200 - } 201 340 activityIndicator.classList.remove('active'); 202 - eventProcessingActive = false; 203 341 } 204 342 205 343 function ensureActivityIndicatorAtBottom() { ··· 217 355 const a=document.createElement('a'); 218 356 a.href=url; 219 357 a.textContent=text; 220 - div.appendChild(a); // This line was missing! 358 + div.appendChild(a); 221 359 222 360 // Add browser's built-in title attribute with formatted JSON 223 361 if(event){ ··· 330 468 messagesDiv.scrollTop=messagesDiv.scrollHeight; 331 469 } 332 470 333 - // Header management 334 - function updateHeader() { 335 - if (!headerInfo) return; 336 - 337 - let header = document.getElementById('chatHeader'); 338 - if (!header) { 339 - header = document.createElement('div'); 340 - header.id = 'chatHeader'; 341 - header.style.cssText = 'background:#e3f2fd;border-bottom:2px solid #2196f3;padding:0.75rem 1rem;margin:0;color:#1565c0;text-align:center;font-size:0.95em;'; 342 - messagesDiv.parentElement.insertBefore(header, messagesDiv); 343 - } 344 - 345 - const startDate = new Date(headerInfo.startTs); 346 - const timeStr = startDate.toLocaleTimeString('en-US', {hour: 'numeric', minute: '2-digit'}); 347 - const dateStr = startDate.toLocaleDateString('en-US', {month: 'short', day: 'numeric'}); 348 - 349 - let headerText = `Chat • Started ${timeStr} on ${dateStr}`; 350 - 351 - // Add duration if complete 352 - if (isComplete && headerInfo.endTs) { 353 - const durationMs = headerInfo.endTs - headerInfo.startTs; 354 - const seconds = Math.floor(durationMs / 1000); 355 - const minutes = Math.floor(seconds / 60); 356 - const remainingSeconds = seconds % 60; 357 - 358 - if (minutes > 0) { 359 - headerText += ` • Completed in ${minutes}m ${remainingSeconds}s`; 360 - } else { 361 - headerText += ` • Completed in ${seconds}s`; 362 - } 363 - } 364 - 365 - header.textContent = headerText; 366 - } 367 - 368 - // Unified agent loading function 471 + // Agent loading function 369 472 async function loadAgent(agentId) { 370 473 currentAgentId = agentId; 371 474 isComplete = false; 372 - headerInfo = null; 373 475 resetEventTracking(); 374 476 375 477 // Clear messages but keep activity indicator ··· 429 531 // Use HTML for bot messages if provided, otherwise plain text 430 532 if(isHtml && cls === 'from-bot'){ 431 533 div.innerHTML = text; 432 - 433 - // Store raw markdown if provided 434 534 if(rawMarkdown) { 435 535 messageMarkdown.set(div, rawMarkdown); 436 536 } 437 - 438 - // Add top copy button for bot messages 439 - const copyBtn = document.createElement('button'); 440 - copyBtn.className = 'copy-markdown-btn'; 441 - copyBtn.innerHTML = '📋'; 442 - copyBtn.onclick = (e) => { 443 - e.stopPropagation(); 444 - copyMessageMarkdown(div, copyBtn); 445 - }; 446 - div.appendChild(copyBtn); 447 537 } else { 448 538 div.textContent = text; 449 - 450 - // Store plain text for user messages 451 539 if(cls === 'from-user') { 452 540 messageMarkdown.set(div, text); 453 541 } 454 542 } 455 543 456 - messagesDiv.insertBefore(div, activityIndicator); 457 - 458 - // Add bottom copy button AFTER the message div for bot messages 459 - if(isHtml && cls === 'from-bot' && rawMarkdown){ 460 - const bottomCopyWrapper = document.createElement('div'); 461 - bottomCopyWrapper.className = 'copy-markdown-btn-bottom'; 462 - const bottomCopyBtn = document.createElement('button'); 463 - bottomCopyBtn.innerHTML = '📋'; 464 - bottomCopyBtn.title = 'Copy to clipboard'; 465 - bottomCopyBtn.onclick = (e) => { 466 - e.stopPropagation(); 467 - copyMessageMarkdown(div, bottomCopyBtn); 468 - }; 469 - bottomCopyWrapper.appendChild(bottomCopyBtn); 470 - messagesDiv.insertBefore(bottomCopyWrapper, activityIndicator); 471 - } 544 + // Create action buttons container (integrated into message) 545 + const actions = document.createElement('div'); 546 + actions.className = 'message-actions'; 472 547 473 - // Add bottom copy button for user messages 474 548 if(cls === 'from-user'){ 475 - const userCopyWrapper = document.createElement('div'); 476 - userCopyWrapper.className = 'copy-user-btn-bottom'; 477 - 478 - // Recycle button (reuse message) 549 + // Recycle button 479 550 const recycleBtn = document.createElement('button'); 480 551 recycleBtn.innerHTML = '♻️'; 481 - recycleBtn.title = 'Reuse this message'; 552 + recycleBtn.title = 'Reuse'; 482 553 recycleBtn.onclick = (e) => { 483 554 e.stopPropagation(); 484 555 reuseMessage(div); 485 556 }; 486 - userCopyWrapper.appendChild(recycleBtn); 557 + actions.appendChild(recycleBtn); 558 + } 487 559 488 - // Copy button 489 - const userCopyBtn = document.createElement('button'); 490 - userCopyBtn.innerHTML = '📋'; 491 - userCopyBtn.title = 'Copy to clipboard'; 492 - userCopyBtn.onclick = (e) => { 493 - e.stopPropagation(); 494 - copyMessageMarkdown(div, userCopyBtn); 495 - }; 496 - userCopyWrapper.appendChild(userCopyBtn); 560 + // Copy button for all messages 561 + const copyBtn = document.createElement('button'); 562 + copyBtn.innerHTML = '📋'; 563 + copyBtn.title = 'Copy'; 564 + copyBtn.onclick = (e) => { 565 + e.stopPropagation(); 566 + copyMessageMarkdown(div, copyBtn); 567 + }; 568 + actions.appendChild(copyBtn); 497 569 498 - messagesDiv.insertBefore(userCopyWrapper, activityIndicator); 499 - } 500 - 570 + div.appendChild(actions); 571 + messagesDiv.insertBefore(div, activityIndicator); 501 572 ensureActivityIndicatorAtBottom(); 502 573 messagesDiv.scrollTop=messagesDiv.scrollHeight; 503 574 } 504 575 505 576 function copyMessageMarkdown(messageDiv, copyBtn) { 506 577 const markdown = messageMarkdown.get(messageDiv); 507 - if(!markdown) return; 578 + if(!markdown) { 579 + // Nothing to copy - brief feedback 580 + copyBtn.innerHTML = '−'; 581 + setTimeout(() => { copyBtn.innerHTML = '📋'; }, 1000); 582 + return; 583 + } 508 584 509 585 navigator.clipboard.writeText(markdown).then(() => { 510 - // Show success feedback 511 586 copyBtn.innerHTML = '✓'; 512 587 copyBtn.classList.add('copied'); 513 - 514 - // Reset after 2 seconds 515 588 setTimeout(() => { 516 589 copyBtn.innerHTML = '📋'; 517 590 copyBtn.classList.remove('copied'); ··· 519 592 }).catch(err => { 520 593 console.error('Failed to copy:', err); 521 594 copyBtn.innerHTML = '❌'; 522 - setTimeout(() => { 523 - copyBtn.innerHTML = '📋'; 524 - }, 2000); 595 + setTimeout(() => { copyBtn.innerHTML = '📋'; }, 2000); 525 596 }); 526 597 } 527 598 ··· 536 607 previousAgent = null; 537 608 currentAgentId = null; 538 609 isComplete = false; 539 - headerInfo = null; 540 610 forceHideActivityIndicator(); 541 611 resetEventTracking(); 542 612 543 613 // Clear backend history 544 614 fetch(clearUrl, {method:'POST'}).catch(err => console.error('Failed to clear history:', err)); 545 - 546 - // Remove header if it exists 547 - const header = document.getElementById('chatHeader'); 548 - if(header) header.remove(); 549 615 550 616 // Enable the form and pre-fill 551 617 if(input) { ··· 602 668 }; 603 669 } 604 670 605 - async function loadHistory(){ 606 - const r=await fetch(historyUrl); 607 - if(!r.ok)return; 608 - const d=await r.json(); 609 - for(const m of d.history){ 610 - addMessage(m.text,m.role==='user'?'from-user':'from-bot'); 611 - } 612 - } 613 - 614 - 615 - // Unified event handler for both live and historical events 671 + // Event handler for both live and historical events 616 672 function processEvent(event) { 617 673 switch(event.event) { 618 674 case 'start': 619 - // Set up header info and show user message 620 - headerInfo = { 621 - startTs: event.ts, 622 - prompt: event.prompt 623 - }; 624 - updateHeader(); 625 675 addMessage(event.prompt || '', 'from-user'); 626 676 break; 627 677 ··· 647 697 addMessage(event.html, 'from-bot', true, event.rawMarkdown); 648 698 } 649 699 650 - // Mark as complete 651 700 isComplete = true; 652 - if (headerInfo) { 653 - headerInfo.endTs = event.ts; 654 - updateHeader(); 655 - } 656 701 forceHideActivityIndicator(); 657 702 658 - // Enable continuation if available 659 - if(event.conversation_id && input){ 703 + // Re-enable input 704 + if(input){ 660 705 input.disabled = false; 661 - input.placeholder = 'Continue conversation...'; 662 - input.dataset.continueAgent = event.agent_id; 706 + if(event.conversation_id){ 707 + input.placeholder = 'Continue conversation...'; 708 + input.dataset.continueAgent = event.agent_id; 709 + } else { 710 + input.placeholder = 'Send a message...'; 711 + } 663 712 } 664 713 break; 665 714 ··· 669 718 addMessage(event.html, 'from-bot', true, event.rawMarkdown); 670 719 } 671 720 672 - // Mark as complete (failed) 673 721 isComplete = true; 674 - if (headerInfo) { 675 - headerInfo.endTs = event.ts; 676 - updateHeader(); 722 + forceHideActivityIndicator(); 723 + 724 + // Re-enable input so user can retry 725 + if(input){ 726 + input.disabled = false; 727 + input.placeholder = 'Send a message...'; 677 728 } 678 - forceHideActivityIndicator(); 679 729 break; 680 730 } 681 731 }