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
+87
-27
+87
-27
@@ -972,37 +972,57 @@
|
|||||||
return text.replace(regex, '<span class="search-highlight">$1</span>');
|
return text.replace(regex, '<span class="search-highlight">$1</span>');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let searchDebounceTimer = null;
|
||||||
|
let activeSearchAbortController = null;
|
||||||
|
|
||||||
async function handleSearch(e) {
|
async function handleSearch(e) {
|
||||||
const q = e.target.value;
|
const q = e.target.value.trim();
|
||||||
// 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) {
|
if (searchDebounceTimer) clearTimeout(searchDebounceTimer);
|
||||||
container.innerHTML = '<div style="padding:15px;color:var(--text-secondary);">No results found.</div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
container.innerHTML = data.results.map(r => `
|
if (!q) {
|
||||||
<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;">
|
if (activeSearchAbortController) activeSearchAbortController.abort();
|
||||||
<div style="display:flex; justify-content:space-between; align-items:flex-start;">
|
document.getElementById('search-results').innerHTML = '';
|
||||||
<div>
|
return;
|
||||||
<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 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 ---
|
// --- 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 ---
|
// --- Start ---
|
||||||
loadVersion();
|
loadVersion();
|
||||||
loadGraph();
|
loadGraph();
|
||||||
@@ -1246,6 +1305,7 @@
|
|||||||
loadActivityHistory();
|
loadActivityHistory();
|
||||||
loadAllExtras();
|
loadAllExtras();
|
||||||
setupWS();
|
setupWS();
|
||||||
|
setupSSE();
|
||||||
|
|
||||||
// Listen to theme toggle changes to update graph font colors
|
// Listen to theme toggle changes to update graph font colors
|
||||||
const observer = new MutationObserver(() => updateGraphData());
|
const observer = new MutationObserver(() => updateGraphData());
|
||||||
|
|||||||
Reference in new issue
Block a user