agent-ui / frontend /app.js
lvwerra's picture
lvwerra HF Staff
Restructure settings into collapsible groups, fix dark theme hardcoded colors
b5bd59f
Raw
History Blame Contribute Delete
13.2 kB
// Initialize
document.addEventListener('DOMContentLoaded', async () => {
// Check if multi-user mode is enabled (retry if server not ready yet)
for (let attempt = 0; attempt < 5; attempt++) {
try {
const configResp = await fetch('/api/config');
const config = await configResp.json();
IS_MULTI_USER = config.multiUser;
if (config.multiUser && !SESSION_ID) {
await showUsernameOverlay();
}
break;
} catch (e) {
if (attempt < 4) {
await new Promise(r => setTimeout(r, 1000));
}
// After all retries fail, continue in single-user mode
}
}
// Fetch agent registry from backend (single source of truth)
try {
const agentsResp = await apiFetch('/api/agents');
const agentsData = await agentsResp.json();
for (const agent of agentsData.agents) {
AGENT_REGISTRY[agent.key] = agent;
}
} catch (e) {
console.error('Failed to fetch agent registry, using fallback');
// Minimal fallback so the app still works if backend is slow
AGENT_REGISTRY = {
command: { label: 'MAIN', hasCounter: false, inMenu: false, inLauncher: false, placeholder: 'Enter message...' },
agent: { label: 'AGENT', hasCounter: true, inMenu: true, inLauncher: true, placeholder: 'Enter message...' },
code: { label: 'CODE', hasCounter: true, inMenu: true, inLauncher: true, placeholder: 'Enter message...' },
research: { label: 'RESEARCH', hasCounter: true, inMenu: true, inLauncher: true, placeholder: 'Enter message...' },
image: { label: 'IMAGE', hasCounter: true, inMenu: true, inLauncher: true, placeholder: 'Describe an image or paste a URL...' },
};
}
// Initialize settings.agents with registry keys (before loadSettings merges saved values)
settings.agents = Object.fromEntries(Object.keys(AGENT_REGISTRY).map(k => [k, '']));
await loadSettings();
applyTheme(settings.themeColor || 'forest');
initializeEventListeners();
initializeSessionListeners();
updateUserIndicator();
// Always show session selector on load (don't auto-resume last session)
const sessionsData = await fetchSessions();
showSessionSelector(sessionsData.sessions);
// Compute line heights for the static welcome diagram (after layout)
requestAnimationFrame(() => updateTimelineLines());
});
// ============================================================
// Dynamic HTML generation from AGENT_REGISTRY
// ============================================================
function renderNewTabMenu() {
const menu = document.getElementById('newTabMenu');
if (!menu) return;
menu.innerHTML = '';
for (const [key, agent] of Object.entries(AGENT_REGISTRY)) {
if (!agent.inMenu) continue;
const item = document.createElement('div');
item.className = 'menu-item';
item.dataset.type = key;
item.textContent = agent.label;
menu.appendChild(item);
}
}
function renderLauncherButtons() {
const container = document.getElementById('launcherButtons');
if (!container) return;
// Insert before the debug button (keep it at the end)
const debugBtn = container.querySelector('#debugBtn');
for (const [key, agent] of Object.entries(AGENT_REGISTRY)) {
if (!agent.inLauncher) continue;
const btn = document.createElement('button');
btn.className = 'launcher-btn';
btn.dataset.type = key;
btn.textContent = agent.label;
container.insertBefore(btn, debugBtn);
}
}
function renderAgentModelSelectors() {
const grid = document.getElementById('agentModelsGrid');
if (!grid) return;
grid.innerHTML = '';
for (const [key, agent] of Object.entries(AGENT_REGISTRY)) {
const label = document.createElement('label');
label.textContent = `${agent.label}:`;
const select = document.createElement('select');
select.id = `setting-agent-${key}`;
select.className = 'settings-select';
grid.appendChild(label);
grid.appendChild(select);
}
}
function initializeEventListeners() {
// Generate dynamic UI elements from registry
renderLauncherButtons();
renderNewTabMenu();
renderAgentModelSelectors();
// Launcher buttons in command center
document.querySelectorAll('.launcher-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const type = btn.dataset.type;
createAgentTab(type);
});
});
// Command center chat functionality
const commandContent = document.querySelector('.tab-content[data-content-id="0"]');
if (commandContent) {
setupInputListeners(commandContent, 0);
}
// Sidebar checkboxes
const compactViewCheckbox = document.getElementById('compactViewCheckbox');
const collapseAgentsCheckbox = document.getElementById('collapseAgentsCheckbox');
const collapseToolsCheckbox = document.getElementById('collapseToolsCheckbox');
// Compact view: affects timeline only (collapse agents + hide turns)
if (compactViewCheckbox) {
compactViewCheckbox.addEventListener('change', () => {
const compact = compactViewCheckbox.checked;
if (compact) {
// Collapse all agent boxes in timeline
Object.entries(timelineData).forEach(([id, data]) => {
if (data.parentTabId !== null) {
collapsedAgents.add(String(id));
}
});
showAllTurns = false;
} else {
collapsedAgents.clear();
showAllTurns = true;
}
renderTimeline();
});
}
// Chat collapse: agents — affects action-widgets in chat only
if (collapseAgentsCheckbox) {
collapseAgentsCheckbox.addEventListener('change', () => {
const collapse = collapseAgentsCheckbox.checked;
document.querySelectorAll('.action-widget').forEach(w => {
const toggle = w.querySelector('.widget-collapse-toggle');
if (collapse) {
w.classList.add('collapsed');
if (toggle) toggle.classList.add('collapsed');
} else {
w.classList.remove('collapsed');
if (toggle) toggle.classList.remove('collapsed');
}
});
});
}
// Chat collapse: tools — affects tool-cells in chat only
if (collapseToolsCheckbox) {
collapseToolsCheckbox.addEventListener('change', () => {
const collapse = collapseToolsCheckbox.checked;
document.querySelectorAll('.tool-cell').forEach(w => {
const toggle = w.querySelector('.widget-collapse-toggle');
if (collapse) {
w.classList.add('collapsed');
if (toggle) toggle.classList.add('collapsed');
} else {
w.classList.remove('collapsed');
if (toggle) toggle.classList.remove('collapsed');
}
});
});
}
// New tab button - toggle menu
const newTabBtn = document.getElementById('newTabBtn');
const newTabMenu = document.getElementById('newTabMenu');
if (newTabBtn && newTabMenu) {
newTabBtn.addEventListener('click', (e) => {
e.stopPropagation();
e.preventDefault();
// Position the menu below the button
const rect = newTabBtn.getBoundingClientRect();
newTabMenu.style.top = `${rect.bottom}px`;
newTabMenu.style.left = `${rect.left}px`;
newTabMenu.classList.toggle('active');
});
}
// Close menu when clicking outside
document.addEventListener('click', (e) => {
if (!e.target.closest('.new-tab-wrapper')) {
newTabMenu.classList.remove('active');
}
});
// Menu items
document.querySelectorAll('.menu-item').forEach(item => {
item.addEventListener('click', () => {
const type = item.dataset.type;
createAgentTab(type);
newTabMenu.classList.remove('active');
});
});
// Tab switching
document.addEventListener('click', (e) => {
const tab = e.target.closest('.tab');
if (tab && !e.target.closest('.new-tab-wrapper')) {
if (e.target.classList.contains('tab-close')) {
closeTab(parseInt(tab.dataset.tabId));
} else {
switchToTab(parseInt(tab.dataset.tabId));
}
}
});
// Settings button - now handled in settings panel section below
// (removed old openSettings() call)
// Save settings button
const saveSettingsBtn = document.getElementById('saveSettingsBtn');
if (saveSettingsBtn) {
saveSettingsBtn.addEventListener('click', () => {
saveSettings();
});
}
// Cancel settings button
const cancelSettingsBtn = document.getElementById('cancelSettingsBtn');
if (cancelSettingsBtn) {
cancelSettingsBtn.addEventListener('click', () => closeAllPanels());
}
// Theme color picker
const themePicker = document.getElementById('theme-color-picker');
if (themePicker) {
themePicker.addEventListener('click', (e) => {
const themeOption = e.target.closest('.theme-option');
if (themeOption) {
// Remove selected from all
themePicker.querySelectorAll('.theme-option').forEach(opt => {
opt.classList.remove('selected');
});
// Add selected to clicked
themeOption.classList.add('selected');
// Update hidden input
const themeValue = themeOption.dataset.theme;
document.getElementById('setting-theme-color').value = themeValue;
}
});
}
// Settings group collapse/expand
document.addEventListener('click', (e) => {
const header = e.target.closest('.settings-group-header');
if (!header) return;
const group = header.closest('.settings-group');
if (!group) return;
group.classList.toggle('collapsed');
const toggle = header.querySelector('.widget-collapse-toggle');
if (toggle) toggle.classList.toggle('collapsed');
});
// Double-click on collapse toggle: collapse/uncollapse all widgets in the same chat
document.addEventListener('dblclick', (e) => {
const toggle = e.target.closest('.widget-collapse-toggle');
if (!toggle) return;
e.stopPropagation();
const chatContainer = toggle.closest('.chat-container');
if (!chatContainer) return;
const widgets = chatContainer.querySelectorAll('.tool-cell, .code-cell, .action-widget');
if (widgets.length === 0) return;
// If clicked widget is now collapsed, collapse all; otherwise uncollapse all
const clickedWidget = toggle.closest('.tool-cell, .code-cell, .action-widget');
const shouldCollapse = !clickedWidget?.classList.contains('collapsed');
widgets.forEach(w => {
w.classList.toggle('collapsed', shouldCollapse);
const t = w.querySelector('.widget-collapse-toggle');
if (t) t.classList.toggle('collapsed', shouldCollapse);
});
});
// Resizable sidebar
const sidebar = document.getElementById('agentsSidebar');
const resizeHandle = document.getElementById('sidebarResizeHandle');
if (sidebar && resizeHandle) {
const savedWidth = localStorage.getItem('agentui_sidebar_width');
if (savedWidth) {
const w = parseInt(savedWidth);
if (w >= 140 && w <= 400) {
sidebar.style.setProperty('--sidebar-width', w + 'px');
sidebar.style.width = w + 'px';
}
}
let dragging = false;
resizeHandle.addEventListener('mousedown', (e) => {
e.preventDefault();
dragging = true;
resizeHandle.classList.add('dragging');
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
});
document.addEventListener('mousemove', (e) => {
if (!dragging) return;
const w = Math.min(400, Math.max(140, e.clientX));
sidebar.style.setProperty('--sidebar-width', w + 'px');
sidebar.style.width = w + 'px';
});
document.addEventListener('mouseup', () => {
if (!dragging) return;
dragging = false;
resizeHandle.classList.remove('dragging');
document.body.style.cursor = '';
document.body.style.userSelect = '';
const w = parseInt(sidebar.style.width);
if (w) localStorage.setItem('agentui_sidebar_width', w);
});
}
}