vibesurf 0.1.20__py3-none-any.whl → 0.1.21__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.
Potentially problematic release.
This version of vibesurf might be problematic. Click here for more details.
- vibe_surf/_version.py +2 -2
- vibe_surf/backend/api/task.py +1 -1
- vibe_surf/backend/api/voices.py +481 -0
- vibe_surf/backend/database/migrations/v004_add_voice_profiles.sql +35 -0
- vibe_surf/backend/database/models.py +38 -1
- vibe_surf/backend/database/queries.py +189 -1
- vibe_surf/backend/main.py +2 -0
- vibe_surf/backend/shared_state.py +1 -1
- vibe_surf/backend/voice_model_config.py +25 -0
- vibe_surf/browser/agen_browser_profile.py +2 -0
- vibe_surf/browser/agent_browser_session.py +3 -3
- vibe_surf/chrome_extension/background.js +224 -9
- vibe_surf/chrome_extension/content.js +147 -0
- vibe_surf/chrome_extension/manifest.json +11 -2
- vibe_surf/chrome_extension/permission-iframe.html +38 -0
- vibe_surf/chrome_extension/permission-request.html +104 -0
- vibe_surf/chrome_extension/scripts/api-client.js +61 -0
- vibe_surf/chrome_extension/scripts/main.js +8 -2
- vibe_surf/chrome_extension/scripts/permission-iframe-request.js +188 -0
- vibe_surf/chrome_extension/scripts/permission-request.js +118 -0
- vibe_surf/chrome_extension/scripts/settings-manager.js +690 -3
- vibe_surf/chrome_extension/scripts/ui-manager.js +730 -119
- vibe_surf/chrome_extension/scripts/user-settings-storage.js +422 -0
- vibe_surf/chrome_extension/scripts/voice-recorder.js +514 -0
- vibe_surf/chrome_extension/sidepanel.html +106 -29
- vibe_surf/chrome_extension/styles/components.css +35 -0
- vibe_surf/chrome_extension/styles/input.css +164 -1
- vibe_surf/chrome_extension/styles/layout.css +1 -1
- vibe_surf/chrome_extension/styles/settings-environment.css +138 -0
- vibe_surf/chrome_extension/styles/settings-forms.css +7 -7
- vibe_surf/chrome_extension/styles/variables.css +51 -0
- vibe_surf/tools/voice_asr.py +79 -8
- {vibesurf-0.1.20.dist-info → vibesurf-0.1.21.dist-info}/METADATA +8 -12
- {vibesurf-0.1.20.dist-info → vibesurf-0.1.21.dist-info}/RECORD +38 -31
- vibe_surf/chrome_extension/icons/convert-svg.js +0 -33
- vibe_surf/chrome_extension/icons/logo-preview.html +0 -187
- {vibesurf-0.1.20.dist-info → vibesurf-0.1.21.dist-info}/WHEEL +0 -0
- {vibesurf-0.1.20.dist-info → vibesurf-0.1.21.dist-info}/entry_points.txt +0 -0
- {vibesurf-0.1.20.dist-info → vibesurf-0.1.21.dist-info}/licenses/LICENSE +0 -0
- {vibesurf-0.1.20.dist-info → vibesurf-0.1.21.dist-info}/top_level.txt +0 -0
|
@@ -7,14 +7,19 @@ class VibeSurfSettingsManager {
|
|
|
7
7
|
this.state = {
|
|
8
8
|
llmProfiles: [],
|
|
9
9
|
mcpProfiles: [],
|
|
10
|
+
voiceProfiles: [],
|
|
10
11
|
settings: {},
|
|
11
12
|
currentProfileForm: null
|
|
12
13
|
};
|
|
13
14
|
this.elements = {};
|
|
14
15
|
this.eventListeners = new Map();
|
|
15
16
|
|
|
17
|
+
// Initialize user settings storage
|
|
18
|
+
this.userSettingsStorage = new VibeSurfUserSettingsStorage();
|
|
19
|
+
|
|
16
20
|
this.bindElements();
|
|
17
21
|
this.bindEvents();
|
|
22
|
+
this.initializeUserSettings();
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
bindElements() {
|
|
@@ -24,6 +29,11 @@ class VibeSurfSettingsManager {
|
|
|
24
29
|
settingsTabs: document.querySelectorAll('.settings-tab'),
|
|
25
30
|
settingsTabContents: document.querySelectorAll('.settings-tab-content'),
|
|
26
31
|
|
|
32
|
+
// General Settings
|
|
33
|
+
themeSelect: document.getElementById('theme-select'),
|
|
34
|
+
defaultAsrSelect: document.getElementById('default-asr-select'),
|
|
35
|
+
defaultTtsSelect: document.getElementById('default-tts-select'),
|
|
36
|
+
|
|
27
37
|
// LLM Profiles
|
|
28
38
|
llmProfilesContainer: document.getElementById('llm-profiles-container'),
|
|
29
39
|
addLlmProfileBtn: document.getElementById('add-llm-profile-btn'),
|
|
@@ -32,6 +42,10 @@ class VibeSurfSettingsManager {
|
|
|
32
42
|
mcpProfilesContainer: document.getElementById('mcp-profiles-container'),
|
|
33
43
|
addMcpProfileBtn: document.getElementById('add-mcp-profile-btn'),
|
|
34
44
|
|
|
45
|
+
// Voice Profiles
|
|
46
|
+
voiceProfilesContainer: document.getElementById('voice-profiles-container'),
|
|
47
|
+
addVoiceProfileBtn: document.getElementById('add-voice-profile-btn'),
|
|
48
|
+
|
|
35
49
|
// Profile Form Modal
|
|
36
50
|
profileFormModal: document.getElementById('profile-form-modal'),
|
|
37
51
|
profileFormTitle: document.getElementById('profile-form-title'),
|
|
@@ -64,6 +78,7 @@ class VibeSurfSettingsManager {
|
|
|
64
78
|
// Profile management
|
|
65
79
|
this.elements.addLlmProfileBtn?.addEventListener('click', () => this.handleAddProfile('llm'));
|
|
66
80
|
this.elements.addMcpProfileBtn?.addEventListener('click', () => this.handleAddProfile('mcp'));
|
|
81
|
+
this.elements.addVoiceProfileBtn?.addEventListener('click', () => this.handleAddProfile('voice'));
|
|
67
82
|
|
|
68
83
|
// Profile form modal
|
|
69
84
|
this.elements.profileFormCancel?.addEventListener('click', this.closeProfileForm.bind(this));
|
|
@@ -78,6 +93,11 @@ class VibeSurfSettingsManager {
|
|
|
78
93
|
this.elements.profileFormSubmit.addEventListener('click', this.handleProfileFormSubmitClick.bind(this));
|
|
79
94
|
}
|
|
80
95
|
|
|
96
|
+
// General settings
|
|
97
|
+
this.elements.themeSelect?.addEventListener('change', this.handleThemeChange.bind(this));
|
|
98
|
+
this.elements.defaultAsrSelect?.addEventListener('change', this.handleDefaultAsrChange.bind(this));
|
|
99
|
+
this.elements.defaultTtsSelect?.addEventListener('change', this.handleDefaultTtsChange.bind(this));
|
|
100
|
+
|
|
81
101
|
// Environment variables
|
|
82
102
|
this.elements.saveEnvVarsBtn?.addEventListener('click', this.handleSaveEnvironmentVariables.bind(this));
|
|
83
103
|
|
|
@@ -88,6 +108,65 @@ class VibeSurfSettingsManager {
|
|
|
88
108
|
document.addEventListener('keydown', this.handleKeydown.bind(this));
|
|
89
109
|
}
|
|
90
110
|
|
|
111
|
+
// Initialize user settings storage
|
|
112
|
+
async initializeUserSettings() {
|
|
113
|
+
try {
|
|
114
|
+
await this.userSettingsStorage.initialize();
|
|
115
|
+
|
|
116
|
+
// Listen to storage events
|
|
117
|
+
this.userSettingsStorage.on('settingChanged', this.handleStorageSettingChanged.bind(this));
|
|
118
|
+
this.userSettingsStorage.on('settingsChanged', this.handleStorageSettingsChanged.bind(this));
|
|
119
|
+
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.error('[SettingsManager] Failed to initialize user settings storage:', error);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Handle individual setting changes from storage
|
|
126
|
+
handleStorageSettingChanged(data) {
|
|
127
|
+
|
|
128
|
+
// Apply setting changes to UI if needed
|
|
129
|
+
switch (data.key) {
|
|
130
|
+
case 'theme':
|
|
131
|
+
this.applyTheme(data.value);
|
|
132
|
+
if (this.elements.themeSelect) {
|
|
133
|
+
this.elements.themeSelect.value = data.value;
|
|
134
|
+
}
|
|
135
|
+
break;
|
|
136
|
+
case 'defaultAsr':
|
|
137
|
+
if (this.elements.defaultAsrSelect) {
|
|
138
|
+
this.elements.defaultAsrSelect.value = data.value;
|
|
139
|
+
}
|
|
140
|
+
break;
|
|
141
|
+
case 'defaultTts':
|
|
142
|
+
if (this.elements.defaultTtsSelect) {
|
|
143
|
+
this.elements.defaultTtsSelect.value = data.value;
|
|
144
|
+
}
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Handle bulk settings changes from storage
|
|
150
|
+
handleStorageSettingsChanged(allSettings) {
|
|
151
|
+
console.log('[SettingsManager] Storage settings changed (bulk):', allSettings);
|
|
152
|
+
|
|
153
|
+
// Apply bulk setting changes to UI if needed
|
|
154
|
+
if (allSettings.theme) {
|
|
155
|
+
this.applyTheme(allSettings.theme);
|
|
156
|
+
if (this.elements.themeSelect) {
|
|
157
|
+
this.elements.themeSelect.value = allSettings.theme;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (allSettings.defaultAsr && this.elements.defaultAsrSelect) {
|
|
162
|
+
this.elements.defaultAsrSelect.value = allSettings.defaultAsr;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (allSettings.defaultTts && this.elements.defaultTtsSelect) {
|
|
166
|
+
this.elements.defaultTtsSelect.value = allSettings.defaultTts;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
91
170
|
handleKeydown(event) {
|
|
92
171
|
// Close settings modal on Escape key
|
|
93
172
|
if (event.key === 'Escape') {
|
|
@@ -140,6 +219,11 @@ class VibeSurfSettingsManager {
|
|
|
140
219
|
if (targetContent) {
|
|
141
220
|
targetContent.classList.add('active');
|
|
142
221
|
}
|
|
222
|
+
|
|
223
|
+
// If switching to general tab, ensure environment variables are loaded
|
|
224
|
+
if (targetTabId === 'general') {
|
|
225
|
+
this.loadEnvironmentVariables();
|
|
226
|
+
}
|
|
143
227
|
}
|
|
144
228
|
|
|
145
229
|
// Data Loading
|
|
@@ -151,13 +235,24 @@ class VibeSurfSettingsManager {
|
|
|
151
235
|
// Load MCP profiles
|
|
152
236
|
await this.loadMCPProfiles();
|
|
153
237
|
|
|
238
|
+
// Load Voice profiles
|
|
239
|
+
await this.loadVoiceProfiles();
|
|
240
|
+
|
|
154
241
|
// Load environment variables
|
|
155
242
|
await this.loadEnvironmentVariables();
|
|
156
243
|
|
|
244
|
+
// Load general settings
|
|
245
|
+
await this.loadGeneralSettings();
|
|
246
|
+
|
|
247
|
+
// Load voice profiles for general settings dropdowns
|
|
248
|
+
await this.loadVoiceProfilesForGeneral();
|
|
249
|
+
|
|
157
250
|
// Emit event to update LLM profile select dropdown
|
|
251
|
+
// This should happen AFTER all data is loaded but BEFORE user selections are restored
|
|
158
252
|
this.emit('profilesUpdated', {
|
|
159
253
|
llmProfiles: this.state.llmProfiles,
|
|
160
|
-
mcpProfiles: this.state.mcpProfiles
|
|
254
|
+
mcpProfiles: this.state.mcpProfiles,
|
|
255
|
+
voiceProfiles: this.state.voiceProfiles
|
|
161
256
|
});
|
|
162
257
|
|
|
163
258
|
} catch (error) {
|
|
@@ -214,6 +309,30 @@ class VibeSurfSettingsManager {
|
|
|
214
309
|
}
|
|
215
310
|
}
|
|
216
311
|
|
|
312
|
+
async loadVoiceProfiles() {
|
|
313
|
+
try {
|
|
314
|
+
const response = await this.apiClient.getVoiceProfiles(false); // Load all profiles, not just active
|
|
315
|
+
console.log('[SettingsManager] Voice profiles loaded:', response);
|
|
316
|
+
|
|
317
|
+
// Handle different response structures
|
|
318
|
+
let profiles = [];
|
|
319
|
+
if (Array.isArray(response)) {
|
|
320
|
+
profiles = response;
|
|
321
|
+
} else if (response.profiles && Array.isArray(response.profiles)) {
|
|
322
|
+
profiles = response.profiles;
|
|
323
|
+
} else if (response.data && Array.isArray(response.data)) {
|
|
324
|
+
profiles = response.data;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
this.state.voiceProfiles = profiles;
|
|
328
|
+
this.renderVoiceProfiles(profiles);
|
|
329
|
+
} catch (error) {
|
|
330
|
+
console.error('[SettingsManager] Failed to load Voice profiles:', error);
|
|
331
|
+
this.state.voiceProfiles = [];
|
|
332
|
+
this.renderVoiceProfiles([]);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
217
336
|
async loadEnvironmentVariables() {
|
|
218
337
|
try {
|
|
219
338
|
const response = await this.apiClient.getEnvironmentVariables();
|
|
@@ -226,6 +345,215 @@ class VibeSurfSettingsManager {
|
|
|
226
345
|
}
|
|
227
346
|
}
|
|
228
347
|
|
|
348
|
+
async loadGeneralSettings() {
|
|
349
|
+
try {
|
|
350
|
+
// Load and apply theme setting from user settings storage
|
|
351
|
+
const savedTheme = await this.userSettingsStorage.getTheme();
|
|
352
|
+
if (this.elements.themeSelect) {
|
|
353
|
+
this.elements.themeSelect.value = savedTheme;
|
|
354
|
+
}
|
|
355
|
+
this.applyTheme(savedTheme);
|
|
356
|
+
|
|
357
|
+
// Voice profile defaults will be handled by autoSelectLatestVoiceProfiles
|
|
358
|
+
// after voice profiles are loaded in loadVoiceProfilesForGeneral
|
|
359
|
+
|
|
360
|
+
console.log('[SettingsManager] General settings loaded successfully');
|
|
361
|
+
} catch (error) {
|
|
362
|
+
console.error('[SettingsManager] Failed to load general settings:', error);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async loadVoiceProfilesForGeneral() {
|
|
367
|
+
try {
|
|
368
|
+
// Load voice profiles for ASR and TTS dropdowns in general settings
|
|
369
|
+
const response = await this.apiClient.getVoiceProfiles(false);
|
|
370
|
+
|
|
371
|
+
// Handle different response structures
|
|
372
|
+
let profiles = [];
|
|
373
|
+
if (Array.isArray(response)) {
|
|
374
|
+
profiles = response;
|
|
375
|
+
} else if (response.profiles && Array.isArray(response.profiles)) {
|
|
376
|
+
profiles = response.profiles;
|
|
377
|
+
} else if (response.data && Array.isArray(response.data)) {
|
|
378
|
+
profiles = response.data;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Filter profiles by type
|
|
382
|
+
const asrProfiles = profiles.filter(p => p.voice_model_type === 'asr' && p.is_active);
|
|
383
|
+
const ttsProfiles = profiles.filter(p => p.voice_model_type === 'tts' && p.is_active);
|
|
384
|
+
|
|
385
|
+
// Populate ASR dropdown
|
|
386
|
+
if (this.elements.defaultAsrSelect) {
|
|
387
|
+
this.elements.defaultAsrSelect.innerHTML = '<option value="">No ASR profile selected</option>';
|
|
388
|
+
asrProfiles.forEach(profile => {
|
|
389
|
+
const option = document.createElement('option');
|
|
390
|
+
option.value = profile.voice_profile_name;
|
|
391
|
+
option.textContent = profile.voice_profile_name;
|
|
392
|
+
this.elements.defaultAsrSelect.appendChild(option);
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Populate TTS dropdown
|
|
397
|
+
if (this.elements.defaultTtsSelect) {
|
|
398
|
+
this.elements.defaultTtsSelect.innerHTML = '<option value="">No TTS profile selected</option>';
|
|
399
|
+
ttsProfiles.forEach(profile => {
|
|
400
|
+
const option = document.createElement('option');
|
|
401
|
+
option.value = profile.voice_profile_name;
|
|
402
|
+
option.textContent = profile.voice_profile_name;
|
|
403
|
+
this.elements.defaultTtsSelect.appendChild(option);
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Auto-select latest updated profiles if no defaults are set
|
|
408
|
+
await this.autoSelectLatestVoiceProfiles(asrProfiles, ttsProfiles);
|
|
409
|
+
|
|
410
|
+
} catch (error) {
|
|
411
|
+
console.error('[SettingsManager] Failed to load voice profiles for general settings:', error);
|
|
412
|
+
// Populate with empty options on error
|
|
413
|
+
if (this.elements.defaultAsrSelect) {
|
|
414
|
+
this.elements.defaultAsrSelect.innerHTML = '<option value="">Failed to load ASR profiles</option>';
|
|
415
|
+
}
|
|
416
|
+
if (this.elements.defaultTtsSelect) {
|
|
417
|
+
this.elements.defaultTtsSelect.innerHTML = '<option value="">Failed to load TTS profiles</option>';
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async autoSelectLatestVoiceProfiles(asrProfiles, ttsProfiles) {
|
|
423
|
+
try {
|
|
424
|
+
// Get current saved defaults
|
|
425
|
+
const savedAsrProfile = await this.userSettingsStorage.getDefaultAsr();
|
|
426
|
+
const savedTtsProfile = await this.userSettingsStorage.getDefaultTts();
|
|
427
|
+
|
|
428
|
+
// Check ASR profile
|
|
429
|
+
if (!savedAsrProfile || !asrProfiles.find(p => p.voice_profile_name === savedAsrProfile)) {
|
|
430
|
+
// No ASR profile selected or saved profile doesn't exist, select latest updated
|
|
431
|
+
if (asrProfiles.length > 0) {
|
|
432
|
+
// Sort by updated_at desc to get the latest updated profile
|
|
433
|
+
const latestAsrProfile = asrProfiles.sort((a, b) => {
|
|
434
|
+
const dateA = new Date(a.updated_at || a.created_at);
|
|
435
|
+
const dateB = new Date(b.updated_at || b.created_at);
|
|
436
|
+
return dateB - dateA; // DESC order
|
|
437
|
+
})[0];
|
|
438
|
+
|
|
439
|
+
console.log('[SettingsManager] Auto-selecting latest ASR profile:', latestAsrProfile.voice_profile_name);
|
|
440
|
+
|
|
441
|
+
// Set as default in storage
|
|
442
|
+
await this.userSettingsStorage.setDefaultAsr(latestAsrProfile.voice_profile_name);
|
|
443
|
+
|
|
444
|
+
// Update UI
|
|
445
|
+
if (this.elements.defaultAsrSelect) {
|
|
446
|
+
this.elements.defaultAsrSelect.value = latestAsrProfile.voice_profile_name;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
this.emit('notification', {
|
|
450
|
+
message: `Auto-selected latest ASR profile: ${latestAsrProfile.voice_profile_name}`,
|
|
451
|
+
type: 'info'
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
} else {
|
|
455
|
+
// Saved ASR profile exists and is valid - restore it to UI
|
|
456
|
+
console.log('[SettingsManager] Restoring saved ASR profile to UI:', savedAsrProfile);
|
|
457
|
+
if (this.elements.defaultAsrSelect) {
|
|
458
|
+
this.elements.defaultAsrSelect.value = savedAsrProfile;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Check TTS profile
|
|
463
|
+
if (!savedTtsProfile || !ttsProfiles.find(p => p.voice_profile_name === savedTtsProfile)) {
|
|
464
|
+
// No TTS profile selected or saved profile doesn't exist, select latest updated
|
|
465
|
+
if (ttsProfiles.length > 0) {
|
|
466
|
+
// Sort by updated_at desc to get the latest updated profile
|
|
467
|
+
const latestTtsProfile = ttsProfiles.sort((a, b) => {
|
|
468
|
+
const dateA = new Date(a.updated_at || a.created_at);
|
|
469
|
+
const dateB = new Date(b.updated_at || b.created_at);
|
|
470
|
+
return dateB - dateA; // DESC order
|
|
471
|
+
})[0];
|
|
472
|
+
|
|
473
|
+
console.log('[SettingsManager] Auto-selecting latest TTS profile:', latestTtsProfile.voice_profile_name);
|
|
474
|
+
|
|
475
|
+
// Set as default in storage
|
|
476
|
+
await this.userSettingsStorage.setDefaultTts(latestTtsProfile.voice_profile_name);
|
|
477
|
+
|
|
478
|
+
// Update UI
|
|
479
|
+
if (this.elements.defaultTtsSelect) {
|
|
480
|
+
this.elements.defaultTtsSelect.value = latestTtsProfile.voice_profile_name;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
this.emit('notification', {
|
|
484
|
+
message: `Auto-selected latest TTS profile: ${latestTtsProfile.voice_profile_name}`,
|
|
485
|
+
type: 'info'
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
} else {
|
|
489
|
+
// Saved TTS profile exists and is valid - restore it to UI
|
|
490
|
+
console.log('[SettingsManager] Restoring saved TTS profile to UI:', savedTtsProfile);
|
|
491
|
+
if (this.elements.defaultTtsSelect) {
|
|
492
|
+
this.elements.defaultTtsSelect.value = savedTtsProfile;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
} catch (error) {
|
|
497
|
+
console.error('[SettingsManager] Failed to auto-select latest voice profiles:', error);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async handleDefaultAsrChange(event) {
|
|
502
|
+
const selectedProfile = event.target.value;
|
|
503
|
+
|
|
504
|
+
try {
|
|
505
|
+
// Store ASR profile preference in user settings storage
|
|
506
|
+
await this.userSettingsStorage.setDefaultAsr(selectedProfile);
|
|
507
|
+
|
|
508
|
+
if (selectedProfile) {
|
|
509
|
+
this.emit('notification', {
|
|
510
|
+
message: `Default ASR profile set to ${selectedProfile}`,
|
|
511
|
+
type: 'success'
|
|
512
|
+
});
|
|
513
|
+
} else {
|
|
514
|
+
this.emit('notification', {
|
|
515
|
+
message: 'Default ASR profile cleared',
|
|
516
|
+
type: 'info'
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
} catch (error) {
|
|
521
|
+
console.error('[SettingsManager] Failed to change default ASR profile:', error);
|
|
522
|
+
this.emit('notification', {
|
|
523
|
+
message: 'Failed to change default ASR profile',
|
|
524
|
+
type: 'error'
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async handleDefaultTtsChange(event) {
|
|
530
|
+
const selectedProfile = event.target.value;
|
|
531
|
+
|
|
532
|
+
try {
|
|
533
|
+
// Store TTS profile preference in user settings storage
|
|
534
|
+
await this.userSettingsStorage.setDefaultTts(selectedProfile);
|
|
535
|
+
|
|
536
|
+
if (selectedProfile) {
|
|
537
|
+
this.emit('notification', {
|
|
538
|
+
message: `Default TTS profile set to ${selectedProfile}`,
|
|
539
|
+
type: 'success'
|
|
540
|
+
});
|
|
541
|
+
} else {
|
|
542
|
+
this.emit('notification', {
|
|
543
|
+
message: 'Default TTS profile cleared',
|
|
544
|
+
type: 'info'
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
} catch (error) {
|
|
549
|
+
console.error('[SettingsManager] Failed to change default TTS profile:', error);
|
|
550
|
+
this.emit('notification', {
|
|
551
|
+
message: 'Failed to change default TTS profile',
|
|
552
|
+
type: 'error'
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
229
557
|
// Profile Management
|
|
230
558
|
async handleAddProfile(type) {
|
|
231
559
|
try {
|
|
@@ -238,7 +566,7 @@ class VibeSurfSettingsManager {
|
|
|
238
566
|
|
|
239
567
|
async showProfileForm(type, profile = null) {
|
|
240
568
|
const isEdit = profile !== null;
|
|
241
|
-
const title = isEdit ? `Edit ${type.toUpperCase()}
|
|
569
|
+
const title = isEdit ? `Edit ${type.toUpperCase()}` : `Add ${type.toUpperCase()}`;
|
|
242
570
|
|
|
243
571
|
if (this.elements.profileFormTitle) {
|
|
244
572
|
this.elements.profileFormTitle.textContent = title;
|
|
@@ -250,6 +578,8 @@ class VibeSurfSettingsManager {
|
|
|
250
578
|
formHTML = await this.generateLLMProfileForm(profile);
|
|
251
579
|
} else if (type === 'mcp') {
|
|
252
580
|
formHTML = this.generateMCPProfileForm(profile);
|
|
581
|
+
} else if (type === 'voice') {
|
|
582
|
+
formHTML = await this.generateVoiceProfileForm(profile);
|
|
253
583
|
}
|
|
254
584
|
|
|
255
585
|
if (this.elements.profileForm) {
|
|
@@ -257,7 +587,7 @@ class VibeSurfSettingsManager {
|
|
|
257
587
|
this.elements.profileForm.dataset.type = type;
|
|
258
588
|
this.elements.profileForm.dataset.mode = isEdit ? 'edit' : 'create';
|
|
259
589
|
if (isEdit && profile) {
|
|
260
|
-
this.elements.profileForm.dataset.profileId = profile.profile_name || profile.mcp_id;
|
|
590
|
+
this.elements.profileForm.dataset.profileId = profile.profile_name || profile.mcp_id || profile.voice_profile_name;
|
|
261
591
|
}
|
|
262
592
|
}
|
|
263
593
|
|
|
@@ -367,6 +697,104 @@ class VibeSurfSettingsManager {
|
|
|
367
697
|
`;
|
|
368
698
|
}
|
|
369
699
|
|
|
700
|
+
async generateVoiceProfileForm(profile = null) {
|
|
701
|
+
// Fetch available voice models
|
|
702
|
+
let models = [];
|
|
703
|
+
try {
|
|
704
|
+
const response = await this.apiClient.getVoiceModels();
|
|
705
|
+
models = response.models || response || [];
|
|
706
|
+
} catch (error) {
|
|
707
|
+
console.error('[SettingsManager] Failed to fetch voice models:', error);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Group models by type
|
|
711
|
+
const asrModels = models.filter(m => m.model_type === 'asr');
|
|
712
|
+
const ttsModels = models.filter(m => m.model_type === 'tts');
|
|
713
|
+
|
|
714
|
+
const selectedModelType = profile?.voice_model_type || 'asr';
|
|
715
|
+
const availableModels = selectedModelType === 'asr' ? asrModels : ttsModels;
|
|
716
|
+
|
|
717
|
+
const modelsOptions = availableModels.map(m =>
|
|
718
|
+
`<option value="${m.model_name}" ${profile?.voice_model_name === m.model_name ? 'selected' : ''}>${m.model_name}</option>`
|
|
719
|
+
).join('');
|
|
720
|
+
|
|
721
|
+
// Convert existing meta params to JSON for editing
|
|
722
|
+
let defaultMetaJson = '{}';
|
|
723
|
+
if (profile?.voice_meta_params) {
|
|
724
|
+
try {
|
|
725
|
+
defaultMetaJson = JSON.stringify(profile.voice_meta_params, null, 2);
|
|
726
|
+
} catch (error) {
|
|
727
|
+
console.warn('[SettingsManager] Failed to stringify existing voice_meta_params:', error);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
return `
|
|
732
|
+
<div class="form-group">
|
|
733
|
+
<label class="form-label required">Profile Name</label>
|
|
734
|
+
<input type="text" name="voice_profile_name" class="form-input" value="${profile?.voice_profile_name || ''}"
|
|
735
|
+
placeholder="Enter a unique name for this profile" required ${profile ? 'readonly' : ''}>
|
|
736
|
+
<div class="form-help">A unique identifier for this voice configuration</div>
|
|
737
|
+
</div>
|
|
738
|
+
|
|
739
|
+
<div class="form-group">
|
|
740
|
+
<label class="form-label required">Model Type</label>
|
|
741
|
+
<select name="voice_model_type" class="form-select" required>
|
|
742
|
+
<option value="asr" ${selectedModelType === 'asr' ? 'selected' : ''}>ASR (Speech Recognition)</option>
|
|
743
|
+
<option value="tts" ${selectedModelType === 'tts' ? 'selected' : ''}>TTS (Text to Speech)</option>
|
|
744
|
+
</select>
|
|
745
|
+
<div class="form-help">Choose the type of voice model</div>
|
|
746
|
+
</div>
|
|
747
|
+
|
|
748
|
+
<div class="form-group">
|
|
749
|
+
<label class="form-label required">Voice Model</label>
|
|
750
|
+
<select name="voice_model_name" class="form-select voice-model-select" required>
|
|
751
|
+
<option value="">Select a model</option>
|
|
752
|
+
${modelsOptions}
|
|
753
|
+
</select>
|
|
754
|
+
<div class="form-help">Choose your voice model</div>
|
|
755
|
+
</div>
|
|
756
|
+
|
|
757
|
+
<div class="form-group api-key-field">
|
|
758
|
+
<label class="form-label required">API Key</label>
|
|
759
|
+
<input type="password" name="api_key" class="form-input api-key-input"
|
|
760
|
+
placeholder="${profile ? 'Leave empty to keep existing key' : 'Enter your API key'}"
|
|
761
|
+
${profile ? '' : 'required'}>
|
|
762
|
+
<button type="button" class="api-key-toggle" title="Toggle visibility">
|
|
763
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
764
|
+
<path d="M1 12S5 4 12 4S23 12 23 12S19 20 12 20S1 12 1 12Z" stroke="currentColor" stroke-width="2"/>
|
|
765
|
+
<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2"/>
|
|
766
|
+
</svg>
|
|
767
|
+
</button>
|
|
768
|
+
<div class="form-help">Your voice provider's API key for authentication</div>
|
|
769
|
+
</div>
|
|
770
|
+
|
|
771
|
+
<div class="form-group">
|
|
772
|
+
<label class="form-label">Model Parameters (JSON)</label>
|
|
773
|
+
<textarea name="voice_meta_params_json" class="form-textarea json-input" rows="4"
|
|
774
|
+
placeholder="Enter JSON configuration for model parameters (optional)">${defaultMetaJson}</textarea>
|
|
775
|
+
<div class="json-validation-feedback"></div>
|
|
776
|
+
<div class="form-help">
|
|
777
|
+
Optional JSON configuration for model-specific parameters. Example:
|
|
778
|
+
<br><code>{"language": "zh", "sample_rate": 16000}</code>
|
|
779
|
+
</div>
|
|
780
|
+
</div>
|
|
781
|
+
|
|
782
|
+
<div class="form-group">
|
|
783
|
+
<label class="form-label">Description</label>
|
|
784
|
+
<textarea name="description" class="form-textarea" placeholder="Optional description for this profile">${profile?.description || ''}</textarea>
|
|
785
|
+
<div class="form-help">Optional description to help identify this profile</div>
|
|
786
|
+
</div>
|
|
787
|
+
|
|
788
|
+
<div class="form-group">
|
|
789
|
+
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
|
|
790
|
+
<input type="checkbox" name="is_active" ${profile?.is_active !== false ? 'checked' : ''}>
|
|
791
|
+
<span class="form-label" style="margin: 0;">Active</span>
|
|
792
|
+
</label>
|
|
793
|
+
<div class="form-help">Whether this voice profile is active and available for use</div>
|
|
794
|
+
</div>
|
|
795
|
+
`;
|
|
796
|
+
}
|
|
797
|
+
|
|
370
798
|
generateMCPProfileForm(profile = null) {
|
|
371
799
|
// Convert existing profile to JSON for editing
|
|
372
800
|
let defaultJson = '{\n "command": "npx",\n "args": [\n "-y",\n "@modelcontextprotocol/server-filesystem",\n "/path/to/directory"\n ]\n}';
|
|
@@ -430,6 +858,12 @@ class VibeSurfSettingsManager {
|
|
|
430
858
|
providerSelect.addEventListener('change', this.handleProviderChange.bind(this));
|
|
431
859
|
}
|
|
432
860
|
|
|
861
|
+
// Voice model type change handler for Voice profiles
|
|
862
|
+
const voiceModelTypeSelect = this.elements.profileForm?.querySelector('select[name="voice_model_type"]');
|
|
863
|
+
if (voiceModelTypeSelect) {
|
|
864
|
+
voiceModelTypeSelect.addEventListener('change', this.handleVoiceModelTypeChange.bind(this));
|
|
865
|
+
}
|
|
866
|
+
|
|
433
867
|
// API key toggle handler
|
|
434
868
|
const apiKeyToggle = this.elements.profileForm?.querySelector('.api-key-toggle');
|
|
435
869
|
const apiKeyInput = this.elements.profileForm?.querySelector('.api-key-input');
|
|
@@ -457,6 +891,16 @@ class VibeSurfSettingsManager {
|
|
|
457
891
|
// Trigger initial validation
|
|
458
892
|
this.handleJsonInputValidation({ target: jsonInput });
|
|
459
893
|
}
|
|
894
|
+
|
|
895
|
+
// JSON validation handler for Voice meta params
|
|
896
|
+
const voiceJsonInput = this.elements.profileForm?.querySelector('textarea[name="voice_meta_params_json"]');
|
|
897
|
+
if (voiceJsonInput) {
|
|
898
|
+
voiceJsonInput.addEventListener('input', this.handleVoiceJsonInputValidation.bind(this));
|
|
899
|
+
voiceJsonInput.addEventListener('blur', this.handleVoiceJsonInputValidation.bind(this));
|
|
900
|
+
|
|
901
|
+
// Trigger initial validation
|
|
902
|
+
this.handleVoiceJsonInputValidation({ target: voiceJsonInput });
|
|
903
|
+
}
|
|
460
904
|
}
|
|
461
905
|
|
|
462
906
|
handleJsonInputValidation(event) {
|
|
@@ -511,6 +955,48 @@ class VibeSurfSettingsManager {
|
|
|
511
955
|
}
|
|
512
956
|
}
|
|
513
957
|
|
|
958
|
+
handleVoiceJsonInputValidation(event) {
|
|
959
|
+
const textarea = event.target;
|
|
960
|
+
const feedbackElement = textarea.parentElement.querySelector('.json-validation-feedback');
|
|
961
|
+
|
|
962
|
+
if (!feedbackElement) return;
|
|
963
|
+
|
|
964
|
+
const jsonText = textarea.value.trim();
|
|
965
|
+
|
|
966
|
+
if (!jsonText) {
|
|
967
|
+
feedbackElement.innerHTML = '';
|
|
968
|
+
textarea.classList.remove('json-valid', 'json-invalid');
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
try {
|
|
973
|
+
const parsed = JSON.parse(jsonText);
|
|
974
|
+
|
|
975
|
+
// Validate that it's an object (not array, string, etc.)
|
|
976
|
+
if (typeof parsed !== 'object' || Array.isArray(parsed) || parsed === null) {
|
|
977
|
+
throw new Error('Voice meta parameters must be a JSON object');
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// Success - no specific validation required for voice meta params (flexible structure)
|
|
981
|
+
feedbackElement.innerHTML = '<span class="json-success">✓ Valid JSON configuration</span>';
|
|
982
|
+
textarea.classList.remove('json-invalid');
|
|
983
|
+
textarea.classList.add('json-valid');
|
|
984
|
+
|
|
985
|
+
// Store valid state for form submission
|
|
986
|
+
textarea.dataset.isValid = 'true';
|
|
987
|
+
|
|
988
|
+
} catch (error) {
|
|
989
|
+
const errorMessage = error.message;
|
|
990
|
+
feedbackElement.innerHTML = `<span class="json-error">✗ Invalid JSON: ${errorMessage}</span>`;
|
|
991
|
+
textarea.classList.remove('json-valid');
|
|
992
|
+
textarea.classList.add('json-invalid');
|
|
993
|
+
|
|
994
|
+
// Store invalid state for form submission
|
|
995
|
+
textarea.dataset.isValid = 'false';
|
|
996
|
+
textarea.dataset.errorMessage = errorMessage;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
514
1000
|
handleProfileFormSubmitClick(event) {
|
|
515
1001
|
console.log('[SettingsManager] Profile form submit button clicked');
|
|
516
1002
|
event.preventDefault();
|
|
@@ -523,6 +1009,41 @@ class VibeSurfSettingsManager {
|
|
|
523
1009
|
}
|
|
524
1010
|
}
|
|
525
1011
|
|
|
1012
|
+
async handleVoiceModelTypeChange(event) {
|
|
1013
|
+
const selectedType = event.target.value;
|
|
1014
|
+
const modelSelect = this.elements.profileForm?.querySelector('select[name="voice_model_name"]');
|
|
1015
|
+
|
|
1016
|
+
if (!selectedType || !modelSelect) {
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// Clear current options
|
|
1021
|
+
modelSelect.innerHTML = '<option value="">Loading...</option>';
|
|
1022
|
+
|
|
1023
|
+
try {
|
|
1024
|
+
const response = await this.apiClient.getVoiceModels(selectedType);
|
|
1025
|
+
const models = response.models || response || [];
|
|
1026
|
+
|
|
1027
|
+
// Models are already filtered by the API, no need to filter again
|
|
1028
|
+
|
|
1029
|
+
// Update select options
|
|
1030
|
+
modelSelect.innerHTML = '<option value="">Select a model</option>' +
|
|
1031
|
+
models.map(model =>
|
|
1032
|
+
`<option value="${model.model_name}">${model.model_name}</option>`
|
|
1033
|
+
).join('');
|
|
1034
|
+
|
|
1035
|
+
} catch (error) {
|
|
1036
|
+
console.error('[SettingsManager] Failed to fetch voice models for type:', error);
|
|
1037
|
+
modelSelect.innerHTML = '<option value="">Failed to load models</option>';
|
|
1038
|
+
|
|
1039
|
+
// Show user-friendly error notification
|
|
1040
|
+
this.emit('notification', {
|
|
1041
|
+
message: `Failed to load models for ${selectedType}. Please try again.`,
|
|
1042
|
+
type: 'warning'
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
526
1047
|
async handleProviderChange(event) {
|
|
527
1048
|
const selectedProvider = event.target.value;
|
|
528
1049
|
const modelInput = this.elements.profileForm?.querySelector('input[name="model"]');
|
|
@@ -623,6 +1144,35 @@ class VibeSurfSettingsManager {
|
|
|
623
1144
|
}
|
|
624
1145
|
}
|
|
625
1146
|
|
|
1147
|
+
// Handle Voice profile meta params structure - parse JSON input
|
|
1148
|
+
if (type === 'voice') {
|
|
1149
|
+
const jsonInput = data.voice_meta_params_json;
|
|
1150
|
+
|
|
1151
|
+
if (jsonInput && jsonInput.trim()) {
|
|
1152
|
+
try {
|
|
1153
|
+
const parsedParams = JSON.parse(jsonInput);
|
|
1154
|
+
|
|
1155
|
+
// Validate that it's an object (not array, string, etc.)
|
|
1156
|
+
if (typeof parsedParams !== 'object' || Array.isArray(parsedParams) || parsedParams === null) {
|
|
1157
|
+
throw new Error('Voice meta parameters must be a JSON object');
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// Set the parsed parameters
|
|
1161
|
+
data.voice_meta_params = parsedParams;
|
|
1162
|
+
|
|
1163
|
+
} catch (error) {
|
|
1164
|
+
console.error('[SettingsManager] Failed to parse Voice meta params JSON:', error);
|
|
1165
|
+
this.emit('error', { message: error.message });
|
|
1166
|
+
form.dataset.submitting = 'false';
|
|
1167
|
+
this.setProfileFormSubmitting(false);
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// Remove the JSON field as it's not needed in the API request
|
|
1173
|
+
delete data.voice_meta_params_json;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
626
1176
|
// Handle MCP server params structure - parse JSON input
|
|
627
1177
|
if (type === 'mcp') {
|
|
628
1178
|
const jsonInput = data.mcp_server_params_json;
|
|
@@ -679,12 +1229,16 @@ class VibeSurfSettingsManager {
|
|
|
679
1229
|
if (mode === 'create') {
|
|
680
1230
|
if (type === 'llm') {
|
|
681
1231
|
response = await this.apiClient.createLLMProfile(data);
|
|
1232
|
+
} else if (type === 'voice') {
|
|
1233
|
+
response = await this.apiClient.createVoiceProfile(data);
|
|
682
1234
|
} else {
|
|
683
1235
|
response = await this.apiClient.createMCPProfile(data);
|
|
684
1236
|
}
|
|
685
1237
|
} else {
|
|
686
1238
|
if (type === 'llm') {
|
|
687
1239
|
response = await this.apiClient.updateLLMProfile(profileId, data);
|
|
1240
|
+
} else if (type === 'voice') {
|
|
1241
|
+
response = await this.apiClient.updateVoiceProfile(profileId, data);
|
|
688
1242
|
} else {
|
|
689
1243
|
response = await this.apiClient.updateMCPProfile(profileId, data);
|
|
690
1244
|
}
|
|
@@ -779,6 +1333,60 @@ class VibeSurfSettingsManager {
|
|
|
779
1333
|
}
|
|
780
1334
|
}
|
|
781
1335
|
|
|
1336
|
+
async handleThemeChange(event) {
|
|
1337
|
+
const selectedTheme = event.target.value;
|
|
1338
|
+
|
|
1339
|
+
try {
|
|
1340
|
+
// Store theme preference in user settings storage
|
|
1341
|
+
await this.userSettingsStorage.setTheme(selectedTheme);
|
|
1342
|
+
|
|
1343
|
+
// Apply theme to document
|
|
1344
|
+
this.applyTheme(selectedTheme);
|
|
1345
|
+
|
|
1346
|
+
this.emit('notification', {
|
|
1347
|
+
message: `Theme changed to ${selectedTheme}`,
|
|
1348
|
+
type: 'success'
|
|
1349
|
+
});
|
|
1350
|
+
|
|
1351
|
+
} catch (error) {
|
|
1352
|
+
console.error('[SettingsManager] Failed to change theme:', error);
|
|
1353
|
+
this.emit('notification', {
|
|
1354
|
+
message: 'Failed to change theme',
|
|
1355
|
+
type: 'error'
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
|
|
1361
|
+
applyTheme(theme) {
|
|
1362
|
+
const root = document.documentElement;
|
|
1363
|
+
|
|
1364
|
+
if (theme === 'dark') {
|
|
1365
|
+
root.setAttribute('data-theme', 'dark');
|
|
1366
|
+
root.classList.add('dark-theme');
|
|
1367
|
+
root.classList.remove('light-theme');
|
|
1368
|
+
} else if (theme === 'light') {
|
|
1369
|
+
root.setAttribute('data-theme', 'light');
|
|
1370
|
+
root.classList.add('light-theme');
|
|
1371
|
+
root.classList.remove('dark-theme');
|
|
1372
|
+
} else { // auto
|
|
1373
|
+
root.classList.remove('dark-theme', 'light-theme');
|
|
1374
|
+
// Let system preference take over
|
|
1375
|
+
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
1376
|
+
root.setAttribute('data-theme', 'dark');
|
|
1377
|
+
root.classList.add('dark-theme');
|
|
1378
|
+
} else {
|
|
1379
|
+
root.setAttribute('data-theme', 'light');
|
|
1380
|
+
root.classList.add('light-theme');
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// Force a repaint to ensure theme changes are applied immediately
|
|
1385
|
+
document.body.style.display = 'none';
|
|
1386
|
+
document.body.offsetHeight; // trigger reflow
|
|
1387
|
+
document.body.style.display = '';
|
|
1388
|
+
}
|
|
1389
|
+
|
|
782
1390
|
async handleBackendUrlChange(event) {
|
|
783
1391
|
const newUrl = event.target.value.trim();
|
|
784
1392
|
|
|
@@ -1006,6 +1614,79 @@ class VibeSurfSettingsManager {
|
|
|
1006
1614
|
});
|
|
1007
1615
|
}
|
|
1008
1616
|
|
|
1617
|
+
renderVoiceProfiles(profiles) {
|
|
1618
|
+
const container = document.getElementById('voice-profiles-list');
|
|
1619
|
+
if (!container) return;
|
|
1620
|
+
|
|
1621
|
+
if (profiles.length === 0) {
|
|
1622
|
+
container.innerHTML = `
|
|
1623
|
+
<div class="empty-state">
|
|
1624
|
+
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
1625
|
+
<path d="M19 14C19 18.5 15.5 22 11 22C6.5 22 3 18.5 3 14V12C3 7.5 6.5 4 11 4S19 7.5 19 12V14ZM11 8C8.8 8 7 9.8 7 12V14C7 16.2 8.8 18 11 18S15 16.2 15 14V12C15 9.8 13.2 8 11 8Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
1626
|
+
<circle cx="11" cy="11" r="2" stroke="currentColor" stroke-width="2"/>
|
|
1627
|
+
</svg>
|
|
1628
|
+
<h3>No Voice Profiles</h3>
|
|
1629
|
+
<p>Create your first voice profile to enable speech features</p>
|
|
1630
|
+
</div>
|
|
1631
|
+
`;
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
const profilesHTML = profiles.map(profile => `
|
|
1636
|
+
<div class="profile-card ${profile.is_active ? 'active' : 'inactive'}" data-profile-id="${profile.voice_profile_name}">
|
|
1637
|
+
<div class="profile-status ${profile.is_active ? 'active' : 'inactive'}">
|
|
1638
|
+
${profile.is_active ? 'Active' : 'Inactive'}
|
|
1639
|
+
</div>
|
|
1640
|
+
<div class="profile-header">
|
|
1641
|
+
<div class="profile-title">
|
|
1642
|
+
<h3>${this.escapeHtml(profile.voice_profile_name)}</h3>
|
|
1643
|
+
<span class="profile-provider">${this.escapeHtml(profile.voice_model_name)} (${profile.voice_model_type.toUpperCase()})</span>
|
|
1644
|
+
</div>
|
|
1645
|
+
<div class="profile-actions">
|
|
1646
|
+
<button class="profile-action-btn edit" title="Edit Profile" data-profile='${JSON.stringify(profile)}'>
|
|
1647
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
1648
|
+
<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"/>
|
|
1649
|
+
<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"/>
|
|
1650
|
+
</svg>
|
|
1651
|
+
</button>
|
|
1652
|
+
<button class="profile-action-btn delete" title="Delete Profile" data-profile-id="${profile.voice_profile_name}">
|
|
1653
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
1654
|
+
<path d="M3 6H5H21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
1655
|
+
<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"/>
|
|
1656
|
+
</svg>
|
|
1657
|
+
</button>
|
|
1658
|
+
</div>
|
|
1659
|
+
</div>
|
|
1660
|
+
<div class="profile-content">
|
|
1661
|
+
${profile.description ? `<p class="profile-description">${this.escapeHtml(profile.description)}</p>` : ''}
|
|
1662
|
+
<div class="profile-details">
|
|
1663
|
+
<div class="profile-detail"><strong>Model:</strong> ${this.escapeHtml(profile.voice_model_name)}</div>
|
|
1664
|
+
<div class="profile-detail"><strong>Type:</strong> ${profile.voice_model_type.toUpperCase()}</div>
|
|
1665
|
+
${profile.voice_meta_params ? `<div class="profile-detail"><strong>Parameters:</strong> ${Object.keys(profile.voice_meta_params).length} custom settings</div>` : ''}
|
|
1666
|
+
</div>
|
|
1667
|
+
</div>
|
|
1668
|
+
</div>
|
|
1669
|
+
`).join('');
|
|
1670
|
+
|
|
1671
|
+
container.innerHTML = profilesHTML;
|
|
1672
|
+
|
|
1673
|
+
// Add event listeners for profile actions
|
|
1674
|
+
container.querySelectorAll('.edit').forEach(btn => {
|
|
1675
|
+
btn.addEventListener('click', (e) => {
|
|
1676
|
+
e.stopPropagation();
|
|
1677
|
+
const profile = JSON.parse(btn.dataset.profile);
|
|
1678
|
+
this.showProfileForm('voice', profile);
|
|
1679
|
+
});
|
|
1680
|
+
});
|
|
1681
|
+
|
|
1682
|
+
container.querySelectorAll('.delete').forEach(btn => {
|
|
1683
|
+
btn.addEventListener('click', async (e) => {
|
|
1684
|
+
e.stopPropagation();
|
|
1685
|
+
await this.handleDeleteProfile('voice', btn.dataset.profileId);
|
|
1686
|
+
});
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1009
1690
|
renderEnvironmentVariables(envVars) {
|
|
1010
1691
|
const container = this.elements.envVariablesList;
|
|
1011
1692
|
if (!container) return;
|
|
@@ -1142,6 +1823,8 @@ class VibeSurfSettingsManager {
|
|
|
1142
1823
|
|
|
1143
1824
|
if (type === 'llm') {
|
|
1144
1825
|
await this.apiClient.deleteLLMProfile(profileId);
|
|
1826
|
+
} else if (type === 'voice') {
|
|
1827
|
+
await this.apiClient.deleteVoiceProfile(profileId);
|
|
1145
1828
|
} else {
|
|
1146
1829
|
await this.apiClient.deleteMCPProfile(profileId);
|
|
1147
1830
|
}
|
|
@@ -1185,6 +1868,10 @@ class VibeSurfSettingsManager {
|
|
|
1185
1868
|
return this.state.mcpProfiles || [];
|
|
1186
1869
|
}
|
|
1187
1870
|
|
|
1871
|
+
getVoiceProfiles() {
|
|
1872
|
+
return this.state.voiceProfiles || [];
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1188
1875
|
showModal() {
|
|
1189
1876
|
if (this.elements.settingsModal) {
|
|
1190
1877
|
this.elements.settingsModal.classList.remove('hidden');
|