Initial commit
This commit is contained in:
171
dashboard/js/components/event-log.js
Normal file
171
dashboard/js/components/event-log.js
Normal file
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* Event Log — real-time SSE events with filtering
|
||||
*/
|
||||
|
||||
const EVENT_CATEGORIES = {
|
||||
'session.created': 'lifecycle',
|
||||
'execution.started': 'lifecycle',
|
||||
'execution.completed': 'lifecycle',
|
||||
'agent.delta': 'content',
|
||||
'tool.started': 'tool',
|
||||
'tool.completed': 'tool',
|
||||
'subagent.assigned': 'orchestration',
|
||||
'error': 'error',
|
||||
'keepalive': 'keepalive',
|
||||
};
|
||||
|
||||
const CATEGORY_LABELS = ['lifecycle', 'content', 'tool', 'orchestration', 'error'];
|
||||
|
||||
let entriesEl;
|
||||
let activeFilters = new Set(CATEGORY_LABELS);
|
||||
let stickToBottom = true;
|
||||
let events = [];
|
||||
|
||||
export function initEventLog() {
|
||||
const panel = document.getElementById('event-log-panel');
|
||||
|
||||
panel.innerHTML = `
|
||||
<div class="event-log-header" id="event-log-toggle">
|
||||
<span class="chevron">▼</span>
|
||||
<span>Event Log</span>
|
||||
<span class="toolbar-spacer"></span>
|
||||
<span class="text-muted text-sm" id="event-count">0 events</span>
|
||||
<button class="btn btn-sm" id="btn-clear-events" style="margin-left:8px">Clear</button>
|
||||
<button class="btn btn-sm" id="btn-export-events">Export</button>
|
||||
</div>
|
||||
<div class="event-log-filters" id="event-filters"></div>
|
||||
<div class="event-log-entries" id="event-entries"></div>
|
||||
`;
|
||||
|
||||
entriesEl = document.getElementById('event-entries');
|
||||
|
||||
// Toggle panel
|
||||
document.getElementById('event-log-toggle').addEventListener('click', () => {
|
||||
panel.classList.toggle('open');
|
||||
});
|
||||
|
||||
// Filters
|
||||
const filtersEl = document.getElementById('event-filters');
|
||||
for (const cat of CATEGORY_LABELS) {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = `event-filter-btn active`;
|
||||
btn.textContent = cat;
|
||||
btn.dataset.category = cat;
|
||||
btn.addEventListener('click', () => {
|
||||
if (activeFilters.has(cat)) {
|
||||
activeFilters.delete(cat);
|
||||
btn.classList.remove('active');
|
||||
} else {
|
||||
activeFilters.add(cat);
|
||||
btn.classList.add('active');
|
||||
}
|
||||
renderEvents();
|
||||
});
|
||||
filtersEl.appendChild(btn);
|
||||
}
|
||||
|
||||
// Auto-scroll detection
|
||||
entriesEl.addEventListener('scroll', () => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = entriesEl;
|
||||
stickToBottom = scrollHeight - scrollTop - clientHeight < 20;
|
||||
});
|
||||
|
||||
// Clear
|
||||
document.getElementById('btn-clear-events').addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
clearEvents();
|
||||
});
|
||||
|
||||
// Export
|
||||
document.getElementById('btn-export-events').addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const blob = new Blob([JSON.stringify(events, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `events-${Date.now()}.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
}
|
||||
|
||||
export function addEvent(event) {
|
||||
events.push(event);
|
||||
document.getElementById('event-count').textContent = `${events.length} events`;
|
||||
|
||||
const type = event.type || '';
|
||||
const category = EVENT_CATEGORIES[type] || 'lifecycle';
|
||||
|
||||
if (!activeFilters.has(category)) return;
|
||||
if (category === 'keepalive') return; // Hide keepalives by default
|
||||
|
||||
const entry = createEventEntry(event, type, category);
|
||||
entriesEl.appendChild(entry);
|
||||
|
||||
if (stickToBottom) {
|
||||
entriesEl.scrollTop = entriesEl.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
function createEventEntry(event, type, category) {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'event-entry';
|
||||
|
||||
const time = event.timestamp
|
||||
? new Date(event.timestamp).toLocaleTimeString('en-US', { hour12: false, fractionalSecondDigits: 3 })
|
||||
: '--:--:--';
|
||||
|
||||
const data = event.data || {};
|
||||
const summary = summarizeEventData(type, data);
|
||||
|
||||
el.innerHTML = `
|
||||
<span class="event-time">${time}</span>
|
||||
<span class="event-type-badge ${category}">${type.split('.').pop()}</span>
|
||||
<span class="event-data" title="Click to expand">${summary}</span>
|
||||
`;
|
||||
|
||||
// Toggle expand
|
||||
const dataEl = el.querySelector('.event-data');
|
||||
dataEl.addEventListener('click', () => {
|
||||
if (dataEl.classList.contains('expanded')) {
|
||||
dataEl.classList.remove('expanded');
|
||||
dataEl.textContent = summary;
|
||||
} else {
|
||||
dataEl.classList.add('expanded');
|
||||
dataEl.textContent = JSON.stringify(data, null, 2);
|
||||
}
|
||||
});
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
function summarizeEventData(type, data) {
|
||||
switch (type) {
|
||||
case 'agent.delta': return `[${data.agent || '?'}] "${(data.delta || '').substring(0, 60)}..."`;
|
||||
case 'tool.started': return `Tool: ${data.tool}`;
|
||||
case 'tool.completed': return `Tool: ${data.tool} → ${data.status}`;
|
||||
case 'subagent.assigned': return `Step ${data.step}/${data.total_steps}: ${data.agent} — ${(data.description || '').substring(0, 50)}`;
|
||||
case 'execution.started': return `Session: ${(data.session_id || '').substring(0, 12)}`;
|
||||
case 'execution.completed': return `Steps: ${data.steps_completed} — ${data.status}`;
|
||||
case 'error': return JSON.stringify(data);
|
||||
default: return JSON.stringify(data).substring(0, 80);
|
||||
}
|
||||
}
|
||||
|
||||
function renderEvents() {
|
||||
entriesEl.innerHTML = '';
|
||||
for (const event of events) {
|
||||
const type = event.type || '';
|
||||
const category = EVENT_CATEGORIES[type] || 'lifecycle';
|
||||
if (!activeFilters.has(category)) continue;
|
||||
if (category === 'keepalive') continue;
|
||||
entriesEl.appendChild(createEventEntry(event, type, category));
|
||||
}
|
||||
}
|
||||
|
||||
export function clearEvents() {
|
||||
events = [];
|
||||
if (entriesEl) entriesEl.innerHTML = '';
|
||||
const countEl = document.getElementById('event-count');
|
||||
if (countEl) countEl.textContent = '0 events';
|
||||
}
|
||||
Reference in New Issue
Block a user