253 lines
6.9 KiB
JavaScript
253 lines
6.9 KiB
JavaScript
/**
|
|
* Application bootstrap — state management, health polling, routing
|
|
*/
|
|
|
|
import { api } from './api.js';
|
|
import { initSidebar, renderSessions } from './components/sidebar.js';
|
|
import { initSessionForm } from './components/session-form.js';
|
|
import { initToolbar, updateToolbar } from './components/toolbar.js';
|
|
import { initChat, addMessage, setStreamingContent, clearChat, setInputEnabled, showExecutionIndicator, hideExecutionIndicator } from './components/chat.js';
|
|
import { initEventLog, addEvent, clearEvents } from './components/event-log.js';
|
|
import { initInspector, updateInspector, clearInspector } from './components/session-inspector.js';
|
|
import { initTimeline, addTimelineStep, updateTimelineStep, clearTimeline } from './components/agent-timeline.js';
|
|
|
|
// --- Global state ---
|
|
export const state = {
|
|
sessions: JSON.parse(localStorage.getItem('agentic_sessions') || '[]'),
|
|
activeSessionId: null,
|
|
sessionState: null,
|
|
health: null,
|
|
isExecuting: false,
|
|
streamingContent: '',
|
|
currentAgent: '',
|
|
};
|
|
|
|
function saveSessions() {
|
|
localStorage.setItem('agentic_sessions', JSON.stringify(state.sessions));
|
|
}
|
|
|
|
// --- Session management ---
|
|
export async function selectSession(sessionId) {
|
|
api.unsubscribeAll();
|
|
state.activeSessionId = sessionId;
|
|
state.streamingContent = '';
|
|
state.currentAgent = '';
|
|
clearChat();
|
|
clearEvents();
|
|
clearTimeline();
|
|
clearInspector();
|
|
|
|
if (!sessionId) {
|
|
updateToolbar(null);
|
|
renderSessions();
|
|
return;
|
|
}
|
|
|
|
location.hash = `session=${sessionId}`;
|
|
renderSessions();
|
|
|
|
try {
|
|
const session = await api.getSession(sessionId);
|
|
state.sessionState = session;
|
|
updateToolbar(session);
|
|
updateInspector(session);
|
|
|
|
const events = await api.getEvents(sessionId);
|
|
for (const ev of events) addEvent(ev);
|
|
|
|
api.subscribeSSE(sessionId);
|
|
setInputEnabled(true);
|
|
} catch (e) {
|
|
addMessage('system', `Error loading session: ${e.message}`);
|
|
}
|
|
}
|
|
|
|
export async function createSession(body) {
|
|
const data = await api.createSession(body);
|
|
if (!state.sessions.includes(data.session_id)) {
|
|
state.sessions.unshift(data.session_id);
|
|
saveSessions();
|
|
}
|
|
await selectSession(data.session_id);
|
|
return data;
|
|
}
|
|
|
|
export async function deleteSession(sessionId) {
|
|
try {
|
|
await api.deleteSession(sessionId);
|
|
} catch { /* ignore 404 */ }
|
|
api.unsubscribeSSE(sessionId);
|
|
state.sessions = state.sessions.filter(s => s !== sessionId);
|
|
saveSessions();
|
|
if (state.activeSessionId === sessionId) {
|
|
state.activeSessionId = null;
|
|
state.sessionState = null;
|
|
location.hash = '';
|
|
clearChat();
|
|
clearEvents();
|
|
clearTimeline();
|
|
clearInspector();
|
|
updateToolbar(null);
|
|
}
|
|
renderSessions();
|
|
}
|
|
|
|
export async function sendMessage(message, stream = false) {
|
|
if (!state.activeSessionId || state.isExecuting) return;
|
|
|
|
state.isExecuting = true;
|
|
state.streamingContent = '';
|
|
setInputEnabled(false);
|
|
addMessage('user', message);
|
|
showExecutionIndicator();
|
|
|
|
try {
|
|
if (stream) {
|
|
await api.sendMessage(state.activeSessionId, message, true);
|
|
// Response comes via SSE — handled by event listeners
|
|
} else {
|
|
const result = await api.sendMessage(state.activeSessionId, message, false);
|
|
hideExecutionIndicator();
|
|
addMessage('assistant', result.content || JSON.stringify(result, null, 2));
|
|
state.isExecuting = false;
|
|
setInputEnabled(true);
|
|
// Refresh state
|
|
const session = await api.getSession(state.activeSessionId);
|
|
state.sessionState = session;
|
|
updateToolbar(session);
|
|
updateInspector(session);
|
|
}
|
|
} catch (e) {
|
|
hideExecutionIndicator();
|
|
addMessage('system', `Error: ${e.message}`);
|
|
state.isExecuting = false;
|
|
setInputEnabled(true);
|
|
}
|
|
}
|
|
|
|
// --- SSE event handlers ---
|
|
function setupSSEListeners() {
|
|
api.on('sse', (e) => {
|
|
addEvent(e.detail);
|
|
});
|
|
|
|
api.on('sse:execution.started', () => {
|
|
state.isExecuting = true;
|
|
showExecutionIndicator('Starting execution...');
|
|
});
|
|
|
|
api.on('sse:subagent.assigned', (e) => {
|
|
const d = e.detail.data || e.detail;
|
|
state.currentAgent = d.agent || '';
|
|
showExecutionIndicator(`Step ${d.step}/${d.total_steps} — ${d.agent}`);
|
|
addTimelineStep({
|
|
step: d.step,
|
|
totalSteps: d.total_steps,
|
|
agent: d.agent,
|
|
description: d.description,
|
|
status: 'executing',
|
|
});
|
|
});
|
|
|
|
api.on('sse:agent.delta', (e) => {
|
|
const d = e.detail.data || e.detail;
|
|
state.streamingContent += d.delta || '';
|
|
setStreamingContent(state.streamingContent, state.currentAgent);
|
|
});
|
|
|
|
api.on('sse:tool.started', (e) => {
|
|
const d = e.detail.data || e.detail;
|
|
showExecutionIndicator(`Running tool: ${d.tool}`);
|
|
});
|
|
|
|
api.on('sse:tool.completed', (e) => {
|
|
const d = e.detail.data || e.detail;
|
|
if (state.currentAgent) {
|
|
updateTimelineStep(state.currentAgent, { tool: d.tool, status: d.status });
|
|
}
|
|
});
|
|
|
|
api.on('sse:execution.completed', async (e) => {
|
|
hideExecutionIndicator();
|
|
if (state.streamingContent) {
|
|
// Finalize the streaming message
|
|
addMessage('assistant', state.streamingContent);
|
|
state.streamingContent = '';
|
|
}
|
|
state.isExecuting = false;
|
|
state.currentAgent = '';
|
|
setInputEnabled(true);
|
|
|
|
// Refresh session state
|
|
if (state.activeSessionId) {
|
|
try {
|
|
const session = await api.getSession(state.activeSessionId);
|
|
state.sessionState = session;
|
|
updateToolbar(session);
|
|
updateInspector(session);
|
|
} catch { /* ignore */ }
|
|
}
|
|
});
|
|
|
|
api.on('sse:error', (e) => {
|
|
const d = e.detail.data || e.detail;
|
|
addMessage('system', `Error: ${JSON.stringify(d)}`);
|
|
hideExecutionIndicator();
|
|
state.isExecuting = false;
|
|
setInputEnabled(true);
|
|
});
|
|
}
|
|
|
|
// --- Health polling ---
|
|
let healthInterval;
|
|
function startHealthPolling() {
|
|
const poll = async () => {
|
|
state.health = await api.checkHealth();
|
|
};
|
|
poll();
|
|
healthInterval = setInterval(poll, 10000);
|
|
}
|
|
|
|
// --- Theme ---
|
|
export function toggleTheme() {
|
|
const current = document.documentElement.getAttribute('data-theme');
|
|
const next = current === 'light' ? 'dark' : 'light';
|
|
document.documentElement.setAttribute('data-theme', next);
|
|
localStorage.setItem('agentic_theme', next);
|
|
}
|
|
|
|
// --- Init ---
|
|
function init() {
|
|
// Restore theme
|
|
const savedTheme = localStorage.getItem('agentic_theme') || 'dark';
|
|
if (savedTheme === 'light') {
|
|
document.documentElement.setAttribute('data-theme', 'light');
|
|
}
|
|
|
|
// Init components
|
|
initToolbar();
|
|
initSidebar();
|
|
initSessionForm();
|
|
initChat();
|
|
initEventLog();
|
|
initInspector();
|
|
initTimeline();
|
|
|
|
// SSE listeners
|
|
setupSSEListeners();
|
|
|
|
// Health
|
|
startHealthPolling();
|
|
|
|
// Restore session from hash
|
|
const hash = location.hash;
|
|
const match = hash.match(/session=([a-f0-9]+)/);
|
|
if (match && state.sessions.includes(match[1])) {
|
|
selectSession(match[1]);
|
|
}
|
|
|
|
renderSessions();
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', init);
|