agent-ui / frontend /utils.js
lvwerra's picture
lvwerra HF Staff
Split frontend script.js (5460 lines) into 8 focused modules
78f4d62
// ============================================================
// 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);
}
}