Spaces:
Running
Running
File size: 8,899 Bytes
78f4d62 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 | // ============================================================
// 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);
}
}
|