feat(dashboard): fix Ctrl+K shortcut, debounce search queries, and add request cancellation

This commit is contained in:
Riz Ashraf committed 2026-10-02 07:27:31 +01:00
1 parent 83aa26634d
commit 87ddb01063
1 file changed
+89 -29
+89 -29
View File
@@ -972,37 +972,57 @@
return text.replace(regex, '<span class="search-highlight">$1</span>');
}
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 = '<div style="padding:15px;color:var(--text-secondary);">No results found.</div>';
return;
}
container.innerHTML = data.results.map(r => `
<div class="search-result-item" onclick="if(rawEntities['${r.id}']){ showInspector('${r.id}'); network.selectNodes(['${r.id}']); network.focus('${r.id}', {scale:1.2, animation:true}); }" style="cursor:pointer;">
<div style="display:flex; justify-content:space-between; align-items:flex-start;">
<div>
<strong>${highlightText(r.title, q)}</strong>
<span style="font-size: 0.7em; color: var(--text-secondary); background: var(--canvas-bg); padding: 2px 6px; border-radius: 4px; margin-left: 8px; text-transform: uppercase; border: 1px solid var(--border-color);">${r.type_name}</span>
</div>
<div style="font-size:0.75em; color:var(--text-secondary);">${r.score.toFixed(2)}</div>
</div>
<div style="margin-top:6px; font-size:0.9em; line-height:1.4;">${highlightText(r.content.substring(0, 150), q)}${r.content.length > 150 ? '...' : ''}</div>
<div style="font-size:0.75em; color:var(--text-secondary); margin-top:8px; font-family:monospace; background: var(--canvas-bg); padding: 2px 4px; border-radius: 3px; display: inline-block;">ID: ${r.id}</div>
</div>
`).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 = '<div style="padding:15px;color:var(--text-secondary);">No results found.</div>';
return;
}
container.innerHTML = data.results.map(r => `
<div class="search-result-item" onclick="if(rawEntities['${r.id}']){ showInspector('${r.id}'); network.selectNodes(['${r.id}']); network.focus('${r.id}', {scale:1.2, animation:true}); }" style="cursor:pointer;">
<div style="display:flex; justify-content:space-between; align-items:flex-start;">
<div>
<strong>${highlightText(r.title, q)}</strong>
<span style="font-size: 0.7em; color: var(--text-secondary); background: var(--canvas-bg); padding: 2px 6px; border-radius: 4px; margin-left: 8px; text-transform: uppercase; border: 1px solid var(--border-color);">${r.type_name}</span>
</div>
<div style="font-size:0.75em; color:var(--text-secondary);">${r.score.toFixed(2)}</div>
</div>
<div style="margin-top:6px; font-size:0.9em; line-height:1.4;">${highlightText(r.content.substring(0, 150), q)}${r.content.length > 150 ? '...' : ''}</div>
<div style="font-size:0.75em; color:var(--text-secondary); margin-top:8px; font-family:monospace; background: var(--canvas-bg); padding: 2px 4px; border-radius: 3px; display: inline-block;">ID: ${r.id}</div>
</div>
`).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 = `<strong>${rec.category || 'activity'}</strong>: ${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());