| |
| |
| |
|
|
| |
| function migrateSettings(oldSettings) { |
| |
| if (oldSettings.settingsVersion >= 2) { |
| return oldSettings; |
| } |
|
|
| console.log('Migrating settings from v1 to v2...'); |
|
|
| const newSettings = { |
| providers: {}, |
| models: {}, |
| agents: { |
| command: '', |
| agent: '', |
| code: '', |
| research: '', |
| chat: '' |
| }, |
| e2bKey: oldSettings.e2bKey || '', |
| serperKey: oldSettings.serperKey || '', |
| hfToken: oldSettings.hfToken || '', |
| imageGenModel: oldSettings.imageGenModel || '', |
| imageEditModel: oldSettings.imageEditModel || '', |
| researchSubAgentModel: oldSettings.researchSubAgentModel || '', |
| researchParallelWorkers: oldSettings.researchParallelWorkers || null, |
| researchMaxWebsites: oldSettings.researchMaxWebsites || null, |
| themeColor: oldSettings.themeColor || 'forest', |
| settingsVersion: 2 |
| }; |
|
|
| |
| if (oldSettings.endpoint) { |
| const providerId = 'provider_default'; |
| newSettings.providers[providerId] = { |
| name: 'Default', |
| endpoint: oldSettings.endpoint, |
| token: oldSettings.token || '' |
| }; |
|
|
| |
| if (oldSettings.model) { |
| const modelId = 'model_default'; |
| newSettings.models[modelId] = { |
| name: oldSettings.model, |
| providerId: providerId, |
| modelId: oldSettings.model |
| }; |
|
|
| |
| newSettings.agents.command = modelId; |
| newSettings.agents.agent = modelId; |
| newSettings.agents.code = modelId; |
| newSettings.agents.research = modelId; |
| newSettings.agents.chat = modelId; |
| } |
|
|
| |
| const oldModels = oldSettings.models || {}; |
| const agentTypes = Object.keys(AGENT_REGISTRY).filter(k => AGENT_REGISTRY[k].hasCounter); |
| agentTypes.forEach(type => { |
| if (oldModels[type]) { |
| const specificModelId = `model_${type}`; |
| newSettings.models[specificModelId] = { |
| name: `${type.charAt(0).toUpperCase() + type.slice(1)} - ${oldModels[type]}`, |
| providerId: providerId, |
| modelId: oldModels[type] |
| }; |
| newSettings.agents[type] = specificModelId; |
| } |
| }); |
| } |
|
|
| console.log('Settings migrated:', newSettings); |
| return newSettings; |
| } |
|
|
| async function loadSettings() { |
| let loadedSettings = null; |
|
|
| |
| try { |
| const response = await apiFetch('/api/settings'); |
| if (response.ok) { |
| loadedSettings = await response.json(); |
| console.log('Settings loaded from file:', loadedSettings); |
| } |
| } catch (e) { |
| console.log('Could not load settings from backend, falling back to localStorage'); |
| } |
|
|
| |
| if (!loadedSettings) { |
| const savedSettings = localStorage.getItem('agentui_settings') || localStorage.getItem('productive_settings'); |
| console.log('Loading settings from localStorage:', savedSettings ? 'found' : 'not found'); |
| if (savedSettings) { |
| try { |
| loadedSettings = JSON.parse(savedSettings); |
| console.log('Settings loaded from localStorage:', loadedSettings); |
| } catch (e) { |
| console.error('Failed to parse settings:', e); |
| } |
| } |
| } |
|
|
| if (loadedSettings) { |
| |
| if (loadedSettings.notebooks && !loadedSettings.agents) { |
| loadedSettings.agents = loadedSettings.notebooks; |
| delete loadedSettings.notebooks; |
| } |
| |
| if (!loadedSettings.settingsVersion || loadedSettings.settingsVersion < 2) { |
| loadedSettings = migrateSettings(loadedSettings); |
| |
| try { |
| await apiFetch('/api/settings', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify(loadedSettings) |
| }); |
| console.log('Migrated settings saved to file'); |
| } catch (e) { |
| console.log('Could not save migrated settings to file'); |
| } |
| } |
| settings = { ...settings, ...loadedSettings }; |
| } else { |
| console.log('Using default settings:', settings); |
| } |
| } |
|
|
| |
| function generateId(prefix) { |
| return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; |
| } |
|
|
| |
| function renderProvidersList() { |
| const container = document.getElementById('providers-list'); |
| if (!container) return; |
|
|
| const providers = settings.providers || {}; |
| let html = ''; |
|
|
| Object.entries(providers).forEach(([id, provider]) => { |
| html += ` |
| <div class="provider-item" data-provider-id="${id}"> |
| <div class="provider-info"> |
| <span class="provider-name">${escapeHtml(provider.name)}</span> |
| <span class="provider-endpoint">${escapeHtml(provider.endpoint)}</span> |
| </div> |
| <div class="provider-actions"> |
| <button class="provider-edit-btn" onclick="editProvider('${id}')" title="Edit">✎</button> |
| <button class="provider-delete-btn" onclick="deleteProvider('${id}')" title="Delete">×</button> |
| </div> |
| </div> |
| `; |
| }); |
|
|
| if (Object.keys(providers).length === 0) { |
| html = '<div class="empty-list">No providers configured. Add one to get started.</div>'; |
| } |
|
|
| container.innerHTML = html; |
| } |
|
|
| |
| function renderModelsList() { |
| const container = document.getElementById('models-list'); |
| if (!container) return; |
|
|
| const models = settings.models || {}; |
| const providers = settings.providers || {}; |
| let html = ''; |
|
|
| Object.entries(models).forEach(([id, model]) => { |
| const provider = providers[model.providerId]; |
| const providerName = provider ? provider.name : 'Unknown'; |
| html += ` |
| <div class="model-item" data-model-id="${id}"> |
| <div class="model-info"> |
| <span class="model-name">${escapeHtml(model.name)}</span> |
| <span class="model-details">${escapeHtml(model.modelId)} @ ${escapeHtml(providerName)}</span> |
| </div> |
| <div class="model-actions"> |
| <button class="model-edit-btn" onclick="editModel('${id}')" title="Edit">✎</button> |
| <button class="model-delete-btn" onclick="deleteModel('${id}')" title="Delete">×</button> |
| </div> |
| </div> |
| `; |
| }); |
|
|
| if (Object.keys(models).length === 0) { |
| html = '<div class="empty-list">No models configured. Add a provider first, then add models.</div>'; |
| } |
|
|
| container.innerHTML = html; |
| } |
|
|
| |
| function populateModelDropdowns() { |
| const models = settings.models || {}; |
| const agents = settings.agents || {}; |
|
|
| |
| const dropdownIds = [ |
| ...Object.keys(AGENT_REGISTRY).map(t => `setting-agent-${t}`), |
| 'setting-research-sub-agent-model', |
| 'setting-image-gen-model', |
| 'setting-image-edit-model' |
| ]; |
|
|
| dropdownIds.forEach(dropdownId => { |
| const dropdown = document.getElementById(dropdownId); |
| if (!dropdown) return; |
|
|
| |
| const currentValue = dropdown.value; |
|
|
| |
| dropdown.innerHTML = '<option value="">-- Select Model --</option>'; |
|
|
| Object.entries(models).forEach(([id, model]) => { |
| const option = document.createElement('option'); |
| option.value = id; |
| option.textContent = `${model.name} (${model.modelId})`; |
| dropdown.appendChild(option); |
| }); |
|
|
| |
| if (currentValue && models[currentValue]) { |
| dropdown.value = currentValue; |
| } |
| }); |
|
|
| |
| for (const type of Object.keys(AGENT_REGISTRY)) { |
| const dropdown = document.getElementById(`setting-agent-${type}`); |
| if (dropdown) dropdown.value = agents[type] || ''; |
| } |
| const subAgentDropdown = document.getElementById('setting-research-sub-agent-model'); |
| if (subAgentDropdown) subAgentDropdown.value = settings.researchSubAgentModel || ''; |
| const imageGenDropdown = document.getElementById('setting-image-gen-model'); |
| if (imageGenDropdown) imageGenDropdown.value = settings.imageGenModel || ''; |
| const imageEditDropdown = document.getElementById('setting-image-edit-model'); |
| if (imageEditDropdown) imageEditDropdown.value = settings.imageEditModel || ''; |
| } |
|
|
| |
| function showProviderDialog(providerId = null) { |
| const isEdit = !!providerId; |
| const provider = isEdit ? settings.providers[providerId] : { name: '', endpoint: '', token: '' }; |
|
|
| const dialog = document.getElementById('provider-dialog'); |
| const title = document.getElementById('provider-dialog-title'); |
| const nameInput = document.getElementById('provider-name'); |
| const endpointInput = document.getElementById('provider-endpoint'); |
| const tokenInput = document.getElementById('provider-token'); |
|
|
| title.textContent = isEdit ? 'Edit Provider' : 'Add Provider'; |
| nameInput.value = provider.name; |
| endpointInput.value = provider.endpoint; |
| tokenInput.value = provider.token; |
|
|
| dialog.dataset.providerId = providerId || ''; |
| dialog.classList.add('active'); |
| } |
|
|
| |
| function hideProviderDialog() { |
| const dialog = document.getElementById('provider-dialog'); |
| dialog.classList.remove('active'); |
| } |
|
|
| |
| function saveProviderFromDialog() { |
| const dialog = document.getElementById('provider-dialog'); |
| const providerId = dialog.dataset.providerId || generateId('provider'); |
| const name = document.getElementById('provider-name').value.trim(); |
| const endpoint = document.getElementById('provider-endpoint').value.trim(); |
| const token = document.getElementById('provider-token').value.trim(); |
|
|
| if (!name || !endpoint) { |
| alert('Provider name and endpoint are required'); |
| return; |
| } |
|
|
| settings.providers[providerId] = { name, endpoint, token }; |
| hideProviderDialog(); |
| renderProvidersList(); |
| populateModelDropdowns(); |
| } |
|
|
| |
| function editProvider(providerId) { |
| showProviderDialog(providerId); |
| } |
|
|
| |
| function deleteProvider(providerId) { |
| |
| const modelsUsingProvider = Object.entries(settings.models || {}) |
| .filter(([_, model]) => model.providerId === providerId); |
|
|
| if (modelsUsingProvider.length > 0) { |
| alert(`Cannot delete provider. ${modelsUsingProvider.length} model(s) are using it.`); |
| return; |
| } |
|
|
| if (confirm('Delete this provider?')) { |
| delete settings.providers[providerId]; |
| renderProvidersList(); |
| } |
| } |
|
|
| |
| function showModelDialog(modelId = null) { |
| const isEdit = !!modelId; |
| const model = isEdit ? settings.models[modelId] : { name: '', providerId: '', modelId: '', extraParams: null, multimodal: false }; |
|
|
| const dialog = document.getElementById('model-dialog'); |
| const title = document.getElementById('model-dialog-title'); |
| const nameInput = document.getElementById('model-name'); |
| const providerSelect = document.getElementById('model-provider'); |
| const modelIdInput = document.getElementById('model-model-id'); |
| const extraParamsInput = document.getElementById('model-extra-params'); |
| const multimodalCheckbox = document.getElementById('model-multimodal'); |
|
|
| title.textContent = isEdit ? 'Edit Model' : 'Add Model'; |
| nameInput.value = model.name; |
| modelIdInput.value = model.modelId; |
| extraParamsInput.value = model.extraParams ? JSON.stringify(model.extraParams, null, 2) : ''; |
| multimodalCheckbox.checked = !!model.multimodal; |
|
|
| |
| providerSelect.innerHTML = '<option value="">-- Select Provider --</option>'; |
| Object.entries(settings.providers || {}).forEach(([id, provider]) => { |
| const option = document.createElement('option'); |
| option.value = id; |
| option.textContent = provider.name; |
| if (id === model.providerId) option.selected = true; |
| providerSelect.appendChild(option); |
| }); |
|
|
| dialog.dataset.modelId = modelId || ''; |
| dialog.classList.add('active'); |
| } |
|
|
| |
| function hideModelDialog() { |
| const dialog = document.getElementById('model-dialog'); |
| dialog.classList.remove('active'); |
| } |
|
|
| |
| function saveModelFromDialog() { |
| const dialog = document.getElementById('model-dialog'); |
| const modelId = dialog.dataset.modelId || generateId('model'); |
| const name = document.getElementById('model-name').value.trim(); |
| const providerId = document.getElementById('model-provider').value; |
| const apiModelId = document.getElementById('model-model-id').value.trim(); |
| const extraParamsStr = document.getElementById('model-extra-params').value.trim(); |
|
|
| if (!name || !providerId || !apiModelId) { |
| alert('Name, provider, and model ID are required'); |
| return; |
| } |
|
|
| |
| let extraParams = null; |
| if (extraParamsStr) { |
| try { |
| extraParams = JSON.parse(extraParamsStr); |
| } catch (e) { |
| alert('Invalid JSON in extra parameters: ' + e.message); |
| return; |
| } |
| } |
|
|
| const multimodal = document.getElementById('model-multimodal').checked; |
| settings.models[modelId] = { name, providerId, modelId: apiModelId, extraParams, multimodal }; |
| hideModelDialog(); |
| renderModelsList(); |
| populateModelDropdowns(); |
| } |
|
|
| |
| function editModel(modelId) { |
| showModelDialog(modelId); |
| } |
|
|
| |
| function deleteModel(modelId) { |
| |
| const agentsUsingModel = Object.entries(settings.agents || {}) |
| .filter(([_, mid]) => mid === modelId); |
|
|
| if (agentsUsingModel.length > 0) { |
| const warning = `This model is used by: ${agentsUsingModel.map(([t]) => t).join(', ')}. Delete anyway?`; |
| if (!confirm(warning)) return; |
|
|
| |
| agentsUsingModel.forEach(([type]) => { |
| settings.agents[type] = ''; |
| }); |
| } else if (!confirm('Delete this model?')) { |
| return; |
| } |
|
|
| delete settings.models[modelId]; |
| renderModelsList(); |
| populateModelDropdowns(); |
| } |
|
|
| function openSettings() { |
| |
| const pathEl = document.getElementById('settingsPath'); |
| if (pathEl) pathEl.textContent = settings._settingsPath || ''; |
|
|
| |
| renderProvidersList(); |
| renderModelsList(); |
| populateModelDropdowns(); |
|
|
| |
| document.getElementById('setting-e2b-key').value = settings.e2bKey || ''; |
| document.getElementById('setting-serper-key').value = settings.serperKey || ''; |
| document.getElementById('setting-hf-token').value = settings.hfToken || ''; |
|
|
| |
| document.getElementById('setting-research-parallel-workers').value = settings.researchParallelWorkers || ''; |
| document.getElementById('setting-research-max-websites').value = settings.researchMaxWebsites || ''; |
|
|
| |
| const themeColor = settings.themeColor || 'forest'; |
| document.getElementById('setting-theme-color').value = themeColor; |
|
|
| |
| const themePicker = document.getElementById('theme-color-picker'); |
| if (themePicker) { |
| themePicker.querySelectorAll('.theme-option').forEach(opt => { |
| opt.classList.remove('selected'); |
| if (opt.dataset.theme === themeColor) { |
| opt.classList.add('selected'); |
| } |
| }); |
| } |
|
|
| |
| const status = document.getElementById('settingsStatus'); |
| status.className = 'settings-status'; |
| status.textContent = ''; |
| } |
|
|
| async function saveSettings() { |
| |
| const agentModels = {}; |
| for (const type of Object.keys(AGENT_REGISTRY)) { |
| agentModels[type] = document.getElementById(`setting-agent-${type}`)?.value || ''; |
| } |
| const researchSubAgentModel = document.getElementById('setting-research-sub-agent-model')?.value || ''; |
|
|
| |
| const e2bKey = document.getElementById('setting-e2b-key').value.trim(); |
| const serperKey = document.getElementById('setting-serper-key').value.trim(); |
| const hfToken = document.getElementById('setting-hf-token').value.trim(); |
| const imageGenModel = document.getElementById('setting-image-gen-model')?.value || ''; |
| const imageEditModel = document.getElementById('setting-image-edit-model')?.value || ''; |
| const researchParallelWorkers = document.getElementById('setting-research-parallel-workers').value.trim(); |
| const researchMaxWebsites = document.getElementById('setting-research-max-websites').value.trim(); |
| const themeColor = document.getElementById('setting-theme-color').value || 'forest'; |
|
|
| |
| if (Object.keys(settings.providers || {}).length === 0) { |
| showSettingsStatus('Please add at least one provider', 'error'); |
| return; |
| } |
|
|
| if (Object.keys(settings.models || {}).length === 0) { |
| showSettingsStatus('Please add at least one model', 'error'); |
| return; |
| } |
|
|
| |
| settings.agents = agentModels; |
| settings.e2bKey = e2bKey; |
| settings.serperKey = serperKey; |
| settings.hfToken = hfToken; |
| settings.imageGenModel = imageGenModel; |
| settings.imageEditModel = imageEditModel; |
| settings.researchSubAgentModel = researchSubAgentModel; |
| settings.researchParallelWorkers = researchParallelWorkers ? parseInt(researchParallelWorkers) : null; |
| settings.researchMaxWebsites = researchMaxWebsites ? parseInt(researchMaxWebsites) : null; |
| settings.themeColor = themeColor; |
| settings.settingsVersion = 2; |
|
|
| |
| try { |
| const response = await apiFetch('/api/settings', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify(settings) |
| }); |
| if (response.ok) { |
| console.log('Settings saved to file:', settings); |
| } else { |
| console.error('Failed to save settings to file, falling back to localStorage'); |
| localStorage.setItem('agentui_settings', JSON.stringify(settings)); |
| } |
| } catch (e) { |
| console.error('Could not save settings to backend, falling back to localStorage:', e); |
| localStorage.setItem('agentui_settings', JSON.stringify(settings)); |
| } |
|
|
| |
| applyTheme(themeColor); |
|
|
| |
| showSettingsStatus('Settings saved successfully', 'success'); |
|
|
| |
| setTimeout(() => { |
| const settingsPanel = document.getElementById('settingsPanel'); |
| const settingsBtn = document.getElementById('settingsBtn'); |
| const appContainer = document.querySelector('.app-container'); |
| if (settingsPanel) settingsPanel.classList.remove('active'); |
| if (settingsBtn) settingsBtn.classList.remove('active'); |
| if (appContainer) appContainer.classList.remove('panel-open'); |
| }, 1000); |
| } |
|
|
| function showSettingsStatus(message, type) { |
| const status = document.getElementById('settingsStatus'); |
| status.textContent = message; |
| status.className = `settings-status ${type}`; |
| } |
|
|
| |
| |
| const lightSurface = { |
| bgPrimary: '#ffffff', |
| bgSecondary: '#f5f5f5', |
| bgTertiary: '#fafafa', |
| bgInput: '#ffffff', |
| bgHover: '#f0f0f0', |
| bgCard: '#ffffff', |
| textPrimary: '#1a1a1a', |
| textSecondary: '#666666', |
| textMuted: '#999999', |
| borderPrimary: '#e0e0e0', |
| borderSubtle: '#f0f0f0' |
| }; |
|
|
| const themeColors = { |
| forest: { |
| border: '#1b5e20', bg: '#e8f5e9', hoverBg: '#c8e6c9', |
| accent: '#1b5e20', accentRgb: '27, 94, 32', |
| ...lightSurface |
| }, |
| sapphire: { |
| border: '#0d47a1', bg: '#e3f2fd', hoverBg: '#bbdefb', |
| accent: '#0d47a1', accentRgb: '13, 71, 161', |
| ...lightSurface |
| }, |
| ocean: { |
| border: '#00796b', bg: '#e0f2f1', hoverBg: '#b2dfdb', |
| accent: '#004d40', accentRgb: '0, 77, 64', |
| ...lightSurface |
| }, |
| midnight: { |
| border: '#283593', bg: '#e8eaf6', hoverBg: '#c5cae9', |
| accent: '#1a237e', accentRgb: '26, 35, 126', |
| ...lightSurface |
| }, |
| steel: { |
| border: '#455a64', bg: '#eceff1', hoverBg: '#cfd8dc', |
| accent: '#263238', accentRgb: '38, 50, 56', |
| ...lightSurface |
| }, |
| depths: { |
| border: '#01579b', bg: '#e3f2fd', hoverBg: '#bbdefb', |
| accent: '#01579b', accentRgb: '1, 87, 155', |
| ...lightSurface |
| }, |
| ember: { |
| border: '#b71c1c', bg: '#fbe9e7', hoverBg: '#ffccbc', |
| accent: '#b71c1c', accentRgb: '183, 28, 28', |
| ...lightSurface |
| }, |
| noir: { |
| border: '#888888', bg: '#1a1a1a', hoverBg: '#2a2a2a', |
| accent: '#999999', accentRgb: '153, 153, 153', |
| bgPrimary: '#111111', |
| bgSecondary: '#1a1a1a', |
| bgTertiary: '#0d0d0d', |
| bgInput: '#0d0d0d', |
| bgHover: '#2a2a2a', |
| bgCard: '#1a1a1a', |
| textPrimary: '#e0e0e0', |
| textSecondary: '#999999', |
| textMuted: '#666666', |
| borderPrimary: '#333333', |
| borderSubtle: '#222222' |
| }, |
| eclipse: { |
| border: '#5c9eff', bg: '#0d1520', hoverBg: '#162030', |
| accent: '#5c9eff', accentRgb: '92, 158, 255', |
| bgPrimary: '#0b1118', |
| bgSecondary: '#111a25', |
| bgTertiary: '#080e14', |
| bgInput: '#080e14', |
| bgHover: '#1a2840', |
| bgCard: '#111a25', |
| textPrimary: '#d0d8e8', |
| textSecondary: '#7088a8', |
| textMuted: '#4a6080', |
| borderPrimary: '#1e2e45', |
| borderSubtle: '#151f30' |
| }, |
| terminal: { |
| border: '#00cc00', bg: '#0a1a0a', hoverBg: '#0d260d', |
| accent: '#00cc00', accentRgb: '0, 204, 0', |
| bgPrimary: '#0a0a0a', |
| bgSecondary: '#0d1a0d', |
| bgTertiary: '#050505', |
| bgInput: '#050505', |
| bgHover: '#1a3a1a', |
| bgCard: '#0d1a0d', |
| textPrimary: '#00cc00', |
| textSecondary: '#009900', |
| textMuted: '#007700', |
| borderPrimary: '#1a3a1a', |
| borderSubtle: '#0d1a0d' |
| } |
| }; |
|
|
| function applyTheme(themeName) { |
| const theme = themeColors[themeName] || themeColors.forest; |
| const root = document.documentElement; |
|
|
| |
| root.style.setProperty('--theme-border', theme.border); |
| root.style.setProperty('--theme-bg', theme.bg); |
| root.style.setProperty('--theme-hover-bg', theme.hoverBg); |
| root.style.setProperty('--theme-accent', theme.accent); |
| root.style.setProperty('--theme-accent-rgb', theme.accentRgb); |
|
|
| |
| root.style.setProperty('--bg-primary', theme.bgPrimary); |
| root.style.setProperty('--bg-secondary', theme.bgSecondary); |
| root.style.setProperty('--bg-tertiary', theme.bgTertiary); |
| root.style.setProperty('--bg-input', theme.bgInput); |
| root.style.setProperty('--bg-hover', theme.bgHover); |
| root.style.setProperty('--bg-card', theme.bgCard); |
| root.style.setProperty('--text-primary', theme.textPrimary); |
| root.style.setProperty('--text-secondary', theme.textSecondary); |
| root.style.setProperty('--text-muted', theme.textMuted); |
| root.style.setProperty('--border-primary', theme.borderPrimary); |
| root.style.setProperty('--border-subtle', theme.borderSubtle); |
|
|
| |
| document.body.setAttribute('data-theme', themeName); |
| } |
|
|
| |
| function getSettings() { |
| return settings; |
| } |
|
|
| |
| |
| function resolveModelConfig(agentType) { |
| const modelId = settings.agents?.[agentType]; |
| if (!modelId) return null; |
|
|
| const model = settings.models?.[modelId]; |
| if (!model) return null; |
|
|
| const provider = settings.providers?.[model.providerId]; |
| if (!provider) return null; |
|
|
| return { |
| endpoint: provider.endpoint, |
| token: provider.token, |
| model: model.modelId, |
| extraParams: model.extraParams || null, |
| multimodal: !!model.multimodal |
| }; |
| } |
|
|
| |
| function getDefaultModelConfig() { |
| const modelIds = Object.keys(settings.models || {}); |
| if (modelIds.length === 0) return null; |
|
|
| const modelId = modelIds[0]; |
| const model = settings.models[modelId]; |
| const provider = settings.providers?.[model.providerId]; |
| if (!provider) return null; |
|
|
| return { |
| endpoint: provider.endpoint, |
| token: provider.token, |
| model: model.modelId, |
| extraParams: model.extraParams || null, |
| multimodal: !!model.multimodal |
| }; |
| } |
|
|
| |
| function getFrontendContext() { |
| const currentThemeName = settings.themeColor || 'forest'; |
| const theme = themeColors[currentThemeName]; |
|
|
| return { |
| theme: theme ? { |
| name: currentThemeName, |
| accent: theme.accent, |
| bg: theme.bg, |
| border: theme.border, |
| bgPrimary: theme.bgPrimary, |
| bgSecondary: theme.bgSecondary, |
| textPrimary: theme.textPrimary, |
| textSecondary: theme.textSecondary |
| } : null, |
| open_agents: getOpenAgentTypes() |
| }; |
| } |
|
|
| |
| function getOpenAgentTypes() { |
| const tabs = document.querySelectorAll('.tab[data-tab-id]'); |
| const types = []; |
| tabs.forEach(tab => { |
| const tabId = tab.dataset.tabId; |
| if (tabId === '0') { |
| types.push('command'); |
| } else { |
| const content = document.querySelector(`[data-content-id="${tabId}"]`); |
| if (content) { |
| const chatContainer = content.querySelector('.chat-container'); |
| if (chatContainer && chatContainer.dataset.agentType) { |
| types.push(chatContainer.dataset.agentType); |
| } |
| } |
| } |
| }); |
| return types; |
| } |
|
|
| |
| async function startSandbox(tabId) { |
| const currentSettings = getSettings(); |
| const backendEndpoint = '/api'; |
|
|
| if (!currentSettings.e2bKey) { |
| console.log('No E2B key configured, skipping sandbox start'); |
| return; |
| } |
|
|
| |
| const uniqueId = `code-${tabId}`; |
| const chatContainer = document.getElementById(`messages-${uniqueId}`); |
| if (chatContainer) { |
| const statusMsg = document.createElement('div'); |
| statusMsg.className = 'system-message'; |
| statusMsg.innerHTML = '<em>⚙️ Starting sandbox...</em>'; |
| chatContainer.appendChild(statusMsg); |
| } |
|
|
| try { |
| const response = await apiFetch(`${backendEndpoint}/sandbox/start`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| session_id: tabId.toString(), |
| e2b_key: currentSettings.e2bKey |
| }) |
| }); |
|
|
| const result = await response.json(); |
|
|
| |
| if (chatContainer) { |
| const statusMsg = chatContainer.querySelector('.system-message'); |
| if (statusMsg) { |
| if (result.success) { |
| |
| statusMsg.remove(); |
| } else { |
| statusMsg.innerHTML = `<em>⚠ Sandbox error: ${result.error}</em>`; |
| statusMsg.style.color = '#c62828'; |
| } |
| } |
| } |
| } catch (error) { |
| console.error('Failed to start sandbox:', error); |
| if (chatContainer) { |
| const statusMsg = chatContainer.querySelector('.system-message'); |
| if (statusMsg) { |
| statusMsg.innerHTML = `<em>⚠ Failed to start sandbox: ${error.message}</em>`; |
| statusMsg.style.color = '#c62828'; |
| } |
| } |
| } |
| } |
|
|
| async function stopSandbox(tabId) { |
| const backendEndpoint = '/api'; |
|
|
| try { |
| await apiFetch(`${backendEndpoint}/sandbox/stop`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| session_id: tabId.toString() |
| }) |
| }); |
| } catch (error) { |
| console.error('Failed to stop sandbox:', error); |
| } |
| } |
|
|
| |
| function openImageModal(src) { |
| |
| let modal = document.getElementById('imageModal'); |
| if (!modal) { |
| modal = document.createElement('div'); |
| modal.id = 'imageModal'; |
| modal.style.cssText = ` |
| display: none; |
| position: fixed; |
| z-index: 10000; |
| left: 0; |
| top: 0; |
| width: 100%; |
| height: 100%; |
| background-color: rgba(0, 0, 0, 0.9); |
| cursor: pointer; |
| `; |
| modal.onclick = function() { |
| modal.style.display = 'none'; |
| }; |
|
|
| const img = document.createElement('img'); |
| img.id = 'imageModalContent'; |
| img.style.cssText = ` |
| margin: auto; |
| display: block; |
| max-width: 95%; |
| max-height: 95%; |
| position: absolute; |
| top: 50%; |
| left: 50%; |
| transform: translate(-50%, -50%); |
| `; |
| modal.appendChild(img); |
| document.body.appendChild(modal); |
| } |
|
|
| |
| const modalImg = document.getElementById('imageModalContent'); |
| modalImg.src = src; |
| modal.style.display = 'block'; |
| } |
|
|
| |
|
|
| const debugPanel = document.getElementById('debugPanel'); |
| const debugBtn = document.getElementById('debugBtn'); |
| const debugClose = document.getElementById('debugClose'); |
| const debugContent = document.getElementById('debugContent'); |
|
|
| |
| if (debugBtn) { |
| debugBtn.addEventListener('click', () => { |
| const isOpening = !debugPanel.classList.contains('active'); |
|
|
| |
| closeAllPanels(); |
|
|
| if (isOpening) { |
| debugPanel.classList.add('active'); |
| debugBtn.classList.add('active'); |
| appContainer.classList.add('panel-open'); |
| loadDebugMessages(); |
| } |
| }); |
| } |
|
|
| |
| if (debugClose) { |
| debugClose.addEventListener('click', () => { |
| debugPanel.classList.remove('active'); |
| debugBtn.classList.remove('active'); |
| appContainer.classList.remove('panel-open'); |
| }); |
| } |
|
|
|
|
| |
| function formatDebugJson(obj) { |
| |
| |
| |
| |
| |
| const images = []; |
| const json = JSON.stringify(obj, null, 2); |
| const placeholder = json.replace( |
| /"(data:image\/[^;]+;base64,)([A-Za-z0-9+/=\n]{200,})"/g, |
| (match, prefix, b64) => { |
| const idx = images.length; |
| const sizeKB = (b64.length * 0.75 / 1024).toFixed(1); |
| images.push(prefix + b64); |
| return `"__DEBUG_IMG_${idx}_${sizeKB}KB__"`; |
| } |
| ); |
| |
| let html = escapeHtml(placeholder); |
| |
| html = html.replace(/__DEBUG_IMG_(\d+)_([\d.]+KB)__/g, (match, idx, size) => { |
| const src = images[parseInt(idx)]; |
| return `<span class="debug-image-placeholder" onmouseenter="this.querySelector('.debug-image-tooltip').style.display='block'" onmouseleave="this.querySelector('.debug-image-tooltip').style.display='none'">[image ${size}]<span class="debug-image-tooltip"><img src="${src}"></span></span>`; |
| }); |
| return html; |
| } |
|
|
| function loadDebugMessages() { |
| const calls = debugHistory[activeTabId] || []; |
|
|
| if (calls.length === 0) { |
| debugContent.innerHTML = '<div style="padding: 10px; color: var(--text-secondary);">No LLM calls recorded yet.<br><br>Send a message in this tab to see the call history here.</div>'; |
| return; |
| } |
|
|
| debugContent.innerHTML = calls.map((call, i) => { |
| const isLast = i === calls.length - 1; |
| const arrow = isLast ? '▼' : '▶'; |
| const display = isLast ? 'block' : 'none'; |
| const msgCount = call.input ? call.input.length : 0; |
|
|
| const inputHtml = call.input ? formatDebugJson(call.input) : '<em>No input</em>'; |
|
|
| let outputHtml; |
| if (call.error) { |
| outputHtml = `<span style="color: #d32f2f;">${escapeHtml(call.error)}</span>`; |
| } else if (call.output) { |
| outputHtml = formatDebugJson(call.output); |
| } else { |
| outputHtml = '<em>Pending...</em>'; |
| } |
|
|
| return `<div class="debug-call-item${isLast ? ' expanded' : ''}" id="callitem-${i}"><div class="debug-call-header" onclick="toggleDebugCall(${i})"><span class="debug-call-arrow" id="arrow-${i}">${arrow}</span><span class="debug-call-title">Call #${i + 1}</span><span class="debug-call-time">${call.timestamp}</span></div><div class="debug-call-content" id="call-${i}" style="display: ${display};"><div class="debug-section-label">INPUT (${msgCount} messages)</div><pre>${inputHtml}</pre><div class="debug-section-label">OUTPUT</div><pre>${outputHtml}</pre></div></div>`; |
| }).join(''); |
| } |
|
|
| |
| window.toggleDebugCall = function(index) { |
| const content = document.getElementById(`call-${index}`); |
| const arrow = document.getElementById(`arrow-${index}`); |
| const item = document.getElementById(`callitem-${index}`); |
| if (content.style.display === 'none') { |
| content.style.display = 'block'; |
| arrow.textContent = '▼'; |
| item.classList.add('expanded'); |
| } else { |
| content.style.display = 'none'; |
| arrow.textContent = '▶'; |
| item.classList.remove('expanded'); |
| } |
| } |
|
|
| |
|
|
| const settingsPanel = document.getElementById('settingsPanel'); |
| const settingsPanelBody = document.getElementById('settingsPanelBody'); |
| const settingsPanelClose = document.getElementById('settingsPanelClose'); |
| const settingsBtn = document.getElementById('settingsBtn'); |
| const appContainer = document.querySelector('.app-container'); |
|
|
|
|
| |
| if (settingsBtn) { |
| settingsBtn.addEventListener('click', () => { |
| closeAllPanels(); |
| openSettings(); |
| settingsPanel.classList.add('active'); |
| settingsBtn.classList.add('active'); |
| appContainer.classList.add('panel-open'); |
| }); |
| } |
|
|
| |
| if (settingsPanelClose) { |
| settingsPanelClose.addEventListener('click', () => { |
| settingsPanel.classList.remove('active'); |
| settingsBtn.classList.remove('active'); |
| appContainer.classList.remove('panel-open'); |
| }); |
| } |
|
|
|
|
| |
|
|
| const filesPanel = document.getElementById('filesPanel'); |
| const filesPanelClose = document.getElementById('filesPanelClose'); |
| const filesBtn = document.getElementById('filesBtn'); |
| const fileTree = document.getElementById('fileTree'); |
| const showHiddenFiles = document.getElementById('showHiddenFiles'); |
| const filesRefresh = document.getElementById('filesRefresh'); |
| const filesUpload = document.getElementById('filesUpload'); |
|
|
| |
| let expandedPaths = new Set(); |
| let filesRoot = ''; |
|
|
| |
| async function loadFileTree() { |
| const showHidden = showHiddenFiles?.checked || false; |
| try { |
| const response = await apiFetch(`/api/files?show_hidden=${showHidden}`); |
| if (response.ok) { |
| const data = await response.json(); |
| filesRoot = data.root; |
| renderFileTree(data.tree, fileTree, data.root); |
| } else { |
| fileTree.innerHTML = '<div class="files-loading">Failed to load files</div>'; |
| } |
| } catch (e) { |
| console.error('Failed to load file tree:', e); |
| fileTree.innerHTML = '<div class="files-loading">Failed to load files</div>'; |
| } |
| } |
|
|
| |
| function renderFileTree(tree, container, rootPath) { |
| container.innerHTML = ''; |
| const rootWrapper = document.createElement('div'); |
| rootWrapper.className = 'file-tree-root'; |
|
|
| |
| const header = document.createElement('div'); |
| header.className = 'file-tree-header'; |
| const folderName = rootPath.split('/').pop() || rootPath; |
| header.textContent = './' + folderName; |
| rootWrapper.appendChild(header); |
|
|
| |
| const treeContainer = document.createElement('div'); |
| treeContainer.className = 'file-tree-container'; |
| renderTreeItems(tree, treeContainer); |
| rootWrapper.appendChild(treeContainer); |
|
|
| container.appendChild(rootWrapper); |
| } |
|
|
| function renderTreeItems(tree, container) { |
| const len = tree.length; |
| for (let i = 0; i < len; i++) { |
| const item = tree[i]; |
| const isLast = (i === len - 1); |
|
|
| const itemEl = document.createElement('div'); |
| itemEl.className = `file-tree-item ${item.type}`; |
| if (isLast) itemEl.classList.add('last'); |
| itemEl.dataset.path = item.path; |
|
|
| |
| const wasExpanded = expandedPaths.has(item.path); |
|
|
| |
| const lineEl = document.createElement('div'); |
| lineEl.className = 'file-tree-line'; |
| lineEl.draggable = true; |
|
|
| |
| const icon = item.type === 'folder' ? (wasExpanded ? '▼' : '▶') : ''; |
| const actionBtn = item.type === 'file' |
| ? '<button class="file-tree-action-btn file-download-btn" title="Download">↓</button>' |
| : '<button class="file-tree-action-btn file-upload-btn" title="Upload file here">+</button>'; |
| lineEl.innerHTML = ` |
| <span class="file-tree-icon">${icon}</span> |
| <span class="file-tree-name">${item.name}</span> |
| <span class="file-tree-actions">${actionBtn}</span> |
| `; |
| itemEl.appendChild(lineEl); |
|
|
| |
| const downloadBtn = lineEl.querySelector('.file-download-btn'); |
| if (downloadBtn) { |
| downloadBtn.addEventListener('click', (e) => { |
| e.stopPropagation(); |
| window.open(`/api/files/download?path=${encodeURIComponent(item.path)}${SESSION_ID ? '&session_id=' + encodeURIComponent(SESSION_ID) : ''}`, '_blank'); |
| }); |
| } |
|
|
| |
| const uploadBtn = lineEl.querySelector('.file-upload-btn'); |
| if (uploadBtn) { |
| uploadBtn.addEventListener('click', (e) => { |
| e.stopPropagation(); |
| const input = document.createElement('input'); |
| input.type = 'file'; |
| input.addEventListener('change', async () => { |
| if (!input.files.length) return; |
| const formData = new FormData(); |
| formData.append('file', input.files[0]); |
| try { |
| await apiFetch(`/api/files/upload?folder=${encodeURIComponent(item.path)}`, { |
| method: 'POST', |
| body: formData |
| }); |
| loadFileTree(); |
| } catch (err) { |
| console.error('Upload failed:', err); |
| } |
| }); |
| input.click(); |
| }); |
| } |
|
|
| container.appendChild(itemEl); |
|
|
| |
| if (item.type === 'folder' && item.children && item.children.length > 0) { |
| const childrenContainer = document.createElement('div'); |
| childrenContainer.className = 'file-tree-children'; |
| if (wasExpanded) { |
| childrenContainer.classList.add('expanded'); |
| itemEl.classList.add('expanded'); |
| } |
| renderTreeItems(item.children, childrenContainer); |
| itemEl.appendChild(childrenContainer); |
|
|
| |
| let clickTimer = null; |
| lineEl.addEventListener('click', (e) => { |
| e.stopPropagation(); |
| if (clickTimer) { |
| |
| clearTimeout(clickTimer); |
| clickTimer = null; |
| const isExpanded = itemEl.classList.toggle('expanded'); |
| childrenContainer.classList.toggle('expanded'); |
| const iconEl = lineEl.querySelector('.file-tree-icon'); |
| if (iconEl) iconEl.textContent = isExpanded ? '▼' : '▶'; |
| if (isExpanded) { |
| expandedPaths.add(item.path); |
| } else { |
| expandedPaths.delete(item.path); |
| } |
| } else { |
| |
| clickTimer = setTimeout(() => { |
| clickTimer = null; |
| insertPathIntoInput('./' + item.path); |
| showClickFeedback(lineEl); |
| }, 250); |
| } |
| }); |
| } else if (item.type === 'file') { |
| |
| lineEl.addEventListener('click', (e) => { |
| e.stopPropagation(); |
| insertPathIntoInput('./' + item.path); |
| showClickFeedback(lineEl); |
| }); |
| } |
|
|
| |
| lineEl.addEventListener('dragstart', (e) => { |
| e.dataTransfer.setData('text/plain', './' + item.path); |
| e.dataTransfer.setData('application/x-file-path', './' + item.path); |
| e.dataTransfer.effectAllowed = 'copy'; |
| }); |
| } |
| } |
|
|
| |
| function insertPathIntoInput(path) { |
| const inputId = activeTabId === 0 ? 'input-command' : `input-${activeTabId}`; |
| const inputEl = document.getElementById(inputId); |
| if (inputEl) { |
| const start = inputEl.selectionStart; |
| const end = inputEl.selectionEnd; |
| const text = inputEl.value; |
| |
| const formattedPath = '`' + path + '` '; |
| inputEl.value = text.substring(0, start) + formattedPath + text.substring(end); |
| inputEl.focus(); |
| inputEl.selectionStart = inputEl.selectionEnd = start + formattedPath.length; |
| } |
| } |
|
|
| |
| async function linkifyFilePaths(container) { |
| |
| const codeEls = [...container.querySelectorAll('code')].filter(c => !c.closest('pre')); |
| if (codeEls.length === 0) return; |
|
|
| |
| const candidates = new Map(); |
| for (const code of codeEls) { |
| const text = code.textContent.trim(); |
| if (!text || text.includes(' ') || text.length > 200) continue; |
| |
| if (!text.includes('.') && !text.includes('/')) continue; |
| const normalized = text.replace(/^\.\//, ''); |
| if (!candidates.has(normalized)) candidates.set(normalized, []); |
| candidates.get(normalized).push(code); |
| } |
| if (candidates.size === 0) return; |
|
|
| |
| try { |
| const resp = await apiFetch('/api/files/check', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ paths: [...candidates.keys()] }) |
| }); |
| if (!resp.ok) return; |
| const { existing } = await resp.json(); |
|
|
| for (const path of existing) { |
| for (const code of candidates.get(path) || []) { |
| if (code.closest('.file-path-link')) continue; |
| const link = document.createElement('a'); |
| link.className = 'file-path-link'; |
| link.href = '#'; |
| link.title = 'Open in file explorer'; |
| link.addEventListener('click', (e) => { |
| e.preventDefault(); |
| navigateToFileInExplorer(path); |
| }); |
| code.parentNode.insertBefore(link, code); |
| link.appendChild(code); |
| } |
| } |
| } catch (e) { |
| |
| } |
| } |
|
|
| |
| function showClickFeedback(el) { |
| const originalColor = el.style.color; |
| el.style.color = 'var(--theme-accent)'; |
| setTimeout(() => { |
| el.style.color = originalColor; |
| }, 300); |
| } |
|
|
| |
| function navigateToFileInExplorer(path) { |
| let relPath = path.replace(/^\.\//, ''); |
|
|
| |
| if (!filesPanel.classList.contains('active')) { |
| filesBtn.click(); |
| } |
|
|
| |
| setTimeout(() => { |
| const segments = relPath.split('/'); |
| let currentPath = ''; |
| for (let i = 0; i < segments.length - 1; i++) { |
| currentPath += (i > 0 ? '/' : '') + segments[i]; |
| const folderItem = fileTree.querySelector(`.file-tree-item[data-path="${currentPath}"]`); |
| if (folderItem && !folderItem.classList.contains('expanded')) { |
| folderItem.classList.add('expanded'); |
| const children = folderItem.querySelector('.file-tree-children'); |
| if (children) children.classList.add('expanded'); |
| const icon = folderItem.querySelector('.file-tree-icon'); |
| if (icon) icon.textContent = '▼'; |
| expandedPaths.add(currentPath); |
| } |
| } |
| const targetItem = fileTree.querySelector(`.file-tree-item[data-path="${relPath}"]`); |
| if (targetItem) { |
| targetItem.scrollIntoView({ behavior: 'smooth', block: 'center' }); |
| const line = targetItem.querySelector('.file-tree-line'); |
| if (line) { |
| line.classList.add('file-tree-highlight'); |
| setTimeout(() => line.classList.remove('file-tree-highlight'), 2000); |
| } |
| } |
| }, 500); |
| } |
|
|
| |
| if (filesBtn) { |
| filesBtn.addEventListener('click', () => { |
| const isOpening = !filesPanel.classList.contains('active'); |
| closeAllPanels(); |
|
|
| if (isOpening) { |
| filesPanel.classList.add('active'); |
| filesBtn.classList.add('active'); |
| appContainer.classList.add('files-panel-open'); |
| loadFileTree(); |
| } |
| }); |
| } |
|
|
| |
| if (filesPanelClose) { |
| filesPanelClose.addEventListener('click', () => { |
| filesPanel.classList.remove('active'); |
| filesBtn.classList.remove('active'); |
| appContainer.classList.remove('files-panel-open'); |
| }); |
| } |
|
|
| |
| if (filesRefresh) { |
| filesRefresh.addEventListener('click', () => { |
| loadFileTree(); |
| }); |
| } |
|
|
| |
| if (filesUpload) { |
| filesUpload.addEventListener('click', () => { |
| const input = document.createElement('input'); |
| input.type = 'file'; |
| input.addEventListener('change', async () => { |
| if (!input.files.length) return; |
| const formData = new FormData(); |
| formData.append('file', input.files[0]); |
| try { |
| await apiFetch('/api/files/upload?folder=', { method: 'POST', body: formData }); |
| loadFileTree(); |
| } catch (err) { |
| console.error('Upload failed:', err); |
| } |
| }); |
| input.click(); |
| }); |
| } |
|
|
| |
| if (showHiddenFiles) { |
| showHiddenFiles.addEventListener('change', () => { |
| loadFileTree(); |
| }); |
| } |
|
|
| |
| if (fileTree) { |
| let dragOverFolder = null; |
|
|
| fileTree.addEventListener('dragover', (e) => { |
| |
| if (!e.dataTransfer.types.includes('Files')) return; |
| e.preventDefault(); |
| e.dataTransfer.dropEffect = 'copy'; |
|
|
| |
| const folderItem = e.target.closest('.file-tree-item.folder'); |
| if (folderItem) { |
| if (dragOverFolder !== folderItem) { |
| if (dragOverFolder) dragOverFolder.classList.remove('drag-over'); |
| fileTree.classList.remove('drag-over-root'); |
| folderItem.classList.add('drag-over'); |
| dragOverFolder = folderItem; |
| } |
| } else { |
| if (dragOverFolder) { dragOverFolder.classList.remove('drag-over'); dragOverFolder = null; } |
| fileTree.classList.add('drag-over-root'); |
| } |
| }); |
|
|
| fileTree.addEventListener('dragleave', (e) => { |
| |
| if (!fileTree.contains(e.relatedTarget)) { |
| if (dragOverFolder) { dragOverFolder.classList.remove('drag-over'); dragOverFolder = null; } |
| fileTree.classList.remove('drag-over-root'); |
| } |
| }); |
|
|
| fileTree.addEventListener('drop', async (e) => { |
| if (!e.dataTransfer.files.length) return; |
| e.preventDefault(); |
|
|
| |
| const folderItem = e.target.closest('.file-tree-item.folder'); |
| const folder = folderItem ? folderItem.dataset.path : ''; |
|
|
| |
| if (dragOverFolder) { dragOverFolder.classList.remove('drag-over'); dragOverFolder = null; } |
| fileTree.classList.remove('drag-over-root'); |
|
|
| |
| for (const file of e.dataTransfer.files) { |
| const formData = new FormData(); |
| formData.append('file', file); |
| try { |
| await apiFetch(`/api/files/upload?folder=${encodeURIComponent(folder)}`, { method: 'POST', body: formData }); |
| } catch (err) { |
| console.error('Upload failed:', err); |
| } |
| } |
| loadFileTree(); |
| }); |
| } |
|
|
| |
| const sessionsPanel = document.getElementById('sessionsPanel'); |
| const sessionsPanelClose = document.getElementById('sessionsPanelClose'); |
| const sessionsBtn = document.getElementById('sessionsBtn'); |
|
|
| if (sessionsBtn && sessionsPanel) { |
| sessionsBtn.addEventListener('click', () => { |
| const isOpening = !sessionsPanel.classList.contains('active'); |
| closeAllPanels(); |
|
|
| if (isOpening) { |
| sessionsPanel.classList.add('active'); |
| sessionsBtn.classList.add('active'); |
| appContainer.classList.add('sessions-panel-open'); |
| refreshSessionsList(); |
| } |
| }); |
| } |
|
|
| if (sessionsPanelClose) { |
| sessionsPanelClose.addEventListener('click', () => { |
| sessionsPanel.classList.remove('active'); |
| sessionsBtn.classList.remove('active'); |
| appContainer.classList.remove('sessions-panel-open'); |
| }); |
| } |
|
|