feat: improve web UI UX with global search, breadcrumbs, and better feedback
This commit is contained in:
+116
-9
@@ -35,7 +35,75 @@
|
||||
updateToggleBtn(getTheme());
|
||||
const btn = document.getElementById('theme-toggle');
|
||||
if (btn) btn.addEventListener('click', cycleTheme);
|
||||
|
||||
// Global Search
|
||||
const searchInput = document.getElementById('global-search');
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
const val = searchInput.value.trim();
|
||||
if (val) handleGlobalSearch(val);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Keyboard Shortcuts
|
||||
document.addEventListener('keydown', (e) => {
|
||||
// '/' to focus search
|
||||
if (e.key === '/' && document.activeElement !== searchInput && !['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement.tagName)) {
|
||||
e.preventDefault();
|
||||
searchInput.focus();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
async function handleGlobalSearch(id) {
|
||||
if (id.length < 8) {
|
||||
showToast('Search ID must be at least 8 characters', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Try fetching as session first
|
||||
const sessionData = await api('/v1/sessions/' + id).catch(() => null);
|
||||
if (sessionData && sessionData.session) {
|
||||
navigate('/sessions/' + id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Then try as run
|
||||
const runData = await api('/v1/runs/' + id).catch(() => null);
|
||||
if (runData && runData.run) {
|
||||
navigate('/runs/' + id);
|
||||
return;
|
||||
}
|
||||
|
||||
showToast('ID not found', 'error');
|
||||
} catch (e) {
|
||||
showToast('Search failed: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function copyToClipboard(text, el) {
|
||||
if (!text) return;
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
showToast('Copied to clipboard', 'success');
|
||||
if (el) {
|
||||
const originalText = el.textContent;
|
||||
el.textContent = 'Copied!';
|
||||
setTimeout(() => { el.textContent = originalText; }, 1500);
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('Failed to copy:', err);
|
||||
showToast('Copy failed', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
function renderCopyButton(text) {
|
||||
return `<button class="copy-btn" title="Copy to clipboard" onclick="event.stopPropagation(); copyToClipboard('${escapeHTML(text)}', this)">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="5.5" y="5.5" width="9" height="9" rx="1.5"/><path d="M10.5 1.5H2.5A1.5 1.5 0 0 0 1 3v8"/></svg>
|
||||
</button>`;
|
||||
}
|
||||
// ─────────────────────────────────────────────────────────
|
||||
|
||||
const app = document.getElementById('app');
|
||||
@@ -184,6 +252,7 @@
|
||||
|
||||
function route() {
|
||||
cleanupLiveViews();
|
||||
renderBreadcrumbs();
|
||||
|
||||
const path = window.location.pathname;
|
||||
if (path === '/') {
|
||||
@@ -204,6 +273,44 @@
|
||||
updateActiveNav();
|
||||
}
|
||||
|
||||
function renderBreadcrumbs() {
|
||||
const el = document.getElementById('breadcrumbs');
|
||||
if (!el) return;
|
||||
|
||||
const path = window.location.pathname;
|
||||
const parts = path.split('/').filter(Boolean);
|
||||
if (parts.length === 0) {
|
||||
el.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const items = [{ label: 'Dashboard', path: '/' }];
|
||||
let currentPath = '';
|
||||
|
||||
parts.forEach((part, i) => {
|
||||
currentPath += '/' + part;
|
||||
let label = part.charAt(0).toUpperCase() + part.slice(1);
|
||||
|
||||
// Special labels for IDs
|
||||
if (part.length > 20 || /^[a-f0-9-]{32,}$/.test(part)) {
|
||||
label = part.substring(0, 8) + '…';
|
||||
}
|
||||
|
||||
if (i === parts.length - 1) {
|
||||
items.push({ label, current: true });
|
||||
} else {
|
||||
items.push({ label, path: currentPath });
|
||||
}
|
||||
});
|
||||
|
||||
el.innerHTML = items.map(item => {
|
||||
if (item.current) {
|
||||
return `<span class="current">${escapeHTML(item.label)}</span>`;
|
||||
}
|
||||
return `<a href="${item.path}" onclick="event.preventDefault(); navigate('${item.path}')">${escapeHTML(item.label)}</a><span class="sep">/</span>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function navigate(path) {
|
||||
history.pushState(null, '', path);
|
||||
route();
|
||||
@@ -293,8 +400,8 @@
|
||||
}
|
||||
|
||||
function statusIcon(status) {
|
||||
if (status === 'success') return '<span class="status-badge status-success"><span class="status-dot"></span>success</span>';
|
||||
if (status === 'error') return '<span class="status-badge status-error"><span class="status-dot"></span>error</span>';
|
||||
if (status === 'success') return '<span class="status-badge status-success"><svg width="10" height="10" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="margin-right:2px"><polyline points="13.5 4.5 6.5 11.5 2.5 7.5"/></svg>success</span>';
|
||||
if (status === 'error') return '<span class="status-badge status-error"><svg width="10" height="10" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="margin-right:2px"><line x1="13.5" y1="2.5" x2="2.5" y2="13.5"/><line x1="2.5" y1="2.5" x2="13.5" y2="13.5"/></svg>error</span>';
|
||||
return '<span class="status-badge status-unknown"><span class="status-dot"></span>unknown</span>';
|
||||
}
|
||||
|
||||
@@ -445,7 +552,7 @@
|
||||
: (active ? 'Open session' : 'Session ended');
|
||||
return `
|
||||
<tr class="clickable ${active ? 'active' : ''}" data-session="${escapeHTML(s.session_id)}">
|
||||
<td class="id-cell">${escapeHTML(s.session_id.substring(0, 12))}…</td>
|
||||
<td class="id-cell" title="${escapeHTML(s.session_id)}">${escapeHTML(s.session_id.substring(0, 12))}…${renderCopyButton(s.session_id)}</td>
|
||||
<td><span class="fw-dot ${escapeHTML(fwClass)} ${dotState}" title="${dotTitle}"></span>${escapeHTML(fw)}</td>
|
||||
<td>${escapeHTML(s.host || '-')}</td>
|
||||
<td>${s.run_count}</td>
|
||||
@@ -546,7 +653,7 @@
|
||||
? 'Currently active session'
|
||||
: (active ? 'Open session' : 'Session ended');
|
||||
return `
|
||||
<td class="id-cell">${escapeHTML(s.session_id.substring(0, 12))}…</td>
|
||||
<td class="id-cell" title="${escapeHTML(s.session_id)}">${escapeHTML(s.session_id.substring(0, 12))}…${renderCopyButton(s.session_id)}</td>
|
||||
<td><span class="fw-dot ${escapeHTML(fwClass)} ${dotState}" title="${dotTitle}"></span>${escapeHTML(fw)}</td>
|
||||
<td>${escapeHTML(s.host || '-')}</td>
|
||||
<td>${s.run_count}</td>
|
||||
@@ -668,7 +775,7 @@
|
||||
app.innerHTML = `
|
||||
<a href="/sessions" class="back-link">← Back to Sessions</a>
|
||||
<div class="page-header">
|
||||
<h2>Session <span style="font-family:var(--font-mono);font-size:1.1rem;color:var(--accent)">${escapeHTML(sessionID.substring(0, 16))}...</span></h2>
|
||||
<h2>Session <span style="font-family:var(--font-mono);font-size:1.1rem;color:var(--accent)" title="${escapeHTML(sessionID)}">${escapeHTML(sessionID.substring(0, 16))}...</span>${renderCopyButton(sessionID)}</h2>
|
||||
<div class="session-status-line">
|
||||
<span class="fw-dot ${escapeHTML((s.framework || 'unknown').replace(/[^a-z0-9-]/g, '-'))} ${active ? 'active' : 'ended'}"></span>
|
||||
<span class="session-status-text">${active ? 'Active' : 'Ended'}</span>
|
||||
@@ -806,7 +913,7 @@
|
||||
return spans.map((sp, i) => {
|
||||
const kindClass = sp.kind || 'unknown';
|
||||
return `
|
||||
<tr class="expandable run-span-row" data-index="${i}">
|
||||
<tr class="expandable run-span-row ${sp.status === 'error' ? 'tr-error' : ''}" data-index="${i}">
|
||||
<td>
|
||||
<span class="expand-icon"></span>
|
||||
<span class="span-kind-badge ${escapeHTML(kindClass)}">${escapeHTML(sp.kind || '?')}</span>
|
||||
@@ -871,7 +978,7 @@
|
||||
app.innerHTML = `
|
||||
<a href="/sessions/${escapeHTML(r.session_id)}" class="back-link">← Back to Session</a>
|
||||
<div class="page-header">
|
||||
<h2>Run <span style="font-family:var(--font-mono);font-size:1.1rem;color:var(--accent)">${escapeHTML(runID.substring(0, 16))}…</span> ${statusIcon(r.status)}</h2>
|
||||
<h2>Run <span style="font-family:var(--font-mono);font-size:1.1rem;color:var(--accent)" title="${escapeHTML(runID)}">${escapeHTML(runID.substring(0, 16))}…</span>${renderCopyButton(runID)} ${statusIcon(r.status)}</h2>
|
||||
<div class="meta-tiles">
|
||||
<div class="meta-tile">
|
||||
<div class="meta-tile-label">Started</div>
|
||||
@@ -1061,8 +1168,8 @@
|
||||
` : '<div class="empty-state" style="padding:0.5rem 0">No spans yet</div>';
|
||||
|
||||
return `
|
||||
<tr class="clickable expandable-run" data-run="${escapeHTML(r.run_id)}" data-index="${i}">
|
||||
<td class="id-cell"><span class="expand-icon"></span>${escapeHTML(r.run_id.substring(0, 12))}...</td>
|
||||
<tr class="clickable expandable-run ${r.status === 'error' ? 'tr-error' : ''}" data-run="${escapeHTML(r.run_id)}" data-index="${i}">
|
||||
<td class="id-cell" title="${escapeHTML(r.run_id)}"><span class="expand-icon"></span>${escapeHTML(r.run_id.substring(0, 12))}...${renderCopyButton(r.run_id)}</td>
|
||||
<td>${statusIcon(r.status)}</td>
|
||||
<td><span class="model-badge">${modelLabel}</span></td>
|
||||
<td>${r.tool_count || 0}</td>
|
||||
|
||||
Reference in New Issue
Block a user