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.
- vibe_surf/_version.py +2 -2
- vibe_surf/agents/vibe_surf_agent.py +25 -15
- vibe_surf/backend/api/browser.py +66 -0
- vibe_surf/backend/api/task.py +2 -1
- vibe_surf/backend/main.py +76 -1
- vibe_surf/backend/shared_state.py +2 -0
- vibe_surf/browser/agent_browser_session.py +312 -62
- vibe_surf/browser/browser_manager.py +57 -92
- vibe_surf/browser/watchdogs/dom_watchdog.py +43 -43
- vibe_surf/chrome_extension/background.js +84 -0
- vibe_surf/chrome_extension/manifest.json +3 -1
- vibe_surf/chrome_extension/scripts/file-manager.js +526 -0
- vibe_surf/chrome_extension/scripts/history-manager.js +658 -0
- vibe_surf/chrome_extension/scripts/modal-manager.js +487 -0
- vibe_surf/chrome_extension/scripts/session-manager.js +31 -8
- vibe_surf/chrome_extension/scripts/settings-manager.js +1214 -0
- vibe_surf/chrome_extension/scripts/ui-manager.js +770 -3186
- vibe_surf/chrome_extension/sidepanel.html +27 -4
- vibe_surf/chrome_extension/styles/activity.css +574 -0
- vibe_surf/chrome_extension/styles/base.css +76 -0
- vibe_surf/chrome_extension/styles/history-modal.css +791 -0
- vibe_surf/chrome_extension/styles/input.css +429 -0
- vibe_surf/chrome_extension/styles/layout.css +186 -0
- vibe_surf/chrome_extension/styles/responsive.css +454 -0
- vibe_surf/chrome_extension/styles/settings-environment.css +165 -0
- vibe_surf/chrome_extension/styles/settings-forms.css +389 -0
- vibe_surf/chrome_extension/styles/settings-modal.css +141 -0
- vibe_surf/chrome_extension/styles/settings-profiles.css +244 -0
- vibe_surf/chrome_extension/styles/settings-responsive.css +144 -0
- vibe_surf/chrome_extension/styles/settings-utilities.css +25 -0
- vibe_surf/chrome_extension/styles/variables.css +54 -0
- vibe_surf/cli.py +1 -0
- vibe_surf/controller/vibesurf_tools.py +0 -2
- {vibesurf-0.1.9a6.dist-info → vibesurf-0.1.10.dist-info}/METADATA +18 -2
- {vibesurf-0.1.9a6.dist-info → vibesurf-0.1.10.dist-info}/RECORD +39 -23
- vibe_surf/chrome_extension/styles/main.css +0 -2338
- vibe_surf/chrome_extension/styles/settings.css +0 -1100
- {vibesurf-0.1.9a6.dist-info → vibesurf-0.1.10.dist-info}/WHEEL +0 -0
- {vibesurf-0.1.9a6.dist-info → vibesurf-0.1.10.dist-info}/entry_points.txt +0 -0
- {vibesurf-0.1.9a6.dist-info → vibesurf-0.1.10.dist-info}/licenses/LICENSE +0 -0
- {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
|
+
}
|