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);
    }
}