Spaces:
Running
Running
| // ============================================================ | |
| // 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); | |
| } | |
| } | |