feat(dashboard): fix Ctrl+K shortcut, debounce search queries, and add request cancellation
This commit is contained in:
1 parent
83aa26634d
commit
87ddb01063
1 file changed
+89
-29
+89
-29
@@ -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());
|
||||
|
||||
Reference in new issue
Block a user