// ============================================================ // Multi-user session management // ============================================================ let SESSION_ID = localStorage.getItem('agentui_username') || ''; function apiFetch(url, options = {}) { if (SESSION_ID) { options.headers = { ...options.headers, 'X-Session-ID': SESSION_ID }; } return fetch(url, options); } function sanitizeUsername(name) { return name.trim().toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9\-]/g, '').substring(0, 30); } // ============================================================ // Agent Type Registry — populated from backend /api/agents at startup // To add a new agent type, add an entry in backend/agents.py (single source of truth) // ============================================================ let AGENT_REGISTRY = {}; // Virtual types used only in timeline rendering (not real agents) const VIRTUAL_TYPE_LABELS = { search: 'SEARCH', browse: 'BROWSE' }; // Derived helpers from registry function getTypeLabel(type) { if (AGENT_REGISTRY[type]) return AGENT_REGISTRY[type].label; if (VIRTUAL_TYPE_LABELS[type]) return VIRTUAL_TYPE_LABELS[type]; return type.toUpperCase(); } function getPlaceholder(type) { return AGENT_REGISTRY[type]?.placeholder || 'Enter message...'; } function getDefaultCounters() { const counters = {}; for (const [key, agent] of Object.entries(AGENT_REGISTRY)) { if (agent.hasCounter) counters[key] = 0; } return counters; } // State management let tabCounter = 1; let activeTabId = 0; let currentSession = null; // Name of the current session const collapsedAgents = new Set(); // Track collapsed agent tab IDs let researchQueryTabIds = {}; // queryIndex -> virtual tabId for research timeline let showAllTurns = true; // Toggle to show/hide individual assistant dots // Fetch random isotope name from backend async function generateSessionName() { try { const response = await apiFetch('/api/sessions/random-name'); const data = await response.json(); return data.name; } catch (error) { // Fallback to timestamp if API fails return `session-${Date.now()}`; } } let settings = { // New provider/model structure providers: {}, // providerId -> {name, endpoint, token} models: {}, // modelId -> {name, providerId, modelId (API model string)} agents: {}, // Populated after AGENT_REGISTRY is fetched // Service API keys e2bKey: '', serperKey: '', hfToken: '', // Image model selections (model IDs from the models list) imageGenModel: '', imageEditModel: '', // Research settings researchSubAgentModel: '', researchParallelWorkers: null, researchMaxWebsites: null, // UI settings themeColor: 'forest', // Schema version for migrations settingsVersion: 2 }; // Track action widgets for result updates (maps tabId -> widget element) const actionWidgets = {}; // Track tool call IDs for result updates (maps tabId -> tool_call_id) const toolCallIds = {}; // Global figure/image registry populated by sub-agents for cross-agent reference resolution // Maps "figure_1" -> {type, data} and "image_1" -> {type: "png", data: base64} const globalFigureRegistry = {}; // Debug: per-tab LLM call history (populated by SSE debug_call_input/output events) // Maps tabId -> [{call_number, timestamp, input, output, error}] const debugHistory = {}; // Track agents by task_id for reuse (maps task_id -> tabId) const taskIdToTabId = {}; // Whether command center input is blocked waiting for agents to finish let commandInputBlocked = false; // Count of agent launches that haven't started generating yet (handles race condition) let pendingAgentLaunches = 0; // Track agent counters for each type (derived from registry) let agentCounters = getDefaultCounters(); // Debounce timer for workspace saving let saveWorkspaceTimer = null; // Abort controllers for in-flight fetch requests (tabId -> AbortController) const activeAbortControllers = {}; // Timeline data structure for sidebar // Maps tabId -> { type, title, events: [{type: 'user'|'assistant'|'agent', content, childTabId?}], parentTabId?, isGenerating } const timelineData = { 0: { type: 'command', title: 'Task Center', events: [], parentTabId: null, isGenerating: false } }; // Reset all local state for session switching (without page reload) function resetLocalState() { // Reset counters tabCounter = 1; activeTabId = 0; currentSession = null; collapsedAgents.clear(); // Clear object maps Object.keys(actionWidgets).forEach(k => delete actionWidgets[k]); Object.keys(toolCallIds).forEach(k => delete toolCallIds[k]); Object.keys(globalFigureRegistry).forEach(k => delete globalFigureRegistry[k]); Object.keys(debugHistory).forEach(k => delete debugHistory[k]); Object.keys(taskIdToTabId).forEach(k => delete taskIdToTabId[k]); researchQueryTabIds = {}; showAllTurns = true; agentCounters = getDefaultCounters(); // Reset sidebar checkboxes const compactCb = document.getElementById('compactViewCheckbox'); if (compactCb) compactCb.checked = false; const collapseAgentsCb = document.getElementById('collapseAgentsCheckbox'); if (collapseAgentsCb) collapseAgentsCb.checked = false; const collapseToolsCb = document.getElementById('collapseToolsCheckbox'); if (collapseToolsCb) collapseToolsCb.checked = false; // Reset timeline data Object.keys(timelineData).forEach(k => delete timelineData[k]); timelineData[0] = { type: 'command', title: 'Task Center', events: [], parentTabId: null, isGenerating: false }; // Clear dynamic tabs from DOM const dynamicTabs = document.getElementById('dynamicTabs'); if (dynamicTabs) dynamicTabs.innerHTML = ''; // Remove all dynamic tab content elements (keep tab-content[data-content-id="0"]) document.querySelectorAll('.tab-content').forEach(el => { if (el.dataset.contentId !== '0') el.remove(); }); // Clear command center messages const commandMessages = document.getElementById('messages-command'); if (commandMessages) commandMessages.innerHTML = ''; // Close any open panels closeAllPanels(); } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function formatDate(timestamp) { const date = new Date(timestamp * 1000); const now = new Date(); const diff = now - date; if (diff < 60000) return 'just now'; if (diff < 3600000) return Math.floor(diff / 60000) + 'm ago'; if (diff < 86400000) return Math.floor(diff / 3600000) + 'h ago'; if (diff < 604800000) return Math.floor(diff / 86400000) + 'd ago'; return date.toLocaleDateString(); } // ============================================================ // Shared UI helpers (deduplication) // ============================================================ // Wire send-button, textarea auto-resize, and Enter-to-send for any agent tab function setupInputListeners(container, tabId) { const input = container.querySelector('textarea'); const sendBtn = container.querySelector('.input-container button'); if (!input || !sendBtn) return; sendBtn.addEventListener('click', () => sendMessage(tabId)); input.addEventListener('input', () => { input.style.height = 'auto'; input.style.height = Math.min(input.scrollHeight, 200) + 'px'; input.style.overflowY = input.scrollHeight > 200 ? 'auto' : 'hidden'; }); input.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(tabId); } }); } // Wire click-to-collapse on tool cells, code cells, and action widgets function setupCollapseToggle(cell, labelSelector) { const label = cell.querySelector(labelSelector || '.tool-cell-label, .code-cell-label'); if (!label) return; label.addEventListener('click', () => { cell.classList.toggle('collapsed'); const toggle = cell.querySelector('.widget-collapse-toggle'); if (toggle) toggle.classList.toggle('collapsed'); }); } // Close all right-side panels (settings, debug, files, sessions) function closeAllPanels() { const app = document.querySelector('.app-container'); for (const [panelId, btnId, cls] of [ ['settingsPanel', 'settingsBtn', 'panel-open'], ['debugPanel', 'debugBtn', 'panel-open'], ['filesPanel', 'filesBtn', 'files-panel-open'], ['sessionsPanel', 'sessionsBtn', 'sessions-panel-open'], ]) { document.getElementById(panelId)?.classList.remove('active'); document.getElementById(btnId)?.classList.remove('active'); if (cls && app) app.classList.remove(cls); } }