diff --git a/server/src/dashboard.html b/server/src/dashboard.html index 7a2c84d..f4ff042 100644 --- a/server/src/dashboard.html +++ b/server/src/dashboard.html @@ -972,37 +972,57 @@ return text.replace(regex, '$1'); } + let searchDebounceTimer = null; + let activeSearchAbortController = null; + async function handleSearch(e) { - const q = e.target.value; - // Provide live feedback if they type at least 3 characters, or hit Enter - if (e.key === 'Enter' || q.length >= 3 || q.length === 0) { - if (!q) { - document.getElementById('search-results').innerHTML = ''; - return; - } - const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`); - const data = await res.json(); - const container = document.getElementById('search-results'); - - if (data.results.length === 0) { - container.innerHTML = '
No results found.
'; - return; - } - - container.innerHTML = data.results.map(r => ` -
-
-
- ${highlightText(r.title, q)} - ${r.type_name} -
-
${r.score.toFixed(2)}
-
-
${highlightText(r.content.substring(0, 150), q)}${r.content.length > 150 ? '...' : ''}
-
ID: ${r.id}
-
- `).join(''); + const q = e.target.value.trim(); + + if (searchDebounceTimer) clearTimeout(searchDebounceTimer); + + if (!q) { + if (activeSearchAbortController) activeSearchAbortController.abort(); + document.getElementById('search-results').innerHTML = ''; + return; } + + const delay = (e.key === 'Enter') ? 0 : 250; + + searchDebounceTimer = setTimeout(async () => { + if (activeSearchAbortController) activeSearchAbortController.abort(); + activeSearchAbortController = new AbortController(); + + try { + const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`, { + signal: activeSearchAbortController.signal + }); + const data = await res.json(); + const container = document.getElementById('search-results'); + + if (!data.results || data.results.length === 0) { + container.innerHTML = '
No results found.
'; + return; + } + + container.innerHTML = data.results.map(r => ` +
+
+
+ ${highlightText(r.title, q)} + ${r.type_name} +
+
${r.score.toFixed(2)}
+
+
${highlightText(r.content.substring(0, 150), q)}${r.content.length > 150 ? '...' : ''}
+
ID: ${r.id}
+
+ `).join(''); + } catch (err) { + if (err.name !== 'AbortError') { + console.error("Search error:", err); + } + } + }, delay); } // --- WebSocket Activity Feed --- @@ -1238,6 +1258,45 @@ } } + function setupSSE() { + try { + const sse = new EventSource('/api/activity/stream'); + sse.onmessage = function(event) { + if (event.data) { + try { + const rec = JSON.parse(event.data); + const feed = document.getElementById('activity-feed'); + if (feed) { + const item = document.createElement('div'); + item.style.padding = '8px 12px'; + item.style.borderBottom = '1px solid var(--border-color)'; + item.style.fontSize = '0.85em'; + item.style.background = 'rgba(39, 174, 96, 0.08)'; + item.innerHTML = `${rec.category || 'activity'}: ${rec.summary || event.data}`; + feed.insertBefore(item, feed.firstChild); + } + } catch(e) {} + } + }; + } catch(e) { console.error('SSE initialization error', e); } + } + + // Ctrl+K / Cmd+K Global Search shortcut + document.addEventListener('keydown', function(e) { + if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'k') { + e.preventDefault(); + const searchTabBtn = document.querySelectorAll('.tab-button')[1]; + if (searchTabBtn) { + switchTab('search-tab', searchTabBtn); + } + const searchInput = document.getElementById('search-box'); + if (searchInput) { + searchInput.focus(); + searchInput.select(); + } + } + }); + // --- Start --- loadVersion(); loadGraph(); @@ -1246,6 +1305,7 @@ loadActivityHistory(); loadAllExtras(); setupWS(); + setupSSE(); // Listen to theme toggle changes to update graph font colors const observer = new MutationObserver(() => updateGraphData());