vibesurf 0.1.9a6__py3-none-any.whl → 0.1.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. vibe_surf/_version.py +2 -2
  2. vibe_surf/agents/vibe_surf_agent.py +25 -15
  3. vibe_surf/backend/api/browser.py +66 -0
  4. vibe_surf/backend/api/task.py +2 -1
  5. vibe_surf/backend/main.py +76 -1
  6. vibe_surf/backend/shared_state.py +2 -0
  7. vibe_surf/browser/agent_browser_session.py +312 -62
  8. vibe_surf/browser/browser_manager.py +57 -92
  9. vibe_surf/browser/watchdogs/dom_watchdog.py +43 -43
  10. vibe_surf/chrome_extension/background.js +84 -0
  11. vibe_surf/chrome_extension/manifest.json +3 -1
  12. vibe_surf/chrome_extension/scripts/file-manager.js +526 -0
  13. vibe_surf/chrome_extension/scripts/history-manager.js +658 -0
  14. vibe_surf/chrome_extension/scripts/modal-manager.js +487 -0
  15. vibe_surf/chrome_extension/scripts/session-manager.js +31 -8
  16. vibe_surf/chrome_extension/scripts/settings-manager.js +1214 -0
  17. vibe_surf/chrome_extension/scripts/ui-manager.js +770 -3186
  18. vibe_surf/chrome_extension/sidepanel.html +27 -4
  19. vibe_surf/chrome_extension/styles/activity.css +574 -0
  20. vibe_surf/chrome_extension/styles/base.css +76 -0
  21. vibe_surf/chrome_extension/styles/history-modal.css +791 -0
  22. vibe_surf/chrome_extension/styles/input.css +429 -0
  23. vibe_surf/chrome_extension/styles/layout.css +186 -0
  24. vibe_surf/chrome_extension/styles/responsive.css +454 -0
  25. vibe_surf/chrome_extension/styles/settings-environment.css +165 -0
  26. vibe_surf/chrome_extension/styles/settings-forms.css +389 -0
  27. vibe_surf/chrome_extension/styles/settings-modal.css +141 -0
  28. vibe_surf/chrome_extension/styles/settings-profiles.css +244 -0
  29. vibe_surf/chrome_extension/styles/settings-responsive.css +144 -0
  30. vibe_surf/chrome_extension/styles/settings-utilities.css +25 -0
  31. vibe_surf/chrome_extension/styles/variables.css +54 -0
  32. vibe_surf/cli.py +1 -0
  33. vibe_surf/controller/vibesurf_tools.py +0 -2
  34. {vibesurf-0.1.9a6.dist-info → vibesurf-0.1.10.dist-info}/METADATA +18 -2
  35. {vibesurf-0.1.9a6.dist-info → vibesurf-0.1.10.dist-info}/RECORD +39 -23
  36. vibe_surf/chrome_extension/styles/main.css +0 -2338
  37. vibe_surf/chrome_extension/styles/settings.css +0 -1100
  38. {vibesurf-0.1.9a6.dist-info → vibesurf-0.1.10.dist-info}/WHEEL +0 -0
  39. {vibesurf-0.1.9a6.dist-info → vibesurf-0.1.10.dist-info}/entry_points.txt +0 -0
  40. {vibesurf-0.1.9a6.dist-info → vibesurf-0.1.10.dist-info}/licenses/LICENSE +0 -0
  41. {vibesurf-0.1.9a6.dist-info → vibesurf-0.1.10.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1214 @@
1
+ // Settings Manager - Handles settings UI, profiles, and environment variables
2
+ // Manages LLM profiles, MCP profiles, and application settings
3
+
4
+ class VibeSurfSettingsManager {
5
+ constructor(apiClient) {
6
+ this.apiClient = apiClient;
7
+ this.state = {
8
+ llmProfiles: [],
9
+ mcpProfiles: [],
10
+ settings: {},
11
+ currentProfileForm: null
12
+ };
13
+ this.elements = {};
14
+ this.eventListeners = new Map();
15
+
16
+ this.bindElements();
17
+ this.bindEvents();
18
+ }
19
+
20
+ bindElements() {
21
+ this.elements = {
22
+ // Settings Modal
23
+ settingsModal: document.getElementById('settings-modal'),
24
+ settingsTabs: document.querySelectorAll('.settings-tab'),
25
+ settingsTabContents: document.querySelectorAll('.settings-tab-content'),
26
+
27
+ // LLM Profiles
28
+ llmProfilesContainer: document.getElementById('llm-profiles-container'),
29
+ addLlmProfileBtn: document.getElementById('add-llm-profile-btn'),
30
+
31
+ // MCP Profiles
32
+ mcpProfilesContainer: document.getElementById('mcp-profiles-container'),
33
+ addMcpProfileBtn: document.getElementById('add-mcp-profile-btn'),
34
+
35
+ // Profile Form Modal
36
+ profileFormModal: document.getElementById('profile-form-modal'),
37
+ profileFormTitle: document.getElementById('profile-form-title'),
38
+ profileForm: document.getElementById('profile-form'),
39
+ profileFormCancel: document.getElementById('profile-form-cancel'),
40
+ profileFormSubmit: document.getElementById('profile-form-submit'),
41
+ profileFormClose: document.querySelector('.profile-form-close'),
42
+
43
+ // Environment Variables
44
+ envVariablesList: document.getElementById('env-variables-list'),
45
+ saveEnvVarsBtn: document.getElementById('save-env-vars-btn'),
46
+
47
+ // Backend URL
48
+ backendUrl: document.getElementById('backend-url')
49
+ };
50
+ }
51
+
52
+ bindEvents() {
53
+ // Settings modal close button
54
+ const settingsModalClose = this.elements.settingsModal?.querySelector('.modal-close');
55
+ if (settingsModalClose) {
56
+ settingsModalClose.addEventListener('click', this.hideModal.bind(this));
57
+ }
58
+
59
+ // Settings tabs
60
+ this.elements.settingsTabs?.forEach(tab => {
61
+ tab.addEventListener('click', this.handleTabSwitch.bind(this));
62
+ });
63
+
64
+ // Profile management
65
+ this.elements.addLlmProfileBtn?.addEventListener('click', () => this.handleAddProfile('llm'));
66
+ this.elements.addMcpProfileBtn?.addEventListener('click', () => this.handleAddProfile('mcp'));
67
+
68
+ // Profile form modal
69
+ this.elements.profileFormCancel?.addEventListener('click', this.closeProfileForm.bind(this));
70
+ this.elements.profileFormClose?.addEventListener('click', this.closeProfileForm.bind(this));
71
+
72
+ // Profile form submission
73
+ if (this.elements.profileForm) {
74
+ this.elements.profileForm.addEventListener('submit', this.handleProfileFormSubmit.bind(this));
75
+ }
76
+
77
+ if (this.elements.profileFormSubmit) {
78
+ this.elements.profileFormSubmit.addEventListener('click', this.handleProfileFormSubmitClick.bind(this));
79
+ }
80
+
81
+ // Environment variables
82
+ this.elements.saveEnvVarsBtn?.addEventListener('click', this.handleSaveEnvironmentVariables.bind(this));
83
+
84
+ // Backend URL
85
+ this.elements.backendUrl?.addEventListener('change', this.handleBackendUrlChange.bind(this));
86
+
87
+ // Global keyboard shortcuts
88
+ document.addEventListener('keydown', this.handleKeydown.bind(this));
89
+ }
90
+
91
+ handleKeydown(event) {
92
+ // Close settings modal on Escape key
93
+ if (event.key === 'Escape') {
94
+ if (this.elements.settingsModal && !this.elements.settingsModal.classList.contains('hidden')) {
95
+ this.hideModal();
96
+ }
97
+ // Close profile form modal on Escape key
98
+ if (this.elements.profileFormModal && !this.elements.profileFormModal.classList.contains('hidden')) {
99
+ this.closeProfileForm();
100
+ }
101
+ }
102
+ }
103
+
104
+ // Event system for communicating with main UI manager
105
+ on(event, callback) {
106
+ if (!this.eventListeners.has(event)) {
107
+ this.eventListeners.set(event, []);
108
+ }
109
+ this.eventListeners.get(event).push(callback);
110
+ }
111
+
112
+ emit(event, data) {
113
+ if (this.eventListeners.has(event)) {
114
+ this.eventListeners.get(event).forEach(callback => {
115
+ try {
116
+ callback(data);
117
+ } catch (error) {
118
+ console.error(`[SettingsManager] Event callback error for ${event}:`, error);
119
+ }
120
+ });
121
+ }
122
+ }
123
+
124
+ // Settings Tab Management
125
+ handleTabSwitch(event) {
126
+ const clickedTab = event.currentTarget;
127
+ const targetTabId = clickedTab.dataset.tab;
128
+
129
+ // Update tab buttons
130
+ this.elements.settingsTabs?.forEach(tab => {
131
+ tab.classList.remove('active');
132
+ });
133
+ clickedTab.classList.add('active');
134
+
135
+ // Update tab content
136
+ this.elements.settingsTabContents?.forEach(content => {
137
+ content.classList.remove('active');
138
+ });
139
+ const targetContent = document.getElementById(`${targetTabId}-tab`);
140
+ if (targetContent) {
141
+ targetContent.classList.add('active');
142
+ }
143
+ }
144
+
145
+ // Data Loading
146
+ async loadSettingsData() {
147
+ try {
148
+ // Load LLM profiles
149
+ await this.loadLLMProfiles();
150
+
151
+ // Load MCP profiles
152
+ await this.loadMCPProfiles();
153
+
154
+ // Load environment variables
155
+ await this.loadEnvironmentVariables();
156
+
157
+ // Emit event to update LLM profile select dropdown
158
+ this.emit('profilesUpdated', {
159
+ llmProfiles: this.state.llmProfiles,
160
+ mcpProfiles: this.state.mcpProfiles
161
+ });
162
+
163
+ } catch (error) {
164
+ console.error('[SettingsManager] Failed to load settings data:', error);
165
+ this.emit('error', { message: 'Failed to load settings data', error });
166
+ }
167
+ }
168
+
169
+ async loadLLMProfiles() {
170
+ try {
171
+ const response = await this.apiClient.getLLMProfiles(false); // Load all profiles, not just active
172
+ console.log('[SettingsManager] LLM profiles loaded:', response);
173
+
174
+ // Handle different response structures
175
+ let profiles = [];
176
+ if (Array.isArray(response)) {
177
+ profiles = response;
178
+ } else if (response.profiles && Array.isArray(response.profiles)) {
179
+ profiles = response.profiles;
180
+ } else if (response.data && Array.isArray(response.data)) {
181
+ profiles = response.data;
182
+ }
183
+
184
+ this.state.llmProfiles = profiles;
185
+ this.renderLLMProfiles(profiles);
186
+ } catch (error) {
187
+ console.error('[SettingsManager] Failed to load LLM profiles:', error);
188
+ this.state.llmProfiles = [];
189
+ this.renderLLMProfiles([]);
190
+ }
191
+ }
192
+
193
+ async loadMCPProfiles() {
194
+ try {
195
+ const response = await this.apiClient.getMCPProfiles(false); // Load all profiles, not just active
196
+ console.log('[SettingsManager] MCP profiles loaded:', response);
197
+
198
+ // Handle different response structures
199
+ let profiles = [];
200
+ if (Array.isArray(response)) {
201
+ profiles = response;
202
+ } else if (response.profiles && Array.isArray(response.profiles)) {
203
+ profiles = response.profiles;
204
+ } else if (response.data && Array.isArray(response.data)) {
205
+ profiles = response.data;
206
+ }
207
+
208
+ this.state.mcpProfiles = profiles;
209
+ this.renderMCPProfiles(profiles);
210
+ } catch (error) {
211
+ console.error('[SettingsManager] Failed to load MCP profiles:', error);
212
+ this.state.mcpProfiles = [];
213
+ this.renderMCPProfiles([]);
214
+ }
215
+ }
216
+
217
+ async loadEnvironmentVariables() {
218
+ try {
219
+ const response = await this.apiClient.getEnvironmentVariables();
220
+ console.log('[SettingsManager] Environment variables loaded:', response);
221
+ const envVars = response.environments || response || {};
222
+ this.renderEnvironmentVariables(envVars);
223
+ } catch (error) {
224
+ console.error('[SettingsManager] Failed to load environment variables:', error);
225
+ this.renderEnvironmentVariables({});
226
+ }
227
+ }
228
+
229
+ // Profile Management
230
+ async handleAddProfile(type) {
231
+ try {
232
+ this.showProfileForm(type);
233
+ } catch (error) {
234
+ console.error(`[SettingsManager] Failed to show ${type} profile form:`, error);
235
+ this.emit('error', { message: `Failed to show ${type} profile form` });
236
+ }
237
+ }
238
+
239
+ async showProfileForm(type, profile = null) {
240
+ const isEdit = profile !== null;
241
+ const title = isEdit ? `Edit ${type.toUpperCase()} Profile` : `Add ${type.toUpperCase()} Profile`;
242
+
243
+ if (this.elements.profileFormTitle) {
244
+ this.elements.profileFormTitle.textContent = title;
245
+ }
246
+
247
+ // Generate form content based on type
248
+ let formHTML = '';
249
+ if (type === 'llm') {
250
+ formHTML = await this.generateLLMProfileForm(profile);
251
+ } else if (type === 'mcp') {
252
+ formHTML = this.generateMCPProfileForm(profile);
253
+ }
254
+
255
+ if (this.elements.profileForm) {
256
+ this.elements.profileForm.innerHTML = formHTML;
257
+ this.elements.profileForm.dataset.type = type;
258
+ this.elements.profileForm.dataset.mode = isEdit ? 'edit' : 'create';
259
+ if (isEdit && profile) {
260
+ this.elements.profileForm.dataset.profileId = profile.profile_name || profile.mcp_id;
261
+ }
262
+ }
263
+
264
+ // Setup form event listeners
265
+ this.setupProfileFormEvents();
266
+
267
+ // Show modal
268
+ if (this.elements.profileFormModal) {
269
+ this.elements.profileFormModal.classList.remove('hidden');
270
+ }
271
+ }
272
+
273
+ async generateLLMProfileForm(profile = null) {
274
+ // Fetch available providers
275
+ let providers = [];
276
+ try {
277
+ const response = await this.apiClient.getLLMProviders();
278
+ providers = response.providers || response || [];
279
+ } catch (error) {
280
+ console.error('[SettingsManager] Failed to fetch LLM providers:', error);
281
+ }
282
+
283
+ const providersOptions = providers.map(p =>
284
+ `<option value="${p.name}" ${profile?.provider === p.name ? 'selected' : ''}>${p.display_name}</option>`
285
+ ).join('');
286
+
287
+ const selectedProvider = profile?.provider || (providers.length > 0 ? providers[0].name : '');
288
+ const selectedProviderData = providers.find(p => p.name === selectedProvider);
289
+ const models = selectedProviderData?.models || [];
290
+
291
+ return `
292
+ <div class="form-group">
293
+ <label class="form-label required">Profile Name</label>
294
+ <input type="text" name="profile_name" class="form-input" value="${profile?.profile_name || ''}"
295
+ placeholder="Enter a unique name for this profile" required ${profile ? 'readonly' : ''}>
296
+ <div class="form-help">A unique identifier for this LLM configuration</div>
297
+ </div>
298
+
299
+ <div class="form-group">
300
+ <label class="form-label required">Provider</label>
301
+ <select name="provider" class="form-select" required>
302
+ <option value="">Select a provider</option>
303
+ ${providersOptions}
304
+ </select>
305
+ <div class="form-help">Choose your LLM provider (OpenAI, Anthropic, etc.)</div>
306
+ </div>
307
+
308
+ <div class="form-group">
309
+ <label class="form-label required">Model</label>
310
+ <input type="text" name="model" class="form-input model-input" value="${profile?.model || ''}"
311
+ list="model-options" placeholder="Select a model or type custom model name" required
312
+ autocomplete="off">
313
+ <datalist id="model-options">
314
+ ${models.map(model => `<option value="${model}">${model}</option>`).join('')}
315
+ </datalist>
316
+ <div class="form-help">Choose from the list or enter a custom model name</div>
317
+ </div>
318
+
319
+ <div class="form-group api-key-field">
320
+ <label class="form-label required">API Key</label>
321
+ <input type="password" name="api_key" class="form-input api-key-input"
322
+ placeholder="${profile ? 'Leave empty to keep existing key' : 'Enter your API key'}"
323
+ ${profile ? '' : 'required'}>
324
+ <button type="button" class="api-key-toggle" title="Toggle visibility">
325
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
326
+ <path d="M1 12S5 4 12 4S23 12 23 12S19 20 12 20S1 12 1 12Z" stroke="currentColor" stroke-width="2"/>
327
+ <circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2"/>
328
+ </svg>
329
+ </button>
330
+ <div class="form-help">Your provider's API key for authentication</div>
331
+ </div>
332
+
333
+ <div class="form-group">
334
+ <label class="form-label">Base URL</label>
335
+ <input type="url" name="base_url" class="form-input" value="${profile?.base_url || ''}"
336
+ placeholder="https://api.openai.com/v1">
337
+ <div class="form-help">Custom API endpoint (leave empty for provider default)</div>
338
+ </div>
339
+
340
+ <div class="form-group">
341
+ <label class="form-label">Temperature</label>
342
+ <input type="number" name="temperature" class="form-input" value="${profile?.temperature || ''}"
343
+ min="0" max="2" step="0.1" placeholder="0.7">
344
+ <div class="form-help">Controls randomness (0.0-2.0, lower = more focused)</div>
345
+ </div>
346
+
347
+ <div class="form-group">
348
+ <label class="form-label">Max Tokens</label>
349
+ <input type="number" name="max_tokens" class="form-input" value="${profile?.max_tokens || ''}"
350
+ min="1" max="128000" placeholder="4096">
351
+ <div class="form-help">Maximum tokens in the response</div>
352
+ </div>
353
+
354
+ <div class="form-group">
355
+ <label class="form-label">Description</label>
356
+ <textarea name="description" class="form-textarea" placeholder="Optional description for this profile">${profile?.description || ''}</textarea>
357
+ <div class="form-help">Optional description to help identify this profile</div>
358
+ </div>
359
+
360
+ <div class="form-group">
361
+ <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
362
+ <input type="checkbox" name="is_default" ${profile?.is_default ? 'checked' : ''}>
363
+ <span class="form-label" style="margin: 0;">Set as default profile</span>
364
+ </label>
365
+ <div class="form-help">This profile will be selected by default for new tasks</div>
366
+ </div>
367
+ `;
368
+ }
369
+
370
+ generateMCPProfileForm(profile = null) {
371
+ // Convert existing profile to JSON for editing
372
+ let defaultJson = '{\n "command": "npx",\n "args": [\n "-y",\n "@modelcontextprotocol/server-filesystem",\n "/path/to/directory"\n ]\n}';
373
+
374
+ if (profile?.mcp_server_params) {
375
+ try {
376
+ defaultJson = JSON.stringify(profile.mcp_server_params, null, 2);
377
+ } catch (error) {
378
+ console.warn('[SettingsManager] Failed to stringify existing mcp_server_params:', error);
379
+ }
380
+ }
381
+
382
+ return `
383
+ <div class="form-group">
384
+ <label class="form-label required">Display Name</label>
385
+ <input type="text" name="display_name" class="form-input" value="${profile?.display_name || ''}"
386
+ placeholder="Enter a friendly name for this MCP profile" required ${profile ? 'readonly' : ''}>
387
+ <div class="form-help">A user-friendly name for this MCP configuration</div>
388
+ </div>
389
+
390
+ <div class="form-group">
391
+ <label class="form-label required">Server Name</label>
392
+ <input type="text" name="mcp_server_name" class="form-input" value="${profile?.mcp_server_name || ''}"
393
+ placeholder="e.g., filesystem, markitdown, brave-search" required>
394
+ <div class="form-help">The MCP server identifier</div>
395
+ </div>
396
+
397
+ <div class="form-group">
398
+ <label class="form-label required">MCP Server Parameters (JSON)</label>
399
+ <textarea name="mcp_server_params_json" class="form-textarea json-input" rows="8"
400
+ placeholder="Enter JSON configuration for MCP server parameters" required>${defaultJson}</textarea>
401
+ <div class="json-validation-feedback"></div>
402
+ <div class="form-help">
403
+ JSON configuration including command and arguments. Example:
404
+ <br><code>{"command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path"]}</code>
405
+ </div>
406
+ </div>
407
+
408
+ <div class="form-group">
409
+ <label class="form-label">Description</label>
410
+ <textarea name="description" class="form-textarea" placeholder="Optional description for this MCP profile">${profile?.description || ''}</textarea>
411
+ <div class="form-help">Optional description to help identify this profile</div>
412
+ </div>
413
+
414
+ <div class="form-group">
415
+ <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
416
+ <input type="checkbox" name="is_active" ${profile?.is_active !== false ? 'checked' : ''}>
417
+ <span class="form-label" style="margin: 0;">Active</span>
418
+ </label>
419
+ <div class="form-help">Whether this MCP profile is active and available for use</div>
420
+ </div>
421
+ `;
422
+ }
423
+
424
+ setupProfileFormEvents() {
425
+ console.log('[SettingsManager] Setting up profile form events');
426
+
427
+ // Provider change handler for LLM profiles
428
+ const providerSelect = this.elements.profileForm?.querySelector('select[name="provider"]');
429
+ if (providerSelect) {
430
+ providerSelect.addEventListener('change', this.handleProviderChange.bind(this));
431
+ }
432
+
433
+ // API key toggle handler
434
+ const apiKeyToggle = this.elements.profileForm?.querySelector('.api-key-toggle');
435
+ const apiKeyInput = this.elements.profileForm?.querySelector('.api-key-input');
436
+ if (apiKeyToggle && apiKeyInput) {
437
+ apiKeyToggle.addEventListener('click', () => {
438
+ const isPassword = apiKeyInput.type === 'password';
439
+ apiKeyInput.type = isPassword ? 'text' : 'password';
440
+
441
+ // Update icon
442
+ const svg = apiKeyToggle.querySelector('svg');
443
+ if (svg) {
444
+ svg.innerHTML = isPassword ?
445
+ '<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20C7 20 2.73 16.39 1 12A18.45 18.45 0 0 1 5.06 5.06L17.94 17.94ZM9.9 4.24A9.12 9.12 0 0 1 12 4C17 4 21.27 7.61 23 12A18.5 18.5 0 0 1 19.42 16.42" stroke="currentColor" stroke-width="2" fill="none"/><path d="M1 1L23 23" stroke="currentColor" stroke-width="2"/><circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2" fill="none"/>' :
446
+ '<path d="M1 12S5 4 12 4S23 12 23 12S19 20 12 20S1 12 1 12Z" stroke="currentColor" stroke-width="2"/><circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2"/>';
447
+ }
448
+ });
449
+ }
450
+
451
+ // JSON validation handler for MCP profiles
452
+ const jsonInput = this.elements.profileForm?.querySelector('textarea[name="mcp_server_params_json"]');
453
+ if (jsonInput) {
454
+ jsonInput.addEventListener('input', this.handleJsonInputValidation.bind(this));
455
+ jsonInput.addEventListener('blur', this.handleJsonInputValidation.bind(this));
456
+
457
+ // Trigger initial validation
458
+ this.handleJsonInputValidation({ target: jsonInput });
459
+ }
460
+ }
461
+
462
+ handleJsonInputValidation(event) {
463
+ const textarea = event.target;
464
+ const feedbackElement = textarea.parentElement.querySelector('.json-validation-feedback');
465
+
466
+ if (!feedbackElement) return;
467
+
468
+ const jsonText = textarea.value.trim();
469
+
470
+ if (!jsonText) {
471
+ feedbackElement.innerHTML = '';
472
+ textarea.classList.remove('json-valid', 'json-invalid');
473
+ return;
474
+ }
475
+
476
+ try {
477
+ const parsed = JSON.parse(jsonText);
478
+
479
+ // Validate that it's an object (not array, string, etc.)
480
+ if (typeof parsed !== 'object' || Array.isArray(parsed) || parsed === null) {
481
+ throw new Error('MCP server parameters must be a JSON object');
482
+ }
483
+
484
+ // Validate required fields
485
+ if (!parsed.command || typeof parsed.command !== 'string') {
486
+ throw new Error('Missing or invalid "command" field (must be a string)');
487
+ }
488
+
489
+ // Validate args if present
490
+ if (parsed.args && !Array.isArray(parsed.args)) {
491
+ throw new Error('"args" field must be an array if provided');
492
+ }
493
+
494
+ // Success
495
+ feedbackElement.innerHTML = '<span class="json-success">✓ Valid JSON configuration</span>';
496
+ textarea.classList.remove('json-invalid');
497
+ textarea.classList.add('json-valid');
498
+
499
+ // Store valid state for form submission
500
+ textarea.dataset.isValid = 'true';
501
+
502
+ } catch (error) {
503
+ const errorMessage = error.message;
504
+ feedbackElement.innerHTML = `<span class="json-error">✗ Invalid JSON: ${errorMessage}</span>`;
505
+ textarea.classList.remove('json-valid');
506
+ textarea.classList.add('json-invalid');
507
+
508
+ // Store invalid state for form submission
509
+ textarea.dataset.isValid = 'false';
510
+ textarea.dataset.errorMessage = errorMessage;
511
+ }
512
+ }
513
+
514
+ handleProfileFormSubmitClick(event) {
515
+ console.log('[SettingsManager] Profile form submit button clicked');
516
+ event.preventDefault();
517
+
518
+ // Find the form and trigger submit
519
+ const form = this.elements.profileForm;
520
+ if (form) {
521
+ const submitEvent = new Event('submit', { cancelable: true, bubbles: true });
522
+ form.dispatchEvent(submitEvent);
523
+ }
524
+ }
525
+
526
+ async handleProviderChange(event) {
527
+ const selectedProvider = event.target.value;
528
+ const modelInput = this.elements.profileForm?.querySelector('input[name="model"]');
529
+ const modelDatalist = this.elements.profileForm?.querySelector('#model-options');
530
+
531
+ if (!selectedProvider || !modelInput || !modelDatalist) {
532
+ return;
533
+ }
534
+
535
+ // Always clear the model input when provider changes
536
+ modelInput.value = '';
537
+ modelInput.placeholder = `Loading ${selectedProvider} models...`;
538
+ modelDatalist.innerHTML = '<option value="">Loading...</option>';
539
+
540
+ try {
541
+ const response = await this.apiClient.getLLMProviderModels(selectedProvider);
542
+ const models = response.models || response || [];
543
+
544
+ // Update datalist options
545
+ modelDatalist.innerHTML = models.map(model =>
546
+ `<option value="${model}">${model}</option>`
547
+ ).join('');
548
+
549
+ // Update placeholder to reflect the new provider
550
+ modelInput.placeholder = models.length > 0
551
+ ? `Select a ${selectedProvider} model or type custom model name`
552
+ : `Enter ${selectedProvider} model name`;
553
+
554
+ } catch (error) {
555
+ console.error('[SettingsManager] Failed to fetch models for provider:', error);
556
+ modelDatalist.innerHTML = '<option value="">Failed to load models</option>';
557
+ modelInput.placeholder = `Enter ${selectedProvider} model name manually`;
558
+
559
+ // Show user-friendly error notification
560
+ this.emit('notification', {
561
+ message: `Failed to load models for ${selectedProvider}. You can enter the model name manually.`,
562
+ type: 'warning'
563
+ });
564
+ }
565
+ }
566
+
567
+ closeProfileForm() {
568
+ if (this.elements.profileFormModal) {
569
+ this.elements.profileFormModal.classList.add('hidden');
570
+ }
571
+ }
572
+
573
+ async handleProfileFormSubmit(event) {
574
+ event.preventDefault();
575
+ console.log('[SettingsManager] Profile form submit triggered');
576
+
577
+ const form = event.target;
578
+
579
+ // Prevent multiple submissions
580
+ if (form.dataset.submitting === 'true') {
581
+ return;
582
+ }
583
+
584
+ const formData = new FormData(form);
585
+ const type = form.dataset.type;
586
+ const mode = form.dataset.mode;
587
+ const profileId = form.dataset.profileId;
588
+
589
+ // Set submitting state and disable form
590
+ form.dataset.submitting = 'true';
591
+ this.setProfileFormSubmitting(true);
592
+
593
+ // Convert FormData to object
594
+ const data = {};
595
+
596
+ // Handle checkbox fields explicitly first
597
+ const checkboxFields = ['is_default', 'is_active'];
598
+ checkboxFields.forEach(fieldName => {
599
+ const checkbox = form.querySelector(`input[name="${fieldName}"]`);
600
+ if (checkbox) {
601
+ data[fieldName] = checkbox.checked;
602
+ }
603
+ });
604
+
605
+ for (const [key, value] of formData.entries()) {
606
+ if (value.trim() !== '') {
607
+ if (key === 'is_default' || key === 'is_active') {
608
+ // Skip - already handled above
609
+ continue;
610
+ } else if (key === 'temperature') {
611
+ const num = parseFloat(value);
612
+ if (!isNaN(num) && num >= 0) {
613
+ data[key] = num;
614
+ }
615
+ } else if (key === 'max_tokens') {
616
+ const num = parseInt(value);
617
+ if (!isNaN(num) && num > 0) {
618
+ data[key] = num;
619
+ }
620
+ } else {
621
+ data[key] = value;
622
+ }
623
+ }
624
+ }
625
+
626
+ // Handle MCP server params structure - parse JSON input
627
+ if (type === 'mcp') {
628
+ const jsonInput = data.mcp_server_params_json;
629
+
630
+ // Check if JSON was pre-validated
631
+ const jsonTextarea = form.querySelector('textarea[name="mcp_server_params_json"]');
632
+ if (jsonTextarea && jsonTextarea.dataset.isValid === 'false') {
633
+ console.error('[SettingsManager] JSON validation failed during form submission');
634
+ this.emit('error', {
635
+ message: jsonTextarea.dataset.errorMessage || 'Invalid JSON format'
636
+ });
637
+ form.dataset.submitting = 'false';
638
+ this.setProfileFormSubmitting(false);
639
+ return;
640
+ }
641
+
642
+ if (jsonInput) {
643
+ try {
644
+ const parsedParams = JSON.parse(jsonInput);
645
+
646
+ // Validate the parsed JSON structure
647
+ if (typeof parsedParams !== 'object' || Array.isArray(parsedParams) || parsedParams === null) {
648
+ throw new Error('MCP server parameters must be a JSON object');
649
+ }
650
+
651
+ if (!parsedParams.command || typeof parsedParams.command !== 'string') {
652
+ throw new Error('Missing or invalid "command" field (must be a string)');
653
+ }
654
+
655
+ if (parsedParams.args && !Array.isArray(parsedParams.args)) {
656
+ throw new Error('"args" field must be an array if provided');
657
+ }
658
+
659
+ // Set the parsed parameters
660
+ data.mcp_server_params = parsedParams;
661
+
662
+ } catch (error) {
663
+ console.error('[SettingsManager] Failed to parse MCP server params JSON:', error);
664
+ this.emit('error', { message: error.message });
665
+ form.dataset.submitting = 'false';
666
+ this.setProfileFormSubmitting(false);
667
+ return;
668
+ }
669
+ }
670
+
671
+ // Remove the JSON field as it's not needed in the API request
672
+ delete data.mcp_server_params_json;
673
+ }
674
+
675
+ try {
676
+ console.log(`[SettingsManager] Starting ${mode} operation for ${type} profile`);
677
+ let response;
678
+
679
+ if (mode === 'create') {
680
+ if (type === 'llm') {
681
+ response = await this.apiClient.createLLMProfile(data);
682
+ } else {
683
+ response = await this.apiClient.createMCPProfile(data);
684
+ }
685
+ } else {
686
+ if (type === 'llm') {
687
+ response = await this.apiClient.updateLLMProfile(profileId, data);
688
+ } else {
689
+ response = await this.apiClient.updateMCPProfile(profileId, data);
690
+ }
691
+ }
692
+
693
+ this.closeProfileForm();
694
+ this.emit('notification', {
695
+ message: `${type.toUpperCase()} profile ${mode === 'create' ? 'created' : 'updated'} successfully`,
696
+ type: 'success'
697
+ });
698
+
699
+ // Refresh the settings data
700
+ await this.loadSettingsData();
701
+
702
+ } catch (error) {
703
+ console.error(`[SettingsManager] Failed to ${mode} ${type} profile:`, error);
704
+
705
+ // Handle specific error types for better user experience
706
+ let errorMessage = error.message || 'Unknown error occurred';
707
+
708
+ if (errorMessage.includes('already exists') || errorMessage.includes('already in use')) {
709
+ this.highlightProfileNameError(errorMessage);
710
+ } else if (errorMessage.includes('UNIQUE constraint')) {
711
+ errorMessage = `Profile name '${data.profile_name || data.display_name}' already exists. Please choose a different name.`;
712
+ this.highlightProfileNameError(errorMessage);
713
+ }
714
+
715
+ this.emit('notification', {
716
+ message: `Failed to ${mode} ${type} profile: ${errorMessage}`,
717
+ type: 'error'
718
+ });
719
+ } finally {
720
+ // Reset form state
721
+ form.dataset.submitting = 'false';
722
+ this.setProfileFormSubmitting(false);
723
+ }
724
+ }
725
+
726
+ setProfileFormSubmitting(isSubmitting) {
727
+ const form = this.elements.profileForm;
728
+ const submitButton = this.elements.profileFormSubmit;
729
+ const cancelButton = this.elements.profileFormCancel;
730
+
731
+ if (!form) return;
732
+
733
+ // Disable/enable form inputs
734
+ const inputs = form.querySelectorAll('input, select, textarea');
735
+ inputs.forEach(input => {
736
+ input.disabled = isSubmitting;
737
+ });
738
+
739
+ // Update submit button
740
+ if (submitButton) {
741
+ submitButton.disabled = isSubmitting;
742
+ submitButton.textContent = isSubmitting ? 'Saving...' : 'Save Profile';
743
+ }
744
+
745
+ // Update cancel button
746
+ if (cancelButton) {
747
+ cancelButton.disabled = isSubmitting;
748
+ }
749
+ }
750
+
751
+ highlightProfileNameError(errorMessage) {
752
+ const nameInput = this.elements.profileForm?.querySelector('input[name="profile_name"], input[name="display_name"]');
753
+
754
+ if (nameInput) {
755
+ // Add error styling
756
+ nameInput.classList.add('form-error');
757
+ nameInput.focus();
758
+
759
+ // Create or update error message
760
+ let errorElement = nameInput.parentElement.querySelector('.profile-name-error');
761
+ if (!errorElement) {
762
+ errorElement = document.createElement('div');
763
+ errorElement.className = 'form-error-message profile-name-error';
764
+ nameInput.parentElement.appendChild(errorElement);
765
+ }
766
+
767
+ errorElement.textContent = errorMessage;
768
+
769
+ // Remove error styling after user starts typing
770
+ const removeError = () => {
771
+ nameInput.classList.remove('form-error');
772
+ if (errorElement) {
773
+ errorElement.remove();
774
+ }
775
+ nameInput.removeEventListener('input', removeError);
776
+ };
777
+
778
+ nameInput.addEventListener('input', removeError);
779
+ }
780
+ }
781
+
782
+ async handleBackendUrlChange(event) {
783
+ const newUrl = event.target.value.trim();
784
+
785
+ if (!newUrl) {
786
+ this.emit('notification', {
787
+ message: 'Backend URL cannot be empty',
788
+ type: 'warning'
789
+ });
790
+ return;
791
+ }
792
+
793
+ try {
794
+ // Validate URL format
795
+ new URL(newUrl);
796
+
797
+ // Update API client
798
+ this.apiClient.setBaseURL(newUrl);
799
+
800
+ // Emit event to update settings
801
+ this.emit('settingsUpdated', { backendUrl: newUrl });
802
+
803
+ this.emit('notification', {
804
+ message: 'Backend URL updated successfully',
805
+ type: 'success'
806
+ });
807
+
808
+ } catch (error) {
809
+ this.emit('notification', {
810
+ message: `Invalid backend URL: ${error.message}`,
811
+ type: 'error'
812
+ });
813
+ }
814
+ }
815
+
816
+ async handleSaveEnvironmentVariables() {
817
+ if (!this.elements.envVariablesList) return;
818
+
819
+ const envVarItems = this.elements.envVariablesList.querySelectorAll('.env-var-item');
820
+ const envVars = {};
821
+
822
+ // Backend URL related keys that should be skipped during save
823
+ const backendUrlKeys = [
824
+ 'BACKEND_URL',
825
+ 'VIBESURF_BACKEND_URL',
826
+ 'API_URL',
827
+ 'BASE_URL',
828
+ 'API_BASE_URL',
829
+ 'BACKEND_API_URL'
830
+ ];
831
+
832
+ envVarItems.forEach(item => {
833
+ const keyInput = item.querySelector('.env-var-key input');
834
+ const valueInput = item.querySelector('.env-var-value input');
835
+
836
+ if (keyInput && valueInput && keyInput.value.trim()) {
837
+ const key = keyInput.value.trim();
838
+ const value = valueInput.value.trim();
839
+
840
+ // Skip backend URL variables (they are readonly)
841
+ if (!backendUrlKeys.includes(key.toUpperCase())) {
842
+ envVars[key] = value;
843
+ }
844
+ }
845
+ });
846
+
847
+ try {
848
+ await this.apiClient.updateEnvironmentVariables(envVars);
849
+ this.emit('notification', {
850
+ message: 'Environment variables updated successfully (backend URL variables are read-only)',
851
+ type: 'success'
852
+ });
853
+ } catch (error) {
854
+ console.error('[SettingsManager] Failed to update environment variables:', error);
855
+ this.emit('notification', {
856
+ message: `Failed to update environment variables: ${error.message}`,
857
+ type: 'error'
858
+ });
859
+ }
860
+ }
861
+
862
+ // Rendering Methods
863
+ renderLLMProfiles(profiles) {
864
+ const container = document.getElementById('llm-profiles-list');
865
+ if (!container) return;
866
+
867
+ if (profiles.length === 0) {
868
+ container.innerHTML = `
869
+ <div class="empty-state">
870
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
871
+ <path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
872
+ <path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
873
+ <path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
874
+ </svg>
875
+ <h3>No LLM Profiles</h3>
876
+ <p>Create your first LLM profile to get started</p>
877
+ </div>
878
+ `;
879
+ return;
880
+ }
881
+
882
+ const profilesHTML = profiles.map(profile => `
883
+ <div class="profile-card ${profile.is_default ? 'default' : ''}" data-profile-id="${profile.profile_name}">
884
+ ${profile.is_default ? '<div class="profile-badge">Default</div>' : ''}
885
+ <div class="profile-header">
886
+ <div class="profile-title">
887
+ <h3>${this.escapeHtml(profile.profile_name)}</h3>
888
+ <span class="profile-provider">${this.escapeHtml(profile.provider)}</span>
889
+ </div>
890
+ <div class="profile-actions">
891
+ <button class="profile-action-btn edit" title="Edit Profile" data-profile='${JSON.stringify(profile)}'>
892
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
893
+ <path d="M11 4H4C3.46957 4 2.96086 4.21071 2.58579 4.58579C2.21071 4.96086 2 5.46957 2 6V20C2 20.5304 2.21071 21.0391 2.58579 21.4142C2.96086 21.7893 3.46957 22 4 22H18C18.5304 22 19.0391 21.7893 19.4142 21.4142C19.7893 21.0391 20 20.5304 20 20V13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
894
+ <path d="M18.5 2.5C18.8978 2.10217 19.4374 1.87868 20 1.87868C20.5626 1.87868 21.1022 2.10217 21.5 2.5C21.8978 2.89783 22.1213 3.43739 22.1213 4C22.1213 4.56261 21.8978 5.10217 21.5 5.5L12 15L8 16L9 12L18.5 2.5Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
895
+ </svg>
896
+ </button>
897
+ <button class="profile-action-btn delete" title="Delete Profile" data-profile-id="${profile.profile_name}">
898
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
899
+ <path d="M3 6H5H21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
900
+ <path d="M8 6V4C8 3.46957 8.21071 2.96086 8.58579 2.58579C8.96086 2.21071 9.46957 2 10 2H14C14.5304 2 15.0391 2.21071 15.4142 2.58579C15.7893 2.96086 16 3.46957 16 4V6M19 6V20C19 20.5304 18.7893 21.0391 18.4142 21.4142C18.0391 21.7893 17.5304 22 17 22H7C6.46957 22 5.96086 21.7893 5.58579 21.4142C5.21071 21.0391 5 20.5304 5 20V6H19Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
901
+ </svg>
902
+ </button>
903
+ </div>
904
+ </div>
905
+ <div class="profile-content">
906
+ <div class="profile-info">
907
+ <span class="profile-model">${this.escapeHtml(profile.model)}</span>
908
+ ${profile.description ? `<p class="profile-description">${this.escapeHtml(profile.description)}</p>` : ''}
909
+ </div>
910
+ <div class="profile-details">
911
+ ${profile.base_url ? `<div class="profile-detail"><strong>Base URL:</strong> ${this.escapeHtml(profile.base_url)}</div>` : ''}
912
+ ${profile.temperature !== undefined ? `<div class="profile-detail"><strong>Temperature:</strong> ${profile.temperature}</div>` : ''}
913
+ ${profile.max_tokens ? `<div class="profile-detail"><strong>Max Tokens:</strong> ${profile.max_tokens}</div>` : ''}
914
+ </div>
915
+ </div>
916
+ </div>
917
+ `).join('');
918
+
919
+ container.innerHTML = profilesHTML;
920
+
921
+ // Add event listeners for profile actions
922
+ container.querySelectorAll('.edit').forEach(btn => {
923
+ btn.addEventListener('click', (e) => {
924
+ e.stopPropagation();
925
+ const profile = JSON.parse(btn.dataset.profile);
926
+ this.showProfileForm('llm', profile);
927
+ });
928
+ });
929
+
930
+ container.querySelectorAll('.delete').forEach(btn => {
931
+ btn.addEventListener('click', async (e) => {
932
+ e.stopPropagation();
933
+ await this.handleDeleteProfile('llm', btn.dataset.profileId);
934
+ });
935
+ });
936
+ }
937
+
938
+ renderMCPProfiles(profiles) {
939
+ const container = document.getElementById('mcp-profiles-list');
940
+ if (!container) return;
941
+
942
+ if (profiles.length === 0) {
943
+ container.innerHTML = `
944
+ <div class="empty-state">
945
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
946
+ <path d="M8 2V8M16 2V8M3 10H21M5 4H19C20.1046 4 21 4.89543 21 6V20C21 21.1046 20.1046 22 19 22H5C3.89543 22 3 21.1046 3 20V6C3 4.89543 3.89543 4 5 4Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
947
+ </svg>
948
+ <h3>No MCP Profiles</h3>
949
+ <p>Create your first MCP profile to enable server integrations</p>
950
+ </div>
951
+ `;
952
+ return;
953
+ }
954
+
955
+ const profilesHTML = profiles.map(profile => `
956
+ <div class="profile-card ${profile.is_active ? 'active' : 'inactive'}" data-profile-id="${profile.mcp_id}">
957
+ <div class="profile-status ${profile.is_active ? 'active' : 'inactive'}">
958
+ ${profile.is_active ? 'Active' : 'Inactive'}
959
+ </div>
960
+ <div class="profile-header">
961
+ <div class="profile-title">
962
+ <h3>${this.escapeHtml(profile.display_name)}</h3>
963
+ <span class="profile-provider">${this.escapeHtml(profile.mcp_server_name)}</span>
964
+ </div>
965
+ <div class="profile-actions">
966
+ <button class="profile-action-btn edit" title="Edit Profile" data-profile='${JSON.stringify(profile)}'>
967
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
968
+ <path d="M11 4H4C3.46957 4 2.96086 4.21071 2.58579 4.58579C2.21071 4.96086 2 5.46957 2 6V20C2 20.5304 2.21071 21.0391 2.58579 21.4142C2.96086 21.7893 3.46957 22 4 22H18C18.5304 22 19.0391 21.7893 19.4142 21.4142C19.7893 21.0391 20 20.5304 20 20V13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
969
+ <path d="M18.5 2.5C18.8978 2.10217 19.4374 1.87868 20 1.87868C20.5626 1.87868 21.1022 2.10217 21.5 2.5C21.8978 2.89783 22.1213 3.43739 22.1213 4C22.1213 4.56261 21.8978 5.10217 21.5 5.5L12 15L8 16L9 12L18.5 2.5Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
970
+ </svg>
971
+ </button>
972
+ <button class="profile-action-btn delete" title="Delete Profile" data-profile-id="${profile.mcp_id}">
973
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
974
+ <path d="M3 6H5H21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
975
+ <path d="M8 6V4C8 3.46957 8.21071 2.96086 8.58579 2.58579C8.96086 2.21071 9.46957 2 10 2H14C14.5304 2 15.0391 2.21071 15.4142 2.58579C15.7893 2.96086 16 3.46957 16 4V6M19 6V20C19 20.5304 18.7893 21.0391 18.4142 21.4142C18.0391 21.7893 17.5304 22 17 22H7C6.46957 22 5.96086 21.7893 5.58579 21.4142C5.21071 21.0391 5 20.5304 5 20V6H19Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
976
+ </svg>
977
+ </button>
978
+ </div>
979
+ </div>
980
+ <div class="profile-content">
981
+ ${profile.description ? `<p class="profile-description">${this.escapeHtml(profile.description)}</p>` : ''}
982
+ <div class="profile-details">
983
+ <div class="profile-detail"><strong>Command:</strong> ${this.escapeHtml(profile.mcp_server_params?.command || 'N/A')}</div>
984
+ ${profile.mcp_server_params?.args?.length ? `<div class="profile-detail"><strong>Args:</strong> ${profile.mcp_server_params.args.join(', ')}</div>` : ''}
985
+ </div>
986
+ </div>
987
+ </div>
988
+ `).join('');
989
+
990
+ container.innerHTML = profilesHTML;
991
+
992
+ // Add event listeners for profile actions
993
+ container.querySelectorAll('.edit').forEach(btn => {
994
+ btn.addEventListener('click', (e) => {
995
+ e.stopPropagation();
996
+ const profile = JSON.parse(btn.dataset.profile);
997
+ this.showProfileForm('mcp', profile);
998
+ });
999
+ });
1000
+
1001
+ container.querySelectorAll('.delete').forEach(btn => {
1002
+ btn.addEventListener('click', async (e) => {
1003
+ e.stopPropagation();
1004
+ await this.handleDeleteProfile('mcp', btn.dataset.profileId);
1005
+ });
1006
+ });
1007
+ }
1008
+
1009
+ renderEnvironmentVariables(envVars) {
1010
+ const container = this.elements.envVariablesList;
1011
+ if (!container) return;
1012
+
1013
+ // Clear existing content
1014
+ container.innerHTML = '';
1015
+
1016
+ // Check if there are any environment variables to display
1017
+ if (Object.keys(envVars).length === 0) {
1018
+ container.innerHTML = `
1019
+ <div class="empty-state">
1020
+ <div class="empty-state-icon">🔧</div>
1021
+ <div class="empty-state-title">No Environment Variables</div>
1022
+ <div class="empty-state-description">Environment variables are configured on the backend. Only updates to existing variables are allowed.</div>
1023
+ </div>
1024
+ `;
1025
+ return;
1026
+ }
1027
+
1028
+ // Backend URL related keys that should be readonly
1029
+ const backendUrlKeys = [
1030
+ 'BACKEND_URL',
1031
+ 'VIBESURF_BACKEND_URL',
1032
+ 'API_URL',
1033
+ 'BASE_URL',
1034
+ 'API_BASE_URL',
1035
+ 'BACKEND_API_URL'
1036
+ ];
1037
+
1038
+ // Add existing environment variables (read-only keys, editable/readonly values based on type)
1039
+ Object.entries(envVars).forEach(([key, value]) => {
1040
+ const envVarItem = document.createElement('div');
1041
+ envVarItem.className = 'env-var-item';
1042
+
1043
+ // Check if this is a backend URL variable
1044
+ const isBackendUrl = backendUrlKeys.includes(key.toUpperCase());
1045
+ const valueReadonly = isBackendUrl ? 'readonly' : '';
1046
+ const valueClass = isBackendUrl ? 'form-input readonly-input' : 'form-input';
1047
+ const valueTitle = isBackendUrl ? 'Backend URL is not editable from settings' : '';
1048
+
1049
+ envVarItem.innerHTML = `
1050
+ <div class="env-var-key">
1051
+ <input type="text" class="form-input" placeholder="Variable name" value="${this.escapeHtml(key)}" readonly>
1052
+ </div>
1053
+ <div class="env-var-value">
1054
+ <input type="text" class="${valueClass}" placeholder="Variable value" value="${this.escapeHtml(value)}" ${valueReadonly} title="${valueTitle}">
1055
+ </div>
1056
+ `;
1057
+
1058
+ container.appendChild(envVarItem);
1059
+ });
1060
+ }
1061
+
1062
+ async handleDeleteProfile(type, profileId) {
1063
+ // Check if this is a default LLM profile
1064
+ if (type === 'llm') {
1065
+ const profile = this.state.llmProfiles.find(p => p.profile_name === profileId);
1066
+ if (profile && profile.is_default) {
1067
+ // Handle default profile deletion differently
1068
+ return await this.handleDeleteDefaultProfile(profileId);
1069
+ }
1070
+ }
1071
+
1072
+ // Emit confirmation request to main UI manager
1073
+ this.emit('confirmDeletion', {
1074
+ type,
1075
+ profileId,
1076
+ callback: () => this.performDeleteProfile(type, profileId)
1077
+ });
1078
+ }
1079
+
1080
+ async handleDeleteDefaultProfile(profileId) {
1081
+ // Get other available profiles
1082
+ const otherProfiles = this.state.llmProfiles.filter(p => p.profile_name !== profileId);
1083
+
1084
+ if (otherProfiles.length === 0) {
1085
+ // No other profiles available - cannot delete
1086
+ this.emit('error', {
1087
+ message: 'This is the only LLM profile configured. You cannot delete it without having at least one other profile.',
1088
+ details: 'Please create another LLM profile first, then you can delete this one.',
1089
+ buttons: [
1090
+ {
1091
+ text: 'Create New Profile',
1092
+ action: () => this.handleAddProfile('llm')
1093
+ }
1094
+ ]
1095
+ });
1096
+ return false;
1097
+ }
1098
+
1099
+ // Show modal to select new default profile
1100
+ this.emit('selectNewDefault', {
1101
+ profileId,
1102
+ otherProfiles,
1103
+ callback: (newDefaultProfileId) => this.setNewDefaultAndDelete(newDefaultProfileId, profileId)
1104
+ });
1105
+ }
1106
+
1107
+ async setNewDefaultAndDelete(newDefaultProfileId, profileToDelete) {
1108
+ try {
1109
+ this.emit('loading', { message: 'Updating default profile...' });
1110
+
1111
+ // First, set the new default profile
1112
+ await this.apiClient.updateLLMProfile(newDefaultProfileId, { is_default: true });
1113
+
1114
+ this.emit('loading', { message: 'Deleting profile...' });
1115
+
1116
+ // Then delete the old default profile
1117
+ await this.apiClient.deleteLLMProfile(profileToDelete);
1118
+
1119
+ this.emit('notification', {
1120
+ message: `Profile "${profileToDelete}" deleted and "${newDefaultProfileId}" set as default`,
1121
+ type: 'success'
1122
+ });
1123
+
1124
+ // Refresh the settings data
1125
+ await this.loadSettingsData();
1126
+
1127
+ this.emit('loading', { hide: true });
1128
+ } catch (error) {
1129
+ this.emit('loading', { hide: true });
1130
+ console.error('[SettingsManager] Failed to set new default and delete profile:', error);
1131
+ this.emit('notification', {
1132
+ message: `Failed to update profiles: ${error.message}`,
1133
+ type: 'error'
1134
+ });
1135
+ throw error;
1136
+ }
1137
+ }
1138
+
1139
+ async performDeleteProfile(type, profileId) {
1140
+ try {
1141
+ this.emit('loading', { message: `Deleting ${type} profile...` });
1142
+
1143
+ if (type === 'llm') {
1144
+ await this.apiClient.deleteLLMProfile(profileId);
1145
+ } else {
1146
+ await this.apiClient.deleteMCPProfile(profileId);
1147
+ }
1148
+
1149
+ this.emit('notification', {
1150
+ message: `${type.toUpperCase()} profile deleted successfully`,
1151
+ type: 'success'
1152
+ });
1153
+
1154
+ // Refresh the settings data
1155
+ await this.loadSettingsData();
1156
+
1157
+ this.emit('loading', { hide: true });
1158
+ } catch (error) {
1159
+ this.emit('loading', { hide: true });
1160
+ console.error(`[SettingsManager] Failed to delete ${type} profile:`, error);
1161
+ this.emit('notification', {
1162
+ message: `Failed to delete ${type} profile: ${error.message}`,
1163
+ type: 'error'
1164
+ });
1165
+ }
1166
+ }
1167
+
1168
+ escapeHtml(text) {
1169
+ if (typeof text !== 'string') return '';
1170
+ const div = document.createElement('div');
1171
+ div.textContent = text;
1172
+ return div.innerHTML;
1173
+ }
1174
+
1175
+ // Public interface
1176
+ getState() {
1177
+ return { ...this.state };
1178
+ }
1179
+
1180
+ getLLMProfiles() {
1181
+ return this.state.llmProfiles || [];
1182
+ }
1183
+
1184
+ getMCPProfiles() {
1185
+ return this.state.mcpProfiles || [];
1186
+ }
1187
+
1188
+ showModal() {
1189
+ if (this.elements.settingsModal) {
1190
+ this.elements.settingsModal.classList.remove('hidden');
1191
+ }
1192
+ }
1193
+
1194
+ showSettings() {
1195
+ this.showModal();
1196
+ }
1197
+
1198
+ hideModal() {
1199
+ if (this.elements.settingsModal) {
1200
+ this.elements.settingsModal.classList.add('hidden');
1201
+ }
1202
+ }
1203
+
1204
+ updateBackendUrl(url) {
1205
+ if (this.elements.backendUrl) {
1206
+ this.elements.backendUrl.value = url;
1207
+ }
1208
+ }
1209
+ }
1210
+
1211
+ // Export for use in other modules
1212
+ if (typeof window !== 'undefined') {
1213
+ window.VibeSurfSettingsManager = VibeSurfSettingsManager;
1214
+ }