vibesurf 0.1.0__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.

Files changed (70) hide show
  1. vibe_surf/__init__.py +12 -0
  2. vibe_surf/_version.py +34 -0
  3. vibe_surf/agents/__init__.py +0 -0
  4. vibe_surf/agents/browser_use_agent.py +1106 -0
  5. vibe_surf/agents/prompts/__init__.py +1 -0
  6. vibe_surf/agents/prompts/vibe_surf_prompt.py +176 -0
  7. vibe_surf/agents/report_writer_agent.py +360 -0
  8. vibe_surf/agents/vibe_surf_agent.py +1632 -0
  9. vibe_surf/backend/__init__.py +0 -0
  10. vibe_surf/backend/api/__init__.py +3 -0
  11. vibe_surf/backend/api/activity.py +243 -0
  12. vibe_surf/backend/api/config.py +740 -0
  13. vibe_surf/backend/api/files.py +322 -0
  14. vibe_surf/backend/api/models.py +257 -0
  15. vibe_surf/backend/api/task.py +300 -0
  16. vibe_surf/backend/database/__init__.py +13 -0
  17. vibe_surf/backend/database/manager.py +129 -0
  18. vibe_surf/backend/database/models.py +164 -0
  19. vibe_surf/backend/database/queries.py +922 -0
  20. vibe_surf/backend/database/schemas.py +100 -0
  21. vibe_surf/backend/llm_config.py +182 -0
  22. vibe_surf/backend/main.py +137 -0
  23. vibe_surf/backend/migrations/__init__.py +16 -0
  24. vibe_surf/backend/migrations/init_db.py +303 -0
  25. vibe_surf/backend/migrations/seed_data.py +236 -0
  26. vibe_surf/backend/shared_state.py +601 -0
  27. vibe_surf/backend/utils/__init__.py +7 -0
  28. vibe_surf/backend/utils/encryption.py +164 -0
  29. vibe_surf/backend/utils/llm_factory.py +225 -0
  30. vibe_surf/browser/__init__.py +8 -0
  31. vibe_surf/browser/agen_browser_profile.py +130 -0
  32. vibe_surf/browser/agent_browser_session.py +416 -0
  33. vibe_surf/browser/browser_manager.py +296 -0
  34. vibe_surf/browser/utils.py +790 -0
  35. vibe_surf/browser/watchdogs/__init__.py +0 -0
  36. vibe_surf/browser/watchdogs/action_watchdog.py +291 -0
  37. vibe_surf/browser/watchdogs/dom_watchdog.py +954 -0
  38. vibe_surf/chrome_extension/background.js +558 -0
  39. vibe_surf/chrome_extension/config.js +48 -0
  40. vibe_surf/chrome_extension/content.js +284 -0
  41. vibe_surf/chrome_extension/dev-reload.js +47 -0
  42. vibe_surf/chrome_extension/icons/convert-svg.js +33 -0
  43. vibe_surf/chrome_extension/icons/logo-preview.html +187 -0
  44. vibe_surf/chrome_extension/icons/logo.png +0 -0
  45. vibe_surf/chrome_extension/manifest.json +53 -0
  46. vibe_surf/chrome_extension/popup.html +134 -0
  47. vibe_surf/chrome_extension/scripts/api-client.js +473 -0
  48. vibe_surf/chrome_extension/scripts/main.js +491 -0
  49. vibe_surf/chrome_extension/scripts/markdown-it.min.js +3 -0
  50. vibe_surf/chrome_extension/scripts/session-manager.js +599 -0
  51. vibe_surf/chrome_extension/scripts/ui-manager.js +3687 -0
  52. vibe_surf/chrome_extension/sidepanel.html +347 -0
  53. vibe_surf/chrome_extension/styles/animations.css +471 -0
  54. vibe_surf/chrome_extension/styles/components.css +670 -0
  55. vibe_surf/chrome_extension/styles/main.css +2307 -0
  56. vibe_surf/chrome_extension/styles/settings.css +1100 -0
  57. vibe_surf/cli.py +357 -0
  58. vibe_surf/controller/__init__.py +0 -0
  59. vibe_surf/controller/file_system.py +53 -0
  60. vibe_surf/controller/mcp_client.py +68 -0
  61. vibe_surf/controller/vibesurf_controller.py +616 -0
  62. vibe_surf/controller/views.py +37 -0
  63. vibe_surf/llm/__init__.py +21 -0
  64. vibe_surf/llm/openai_compatible.py +237 -0
  65. vibesurf-0.1.0.dist-info/METADATA +97 -0
  66. vibesurf-0.1.0.dist-info/RECORD +70 -0
  67. vibesurf-0.1.0.dist-info/WHEEL +5 -0
  68. vibesurf-0.1.0.dist-info/entry_points.txt +2 -0
  69. vibesurf-0.1.0.dist-info/licenses/LICENSE +201 -0
  70. vibesurf-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,3687 @@
1
+ // UI Manager - Handles all user interface interactions and state updates
2
+ // Manages DOM manipulation, form handling, modal displays, and UI state
3
+
4
+ class VibeSurfUIManager {
5
+ constructor(sessionManager, apiClient) {
6
+ this.sessionManager = sessionManager;
7
+ this.apiClient = apiClient;
8
+ this.elements = {};
9
+ this.state = {
10
+ isLoading: false,
11
+ currentModal: null,
12
+ llmProfiles: [],
13
+ mcpProfiles: [],
14
+ settings: {},
15
+ isTaskRunning: false,
16
+ taskInfo: null,
17
+ // File upload state
18
+ uploadedFiles: [],
19
+ // History-related state
20
+ historyMode: 'recent', // 'recent' or 'all'
21
+ currentPage: 1,
22
+ totalPages: 1,
23
+ pageSize: 10,
24
+ searchQuery: '',
25
+ statusFilter: 'all',
26
+ recentTasks: [],
27
+ allSessions: []
28
+ };
29
+
30
+ this.bindElements();
31
+ this.bindEvents();
32
+ this.setupSessionListeners();
33
+ this.setupHistoryModalHandlers();
34
+
35
+ }
36
+
37
+ bindElements() {
38
+ // Main UI elements
39
+ this.elements = {
40
+ // Header elements
41
+ newSessionBtn: document.getElementById('new-session-btn'),
42
+ historyBtn: document.getElementById('history-btn'),
43
+ settingsBtn: document.getElementById('settings-btn'),
44
+
45
+ // Session info
46
+ sessionId: document.getElementById('session-id'),
47
+ copySessionBtn: document.getElementById('copy-session-btn'),
48
+
49
+ // Activity area
50
+ activityLog: document.getElementById('activity-log'),
51
+
52
+ // Control panel
53
+ controlPanel: document.getElementById('control-panel'),
54
+ cancelBtn: document.getElementById('cancel-btn'),
55
+ resumeBtn: document.getElementById('resume-btn'),
56
+ terminateBtn: document.getElementById('terminate-btn'),
57
+
58
+ // Input area
59
+ llmProfileSelect: document.getElementById('llm-profile-select'),
60
+ taskInput: document.getElementById('task-input'),
61
+ attachFileBtn: document.getElementById('attach-file-btn'),
62
+ sendBtn: document.getElementById('send-btn'),
63
+ fileInput: document.getElementById('file-input'),
64
+
65
+ // Modals
66
+ historyModal: document.getElementById('history-modal'),
67
+ settingsModal: document.getElementById('settings-modal'),
68
+
69
+ // History Modal Elements
70
+ recentTasksList: document.getElementById('recent-tasks-list'),
71
+ viewMoreTasksBtn: document.getElementById('view-more-tasks-btn'),
72
+ allSessionsSection: document.getElementById('all-sessions-section'),
73
+ backToRecentBtn: document.getElementById('back-to-recent-btn'),
74
+ sessionSearch: document.getElementById('session-search'),
75
+ sessionFilter: document.getElementById('session-filter'),
76
+ allSessionsList: document.getElementById('all-sessions-list'),
77
+ prevPageBtn: document.getElementById('prev-page-btn'),
78
+ nextPageBtn: document.getElementById('next-page-btn'),
79
+ pageInfo: document.getElementById('page-info'),
80
+
81
+ // Settings - New Structure
82
+ settingsTabs: document.querySelectorAll('.settings-tab'),
83
+ settingsTabContents: document.querySelectorAll('.settings-tab-content'),
84
+ llmProfilesContainer: document.getElementById('llm-profiles-container'),
85
+ mcpProfilesContainer: document.getElementById('mcp-profiles-container'),
86
+ addLlmProfileBtn: document.getElementById('add-llm-profile-btn'),
87
+ addMcpProfileBtn: document.getElementById('add-mcp-profile-btn'),
88
+ backendUrl: document.getElementById('backend-url'),
89
+
90
+ // Profile Form Modal
91
+ profileFormModal: document.getElementById('profile-form-modal'),
92
+ profileFormTitle: document.getElementById('profile-form-title'),
93
+ profileForm: document.getElementById('profile-form'),
94
+ profileFormCancel: document.getElementById('profile-form-cancel'),
95
+ profileFormSubmit: document.getElementById('profile-form-submit'),
96
+ profileFormClose: document.querySelector('.profile-form-close'),
97
+
98
+ // Environment Variables
99
+ envVariablesList: document.getElementById('env-variables-list'),
100
+ saveEnvVarsBtn: document.getElementById('save-env-vars-btn'),
101
+
102
+ // Loading
103
+ loadingOverlay: document.getElementById('loading-overlay')
104
+ };
105
+
106
+ // Validate critical elements
107
+ const criticalElements = ['activityLog', 'taskInput', 'sendBtn', 'sessionId'];
108
+ for (const key of criticalElements) {
109
+ if (!this.elements[key]) {
110
+ console.error(`[UIManager] Critical element not found: ${key}`);
111
+ }
112
+ }
113
+ }
114
+
115
+ bindEvents() {
116
+ // Header buttons
117
+ this.elements.newSessionBtn?.addEventListener('click', this.handleNewSession.bind(this));
118
+ this.elements.historyBtn?.addEventListener('click', this.handleShowHistory.bind(this));
119
+ this.elements.settingsBtn?.addEventListener('click', this.handleShowSettings.bind(this));
120
+
121
+ // Session controls
122
+ this.elements.copySessionBtn?.addEventListener('click', this.handleCopySession.bind(this));
123
+
124
+ // Task controls
125
+ this.elements.cancelBtn?.addEventListener('click', this.handleCancelTask.bind(this));
126
+ this.elements.resumeBtn?.addEventListener('click', this.handleResumeTask.bind(this));
127
+ this.elements.terminateBtn?.addEventListener('click', this.handleTerminateTask.bind(this));
128
+
129
+ // Input handling
130
+ this.elements.sendBtn?.addEventListener('click', this.handleSendTask.bind(this));
131
+ this.elements.attachFileBtn?.addEventListener('click', this.handleAttachFiles.bind(this));
132
+ this.elements.fileInput?.addEventListener('change', this.handleFileSelection.bind(this));
133
+
134
+ // Task input handling
135
+ this.elements.taskInput?.addEventListener('keydown', this.handleTaskInputKeydown.bind(this));
136
+ this.elements.taskInput?.addEventListener('input', this.handleTaskInputChange.bind(this));
137
+
138
+ // LLM profile selection handling
139
+ this.elements.llmProfileSelect?.addEventListener('change', this.handleLlmProfileChange.bind(this));
140
+
141
+ // Initialize auto-resize for textarea
142
+ if (this.elements.taskInput) {
143
+ this.autoResizeTextarea(this.elements.taskInput);
144
+ // Set initial send button state
145
+ this.handleTaskInputChange({ target: this.elements.taskInput });
146
+ }
147
+
148
+ // Bind initial task suggestions if present
149
+ this.bindTaskSuggestionEvents();
150
+
151
+ // Settings handling
152
+ this.elements.backendUrl?.addEventListener('change', this.handleBackendUrlChange.bind(this));
153
+ this.elements.addLlmProfileBtn?.addEventListener('click', () => this.handleAddProfile('llm'));
154
+ this.elements.addMcpProfileBtn?.addEventListener('click', () => this.handleAddProfile('mcp'));
155
+
156
+ // Settings tabs
157
+ this.elements.settingsTabs?.forEach(tab => {
158
+ tab.addEventListener('click', this.handleTabSwitch.bind(this));
159
+ });
160
+
161
+ // Profile form modal
162
+ this.elements.profileFormCancel?.addEventListener('click', this.closeProfileForm.bind(this));
163
+ this.elements.profileFormClose?.addEventListener('click', this.closeProfileForm.bind(this));
164
+
165
+ // Profile form submission - add both form submit and button click handlers
166
+ if (this.elements.profileForm) {
167
+ this.elements.profileForm.addEventListener('submit', this.handleProfileFormSubmit.bind(this));
168
+ console.log('[UIManager] Profile form submit listener added');
169
+ } else {
170
+ console.warn('[UIManager] Profile form element not found during initialization');
171
+ }
172
+
173
+ if (this.elements.profileFormSubmit) {
174
+ this.elements.profileFormSubmit.addEventListener('click', this.handleProfileFormSubmitClick.bind(this));
175
+ console.log('[UIManager] Profile form submit button listener added');
176
+ } else {
177
+ console.warn('[UIManager] Profile form submit button not found during initialization');
178
+ }
179
+
180
+ // Environment variables
181
+ this.elements.saveEnvVarsBtn?.addEventListener('click', this.handleSaveEnvironmentVariables.bind(this));
182
+
183
+ // Modal handling
184
+ this.bindModalEvents();
185
+
186
+ // File link handling
187
+ this.bindFileLinkEvents();
188
+
189
+ }
190
+
191
+ bindModalEvents() {
192
+ // Close modals when clicking overlay or close button
193
+ document.addEventListener('click', (event) => {
194
+ if (event.target.classList.contains('modal-overlay') ||
195
+ event.target.classList.contains('modal-close') ||
196
+ event.target.closest('.modal-close')) {
197
+ this.closeModal();
198
+ }
199
+ });
200
+
201
+ // Close modals with Escape key
202
+ document.addEventListener('keydown', (event) => {
203
+ if (event.key === 'Escape' && this.state.currentModal) {
204
+ this.closeModal();
205
+ }
206
+ });
207
+ }
208
+
209
+ bindFileLinkEvents() {
210
+ // Handle file:// link clicks with delegation
211
+ document.addEventListener('click', (event) => {
212
+ const target = event.target;
213
+
214
+ // Check if clicked element is a file link
215
+ if (target.matches('a.file-link') || target.closest('a.file-link')) {
216
+ event.preventDefault();
217
+
218
+ const fileLink = target.matches('a.file-link') ? target : target.closest('a.file-link');
219
+ const filePath = fileLink.getAttribute('data-file-path');
220
+
221
+ this.handleFileLink(filePath);
222
+ }
223
+ });
224
+
225
+ }
226
+
227
+ async handleFileLink(filePath) {
228
+ // Prevent multiple simultaneous calls
229
+ if (this._isHandlingFileLink) {
230
+ return;
231
+ }
232
+
233
+ this._isHandlingFileLink = true;
234
+
235
+ try {
236
+ // First decode the URL-encoded path
237
+ let decodedPath = decodeURIComponent(filePath);
238
+
239
+ // Remove file:// protocol prefix and normalize
240
+ let cleanPath = decodedPath.replace(/^file:\/\/\//, '').replace(/^file:\/\//, '');
241
+
242
+ // Ensure path starts with / for Unix paths if not Windows drive
243
+ if (!cleanPath.startsWith('/') && !cleanPath.match(/^[A-Za-z]:/)) {
244
+ cleanPath = '/' + cleanPath;
245
+ }
246
+
247
+ // Convert all backslashes to forward slashes
248
+ cleanPath = cleanPath.replace(/\\/g, '/');
249
+
250
+ // Create proper file URL - always use triple slash for proper format
251
+ const fileUrl = cleanPath.match(/^[A-Za-z]:/) ?
252
+ `file:///${cleanPath}` :
253
+ `file:///${cleanPath.replace(/^\//, '')}`; // Remove leading slash and add triple slash
254
+
255
+ // Create Windows format path for system open
256
+ const windowsPath = cleanPath.replace(/\//g, '\\');
257
+
258
+ // Show user notification about the action
259
+ this.showNotification(`Opening file: ${cleanPath}`, 'info');
260
+
261
+ // Use setTimeout to prevent UI blocking
262
+ setTimeout(async () => {
263
+ try {
264
+ // Primary strategy: Try browser open first for HTML files (more reliable)
265
+ if (fileUrl.toLowerCase().endsWith('.html') || fileUrl.toLowerCase().endsWith('.htm')) {
266
+ try {
267
+ const opened = window.open(fileUrl, '_blank', 'noopener,noreferrer');
268
+ if (opened) {
269
+ this.showNotification('File opened in browser', 'success');
270
+ return;
271
+ } else {
272
+ // If browser is blocked, try system open
273
+ await this.trySystemOpen(windowsPath, fileUrl);
274
+ return;
275
+ }
276
+ } catch (browserError) {
277
+ await this.trySystemOpen(windowsPath, fileUrl);
278
+ return;
279
+ }
280
+ } else {
281
+ // For non-HTML files, try system open first
282
+ const systemSuccess = await this.trySystemOpen(windowsPath, fileUrl);
283
+ if (systemSuccess) return;
284
+
285
+ // Fallback to browser if system open fails
286
+ try {
287
+ const opened = window.open(fileUrl, '_blank', 'noopener,noreferrer');
288
+ if (opened) {
289
+ this.showNotification('File opened in browser', 'success');
290
+ return;
291
+ }
292
+ } catch (browserError) {
293
+ console.error('[UIManager] Failed to open file:', browserError);
294
+ }
295
+ }
296
+
297
+ // Last resort: Copy path to clipboard
298
+ this.copyToClipboardFallback(fileUrl);
299
+
300
+ } catch (error) {
301
+ console.error('[UIManager] Error in async file handling:', error);
302
+ this.showNotification(`Unable to open file: ${error.message}`, 'error');
303
+ } finally {
304
+ this._isHandlingFileLink = false;
305
+ }
306
+ }, 50); // Small delay to prevent UI blocking
307
+
308
+ } catch (error) {
309
+ console.error('[UIManager] Error handling file link:', error);
310
+ this.showNotification(`Unable to open file: ${error.message}`, 'error');
311
+ this._isHandlingFileLink = false;
312
+ }
313
+ }
314
+
315
+ async trySystemOpen(windowsPath, fileUrl) {
316
+ try {
317
+ const systemOpenPromise = chrome.runtime.sendMessage({
318
+ type: 'OPEN_FILE_SYSTEM',
319
+ data: { filePath: windowsPath }
320
+ });
321
+
322
+ // Add timeout to prevent hanging
323
+ const systemOpenResponse = await Promise.race([
324
+ systemOpenPromise,
325
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 3000))
326
+ ]);
327
+
328
+ if (systemOpenResponse && systemOpenResponse.success) {
329
+ this.showNotification('File opened with system default application', 'success');
330
+ return true;
331
+ }
332
+ return false;
333
+ } catch (systemError) {
334
+ return false;
335
+ }
336
+ }
337
+
338
+ async copyToClipboardFallback(fileUrl) {
339
+ try {
340
+ await navigator.clipboard.writeText(fileUrl);
341
+ this.showNotification('File URL copied to clipboard - paste in browser address bar', 'info');
342
+ } catch (clipboardError) {
343
+ console.error('[UIManager] Clipboard failed:', clipboardError);
344
+ this.showNotification('Unable to open file. URL: ' + fileUrl, 'warning');
345
+ }
346
+ }
347
+
348
+ showFileAccessInstructions(windowsPath, fileUrl, unixPath) {
349
+ const modal = this.createWarningModal({
350
+ title: 'File Access Options',
351
+ message: 'Chrome extensions cannot directly open local files due to security restrictions. Choose one of these methods to access your file:',
352
+ details: `Windows Path: ${windowsPath}\nFile URL: ${fileUrl}\n\nRecommended Methods:\n1. Copy path and open in File Explorer\n2. Copy URL and paste in new browser tab\n3. Use "Open with" from File Explorer`,
353
+ buttons: [
354
+ {
355
+ text: 'Copy Windows Path',
356
+ style: 'primary',
357
+ action: async () => {
358
+ try {
359
+ await navigator.clipboard.writeText(windowsPath);
360
+ this.showNotification('Windows file path copied to clipboard', 'success');
361
+ } catch (error) {
362
+ console.error('Failed to copy path:', error);
363
+ this.showNotification('Failed to copy, please copy manually', 'error');
364
+ }
365
+ this.closeWarningModal();
366
+ }
367
+ },
368
+ {
369
+ text: 'Copy File URL',
370
+ style: 'secondary',
371
+ action: async () => {
372
+ try {
373
+ await navigator.clipboard.writeText(fileUrl);
374
+ this.showNotification('File URL copied to clipboard', 'success');
375
+ } catch (error) {
376
+ console.error('Failed to copy URL:', error);
377
+ this.showNotification('Failed to copy, please copy manually', 'error');
378
+ }
379
+ this.closeWarningModal();
380
+ }
381
+ },
382
+ {
383
+ text: 'Try Open URL',
384
+ style: 'secondary',
385
+ action: async () => {
386
+ try {
387
+
388
+ // Try via background script first
389
+ const response = await chrome.runtime.sendMessage({
390
+ type: 'OPEN_FILE_URL',
391
+ data: { fileUrl: fileUrl }
392
+ });
393
+
394
+
395
+ if (response && response.success) {
396
+ this.showNotification('File opened successfully', 'success');
397
+ this.closeWarningModal();
398
+ return;
399
+ }
400
+
401
+
402
+ // Fallback to window.open
403
+ const opened = window.open(fileUrl, '_blank');
404
+
405
+ if (!opened) {
406
+ this.showNotification('Popup blocked. Try copying URL instead.', 'warning');
407
+ } else {
408
+ this.showNotification('Attempting to open file in new tab...', 'info');
409
+
410
+ // Check if the tab actually loaded the file after a delay
411
+ setTimeout(() => {
412
+ }, 2000);
413
+ }
414
+ } catch (error) {
415
+ console.error('[UIManager] Failed to open file:', error);
416
+ this.showNotification('Failed to open. Use copy options instead.', 'error');
417
+ }
418
+ this.closeWarningModal();
419
+ }
420
+ },
421
+ {
422
+ text: 'Close',
423
+ style: 'secondary',
424
+ action: () => {
425
+ this.closeWarningModal();
426
+ }
427
+ }
428
+ ]
429
+ });
430
+
431
+ document.body.appendChild(modal);
432
+ }
433
+
434
+ setupSessionListeners() {
435
+ // Listen to session manager events
436
+ this.sessionManager.on('sessionCreated', this.handleSessionCreated.bind(this));
437
+ this.sessionManager.on('sessionLoaded', this.handleSessionLoaded.bind(this));
438
+ this.sessionManager.on('taskSubmitted', this.handleTaskSubmitted.bind(this));
439
+ this.sessionManager.on('taskPaused', this.handleTaskPaused.bind(this));
440
+ this.sessionManager.on('taskResumed', this.handleTaskResumed.bind(this));
441
+ this.sessionManager.on('taskStopped', this.handleTaskStopped.bind(this));
442
+ this.sessionManager.on('taskCompleted', this.handleTaskCompleted.bind(this));
443
+ this.sessionManager.on('newActivity', this.handleNewActivity.bind(this));
444
+ this.sessionManager.on('pollingStarted', this.handlePollingStarted.bind(this));
445
+ this.sessionManager.on('pollingStopped', this.handlePollingStopped.bind(this));
446
+ this.sessionManager.on('sessionError', this.handleSessionError.bind(this));
447
+ this.sessionManager.on('taskError', this.handleTaskError.bind(this));
448
+
449
+ // Start periodic task status check
450
+ this.startTaskStatusMonitoring();
451
+ }
452
+
453
+ // Session event handlers
454
+ handleSessionCreated(data) {
455
+ this.updateSessionDisplay(data.sessionId);
456
+ this.clearActivityLog();
457
+ this.showWelcomeMessage();
458
+ this.updateControlPanel('ready');
459
+ }
460
+
461
+ handleSessionLoaded(data) {
462
+
463
+ // Update session display
464
+ this.updateSessionDisplay(data.sessionId);
465
+
466
+ // Load and display activity logs
467
+ const activityLogs = this.sessionManager.getActivityLogs();
468
+ this.displayActivityLogs(activityLogs);
469
+
470
+ // Update control panel
471
+ const taskStatus = this.sessionManager.getTaskStatus();
472
+ if (taskStatus) {
473
+ this.updateControlPanel(taskStatus);
474
+ }
475
+
476
+ }
477
+
478
+ handleTaskSubmitted(data) {
479
+ console.log('[UIManager] Task submitted successfully, showing control panel');
480
+ this.updateControlPanel('running');
481
+ this.clearTaskInput();
482
+ }
483
+
484
+ handleTaskPaused(data) {
485
+ this.updateControlPanel('paused');
486
+ this.showNotification('Task paused successfully', 'info');
487
+ }
488
+
489
+ handleTaskResumed(data) {
490
+ this.updateControlPanel('running');
491
+ this.showNotification('Task resumed successfully', 'info');
492
+ }
493
+
494
+ handleTaskStopped(data) {
495
+ this.updateControlPanel('ready');
496
+ this.showNotification('Task stopped successfully', 'info');
497
+ }
498
+
499
+ handleTaskCompleted(data) {
500
+ console.log('[UIManager] Task completed with status:', data.status);
501
+
502
+ const message = data.status === 'done' ? 'Task completed successfully!' : 'Task completed with errors';
503
+ const type = data.status === 'done' ? 'success' : 'error';
504
+
505
+ // Check if we need to respect minimum visibility period
506
+ if (this.controlPanelMinVisibilityActive) {
507
+ console.log('[UIManager] Task completed during minimum visibility period, delaying control panel hide');
508
+ // Wait for minimum visibility period to end before hiding
509
+ const remainingTime = 2000; // Could be calculated more precisely, but 2s is reasonable
510
+ setTimeout(() => {
511
+ console.log('[UIManager] Minimum visibility period respected, now hiding control panel');
512
+ this.updateControlPanel('ready');
513
+ }, remainingTime);
514
+ } else {
515
+ console.log('[UIManager] Task completed, hiding control panel immediately');
516
+ this.updateControlPanel('ready');
517
+ }
518
+
519
+ this.showNotification(message, type);
520
+
521
+ // Clear uploaded files when task is completed
522
+ this.clearUploadedFiles();
523
+ }
524
+
525
+ handleNewActivity(data) {
526
+ this.addActivityLog(data.activity);
527
+ this.scrollActivityToBottom();
528
+ }
529
+
530
+ handlePollingStarted(data) {
531
+ // Could add polling indicator here
532
+ }
533
+
534
+ handlePollingStopped(data) {
535
+ // Could remove polling indicator here
536
+ }
537
+
538
+ handleSessionError(data) {
539
+ console.error('[UIManager] Session error:', data.error);
540
+ this.showNotification(`Session error: ${data.error}`, 'error');
541
+ }
542
+
543
+ handleTaskError(data) {
544
+ console.error('[UIManager] Task error:', data.error);
545
+ this.showNotification(`Task error: ${data.error}`, 'error');
546
+ this.updateControlPanel('ready');
547
+ }
548
+
549
+ // Task Status Monitoring
550
+ async checkTaskStatus() {
551
+ try {
552
+ const statusCheck = await this.apiClient.checkTaskRunning();
553
+ const wasRunning = this.state.isTaskRunning;
554
+
555
+ this.state.isTaskRunning = statusCheck.isRunning;
556
+ this.state.taskInfo = statusCheck.taskInfo;
557
+
558
+ // Update UI state when task status changes
559
+ if (wasRunning !== statusCheck.isRunning) {
560
+ this.updateUIForTaskStatus(statusCheck.isRunning);
561
+ }
562
+
563
+ return statusCheck;
564
+ } catch (error) {
565
+ console.error('[UIManager] Failed to check task status:', error);
566
+ return { isRunning: false, taskInfo: null };
567
+ }
568
+ }
569
+
570
+ startTaskStatusMonitoring() {
571
+ // Check task status every 2 seconds
572
+ this.taskStatusInterval = setInterval(() => {
573
+ this.checkTaskStatus();
574
+ }, 2000);
575
+ }
576
+
577
+ stopTaskStatusMonitoring() {
578
+ if (this.taskStatusInterval) {
579
+ clearInterval(this.taskStatusInterval);
580
+ this.taskStatusInterval = null;
581
+ }
582
+ }
583
+
584
+ updateUIForTaskStatus(isRunning) {
585
+ // Disable/enable input elements based on task status
586
+ if (this.elements.taskInput) {
587
+ this.elements.taskInput.disabled = isRunning;
588
+ this.elements.taskInput.placeholder = isRunning ?
589
+ 'Task is running - please wait...' :
590
+ 'Enter your task description...';
591
+ }
592
+
593
+ if (this.elements.sendBtn) {
594
+ this.elements.sendBtn.disabled = isRunning || !this.canSubmitTask();
595
+ }
596
+
597
+ if (this.elements.llmProfileSelect) {
598
+ this.elements.llmProfileSelect.disabled = isRunning;
599
+ }
600
+
601
+ if (this.elements.attachFileBtn) {
602
+ this.elements.attachFileBtn.disabled = isRunning;
603
+ }
604
+
605
+ // Also disable header buttons when task is running
606
+ if (this.elements.newSessionBtn) {
607
+ this.elements.newSessionBtn.disabled = isRunning;
608
+ }
609
+
610
+ if (this.elements.historyBtn) {
611
+ this.elements.historyBtn.disabled = isRunning;
612
+ }
613
+
614
+ if (this.elements.settingsBtn) {
615
+ this.elements.settingsBtn.disabled = isRunning;
616
+ }
617
+
618
+ // Add visual feedback to indicate locked state
619
+ const lockableElements = [
620
+ this.elements.taskInput,
621
+ this.elements.sendBtn,
622
+ this.elements.llmProfileSelect,
623
+ this.elements.attachFileBtn,
624
+ this.elements.newSessionBtn,
625
+ this.elements.historyBtn,
626
+ this.elements.settingsBtn
627
+ ];
628
+
629
+ lockableElements.forEach(element => {
630
+ if (element) {
631
+ if (isRunning) {
632
+ element.classList.add('task-running-disabled');
633
+ element.setAttribute('title', 'Disabled while task is running');
634
+ } else {
635
+ element.classList.remove('task-running-disabled');
636
+ element.removeAttribute('title');
637
+ }
638
+ }
639
+ });
640
+
641
+ }
642
+
643
+ canSubmitTask() {
644
+ const hasText = this.elements.taskInput?.value.trim().length > 0;
645
+ const llmProfile = this.elements.llmProfileSelect?.value;
646
+ const hasLlmProfile = llmProfile && llmProfile.trim() !== '';
647
+ return hasText && hasLlmProfile && !this.state.isTaskRunning;
648
+ }
649
+
650
+ async showTaskRunningWarning(action) {
651
+ const taskInfo = this.state.taskInfo;
652
+ const taskId = taskInfo?.task_id || 'unknown';
653
+ const sessionId = taskInfo?.session_id || 'unknown';
654
+
655
+ return new Promise((resolve) => {
656
+ const modal = this.createWarningModal({
657
+ title: 'Task Currently Running',
658
+ message: `A task is currently ${taskInfo?.status || 'running'}. You must stop the current task before you can ${action}.`,
659
+ details: `Task ID: ${taskId}\nSession ID: ${sessionId}`,
660
+ buttons: [
661
+ {
662
+ text: 'Stop Current Task',
663
+ style: 'danger',
664
+ action: async () => {
665
+ try {
666
+ await this.sessionManager.stopTask('User wants to perform new action');
667
+ this.closeWarningModal();
668
+ resolve(true);
669
+ } catch (error) {
670
+ this.showNotification(`Failed to stop task: ${error.message}`, 'error');
671
+ resolve(false);
672
+ }
673
+ }
674
+ },
675
+ {
676
+ text: 'Cancel',
677
+ style: 'secondary',
678
+ action: () => {
679
+ this.closeWarningModal();
680
+ resolve(false);
681
+ }
682
+ }
683
+ ]
684
+ });
685
+
686
+ document.body.appendChild(modal);
687
+ });
688
+ }
689
+
690
+ createWarningModal({ title, message, details, buttons }) {
691
+ const modal = document.createElement('div');
692
+ modal.className = 'modal-overlay warning-modal';
693
+ modal.innerHTML = `
694
+ <div class="modal-content warning-content">
695
+ <div class="warning-header">
696
+ <div class="warning-icon">⚠️</div>
697
+ <h3>${title}</h3>
698
+ </div>
699
+ <div class="warning-body">
700
+ <p>${message}</p>
701
+ ${details ? `<pre class="warning-details">${details}</pre>` : ''}
702
+ </div>
703
+ <div class="warning-actions">
704
+ ${buttons.map((btn, index) =>
705
+ `<button class="btn btn-${btn.style}" data-action="${index}">${btn.text}</button>`
706
+ ).join('')}
707
+ </div>
708
+ </div>
709
+ `;
710
+
711
+ // Add click handlers
712
+ buttons.forEach((btn, index) => {
713
+ const btnElement = modal.querySelector(`[data-action="${index}"]`);
714
+ btnElement.addEventListener('click', btn.action);
715
+ });
716
+
717
+ return modal;
718
+ }
719
+
720
+ closeWarningModal() {
721
+ const modal = document.querySelector('.warning-modal');
722
+ if (modal) {
723
+ modal.remove();
724
+ }
725
+ }
726
+
727
+ // UI Action Handlers
728
+ async handleNewSession() {
729
+ // Enhanced task running check
730
+ const statusCheck = await this.checkTaskStatus();
731
+ if (statusCheck.isRunning) {
732
+ const canProceed = await this.showTaskRunningWarning('create a new session');
733
+ if (!canProceed) return;
734
+ }
735
+
736
+ try {
737
+ this.showLoading('Creating new session...');
738
+
739
+ const sessionId = await this.sessionManager.createSession();
740
+
741
+ this.hideLoading();
742
+ } catch (error) {
743
+ this.hideLoading();
744
+ this.showNotification(`Failed to create session: ${error.message}`, 'error');
745
+ }
746
+ }
747
+
748
+ async handleShowHistory() {
749
+ // Enhanced task running check
750
+ const statusCheck = await this.checkTaskStatus();
751
+ if (statusCheck.isRunning) {
752
+ const canProceed = await this.showTaskRunningWarning('view session history');
753
+ if (!canProceed) return;
754
+ }
755
+
756
+ try {
757
+ this.showLoading('Loading recent tasks...');
758
+
759
+ // Reset to recent tasks view
760
+ this.state.historyMode = 'recent';
761
+ await this.loadRecentTasks();
762
+ this.displayHistoryModal();
763
+
764
+ this.hideLoading();
765
+ } catch (error) {
766
+ this.hideLoading();
767
+ this.showNotification(`Failed to load history: ${error.message}`, 'error');
768
+ }
769
+ }
770
+
771
+ async handleShowSettings() {
772
+ // Enhanced task running check
773
+ const statusCheck = await this.checkTaskStatus();
774
+ if (statusCheck.isRunning) {
775
+ const canProceed = await this.showTaskRunningWarning('access settings');
776
+ if (!canProceed) return;
777
+ }
778
+
779
+ try {
780
+ this.showLoading('Loading settings...');
781
+
782
+ await this.loadSettingsData();
783
+ this.displaySettingsModal();
784
+
785
+ this.hideLoading();
786
+ } catch (error) {
787
+ this.hideLoading();
788
+ this.showNotification(`Failed to load settings: ${error.message}`, 'error');
789
+ }
790
+ }
791
+
792
+ async handleCopySession() {
793
+ const sessionId = this.sessionManager.getCurrentSessionId();
794
+
795
+ if (!sessionId) {
796
+ this.showNotification('No active session to copy', 'warning');
797
+ return;
798
+ }
799
+
800
+ try {
801
+ await navigator.clipboard.writeText(sessionId);
802
+ this.showNotification('Session ID copied to clipboard!', 'success');
803
+ } catch (error) {
804
+ console.error('[UIManager] Failed to copy session ID:', error);
805
+ this.showNotification('Failed to copy session ID', 'error');
806
+ }
807
+ }
808
+
809
+ async handleSendTask() {
810
+ // Check if task is already running with enhanced blocking
811
+ const statusCheck = await this.checkTaskStatus();
812
+ if (statusCheck.isRunning) {
813
+ const canProceed = await this.showTaskRunningWarning('send a new task');
814
+ if (!canProceed) {
815
+ this.showNotification('Cannot send task while another task is running. Please stop the current task first.', 'warning');
816
+ return;
817
+ }
818
+ }
819
+
820
+ const taskDescription = this.elements.taskInput?.value.trim();
821
+ const llmProfile = this.elements.llmProfileSelect?.value;
822
+
823
+ if (!taskDescription) {
824
+ this.showNotification('Please enter a task description', 'warning');
825
+ this.elements.taskInput?.focus();
826
+ return;
827
+ }
828
+
829
+ // Check if LLM profile is selected
830
+ if (!llmProfile || llmProfile.trim() === '') {
831
+ // Check if there are any LLM profiles available
832
+ if (this.state.llmProfiles.length === 0) {
833
+ // No LLM profiles configured at all
834
+ this.showLLMProfileRequiredModal('configure');
835
+ } else {
836
+ // LLM profiles exist but none selected
837
+ this.showLLMProfileRequiredModal('select');
838
+ }
839
+ return;
840
+ }
841
+
842
+ try {
843
+ const taskData = {
844
+ task_description: taskDescription,
845
+ llm_profile_name: llmProfile
846
+ };
847
+
848
+ // Add uploaded files path if any
849
+ if (this.state.uploadedFiles.length > 0) {
850
+ console.log('[UIManager] Raw uploaded files state:', this.state.uploadedFiles);
851
+
852
+ // Extract the first file path (backend expects single string)
853
+ const firstFile = this.state.uploadedFiles[0];
854
+ let filePath = null;
855
+
856
+ if (typeof firstFile === 'string') {
857
+ filePath = firstFile;
858
+ } else if (firstFile && typeof firstFile === 'object') {
859
+ // Extract path and normalize
860
+ filePath = firstFile.file_path || firstFile.path || firstFile.stored_filename || firstFile.file_path;
861
+ if (filePath) {
862
+ filePath = filePath.replace(/\\/g, '/');
863
+ console.log('[UIManager] Normalized file path:', filePath);
864
+ }
865
+ }
866
+
867
+ if (filePath) {
868
+ // Backend expects 'upload_files_path' as a single string
869
+ taskData.upload_files_path = filePath;
870
+ console.log('[UIManager] Set upload_files_path to:', filePath);
871
+
872
+ // Show info if multiple files uploaded but only first will be processed
873
+ if (this.state.uploadedFiles.length > 1) {
874
+ console.warn('[UIManager] Multiple files uploaded, but backend only supports single file. Using first file:', filePath);
875
+ this.showNotification(`Multiple files uploaded. Only the first file "${firstFile.name || filePath}" will be processed.`, 'warning');
876
+ }
877
+ } else {
878
+ console.error('[UIManager] Could not extract file path from uploaded file:', firstFile);
879
+ }
880
+ }
881
+
882
+ console.log('[UIManager] Complete task data being submitted:', JSON.stringify(taskData, null, 2));
883
+ await this.sessionManager.submitTask(taskData);
884
+
885
+ // Clear uploaded files after successful task submission
886
+ this.clearUploadedFiles();
887
+ } catch (error) {
888
+ this.showNotification(`Failed to submit task: ${error.message}`, 'error');
889
+ }
890
+ }
891
+
892
+ async handleCancelTask() {
893
+ try {
894
+ await this.sessionManager.pauseTask('User clicked cancel');
895
+ } catch (error) {
896
+ this.showNotification(`Failed to cancel task: ${error.message}`, 'error');
897
+ }
898
+ }
899
+
900
+ async handleResumeTask() {
901
+ try {
902
+ await this.sessionManager.resumeTask('User clicked resume');
903
+ } catch (error) {
904
+ this.showNotification(`Failed to resume task: ${error.message}`, 'error');
905
+ }
906
+ }
907
+
908
+ async handleTerminateTask() {
909
+ try {
910
+ await this.sessionManager.stopTask('User clicked terminate');
911
+ } catch (error) {
912
+ this.showNotification(`Failed to terminate task: ${error.message}`, 'error');
913
+ }
914
+ }
915
+
916
+ handleAttachFiles() {
917
+ this.elements.fileInput?.click();
918
+ }
919
+
920
+ async handleFileSelection(event) {
921
+ const files = Array.from(event.target.files);
922
+
923
+ if (files.length === 0) return;
924
+
925
+ try {
926
+ this.showLoading(`Uploading ${files.length} file(s)...`);
927
+
928
+ const response = await this.sessionManager.uploadFiles(files);
929
+
930
+ console.log('[UIManager] File upload response:', response);
931
+
932
+ // If SessionManager doesn't trigger the event, handle it directly
933
+ if (response && response.files) {
934
+ console.log('[UIManager] Manually handling uploaded files');
935
+ this.handleFilesUploaded({
936
+ sessionId: this.sessionManager.getCurrentSessionId(),
937
+ files: response.files
938
+ });
939
+ }
940
+
941
+ this.hideLoading();
942
+ this.showNotification(`${files.length} file(s) uploaded successfully`, 'success');
943
+
944
+ // Clear file input
945
+ event.target.value = '';
946
+ } catch (error) {
947
+ this.hideLoading();
948
+ this.showNotification(`File upload failed: ${error.message}`, 'error');
949
+ }
950
+ }
951
+
952
+ handleFilesUploaded(data) {
953
+ console.log('[UIManager] Files uploaded event received:', data);
954
+
955
+ // Ensure data.files is always an array - handle both single file and array cases
956
+ let filesArray = [];
957
+ if (data.files) {
958
+ if (Array.isArray(data.files)) {
959
+ filesArray = data.files;
960
+ } else {
961
+ // If single file object, wrap in array
962
+ filesArray = [data.files];
963
+ console.log('[UIManager] Single file detected, wrapping in array');
964
+ }
965
+ }
966
+
967
+ console.log('[UIManager] Processing files array:', filesArray);
968
+
969
+ if (filesArray.length > 0) {
970
+ // Append new files to existing uploaded files (for multiple uploads)
971
+ const newFiles = filesArray.map(file => ({
972
+ id: file.file_id,
973
+ name: file.original_filename,
974
+ path: file.file_path, // Updated to use file_path field
975
+ size: file.file_size,
976
+ type: file.mime_type,
977
+ stored_filename: file.stored_filename,
978
+ file_path: file.file_path // Add file_path for backward compatibility
979
+ }));
980
+
981
+ console.log('[UIManager] Mapped new files:', newFiles);
982
+
983
+ // Add to existing files instead of replacing
984
+ this.state.uploadedFiles = [...this.state.uploadedFiles, ...newFiles];
985
+
986
+ console.log('[UIManager] Updated uploaded files state:', this.state.uploadedFiles);
987
+
988
+ // Update the visual file list
989
+ this.updateFilesList();
990
+ } else {
991
+ console.warn('[UIManager] No files to process in uploaded data');
992
+ }
993
+ }
994
+
995
+ updateFilesList() {
996
+ const container = this.getOrCreateFilesListContainer();
997
+
998
+ // Debug logging to identify the issue
999
+ console.log('[UIManager] updateFilesList called');
1000
+ console.log('[UIManager] uploadedFiles type:', typeof this.state.uploadedFiles);
1001
+ console.log('[UIManager] uploadedFiles isArray:', Array.isArray(this.state.uploadedFiles));
1002
+ console.log('[UIManager] uploadedFiles value:', this.state.uploadedFiles);
1003
+
1004
+ // Ensure uploadedFiles is always an array
1005
+ if (!Array.isArray(this.state.uploadedFiles)) {
1006
+ console.error('[UIManager] uploadedFiles is not an array, resetting to empty array');
1007
+ this.state.uploadedFiles = [];
1008
+ }
1009
+
1010
+ if (this.state.uploadedFiles.length === 0) {
1011
+ container.style.display = 'none';
1012
+ return;
1013
+ }
1014
+
1015
+ container.style.display = 'block';
1016
+
1017
+ // Build HTML safely with proper validation
1018
+ let filesHTML = '';
1019
+ try {
1020
+ filesHTML = this.state.uploadedFiles.map((file, index) => {
1021
+ console.log(`[UIManager] Processing file ${index}:`, file);
1022
+
1023
+ // Validate file object structure
1024
+ if (!file || typeof file !== 'object') {
1025
+ console.error(`[UIManager] Invalid file object at index ${index}:`, file);
1026
+ return '';
1027
+ }
1028
+
1029
+ // Extract properties safely with fallbacks
1030
+ const fileId = file.id || file.file_id || `file_${index}`;
1031
+ const fileName = file.name || file.original_filename || 'Unknown file';
1032
+ const filePath = file.path || file.file_path || file.stored_filename || 'Unknown path';
1033
+
1034
+ console.log(`[UIManager] File display data: id=${fileId}, name=${fileName}, path=${filePath}`);
1035
+
1036
+ return `
1037
+ <div class="file-item" data-file-id="${fileId}">
1038
+ <span class="file-name" title="${filePath}">${fileName}</span>
1039
+ <button class="file-remove-btn" title="Remove file" data-file-id="${fileId}">×</button>
1040
+ </div>
1041
+ `;
1042
+ }).join('');
1043
+ } catch (error) {
1044
+ console.error('[UIManager] Error generating files HTML:', error);
1045
+ filesHTML = '<div class="error-message">Error displaying files</div>';
1046
+ }
1047
+
1048
+ container.innerHTML = `
1049
+ <div class="files-items">
1050
+ ${filesHTML}
1051
+ </div>
1052
+ `;
1053
+
1054
+ // Add event listeners for remove buttons
1055
+ container.querySelectorAll('.file-remove-btn').forEach(btn => {
1056
+ btn.addEventListener('click', (e) => {
1057
+ e.preventDefault();
1058
+ const fileId = btn.dataset.fileId;
1059
+ this.removeUploadedFile(fileId);
1060
+ });
1061
+ });
1062
+ }
1063
+
1064
+ getOrCreateFilesListContainer() {
1065
+ let container = document.getElementById('uploaded-files-list');
1066
+
1067
+ if (!container) {
1068
+ container = document.createElement('div');
1069
+ container.id = 'uploaded-files-list';
1070
+ container.className = 'uploaded-files-container';
1071
+
1072
+ // Insert after the textarea-container to avoid affecting button layout
1073
+ if (this.elements.taskInput) {
1074
+ const textareaContainer = this.elements.taskInput.closest('.textarea-container');
1075
+ if (textareaContainer && textareaContainer.parentElement) {
1076
+ // Insert after the textarea-container but before the input-footer
1077
+ const inputFooter = textareaContainer.parentElement.querySelector('.input-footer');
1078
+ if (inputFooter) {
1079
+ textareaContainer.parentElement.insertBefore(container, inputFooter);
1080
+ } else {
1081
+ textareaContainer.parentElement.insertBefore(container, textareaContainer.nextSibling);
1082
+ }
1083
+ }
1084
+ }
1085
+ }
1086
+
1087
+ return container;
1088
+ }
1089
+
1090
+ removeUploadedFile(fileId) {
1091
+ console.log('[UIManager] Removing uploaded file:', fileId);
1092
+
1093
+ // Remove from state
1094
+ this.state.uploadedFiles = this.state.uploadedFiles.filter(file => file.id !== fileId);
1095
+
1096
+ // Update visual list
1097
+ this.updateFilesList();
1098
+
1099
+ this.showNotification('File removed from upload list', 'info');
1100
+ }
1101
+
1102
+ formatFileSize(bytes) {
1103
+ if (!bytes) return '0 B';
1104
+
1105
+ const k = 1024;
1106
+ const sizes = ['B', 'KB', 'MB', 'GB'];
1107
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
1108
+
1109
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
1110
+ }
1111
+
1112
+ clearUploadedFiles() {
1113
+ this.state.uploadedFiles = [];
1114
+ this.updateFilesList();
1115
+ }
1116
+
1117
+ handleTaskInputKeydown(event) {
1118
+ if (event.key === 'Enter' && !event.shiftKey) {
1119
+ event.preventDefault();
1120
+ this.handleSendTask();
1121
+ }
1122
+ }
1123
+
1124
+ handleLlmProfileChange(event) {
1125
+ // Re-validate send button state when LLM profile changes
1126
+ if (this.elements.taskInput) {
1127
+ this.handleTaskInputChange({ target: this.elements.taskInput });
1128
+ }
1129
+ }
1130
+
1131
+ handleTaskInputChange(event) {
1132
+ const hasText = event.target.value.trim().length > 0;
1133
+ const textarea = event.target;
1134
+ const llmProfile = this.elements.llmProfileSelect?.value;
1135
+ const hasLlmProfile = llmProfile && llmProfile.trim() !== '';
1136
+
1137
+ // Update send button state - require both text and LLM profile and no running task
1138
+ if (this.elements.sendBtn) {
1139
+ this.elements.sendBtn.disabled = !(hasText && hasLlmProfile && !this.state.isTaskRunning);
1140
+ }
1141
+
1142
+ // Auto-resize textarea based on content
1143
+ this.autoResizeTextarea(textarea);
1144
+ }
1145
+
1146
+ autoResizeTextarea(textarea) {
1147
+ if (!textarea) return;
1148
+
1149
+ // Reset height to auto to get the natural scrollHeight
1150
+ textarea.style.height = 'auto';
1151
+
1152
+ // Calculate the new height based on content
1153
+ const minHeight = 44; // Min height from CSS
1154
+ const maxHeight = 200; // Max height from CSS
1155
+ const newHeight = Math.max(minHeight, Math.min(maxHeight, textarea.scrollHeight));
1156
+
1157
+ // Apply the new height
1158
+ textarea.style.height = newHeight + 'px';
1159
+ }
1160
+
1161
+ async handleBackendUrlChange(event) {
1162
+ const newUrl = event.target.value.trim();
1163
+
1164
+ if (!newUrl) {
1165
+ this.showNotification('Backend URL cannot be empty', 'warning');
1166
+ return;
1167
+ }
1168
+
1169
+ try {
1170
+ // Validate URL format
1171
+ new URL(newUrl);
1172
+
1173
+ // Update API client
1174
+ this.apiClient.setBaseURL(newUrl);
1175
+
1176
+ // Save to settings via main app
1177
+ if (window.vibeSurfApp) {
1178
+ await window.vibeSurfApp.updateSettings({ backendUrl: newUrl });
1179
+ }
1180
+
1181
+ this.showNotification('Backend URL updated successfully', 'success');
1182
+ console.log('[UIManager] Backend URL updated to:', newUrl);
1183
+
1184
+ } catch (error) {
1185
+ this.showNotification(`Invalid backend URL: ${error.message}`, 'error');
1186
+ console.error('[UIManager] Backend URL update failed:', error);
1187
+ }
1188
+ }
1189
+
1190
+ // Settings Tab Management
1191
+ handleTabSwitch(event) {
1192
+ const clickedTab = event.currentTarget;
1193
+ const targetTabId = clickedTab.dataset.tab;
1194
+
1195
+ // Update tab buttons
1196
+ this.elements.settingsTabs?.forEach(tab => {
1197
+ tab.classList.remove('active');
1198
+ });
1199
+ clickedTab.classList.add('active');
1200
+
1201
+ // Update tab content
1202
+ this.elements.settingsTabContents?.forEach(content => {
1203
+ content.classList.remove('active');
1204
+ });
1205
+ const targetContent = document.getElementById(`${targetTabId}-tab`);
1206
+ if (targetContent) {
1207
+ targetContent.classList.add('active');
1208
+ }
1209
+ }
1210
+
1211
+ // Profile Management
1212
+ async handleAddProfile(type) {
1213
+ try {
1214
+ this.showProfileForm(type);
1215
+ } catch (error) {
1216
+ console.error(`[UIManager] Failed to show ${type} profile form:`, error);
1217
+ this.showNotification(`Failed to show ${type} profile form`, 'error');
1218
+ }
1219
+ }
1220
+
1221
+ async showProfileForm(type, profile = null) {
1222
+ const isEdit = profile !== null;
1223
+ const title = isEdit ? `Edit ${type.toUpperCase()} Profile` : `Add ${type.toUpperCase()} Profile`;
1224
+
1225
+ if (this.elements.profileFormTitle) {
1226
+ this.elements.profileFormTitle.textContent = title;
1227
+ }
1228
+
1229
+ // Generate form content based on type
1230
+ let formHTML = '';
1231
+ if (type === 'llm') {
1232
+ formHTML = await this.generateLLMProfileForm(profile);
1233
+ } else if (type === 'mcp') {
1234
+ formHTML = this.generateMCPProfileForm(profile);
1235
+ }
1236
+
1237
+ if (this.elements.profileForm) {
1238
+ this.elements.profileForm.innerHTML = formHTML;
1239
+ this.elements.profileForm.dataset.type = type;
1240
+ this.elements.profileForm.dataset.mode = isEdit ? 'edit' : 'create';
1241
+ if (isEdit && profile) {
1242
+ this.elements.profileForm.dataset.profileId = profile.profile_name || profile.mcp_id;
1243
+ }
1244
+ }
1245
+
1246
+ // Setup form event listeners
1247
+ this.setupProfileFormEvents();
1248
+
1249
+ // Show modal
1250
+ if (this.elements.profileFormModal) {
1251
+ this.elements.profileFormModal.classList.remove('hidden');
1252
+ }
1253
+ }
1254
+
1255
+ async generateLLMProfileForm(profile = null) {
1256
+ // Fetch available providers
1257
+ let providers = [];
1258
+ try {
1259
+ const response = await this.apiClient.getLLMProviders();
1260
+ providers = response.providers || response || [];
1261
+ } catch (error) {
1262
+ console.error('[UIManager] Failed to fetch LLM providers:', error);
1263
+ }
1264
+
1265
+ const providersOptions = providers.map(p =>
1266
+ `<option value="${p.name}" ${profile?.provider === p.name ? 'selected' : ''}>${p.display_name}</option>`
1267
+ ).join('');
1268
+
1269
+ const selectedProvider = profile?.provider || (providers.length > 0 ? providers[0].name : '');
1270
+ const selectedProviderData = providers.find(p => p.name === selectedProvider);
1271
+ const models = selectedProviderData?.models || [];
1272
+
1273
+ const modelsOptions = models.map(model =>
1274
+ `<option value="${model}" ${profile?.model === model ? 'selected' : ''}>${model}</option>`
1275
+ ).join('');
1276
+
1277
+ return `
1278
+ <div class="form-group">
1279
+ <label class="form-label required">Profile Name</label>
1280
+ <input type="text" name="profile_name" class="form-input" value="${profile?.profile_name || ''}"
1281
+ placeholder="Enter a unique name for this profile" required ${profile ? 'readonly' : ''}>
1282
+ <div class="form-help">A unique identifier for this LLM configuration</div>
1283
+ </div>
1284
+
1285
+ <div class="form-group">
1286
+ <label class="form-label required">Provider</label>
1287
+ <select name="provider" class="form-select" required>
1288
+ <option value="">Select a provider</option>
1289
+ ${providersOptions}
1290
+ </select>
1291
+ <div class="form-help">Choose your LLM provider (OpenAI, Anthropic, etc.)</div>
1292
+ </div>
1293
+
1294
+ <div class="form-group">
1295
+ <label class="form-label required">Model</label>
1296
+ <input type="text" name="model" class="form-input model-input" value="${profile?.model || ''}"
1297
+ list="model-options" placeholder="Select a model or type custom model name" required
1298
+ autocomplete="off">
1299
+ <datalist id="model-options">
1300
+ ${models.map(model => `<option value="${model}">${model}</option>`).join('')}
1301
+ </datalist>
1302
+ <div class="form-help">Choose from the list or enter a custom model name</div>
1303
+ </div>
1304
+
1305
+ <div class="form-group api-key-field">
1306
+ <label class="form-label required">API Key</label>
1307
+ <input type="password" name="api_key" class="form-input api-key-input"
1308
+ placeholder="${profile ? 'Leave empty to keep existing key' : 'Enter your API key'}"
1309
+ ${profile ? '' : 'required'}>
1310
+ <button type="button" class="api-key-toggle" title="Toggle visibility">
1311
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1312
+ <path d="M1 12S5 4 12 4S23 12 23 12S19 20 12 20S1 12 1 12Z" stroke="currentColor" stroke-width="2"/>
1313
+ <circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2"/>
1314
+ </svg>
1315
+ </button>
1316
+ <div class="form-help">Your provider's API key for authentication</div>
1317
+ </div>
1318
+
1319
+ <div class="form-group">
1320
+ <label class="form-label">Base URL</label>
1321
+ <input type="url" name="base_url" class="form-input" value="${profile?.base_url || ''}"
1322
+ placeholder="https://api.openai.com/v1">
1323
+ <div class="form-help">Custom API endpoint (leave empty for provider default)</div>
1324
+ </div>
1325
+
1326
+ <div class="form-group">
1327
+ <label class="form-label">Temperature</label>
1328
+ <input type="number" name="temperature" class="form-input" value="${profile?.temperature || ''}"
1329
+ min="0" max="2" step="0.1" placeholder="0.7">
1330
+ <div class="form-help">Controls randomness (0.0-2.0, lower = more focused)</div>
1331
+ </div>
1332
+
1333
+ <div class="form-group">
1334
+ <label class="form-label">Max Tokens</label>
1335
+ <input type="number" name="max_tokens" class="form-input" value="${profile?.max_tokens || ''}"
1336
+ min="1" max="128000" placeholder="4096">
1337
+ <div class="form-help">Maximum tokens in the response</div>
1338
+ </div>
1339
+
1340
+ <div class="form-group">
1341
+ <label class="form-label">Description</label>
1342
+ <textarea name="description" class="form-textarea" placeholder="Optional description for this profile">${profile?.description || ''}</textarea>
1343
+ <div class="form-help">Optional description to help identify this profile</div>
1344
+ </div>
1345
+
1346
+ <div class="form-group">
1347
+ <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
1348
+ <input type="checkbox" name="is_default" ${profile?.is_default ? 'checked' : ''}>
1349
+ <span class="form-label" style="margin: 0;">Set as default profile</span>
1350
+ </label>
1351
+ <div class="form-help">This profile will be selected by default for new tasks</div>
1352
+ </div>
1353
+ `;
1354
+ }
1355
+
1356
+ generateMCPProfileForm(profile = null) {
1357
+ // Convert existing profile to JSON for editing
1358
+ let defaultJson = '{\n "command": "npx",\n "args": [\n "-y",\n "@modelcontextprotocol/server-filesystem",\n "/path/to/directory"\n ]\n}';
1359
+
1360
+ if (profile?.mcp_server_params) {
1361
+ try {
1362
+ defaultJson = JSON.stringify(profile.mcp_server_params, null, 2);
1363
+ } catch (error) {
1364
+ console.warn('[UIManager] Failed to stringify existing mcp_server_params:', error);
1365
+ }
1366
+ }
1367
+
1368
+ return `
1369
+ <div class="form-group">
1370
+ <label class="form-label required">Display Name</label>
1371
+ <input type="text" name="display_name" class="form-input" value="${profile?.display_name || ''}"
1372
+ placeholder="Enter a friendly name for this MCP profile" required ${profile ? 'readonly' : ''}>
1373
+ <div class="form-help">A user-friendly name for this MCP configuration</div>
1374
+ </div>
1375
+
1376
+ <div class="form-group">
1377
+ <label class="form-label required">Server Name</label>
1378
+ <input type="text" name="mcp_server_name" class="form-input" value="${profile?.mcp_server_name || ''}"
1379
+ placeholder="e.g., filesystem, markitdown, brave-search" required>
1380
+ <div class="form-help">The MCP server identifier</div>
1381
+ </div>
1382
+
1383
+ <div class="form-group">
1384
+ <label class="form-label required">MCP Server Parameters (JSON)</label>
1385
+ <textarea name="mcp_server_params_json" class="form-textarea json-input" rows="8"
1386
+ placeholder="Enter JSON configuration for MCP server parameters" required>${defaultJson}</textarea>
1387
+ <div class="json-validation-feedback"></div>
1388
+ <div class="form-help">
1389
+ JSON configuration including command and arguments. Example:
1390
+ <br><code>{"command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path"]}</code>
1391
+ </div>
1392
+ </div>
1393
+
1394
+ <div class="form-group">
1395
+ <label class="form-label">Description</label>
1396
+ <textarea name="description" class="form-textarea" placeholder="Optional description for this MCP profile">${profile?.description || ''}</textarea>
1397
+ <div class="form-help">Optional description to help identify this profile</div>
1398
+ </div>
1399
+
1400
+ <div class="form-group">
1401
+ <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
1402
+ <input type="checkbox" name="is_active" ${profile?.is_active !== false ? 'checked' : ''}>
1403
+ <span class="form-label" style="margin: 0;">Active</span>
1404
+ </label>
1405
+ <div class="form-help">Whether this MCP profile is active and available for use</div>
1406
+ </div>
1407
+ `;
1408
+ }
1409
+
1410
+ setupProfileFormEvents() {
1411
+ console.log('[UIManager] Setting up profile form events');
1412
+
1413
+ // Provider change handler for LLM profiles
1414
+ const providerSelect = this.elements.profileForm?.querySelector('select[name="provider"]');
1415
+ if (providerSelect) {
1416
+ providerSelect.addEventListener('change', this.handleProviderChange.bind(this));
1417
+ console.log('[UIManager] Provider select change listener added');
1418
+ }
1419
+
1420
+ // API key toggle handler
1421
+ const apiKeyToggle = this.elements.profileForm?.querySelector('.api-key-toggle');
1422
+ const apiKeyInput = this.elements.profileForm?.querySelector('.api-key-input');
1423
+ if (apiKeyToggle && apiKeyInput) {
1424
+ apiKeyToggle.addEventListener('click', () => {
1425
+ const isPassword = apiKeyInput.type === 'password';
1426
+ apiKeyInput.type = isPassword ? 'text' : 'password';
1427
+
1428
+ // Update icon
1429
+ const svg = apiKeyToggle.querySelector('svg');
1430
+ if (svg) {
1431
+ svg.innerHTML = isPassword ?
1432
+ '<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"/>' :
1433
+ '<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"/>';
1434
+ }
1435
+ });
1436
+ console.log('[UIManager] API key toggle listener added');
1437
+ }
1438
+
1439
+ // JSON validation handler for MCP profiles
1440
+ const jsonInput = this.elements.profileForm?.querySelector('textarea[name="mcp_server_params_json"]');
1441
+ if (jsonInput) {
1442
+ jsonInput.addEventListener('input', this.handleJsonInputValidation.bind(this));
1443
+ jsonInput.addEventListener('blur', this.handleJsonInputValidation.bind(this));
1444
+ console.log('[UIManager] JSON validation listener added');
1445
+
1446
+ // Trigger initial validation
1447
+ this.handleJsonInputValidation({ target: jsonInput });
1448
+ }
1449
+
1450
+ // Re-add form submission handler in case it was lost
1451
+ if (this.elements.profileForm) {
1452
+ // Remove existing listeners to avoid duplicates
1453
+ const newForm = this.elements.profileForm.cloneNode(true);
1454
+ this.elements.profileForm.parentNode.replaceChild(newForm, this.elements.profileForm);
1455
+ this.elements.profileForm = newForm;
1456
+
1457
+ this.elements.profileForm.addEventListener('submit', this.handleProfileFormSubmit.bind(this));
1458
+ console.log('[UIManager] Form submit listener re-added in setupProfileFormEvents');
1459
+ }
1460
+ }
1461
+
1462
+ handleJsonInputValidation(event) {
1463
+ const textarea = event.target;
1464
+ const feedbackElement = textarea.parentElement.querySelector('.json-validation-feedback');
1465
+
1466
+ if (!feedbackElement) return;
1467
+
1468
+ const jsonText = textarea.value.trim();
1469
+
1470
+ if (!jsonText) {
1471
+ feedbackElement.innerHTML = '';
1472
+ textarea.classList.remove('json-valid', 'json-invalid');
1473
+ return;
1474
+ }
1475
+
1476
+ try {
1477
+ const parsed = JSON.parse(jsonText);
1478
+
1479
+ // Validate that it's an object (not array, string, etc.)
1480
+ if (typeof parsed !== 'object' || Array.isArray(parsed) || parsed === null) {
1481
+ throw new Error('MCP server parameters must be a JSON object');
1482
+ }
1483
+
1484
+ // Validate required fields
1485
+ if (!parsed.command || typeof parsed.command !== 'string') {
1486
+ throw new Error('Missing or invalid "command" field (must be a string)');
1487
+ }
1488
+
1489
+ // Validate args if present
1490
+ if (parsed.args && !Array.isArray(parsed.args)) {
1491
+ throw new Error('"args" field must be an array if provided');
1492
+ }
1493
+
1494
+ // Success
1495
+ feedbackElement.innerHTML = '<span class="json-success">✓ Valid JSON configuration</span>';
1496
+ textarea.classList.remove('json-invalid');
1497
+ textarea.classList.add('json-valid');
1498
+
1499
+ // Store valid state for form submission
1500
+ textarea.dataset.isValid = 'true';
1501
+
1502
+ } catch (error) {
1503
+ const errorMessage = error.message;
1504
+ feedbackElement.innerHTML = `<span class="json-error">✗ Invalid JSON: ${errorMessage}</span>`;
1505
+ textarea.classList.remove('json-valid');
1506
+ textarea.classList.add('json-invalid');
1507
+
1508
+ // Store invalid state for form submission
1509
+ textarea.dataset.isValid = 'false';
1510
+ textarea.dataset.errorMessage = errorMessage;
1511
+
1512
+ // Show error modal for critical validation errors - trigger on both blur and input events
1513
+ if ((event.type === 'blur' || event.type === 'input') && jsonText.length > 0) {
1514
+ console.log('[UIManager] JSON validation failed, showing error modal:', errorMessage);
1515
+ setTimeout(() => {
1516
+ this.showJsonValidationErrorModal(errorMessage);
1517
+ }, event.type === 'blur' ? 100 : 500); // Longer delay for input events
1518
+ }
1519
+ }
1520
+ }
1521
+
1522
+ showJsonValidationErrorModal(errorMessage) {
1523
+ console.log('[UIManager] Creating JSON validation error modal');
1524
+ const modal = this.createWarningModal({
1525
+ title: 'JSON Validation Error',
1526
+ message: 'The MCP server parameters contain invalid JSON format.',
1527
+ details: `Error: ${errorMessage}\n\nPlease correct the JSON format before submitting the form.`,
1528
+ buttons: [
1529
+ {
1530
+ text: 'OK',
1531
+ style: 'primary',
1532
+ action: () => {
1533
+ console.log('[UIManager] JSON validation error modal OK clicked');
1534
+ this.closeWarningModal();
1535
+ }
1536
+ }
1537
+ ]
1538
+ });
1539
+
1540
+ console.log('[UIManager] Appending JSON validation error modal to body');
1541
+ document.body.appendChild(modal);
1542
+ }
1543
+
1544
+ // Add separate method to handle submit button clicks
1545
+ handleProfileFormSubmitClick(event) {
1546
+ console.log('[UIManager] Profile form submit button clicked');
1547
+ event.preventDefault();
1548
+
1549
+ // Find the form and trigger submit
1550
+ const form = this.elements.profileForm;
1551
+ if (form) {
1552
+ console.log('[UIManager] Triggering form submit via button click');
1553
+ const submitEvent = new Event('submit', { cancelable: true, bubbles: true });
1554
+ form.dispatchEvent(submitEvent);
1555
+ } else {
1556
+ console.error('[UIManager] Profile form not found when submit button clicked');
1557
+ }
1558
+ }
1559
+
1560
+ async handleProviderChange(event) {
1561
+ const selectedProvider = event.target.value;
1562
+ const modelInput = this.elements.profileForm?.querySelector('input[name="model"]');
1563
+ const modelDatalist = this.elements.profileForm?.querySelector('#model-options');
1564
+
1565
+ console.log('[UIManager] Provider changed to:', selectedProvider);
1566
+
1567
+ if (!selectedProvider || !modelInput || !modelDatalist) {
1568
+ console.warn('[UIManager] Missing elements for provider change');
1569
+ return;
1570
+ }
1571
+
1572
+ // Always clear the model input when provider changes
1573
+ modelInput.value = '';
1574
+ modelInput.placeholder = `Loading ${selectedProvider} models...`;
1575
+ modelDatalist.innerHTML = '<option value="">Loading...</option>';
1576
+
1577
+ try {
1578
+ console.log('[UIManager] Fetching models for provider:', selectedProvider);
1579
+ const response = await this.apiClient.getLLMProviderModels(selectedProvider);
1580
+ const models = response.models || response || [];
1581
+
1582
+ console.log('[UIManager] Received models:', models);
1583
+
1584
+ // Update datalist options
1585
+ modelDatalist.innerHTML = models.map(model =>
1586
+ `<option value="${model}">${model}</option>`
1587
+ ).join('');
1588
+
1589
+ // Update placeholder to reflect the new provider
1590
+ modelInput.placeholder = models.length > 0
1591
+ ? `Select a ${selectedProvider} model or type custom model name`
1592
+ : `Enter ${selectedProvider} model name`;
1593
+
1594
+ console.log('[UIManager] Model list updated for provider:', selectedProvider);
1595
+
1596
+ } catch (error) {
1597
+ console.error('[UIManager] Failed to fetch models for provider:', error);
1598
+ modelDatalist.innerHTML = '<option value="">Failed to load models</option>';
1599
+ modelInput.placeholder = `Enter ${selectedProvider} model name manually`;
1600
+
1601
+ // Show user-friendly error notification
1602
+ this.showNotification(`Failed to load models for ${selectedProvider}. You can enter the model name manually.`, 'warning');
1603
+ }
1604
+ }
1605
+
1606
+ closeProfileForm() {
1607
+ if (this.elements.profileFormModal) {
1608
+ this.elements.profileFormModal.classList.add('hidden');
1609
+ }
1610
+ }
1611
+
1612
+ async handleProfileFormSubmit(event) {
1613
+ event.preventDefault();
1614
+ console.log('[UIManager] Profile form submit triggered');
1615
+
1616
+ const form = event.target;
1617
+ const formData = new FormData(form);
1618
+ const type = form.dataset.type;
1619
+ const mode = form.dataset.mode;
1620
+ const profileId = form.dataset.profileId;
1621
+
1622
+ console.log('[UIManager] Form submission details:', { type, mode, profileId });
1623
+
1624
+ // Convert FormData to object
1625
+ const data = {};
1626
+
1627
+ // Handle checkbox fields explicitly first
1628
+ const checkboxFields = ['is_default', 'is_active'];
1629
+ checkboxFields.forEach(fieldName => {
1630
+ const checkbox = form.querySelector(`input[name="${fieldName}"]`);
1631
+ if (checkbox) {
1632
+ data[fieldName] = checkbox.checked;
1633
+ console.log(`[UIManager] Checkbox field: ${fieldName} = ${checkbox.checked}`);
1634
+ }
1635
+ });
1636
+
1637
+ for (const [key, value] of formData.entries()) {
1638
+ console.log(`[UIManager] Form field: ${key} = ${value}`);
1639
+ if (value.trim() !== '') {
1640
+ if (key === 'args' && type === 'mcp') {
1641
+ // Split args by newlines for MCP profiles
1642
+ data[key] = value.split('\n').map(arg => arg.trim()).filter(arg => arg);
1643
+ } else if (key === 'is_default' || key === 'is_active') {
1644
+ // Skip - already handled above with explicit checkbox checking
1645
+ continue;
1646
+ } else if (key === 'temperature') {
1647
+ const num = parseFloat(value);
1648
+ if (!isNaN(num) && num >= 0) {
1649
+ data[key] = num;
1650
+ }
1651
+ // 如果不设置或无效值,就不传给后端
1652
+ } else if (key === 'max_tokens') {
1653
+ const num = parseInt(value);
1654
+ if (!isNaN(num) && num > 0) {
1655
+ data[key] = num;
1656
+ }
1657
+ // Max Tokens如果不设置的话,不用传到后端
1658
+ } else {
1659
+ data[key] = value;
1660
+ }
1661
+ }
1662
+ }
1663
+
1664
+ console.log('[UIManager] Processed form data:', data);
1665
+
1666
+ // Handle MCP server params structure - parse JSON input
1667
+ if (type === 'mcp') {
1668
+ const jsonInput = data.mcp_server_params_json;
1669
+
1670
+ // Check if JSON was pre-validated
1671
+ const jsonTextarea = form.querySelector('textarea[name="mcp_server_params_json"]');
1672
+ if (jsonTextarea && jsonTextarea.dataset.isValid === 'false') {
1673
+ console.error('[UIManager] JSON validation failed during form submission');
1674
+ this.showJsonValidationErrorModal(jsonTextarea.dataset.errorMessage || 'Invalid JSON format');
1675
+ return; // Don't submit the form if JSON is invalid
1676
+ }
1677
+
1678
+ if (jsonInput) {
1679
+ try {
1680
+ const parsedParams = JSON.parse(jsonInput);
1681
+
1682
+ // Validate the parsed JSON structure
1683
+ if (typeof parsedParams !== 'object' || Array.isArray(parsedParams) || parsedParams === null) {
1684
+ throw new Error('MCP server parameters must be a JSON object');
1685
+ }
1686
+
1687
+ if (!parsedParams.command || typeof parsedParams.command !== 'string') {
1688
+ throw new Error('Missing or invalid "command" field (must be a string)');
1689
+ }
1690
+
1691
+ if (parsedParams.args && !Array.isArray(parsedParams.args)) {
1692
+ throw new Error('"args" field must be an array if provided');
1693
+ }
1694
+
1695
+ // Set the parsed parameters
1696
+ data.mcp_server_params = parsedParams;
1697
+ console.log('[UIManager] MCP server params parsed from JSON:', data.mcp_server_params);
1698
+
1699
+ } catch (error) {
1700
+ console.error('[UIManager] Failed to parse MCP server params JSON:', error);
1701
+ this.showJsonValidationErrorModal(error.message);
1702
+ return; // Don't submit the form if JSON is invalid
1703
+ }
1704
+ } else {
1705
+ console.error('[UIManager] Missing mcp_server_params_json in form data');
1706
+ this.showNotification('MCP server parameters JSON is required', 'error');
1707
+ return;
1708
+ }
1709
+
1710
+ // Remove the JSON field as it's not needed in the API request
1711
+ delete data.mcp_server_params_json;
1712
+ console.log('[UIManager] MCP data structure updated:', data);
1713
+ }
1714
+
1715
+ try {
1716
+ console.log(`[UIManager] Starting ${mode} operation for ${type} profile`);
1717
+ let response;
1718
+ const endpoint = type === 'llm' ? '/config/llm-profiles' : '/config/mcp-profiles';
1719
+
1720
+ if (mode === 'create') {
1721
+ console.log('[UIManager] Creating new profile...');
1722
+ if (type === 'llm') {
1723
+ response = await this.apiClient.createLLMProfile(data);
1724
+ } else {
1725
+ response = await this.apiClient.createMCPProfile(data);
1726
+ }
1727
+ } else {
1728
+ console.log('[UIManager] Updating existing profile...');
1729
+ if (type === 'llm') {
1730
+ response = await this.apiClient.updateLLMProfile(profileId, data);
1731
+ } else {
1732
+ response = await this.apiClient.updateMCPProfile(profileId, data);
1733
+ }
1734
+ }
1735
+
1736
+ console.log('[UIManager] API response:', response);
1737
+
1738
+ this.closeProfileForm();
1739
+ this.showNotification(`${type.toUpperCase()} profile ${mode === 'create' ? 'created' : 'updated'} successfully`, 'success');
1740
+
1741
+ console.log('[UIManager] Refreshing settings data...');
1742
+ // Refresh the settings data
1743
+ await this.loadSettingsData();
1744
+ console.log('[UIManager] Settings data refreshed');
1745
+
1746
+ // Force re-render of MCP profiles to ensure status is updated
1747
+ if (type === 'mcp') {
1748
+ console.log('[UIManager] Force updating MCP profiles display');
1749
+ this.renderMCPProfiles(this.state.mcpProfiles);
1750
+ }
1751
+
1752
+ } catch (error) {
1753
+ console.error(`[UIManager] Failed to ${mode} ${type} profile:`, error);
1754
+ this.showNotification(`Failed to ${mode} ${type} profile: ${error.message}`, 'error');
1755
+ }
1756
+ }
1757
+
1758
+
1759
+ // Settings Data Management
1760
+ async loadSettingsData() {
1761
+ try {
1762
+ // Load LLM profiles
1763
+ await this.loadLLMProfiles();
1764
+
1765
+ // Load MCP profiles
1766
+ await this.loadMCPProfiles();
1767
+
1768
+ // Load environment variables
1769
+ await this.loadEnvironmentVariables();
1770
+
1771
+ // Update LLM profile select dropdown
1772
+ this.updateLLMProfileSelect();
1773
+
1774
+ } catch (error) {
1775
+ console.error('[UIManager] Failed to load settings data:', error);
1776
+ this.showNotification('Failed to load settings data', 'error');
1777
+ }
1778
+ }
1779
+
1780
+ async loadLLMProfiles() {
1781
+ try {
1782
+ const response = await this.apiClient.getLLMProfiles(false); // Load all profiles, not just active
1783
+ console.log('[UIManager] LLM profiles loaded:', response);
1784
+
1785
+ // Handle different response structures
1786
+ let profiles = [];
1787
+ if (Array.isArray(response)) {
1788
+ profiles = response;
1789
+ } else if (response.profiles && Array.isArray(response.profiles)) {
1790
+ profiles = response.profiles;
1791
+ } else if (response.data && Array.isArray(response.data)) {
1792
+ profiles = response.data;
1793
+ }
1794
+
1795
+ this.state.llmProfiles = profiles;
1796
+ this.renderLLMProfiles(profiles);
1797
+ this.updateLLMProfileSelect();
1798
+ } catch (error) {
1799
+ console.error('[UIManager] Failed to load LLM profiles:', error);
1800
+ this.state.llmProfiles = [];
1801
+ this.renderLLMProfiles([]);
1802
+ this.updateLLMProfileSelect();
1803
+ }
1804
+ }
1805
+
1806
+ async loadMCPProfiles() {
1807
+ try {
1808
+ const response = await this.apiClient.getMCPProfiles(false); // Load all profiles, not just active
1809
+ console.log('[UIManager] MCP profiles loaded:', response);
1810
+
1811
+ // Handle different response structures
1812
+ let profiles = [];
1813
+ if (Array.isArray(response)) {
1814
+ profiles = response;
1815
+ } else if (response.profiles && Array.isArray(response.profiles)) {
1816
+ profiles = response.profiles;
1817
+ } else if (response.data && Array.isArray(response.data)) {
1818
+ profiles = response.data;
1819
+ }
1820
+
1821
+ this.state.mcpProfiles = profiles;
1822
+ this.renderMCPProfiles(profiles);
1823
+ } catch (error) {
1824
+ console.error('[UIManager] Failed to load MCP profiles:', error);
1825
+ this.state.mcpProfiles = [];
1826
+ this.renderMCPProfiles([]);
1827
+ }
1828
+ }
1829
+
1830
+ async loadEnvironmentVariables() {
1831
+ try {
1832
+ const response = await this.apiClient.getEnvironmentVariables();
1833
+ console.log('[UIManager] Environment variables loaded:', response);
1834
+ const envVars = response.environments || response || {};
1835
+ this.renderEnvironmentVariables(envVars);
1836
+ } catch (error) {
1837
+ console.error('[UIManager] Failed to load environment variables:', error);
1838
+ this.renderEnvironmentVariables({});
1839
+ }
1840
+ }
1841
+
1842
+ renderLLMProfiles(profiles) {
1843
+ const container = document.getElementById('llm-profiles-list');
1844
+ if (!container) return;
1845
+
1846
+ if (profiles.length === 0) {
1847
+ container.innerHTML = `
1848
+ <div class="empty-state">
1849
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1850
+ <path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
1851
+ <path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
1852
+ <path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
1853
+ </svg>
1854
+ <h3>No LLM Profiles</h3>
1855
+ <p>Create your first LLM profile to get started</p>
1856
+ </div>
1857
+ `;
1858
+ return;
1859
+ }
1860
+
1861
+ const profilesHTML = profiles.map(profile => `
1862
+ <div class="profile-card ${profile.is_default ? 'default' : ''}" data-profile-id="${profile.profile_name}">
1863
+ ${profile.is_default ? '<div class="profile-badge">Default</div>' : ''}
1864
+ <div class="profile-header">
1865
+ <div class="profile-title">
1866
+ <h3>${this.escapeHtml(profile.profile_name)}</h3>
1867
+ <span class="profile-provider">${this.escapeHtml(profile.provider)}</span>
1868
+ </div>
1869
+ <div class="profile-actions">
1870
+ <button class="profile-action-btn edit" title="Edit Profile" data-profile='${JSON.stringify(profile)}'>
1871
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1872
+ <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"/>
1873
+ <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"/>
1874
+ </svg>
1875
+ </button>
1876
+ <button class="profile-action-btn delete" title="Delete Profile" data-profile-id="${profile.profile_name}">
1877
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1878
+ <path d="M3 6H5H21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
1879
+ <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"/>
1880
+ </svg>
1881
+ </button>
1882
+ </div>
1883
+ </div>
1884
+ <div class="profile-content">
1885
+ <div class="profile-info">
1886
+ <span class="profile-model">${this.escapeHtml(profile.model)}</span>
1887
+ ${profile.description ? `<p class="profile-description">${this.escapeHtml(profile.description)}</p>` : ''}
1888
+ </div>
1889
+ <div class="profile-details">
1890
+ ${profile.base_url ? `<div class="profile-detail"><strong>Base URL:</strong> ${this.escapeHtml(profile.base_url)}</div>` : ''}
1891
+ ${profile.temperature !== undefined ? `<div class="profile-detail"><strong>Temperature:</strong> ${profile.temperature}</div>` : ''}
1892
+ ${profile.max_tokens ? `<div class="profile-detail"><strong>Max Tokens:</strong> ${profile.max_tokens}</div>` : ''}
1893
+ </div>
1894
+ </div>
1895
+ </div>
1896
+ `).join('');
1897
+
1898
+ container.innerHTML = profilesHTML;
1899
+
1900
+ // Add event listeners for profile actions
1901
+ container.querySelectorAll('.edit').forEach(btn => {
1902
+ btn.addEventListener('click', (e) => {
1903
+ e.stopPropagation();
1904
+ const profile = JSON.parse(btn.dataset.profile);
1905
+ this.showProfileForm('llm', profile);
1906
+ });
1907
+ });
1908
+
1909
+ container.querySelectorAll('.delete').forEach(btn => {
1910
+ btn.addEventListener('click', async (e) => {
1911
+ e.stopPropagation();
1912
+ await this.handleDeleteProfile('llm', btn.dataset.profileId);
1913
+ });
1914
+ });
1915
+ }
1916
+
1917
+ renderMCPProfiles(profiles) {
1918
+ const container = document.getElementById('mcp-profiles-list');
1919
+ if (!container) return;
1920
+
1921
+ if (profiles.length === 0) {
1922
+ container.innerHTML = `
1923
+ <div class="empty-state">
1924
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1925
+ <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"/>
1926
+ </svg>
1927
+ <h3>No MCP Profiles</h3>
1928
+ <p>Create your first MCP profile to enable server integrations</p>
1929
+ </div>
1930
+ `;
1931
+ return;
1932
+ }
1933
+
1934
+ const profilesHTML = profiles.map(profile => `
1935
+ <div class="profile-card ${profile.is_active ? 'active' : 'inactive'}" data-profile-id="${profile.mcp_id}">
1936
+ <div class="profile-status ${profile.is_active ? 'active' : 'inactive'}">
1937
+ ${profile.is_active ? 'Active' : 'Inactive'}
1938
+ </div>
1939
+ <div class="profile-header">
1940
+ <div class="profile-title">
1941
+ <h3>${this.escapeHtml(profile.display_name)}</h3>
1942
+ <span class="profile-provider">${this.escapeHtml(profile.mcp_server_name)}</span>
1943
+ </div>
1944
+ <div class="profile-actions">
1945
+ <button class="profile-action-btn edit" title="Edit Profile" data-profile='${JSON.stringify(profile)}'>
1946
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1947
+ <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"/>
1948
+ <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"/>
1949
+ </svg>
1950
+ </button>
1951
+ <button class="profile-action-btn delete" title="Delete Profile" data-profile-id="${profile.mcp_id}">
1952
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1953
+ <path d="M3 6H5H21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
1954
+ <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"/>
1955
+ </svg>
1956
+ </button>
1957
+ </div>
1958
+ </div>
1959
+ <div class="profile-content">
1960
+ ${profile.description ? `<p class="profile-description">${this.escapeHtml(profile.description)}</p>` : ''}
1961
+ <div class="profile-details">
1962
+ <div class="profile-detail"><strong>Command:</strong> ${this.escapeHtml(profile.mcp_server_params?.command || 'N/A')}</div>
1963
+ ${profile.mcp_server_params?.args?.length ? `<div class="profile-detail"><strong>Args:</strong> ${profile.mcp_server_params.args.join(', ')}</div>` : ''}
1964
+ </div>
1965
+ </div>
1966
+ </div>
1967
+ `).join('');
1968
+
1969
+ container.innerHTML = profilesHTML;
1970
+
1971
+ // Add event listeners for profile actions
1972
+ container.querySelectorAll('.edit').forEach(btn => {
1973
+ btn.addEventListener('click', (e) => {
1974
+ e.stopPropagation();
1975
+ const profile = JSON.parse(btn.dataset.profile);
1976
+ this.showProfileForm('mcp', profile);
1977
+ });
1978
+ });
1979
+
1980
+ container.querySelectorAll('.delete').forEach(btn => {
1981
+ btn.addEventListener('click', async (e) => {
1982
+ e.stopPropagation();
1983
+ await this.handleDeleteProfile('mcp', btn.dataset.profileId);
1984
+ });
1985
+ });
1986
+ }
1987
+
1988
+ renderEnvironmentVariables(envVars) {
1989
+ const container = this.elements.envVariablesList;
1990
+ if (!container) return;
1991
+
1992
+ // Clear existing content
1993
+ container.innerHTML = '';
1994
+
1995
+ // Check if there are any environment variables to display
1996
+ if (Object.keys(envVars).length === 0) {
1997
+ container.innerHTML = `
1998
+ <div class="empty-state">
1999
+ <div class="empty-state-icon">🔧</div>
2000
+ <div class="empty-state-title">No Environment Variables</div>
2001
+ <div class="empty-state-description">Environment variables are configured on the backend. Only updates to existing variables are allowed.</div>
2002
+ </div>
2003
+ `;
2004
+ return;
2005
+ }
2006
+
2007
+ // Backend URL related keys that should be readonly
2008
+ const backendUrlKeys = [
2009
+ 'BACKEND_URL',
2010
+ 'VIBESURF_BACKEND_URL',
2011
+ 'API_URL',
2012
+ 'BASE_URL',
2013
+ 'API_BASE_URL',
2014
+ 'BACKEND_API_URL'
2015
+ ];
2016
+
2017
+ // Add existing environment variables (read-only keys, editable/readonly values based on type)
2018
+ Object.entries(envVars).forEach(([key, value]) => {
2019
+ const envVarItem = document.createElement('div');
2020
+ envVarItem.className = 'env-var-item';
2021
+
2022
+ // Check if this is a backend URL variable
2023
+ const isBackendUrl = backendUrlKeys.includes(key.toUpperCase());
2024
+ const valueReadonly = isBackendUrl ? 'readonly' : '';
2025
+ const valueClass = isBackendUrl ? 'form-input readonly-input' : 'form-input';
2026
+ const valueTitle = isBackendUrl ? 'Backend URL is not editable from settings' : '';
2027
+
2028
+ envVarItem.innerHTML = `
2029
+ <div class="env-var-key">
2030
+ <input type="text" class="form-input" placeholder="Variable name" value="${this.escapeHtml(key)}" readonly>
2031
+ </div>
2032
+ <div class="env-var-value">
2033
+ <input type="text" class="${valueClass}" placeholder="Variable value" value="${this.escapeHtml(value)}" ${valueReadonly} title="${valueTitle}">
2034
+ </div>
2035
+ `;
2036
+
2037
+ container.appendChild(envVarItem);
2038
+ });
2039
+ }
2040
+
2041
+ async handleDeleteProfile(type, profileId) {
2042
+ // Check if this is a default LLM profile
2043
+ if (type === 'llm') {
2044
+ const profile = this.state.llmProfiles.find(p => p.profile_name === profileId);
2045
+ if (profile && profile.is_default) {
2046
+ // Handle default profile deletion differently
2047
+ return await this.handleDeleteDefaultProfile(profileId);
2048
+ }
2049
+ }
2050
+
2051
+ // Create a modern confirmation modal instead of basic confirm()
2052
+ return new Promise((resolve) => {
2053
+ const modal = this.createWarningModal({
2054
+ title: `Delete ${type.toUpperCase()} Profile`,
2055
+ message: `Are you sure you want to delete the "${profileId}" profile? This action cannot be undone.`,
2056
+ details: `This will permanently remove the ${type.toUpperCase()} profile and all its configurations.`,
2057
+ buttons: [
2058
+ {
2059
+ text: 'Delete Profile',
2060
+ style: 'danger',
2061
+ action: async () => {
2062
+ this.closeWarningModal();
2063
+ await this.performDeleteProfile(type, profileId);
2064
+ resolve(true);
2065
+ }
2066
+ },
2067
+ {
2068
+ text: 'Cancel',
2069
+ style: 'secondary',
2070
+ action: () => {
2071
+ this.closeWarningModal();
2072
+ resolve(false);
2073
+ }
2074
+ }
2075
+ ]
2076
+ });
2077
+
2078
+ document.body.appendChild(modal);
2079
+ });
2080
+ }
2081
+
2082
+ async handleDeleteDefaultProfile(profileId) {
2083
+ // Get other available profiles
2084
+ const otherProfiles = this.state.llmProfiles.filter(p => p.profile_name !== profileId);
2085
+
2086
+ if (otherProfiles.length === 0) {
2087
+ // No other profiles available - cannot delete
2088
+ const modal = this.createWarningModal({
2089
+ title: 'Cannot Delete Default Profile',
2090
+ message: 'This is the only LLM profile configured. You cannot delete it without having at least one other profile.',
2091
+ details: 'Please create another LLM profile first, then you can delete this one.',
2092
+ buttons: [
2093
+ {
2094
+ text: 'Create New Profile',
2095
+ style: 'primary',
2096
+ action: () => {
2097
+ this.closeWarningModal();
2098
+ this.handleAddProfile('llm');
2099
+ }
2100
+ },
2101
+ {
2102
+ text: 'Cancel',
2103
+ style: 'secondary',
2104
+ action: () => {
2105
+ this.closeWarningModal();
2106
+ }
2107
+ }
2108
+ ]
2109
+ });
2110
+
2111
+ document.body.appendChild(modal);
2112
+ return false;
2113
+ }
2114
+
2115
+ // Show modal to select new default profile
2116
+ return new Promise((resolve) => {
2117
+ const profileOptions = otherProfiles.map(profile =>
2118
+ `<label style="display: flex; align-items: center; gap: 8px; padding: 8px; border: 1px solid #ddd; border-radius: 4px; margin: 4px 0; cursor: pointer;">
2119
+ <input type="radio" name="newDefault" value="${profile.profile_name}" required>
2120
+ <div>
2121
+ <div style="font-weight: bold;">${profile.profile_name}</div>
2122
+ <div style="font-size: 12px; color: #666;">${profile.provider} - ${profile.model}</div>
2123
+ </div>
2124
+ </label>`
2125
+ ).join('');
2126
+
2127
+ const modal = this.createWarningModal({
2128
+ title: 'Delete Default Profile',
2129
+ message: `"${profileId}" is currently the default profile. Please select a new default profile before deleting it.`,
2130
+ details: `<div style="margin: 16px 0;">
2131
+ <div style="font-weight: bold; margin-bottom: 8px;">Select new default profile:</div>
2132
+ <form id="newDefaultForm" style="max-height: 200px; overflow-y: auto;">
2133
+ ${profileOptions}
2134
+ </form>
2135
+ </div>`,
2136
+ buttons: [
2137
+ {
2138
+ text: 'Set Default & Delete',
2139
+ style: 'danger',
2140
+ action: async () => {
2141
+ const form = document.getElementById('newDefaultForm');
2142
+ const selectedProfile = form.querySelector('input[name="newDefault"]:checked');
2143
+
2144
+ if (!selectedProfile) {
2145
+ this.showNotification('Please select a new default profile', 'warning');
2146
+ return;
2147
+ }
2148
+
2149
+ try {
2150
+ this.closeWarningModal();
2151
+ await this.setNewDefaultAndDelete(selectedProfile.value, profileId);
2152
+ resolve(true);
2153
+ } catch (error) {
2154
+ this.showNotification(`Failed to update default profile: ${error.message}`, 'error');
2155
+ resolve(false);
2156
+ }
2157
+ }
2158
+ },
2159
+ {
2160
+ text: 'Cancel',
2161
+ style: 'secondary',
2162
+ action: () => {
2163
+ this.closeWarningModal();
2164
+ resolve(false);
2165
+ }
2166
+ }
2167
+ ]
2168
+ });
2169
+
2170
+ document.body.appendChild(modal);
2171
+ });
2172
+ }
2173
+
2174
+ async setNewDefaultAndDelete(newDefaultProfileId, profileToDelete) {
2175
+ try {
2176
+ this.showLoading('Updating default profile...');
2177
+
2178
+ // First, set the new default profile
2179
+ await this.apiClient.updateLLMProfile(newDefaultProfileId, { is_default: true });
2180
+
2181
+ this.showLoading('Deleting profile...');
2182
+
2183
+ // Then delete the old default profile
2184
+ await this.apiClient.deleteLLMProfile(profileToDelete);
2185
+
2186
+ this.showNotification(`Profile "${profileToDelete}" deleted and "${newDefaultProfileId}" set as default`, 'success');
2187
+
2188
+ // Refresh the settings data
2189
+ await this.loadSettingsData();
2190
+
2191
+ this.hideLoading();
2192
+ } catch (error) {
2193
+ this.hideLoading();
2194
+ console.error('[UIManager] Failed to set new default and delete profile:', error);
2195
+ this.showNotification(`Failed to update profiles: ${error.message}`, 'error');
2196
+ throw error;
2197
+ }
2198
+ }
2199
+
2200
+ async performDeleteProfile(type, profileId) {
2201
+ try {
2202
+ this.showLoading(`Deleting ${type} profile...`);
2203
+
2204
+ if (type === 'llm') {
2205
+ await this.apiClient.deleteLLMProfile(profileId);
2206
+ } else {
2207
+ await this.apiClient.deleteMCPProfile(profileId);
2208
+ }
2209
+
2210
+ this.showNotification(`${type.toUpperCase()} profile deleted successfully`, 'success');
2211
+
2212
+ // Refresh the settings data
2213
+ await this.loadSettingsData();
2214
+
2215
+ this.hideLoading();
2216
+ } catch (error) {
2217
+ this.hideLoading();
2218
+ console.error(`[UIManager] Failed to delete ${type} profile:`, error);
2219
+ this.showNotification(`Failed to delete ${type} profile: ${error.message}`, 'error');
2220
+ }
2221
+ }
2222
+
2223
+ async handleSaveEnvironmentVariables() {
2224
+ if (!this.elements.envVariablesList) return;
2225
+
2226
+ const envVarItems = this.elements.envVariablesList.querySelectorAll('.env-var-item');
2227
+ const envVars = {};
2228
+
2229
+ // Backend URL related keys that should be skipped during save
2230
+ const backendUrlKeys = [
2231
+ 'BACKEND_URL',
2232
+ 'VIBESURF_BACKEND_URL',
2233
+ 'API_URL',
2234
+ 'BASE_URL',
2235
+ 'API_BASE_URL',
2236
+ 'BACKEND_API_URL'
2237
+ ];
2238
+
2239
+ envVarItems.forEach(item => {
2240
+ const keyInput = item.querySelector('.env-var-key input');
2241
+ const valueInput = item.querySelector('.env-var-value input');
2242
+
2243
+ if (keyInput && valueInput && keyInput.value.trim()) {
2244
+ const key = keyInput.value.trim();
2245
+ const value = valueInput.value.trim();
2246
+
2247
+ // Skip backend URL variables (they are readonly)
2248
+ if (!backendUrlKeys.includes(key.toUpperCase())) {
2249
+ envVars[key] = value;
2250
+ }
2251
+ }
2252
+ });
2253
+
2254
+ try {
2255
+ await this.apiClient.updateEnvironmentVariables(envVars);
2256
+ this.showNotification('Environment variables updated successfully (backend URL variables are read-only)', 'success');
2257
+ } catch (error) {
2258
+ console.error('[UIManager] Failed to update environment variables:', error);
2259
+ this.showNotification(`Failed to update environment variables: ${error.message}`, 'error');
2260
+ }
2261
+ }
2262
+
2263
+ escapeHtml(text) {
2264
+ if (typeof text !== 'string') return '';
2265
+ const div = document.createElement('div');
2266
+ div.textContent = text;
2267
+ return div.innerHTML;
2268
+ }
2269
+
2270
+ // UI Display Methods
2271
+ updateSessionDisplay(sessionId) {
2272
+ console.log('[UIManager] Updating session display with ID:', sessionId);
2273
+ if (this.elements.sessionId) {
2274
+ this.elements.sessionId.textContent = sessionId || '-';
2275
+ console.log('[UIManager] Session ID display updated to:', sessionId);
2276
+ } else {
2277
+ console.error('[UIManager] Session ID element not found');
2278
+ }
2279
+ }
2280
+
2281
+ updateControlPanel(status) {
2282
+ const panel = this.elements.controlPanel;
2283
+ const cancelBtn = this.elements.cancelBtn;
2284
+ const resumeBtn = this.elements.resumeBtn;
2285
+ const terminateBtn = this.elements.terminateBtn;
2286
+
2287
+ console.log(`[UIManager] updateControlPanel called with status: ${status}`);
2288
+
2289
+ if (!panel) {
2290
+ console.error('[UIManager] Control panel element not found');
2291
+ return;
2292
+ }
2293
+
2294
+ // Clear any existing auto-hide timeout
2295
+ if (this.controlPanelTimeout) {
2296
+ clearTimeout(this.controlPanelTimeout);
2297
+ this.controlPanelTimeout = null;
2298
+ }
2299
+
2300
+ switch (status) {
2301
+ case 'ready':
2302
+ console.log('[UIManager] Setting control panel to ready (hidden)');
2303
+ panel.classList.add('hidden');
2304
+ break;
2305
+
2306
+ case 'running':
2307
+ console.log('[UIManager] Setting control panel to running (showing cancel button)');
2308
+ panel.classList.remove('hidden');
2309
+ cancelBtn?.classList.remove('hidden');
2310
+ resumeBtn?.classList.add('hidden');
2311
+ terminateBtn?.classList.add('hidden');
2312
+
2313
+ // For fast-completing tasks, ensure minimum visibility duration
2314
+ this.ensureMinimumControlPanelVisibility();
2315
+ break;
2316
+
2317
+ case 'paused':
2318
+ console.log('[UIManager] Setting control panel to paused (showing resume/terminate buttons)');
2319
+ panel.classList.remove('hidden');
2320
+ cancelBtn?.classList.add('hidden');
2321
+ resumeBtn?.classList.remove('hidden');
2322
+ terminateBtn?.classList.remove('hidden');
2323
+ break;
2324
+
2325
+ default:
2326
+ console.log(`[UIManager] Unknown control panel status: ${status}, hiding panel`);
2327
+ panel.classList.add('hidden');
2328
+ }
2329
+ }
2330
+
2331
+ ensureMinimumControlPanelVisibility() {
2332
+ // Set a flag to prevent immediate hiding of control panel for fast tasks
2333
+ this.controlPanelMinVisibilityActive = true;
2334
+
2335
+ // Clear the flag after minimum visibility period (2 seconds)
2336
+ setTimeout(() => {
2337
+ this.controlPanelMinVisibilityActive = false;
2338
+ console.log('[UIManager] Minimum control panel visibility period ended');
2339
+ }, 2000);
2340
+ }
2341
+
2342
+ clearActivityLog() {
2343
+ if (this.elements.activityLog) {
2344
+ this.elements.activityLog.innerHTML = '';
2345
+ }
2346
+ }
2347
+
2348
+ showWelcomeMessage() {
2349
+ const welcomeHTML = `
2350
+ <div class="welcome-message">
2351
+ <div class="welcome-text">
2352
+ <h4>Welcome to VibeSurf</h4>
2353
+ <p>Let's vibe surfing the world with AI automation</p>
2354
+ </div>
2355
+ <div class="quick-tasks">
2356
+ <div class="task-suggestion" data-task="research">
2357
+ <div class="task-icon">🔍</div>
2358
+ <div class="task-content">
2359
+ <div class="task-title">Research Founders</div>
2360
+ <div class="task-description">Search information about browser-use and browser-use-webui, write a brief report</div>
2361
+ </div>
2362
+ </div>
2363
+ <div class="task-suggestion" data-task="news">
2364
+ <div class="task-icon">📰</div>
2365
+ <div class="task-content">
2366
+ <div class="task-title">HackerNews Summary</div>
2367
+ <div class="task-description">Get top 10 news from HackerNews and provide a summary</div>
2368
+ </div>
2369
+ </div>
2370
+ <div class="task-suggestion" data-task="analysis">
2371
+ <div class="task-icon">📈</div>
2372
+ <div class="task-content">
2373
+ <div class="task-title">Stock Market Analysis</div>
2374
+ <div class="task-description">Analyze recent week stock market trends for major tech companies</div>
2375
+ </div>
2376
+ </div>
2377
+ </div>
2378
+ </div>
2379
+ `;
2380
+
2381
+ if (this.elements.activityLog) {
2382
+ this.elements.activityLog.innerHTML = welcomeHTML;
2383
+ this.bindTaskSuggestionEvents();
2384
+ }
2385
+ }
2386
+
2387
+ bindTaskSuggestionEvents() {
2388
+ const taskSuggestions = document.querySelectorAll('.task-suggestion');
2389
+
2390
+ taskSuggestions.forEach(suggestion => {
2391
+ suggestion.addEventListener('click', () => {
2392
+ const taskDescription = suggestion.querySelector('.task-description').textContent;
2393
+
2394
+ // Populate task input with suggestion (only description, no title prefix)
2395
+ if (this.elements.taskInput) {
2396
+ this.elements.taskInput.value = taskDescription;
2397
+ this.elements.taskInput.focus();
2398
+
2399
+ // Trigger input change event for validation and auto-resize
2400
+ this.handleTaskInputChange({ target: this.elements.taskInput });
2401
+
2402
+ // Auto-send the task
2403
+ setTimeout(() => {
2404
+ this.handleSendTask();
2405
+ }, 100); // Small delay to ensure input processing is complete
2406
+ }
2407
+ });
2408
+ });
2409
+ }
2410
+
2411
+ displayActivityLogs(logs) {
2412
+ this.clearActivityLog();
2413
+
2414
+ if (logs.length === 0) {
2415
+ this.showWelcomeMessage();
2416
+ return;
2417
+ }
2418
+
2419
+ logs.forEach(log => this.addActivityLog(log));
2420
+ this.scrollActivityToBottom();
2421
+ }
2422
+
2423
+ addActivityLog(activityData) {
2424
+
2425
+ // Filter out "done" status messages from UI display only
2426
+ const agentStatus = activityData.agent_status || activityData.status || '';
2427
+ const agentMsg = activityData.agent_msg || activityData.message || '';
2428
+
2429
+
2430
+ // Filter done messages that are just status updates without meaningful content
2431
+ if (agentStatus.toLowerCase() === 'done') {
2432
+ return;
2433
+ }
2434
+
2435
+ const activityItem = this.createActivityItem(activityData);
2436
+
2437
+ if (this.elements.activityLog) {
2438
+ // Remove welcome message if present
2439
+ const welcomeMsg = this.elements.activityLog.querySelector('.welcome-message');
2440
+ if (welcomeMsg) {
2441
+ welcomeMsg.remove();
2442
+ }
2443
+
2444
+ this.elements.activityLog.appendChild(activityItem);
2445
+ activityItem.classList.add('fade-in');
2446
+ }
2447
+ }
2448
+
2449
+ addActivityItem(data) {
2450
+ const activityItem = this.createActivityItem(data);
2451
+
2452
+ if (this.elements.activityLog) {
2453
+ // Remove welcome message if present
2454
+ const welcomeMsg = this.elements.activityLog.querySelector('.welcome-message');
2455
+ if (welcomeMsg) {
2456
+ welcomeMsg.remove();
2457
+ }
2458
+
2459
+ this.elements.activityLog.appendChild(activityItem);
2460
+ activityItem.classList.add('fade-in');
2461
+ this.scrollActivityToBottom();
2462
+ }
2463
+ }
2464
+
2465
+ createActivityItem(data) {
2466
+ const item = document.createElement('div');
2467
+
2468
+ // Extract activity data with correct keys
2469
+ const agentName = data.agent_name || 'system';
2470
+ const agentStatus = data.agent_status || data.status || 'info';
2471
+ const agentMsg = data.agent_msg || data.message || data.action_description || 'No description';
2472
+ const timestamp = new Date(data.timestamp || Date.now()).toLocaleTimeString();
2473
+
2474
+ // Determine if this is a user message (should be on the right)
2475
+ const isUser = agentName.toLowerCase() === 'user';
2476
+
2477
+ // Set CSS classes based on agent type and status
2478
+ item.className = `activity-item ${isUser ? 'user-message' : 'agent-message'} ${agentStatus}`;
2479
+
2480
+ // Create the message structure similar to chat interface
2481
+ item.innerHTML = `
2482
+ <div class="message-container ${isUser ? 'user-container' : 'agent-container'}">
2483
+ <div class="message-header">
2484
+ <span class="agent-name">${agentName}</span>
2485
+ <span class="message-time">${timestamp}</span>
2486
+ </div>
2487
+ <div class="message-bubble ${isUser ? 'user-bubble' : 'agent-bubble'}">
2488
+ <div class="message-status">
2489
+ <span class="status-indicator ${agentStatus}">${this.getStatusIcon(agentStatus)}</span>
2490
+ <span class="status-text">${agentStatus}</span>
2491
+ </div>
2492
+ <div class="message-content">
2493
+ ${this.formatActivityContent(agentMsg)}
2494
+ </div>
2495
+ </div>
2496
+ </div>
2497
+ `;
2498
+
2499
+ return item;
2500
+ }
2501
+
2502
+ getStatusIcon(status) {
2503
+ switch (status.toLowerCase()) {
2504
+ case 'working':
2505
+ return '⚙️';
2506
+ case 'thinking':
2507
+ return '🤔';
2508
+ case 'result':
2509
+ case 'done':
2510
+ return '✅';
2511
+ case 'error':
2512
+ return '❌';
2513
+ case 'paused':
2514
+ return '⏸️';
2515
+ case 'running':
2516
+ return '🔄';
2517
+ case 'request':
2518
+ return '💡';
2519
+ default:
2520
+ return '💡';
2521
+ }
2522
+ }
2523
+
2524
+ formatActivityContent(content) {
2525
+
2526
+ if (!content) return 'No content';
2527
+
2528
+ // Handle object content
2529
+ if (typeof content === 'object') {
2530
+ return `<pre class="json-content">${JSON.stringify(content, null, 2)}</pre>`;
2531
+ }
2532
+
2533
+ // Convert content to string
2534
+ let formattedContent = String(content);
2535
+
2536
+ // Use markdown-it library for proper markdown rendering if available
2537
+ if (typeof markdownit !== 'undefined') {
2538
+ try {
2539
+ // Create markdown-it instance with enhanced options
2540
+ const md = markdownit({
2541
+ html: true, // Enable HTML tags in source
2542
+ breaks: true, // Convert '\n' in paragraphs into <br>
2543
+ linkify: true, // Autoconvert URL-like text to links
2544
+ typographer: true // Enable some language-neutral replacement + quotes beautification
2545
+ });
2546
+
2547
+ // Override link renderer to handle file:// protocol
2548
+ const defaultLinkRenderer = md.renderer.rules.link_open || function(tokens, idx, options, env, renderer) {
2549
+ return renderer.renderToken(tokens, idx, options);
2550
+ };
2551
+
2552
+ md.renderer.rules.link_open = function (tokens, idx, options, env, renderer) {
2553
+ const token = tokens[idx];
2554
+ const href = token.attrGet('href');
2555
+
2556
+ if (href && href.startsWith('file://')) {
2557
+ // Convert file:// links to our custom file-link format
2558
+ token.attrSet('href', '#');
2559
+ token.attrSet('class', 'file-link');
2560
+ token.attrSet('data-file-path', href);
2561
+ token.attrSet('title', `Click to open file: ${href}`);
2562
+ }
2563
+
2564
+ return defaultLinkRenderer(tokens, idx, options, env, renderer);
2565
+ };
2566
+
2567
+ // Add task list support manually (markdown-it doesn't have built-in task lists)
2568
+ formattedContent = this.preprocessTaskLists(formattedContent);
2569
+
2570
+ // Pre-process file:// markdown links since markdown-it doesn't recognize them
2571
+ const markdownFileLinkRegex = /\[([^\]]+)\]\((file:\/\/[^)]+)\)/g;
2572
+ formattedContent = formattedContent.replace(markdownFileLinkRegex, (match, linkText, fileUrl) => {
2573
+ // Convert to HTML format that markdown-it will preserve
2574
+ return `<a href="${fileUrl}" class="file-link-markdown">${linkText}</a>`;
2575
+ });
2576
+
2577
+ // Parse markdown
2578
+ const htmlContent = md.render(formattedContent);
2579
+
2580
+ // Post-process to handle local file path links (Windows paths and file:// URLs)
2581
+ let processedContent = htmlContent;
2582
+
2583
+ // Convert our pre-processed file:// links to proper file-link format
2584
+ const preProcessedFileLinkRegex = /<a href="(file:\/\/[^"]+)"[^>]*class="file-link-markdown"[^>]*>([^<]*)<\/a>/g;
2585
+ processedContent = processedContent.replace(preProcessedFileLinkRegex, (match, fileUrl, linkText) => {
2586
+ try {
2587
+ // Decode and fix the file URL
2588
+ let decodedUrl = decodeURIComponent(fileUrl);
2589
+ let cleanPath = decodedUrl.replace(/^file:\/\/\//, '').replace(/^file:\/\//, '');
2590
+ cleanPath = cleanPath.replace(/\\/g, '/');
2591
+
2592
+ // Ensure path starts with / for Unix paths or has drive letter for Windows
2593
+ if (!cleanPath.startsWith('/') && !cleanPath.match(/^[A-Za-z]:/)) {
2594
+ cleanPath = '/' + cleanPath;
2595
+ }
2596
+
2597
+ // Recreate proper file URL - always use triple slash for proper format
2598
+ let fixedUrl = cleanPath.match(/^[A-Za-z]:/) ?
2599
+ `file:///${cleanPath}` :
2600
+ `file://${cleanPath}`;
2601
+
2602
+
2603
+ return `<a href="#" class="file-link" data-file-path="${fixedUrl}" title="Click to open file">${linkText}</a>`;
2604
+ } catch (error) {
2605
+ console.error('[UIManager] Error processing pre-processed file:// link:', error);
2606
+ return match;
2607
+ }
2608
+ });
2609
+
2610
+ // Detect and convert local Windows file path links
2611
+ const windowsPathLinkRegex = /<a href="([A-Za-z]:\\[^"]+\.html?)"([^>]*)>([^<]*)<\/a>/g;
2612
+ processedContent = processedContent.replace(windowsPathLinkRegex, (match, filePath, attributes, linkText) => {
2613
+
2614
+ try {
2615
+ // Convert Windows path to file:// URL
2616
+ let normalizedPath = filePath.replace(/\\/g, '/');
2617
+ let fileUrl = `file:///${normalizedPath}`;
2618
+
2619
+
2620
+ return `<a href="#" class="file-link" data-file-path="${fileUrl}" title="Click to open file: ${filePath}"${attributes}>${linkText}</a>`;
2621
+ } catch (error) {
2622
+ console.error('[UIManager] Error converting Windows path:', error);
2623
+ return match; // Return original if conversion fails
2624
+ }
2625
+ });
2626
+
2627
+ // Detect and convert file:// protocol links
2628
+ const fileProtocolLinkRegex = /<a href="(file:\/\/[^"]+\.html?)"([^>]*)>([^<]*)<\/a>/g;
2629
+ processedContent = processedContent.replace(fileProtocolLinkRegex, (match, fileUrl, attributes, linkText) => {
2630
+
2631
+ try {
2632
+ // Decode and fix the file URL
2633
+ let decodedUrl = decodeURIComponent(fileUrl);
2634
+ let cleanPath = decodedUrl.replace(/^file:\/\/\//, '').replace(/^file:\/\//, '');
2635
+ cleanPath = cleanPath.replace(/\\/g, '/');
2636
+
2637
+ // Recreate proper file URL
2638
+ let fixedUrl = cleanPath.match(/^[A-Za-z]:/) ?
2639
+ `file:///${cleanPath}` :
2640
+ `file://${cleanPath}`;
2641
+
2642
+
2643
+ return `<a href="#" class="file-link" data-file-path="${fixedUrl}" title="Click to open file"${attributes}>${linkText}</a>`;
2644
+ } catch (error) {
2645
+ console.error('[UIManager] Error processing file:// link:', error);
2646
+ return match; // Return original if conversion fails
2647
+ }
2648
+ });
2649
+
2650
+ // Add custom classes and post-process
2651
+ return processedContent
2652
+ .replace(/<pre><code/g, '<pre class="code-block"><code')
2653
+ .replace(/<code>/g, '<code class="inline-code">')
2654
+ .replace(/<table>/g, '<table class="markdown-table">')
2655
+ .replace(/<blockquote>/g, '<blockquote class="markdown-quote">');
2656
+
2657
+ } catch (error) {
2658
+ console.warn('[UIManager] markdown-it parsing failed, falling back to basic formatting:', error);
2659
+ // Fall through to basic formatting
2660
+ }
2661
+ } else {
2662
+ }
2663
+
2664
+ // Fallback: Enhanced basic markdown-like formatting
2665
+
2666
+ // Task lists (checkboxes)
2667
+ formattedContent = this.preprocessTaskLists(formattedContent);
2668
+
2669
+ // Bold text **text**
2670
+ formattedContent = formattedContent.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
2671
+
2672
+ // Italic text *text*
2673
+ formattedContent = formattedContent.replace(/\*(.*?)\*/g, '<em>$1</em>');
2674
+
2675
+ // Code blocks ```code```
2676
+ formattedContent = formattedContent.replace(/```([\s\S]*?)```/g, '<pre class="code-block">$1</pre>');
2677
+
2678
+ // Inline code `code`
2679
+ formattedContent = formattedContent.replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>');
2680
+
2681
+ // Headers
2682
+ formattedContent = formattedContent.replace(/^### (.*$)/gm, '<h3>$1</h3>');
2683
+ formattedContent = formattedContent.replace(/^## (.*$)/gm, '<h2>$1</h2>');
2684
+ formattedContent = formattedContent.replace(/^# (.*$)/gm, '<h1>$1</h1>');
2685
+
2686
+ // Lists - regular
2687
+ formattedContent = formattedContent.replace(/^- (.*$)/gm, '<li>$1</li>');
2688
+ formattedContent = formattedContent.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>');
2689
+
2690
+ // Handle markdown-style local file links first [text](local_path) and [text](file://path)
2691
+ const markdownLocalLinkRegex = /\[([^\]]+)\]\(([A-Za-z]:\\[^)]+\.html?)\)/g;
2692
+ const markdownFileLinkRegex = /\[([^\]]+)\]\((file:\/\/[^)]+\.html?)\)/g;
2693
+
2694
+ // Handle [text](local_path) format - Enhanced path detection and conversion
2695
+ formattedContent = formattedContent.replace(markdownLocalLinkRegex, (match, linkText, filePath) => {
2696
+ try {
2697
+ // Normalize path separators and handle absolute paths
2698
+ let normalizedPath = filePath.replace(/\\/g, '/');
2699
+
2700
+ // Ensure proper file:// URL format
2701
+ let fileUrl;
2702
+ if (normalizedPath.startsWith('/')) {
2703
+ // Unix-style absolute path
2704
+ fileUrl = `file://${normalizedPath}`;
2705
+ } else if (normalizedPath.match(/^[A-Za-z]:/)) {
2706
+ // Windows-style absolute path (C:, D:, etc.)
2707
+ fileUrl = `file:///${normalizedPath}`;
2708
+ } else {
2709
+ // Relative path - make it absolute based on workspace
2710
+ fileUrl = `file:///${normalizedPath}`;
2711
+ }
2712
+
2713
+
2714
+ return `<a href="#" class="file-link" data-file-path="${fileUrl}" title="Click to open file: ${filePath}">${linkText}</a>`;
2715
+ } catch (error) {
2716
+ console.error('[UIManager] Error converting markdown local link:', error);
2717
+ return match;
2718
+ }
2719
+ });
2720
+
2721
+ // Handle [text](file://path) format
2722
+ formattedContent = formattedContent.replace(markdownFileLinkRegex, (match, linkText, fileUrl) => {
2723
+ try {
2724
+ // Decode and fix the file URL
2725
+ let decodedUrl = decodeURIComponent(fileUrl);
2726
+ let cleanPath = decodedUrl.replace(/^file:\/\/\//, '').replace(/^file:\/\//, '');
2727
+ cleanPath = cleanPath.replace(/\\/g, '/');
2728
+
2729
+ // Recreate proper file URL
2730
+ let fixedUrl = cleanPath.match(/^[A-Za-z]:/) ?
2731
+ `file:///${cleanPath}` :
2732
+ `file://${cleanPath}`;
2733
+
2734
+
2735
+ return `<a href="#" class="file-link" data-file-path="${fixedUrl}" title="Click to open file">${linkText}</a>`;
2736
+ } catch (error) {
2737
+ console.error('[UIManager] Error processing markdown file:// link:', error);
2738
+ return match;
2739
+ }
2740
+ });
2741
+
2742
+
2743
+ // Handle [text](file://path) format
2744
+ formattedContent = formattedContent.replace(markdownFileLinkRegex, (match, linkText, fileUrl) => {
2745
+ try {
2746
+ // Decode and fix the file URL
2747
+ let decodedUrl = decodeURIComponent(fileUrl);
2748
+ let cleanPath = decodedUrl.replace(/^file:\/\/\//, '').replace(/^file:\/\//, '');
2749
+ cleanPath = cleanPath.replace(/\\/g, '/');
2750
+
2751
+ // Recreate proper file URL
2752
+ let fixedUrl = cleanPath.match(/^[A-Za-z]:/) ?
2753
+ `file:///${cleanPath}` :
2754
+ `file://${cleanPath}`;
2755
+
2756
+
2757
+ return `<a href="#" class="file-link" data-file-path="${fixedUrl}" title="Click to open file">${linkText}</a>`;
2758
+ } catch (error) {
2759
+ console.error('[UIManager] Error processing markdown file:// link:', error);
2760
+ return match;
2761
+ }
2762
+ });
2763
+
2764
+ // Convert URLs to links - Enhanced for file:// protocol handling
2765
+ const httpUrlRegex = /(https?:\/\/[^\s]+)/g;
2766
+ const fileUrlRegex = /(file:\/\/[^\s]+)/g;
2767
+
2768
+ // Handle HTTP/HTTPS URLs normally
2769
+ formattedContent = formattedContent.replace(httpUrlRegex, '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>');
2770
+
2771
+ // Handle file:// URLs with custom class for special handling
2772
+ formattedContent = formattedContent.replace(fileUrlRegex, (match, fileUrl) => {
2773
+ try {
2774
+ // Immediately decode and fix the file URL
2775
+ let decodedUrl = decodeURIComponent(fileUrl);
2776
+
2777
+ // Remove file:// protocol and normalize path
2778
+ let cleanPath = decodedUrl.replace(/^file:\/\/\//, '').replace(/^file:\/\//, '');
2779
+ cleanPath = cleanPath.replace(/\\/g, '/');
2780
+
2781
+ // Recreate proper file URL
2782
+ let fixedUrl = cleanPath.match(/^[A-Za-z]:/) ?
2783
+ `file:///${cleanPath}` :
2784
+ `file://${cleanPath}`;
2785
+
2786
+
2787
+ return `<a href="#" class="file-link" data-file-path="${fixedUrl}" title="Click to open file">${fixedUrl}</a>`;
2788
+ } catch (error) {
2789
+ console.error('[UIManager] Error processing file URL in markdown:', error);
2790
+ return `<a href="#" class="file-link" data-file-path="${fileUrl}" title="Click to open file">${fileUrl}</a>`;
2791
+ }
2792
+ });
2793
+
2794
+
2795
+ // Convert newlines to br tags
2796
+ formattedContent = formattedContent.replace(/\n/g, '<br>');
2797
+
2798
+ return formattedContent;
2799
+ }
2800
+
2801
+ preprocessTaskLists(content) {
2802
+ // Convert markdown task lists to HTML checkboxes
2803
+ // - [ ] unchecked task -> checkbox unchecked
2804
+ // - [x] checked task -> checkbox checked
2805
+ // - [X] checked task -> checkbox checked
2806
+ return content
2807
+ .replace(/^- \[ \] (.*)$/gm, '<div class="task-item"><input type="checkbox" disabled> $1</div>')
2808
+ .replace(/^- \[[xX]\] (.*)$/gm, '<div class="task-item"><input type="checkbox" checked disabled> $1</div>');
2809
+ }
2810
+
2811
+ scrollActivityToBottom() {
2812
+ if (this.elements.activityLog) {
2813
+ this.elements.activityLog.scrollTop = this.elements.activityLog.scrollHeight;
2814
+ }
2815
+ }
2816
+
2817
+ clearTaskInput() {
2818
+ if (this.elements.taskInput) {
2819
+ this.elements.taskInput.value = '';
2820
+ }
2821
+
2822
+ if (this.elements.sendBtn) {
2823
+ this.elements.sendBtn.disabled = true;
2824
+ }
2825
+ }
2826
+
2827
+ // Modal Management
2828
+ displayHistoryModal(sessions) {
2829
+ if (!this.elements.historyList) return;
2830
+
2831
+ this.elements.historyList.innerHTML = '';
2832
+
2833
+ if (sessions.length === 0) {
2834
+ this.elements.historyList.innerHTML = `
2835
+ <div class="empty-state">
2836
+ <div class="empty-state-icon">📝</div>
2837
+ <div class="empty-state-title">No Sessions Found</div>
2838
+ <div class="empty-state-description">Create a new session to get started.</div>
2839
+ </div>
2840
+ `;
2841
+ } else {
2842
+ sessions.forEach(session => {
2843
+ const item = this.createHistoryItem(session);
2844
+ this.elements.historyList.appendChild(item);
2845
+ });
2846
+ }
2847
+
2848
+ this.openModal('history');
2849
+ }
2850
+
2851
+ createHistoryItem(session) {
2852
+ const item = document.createElement('div');
2853
+ item.className = 'history-item';
2854
+
2855
+ const createdAt = new Date(session.createdAt || session.lastUpdated).toLocaleString();
2856
+ const taskCount = session.taskHistory?.length || 0;
2857
+
2858
+ item.innerHTML = `
2859
+ <div class="history-item-header">
2860
+ <span class="history-session-id">${session.sessionId}</span>
2861
+ <span class="history-timestamp">${createdAt}</span>
2862
+ </div>
2863
+ <div class="history-task">${taskCount} task(s)</div>
2864
+ <div class="history-status">
2865
+ <span class="status-dot ${session.status || 'active'}"></span>
2866
+ ${session.status || 'active'}
2867
+ </div>
2868
+ `;
2869
+
2870
+ item.addEventListener('click', () => {
2871
+ this.loadSession(session.sessionId);
2872
+ this.closeModal();
2873
+ });
2874
+
2875
+ return item;
2876
+ }
2877
+
2878
+ async loadSession(sessionId) {
2879
+ // Check if task is running
2880
+ if (this.state.isTaskRunning) {
2881
+ const canProceed = await this.showTaskRunningWarning('load a different session');
2882
+ if (!canProceed) return;
2883
+ }
2884
+
2885
+ try {
2886
+ this.showLoading('Loading session...');
2887
+
2888
+ const success = await this.sessionManager.loadSession(sessionId);
2889
+
2890
+ this.hideLoading();
2891
+
2892
+ if (!success) {
2893
+ this.showNotification('Failed to load session', 'error');
2894
+ }
2895
+ } catch (error) {
2896
+ this.hideLoading();
2897
+ this.showNotification(`Failed to load session: ${error.message}`, 'error');
2898
+ }
2899
+ }
2900
+
2901
+ async displaySettingsModal() {
2902
+ // Load current backend URL from API client
2903
+ if (this.elements.backendUrl && this.apiClient) {
2904
+ this.elements.backendUrl.value = this.apiClient.baseURL;
2905
+ }
2906
+
2907
+ this.openModal('settings');
2908
+ }
2909
+
2910
+
2911
+ updateLLMProfileSelect() {
2912
+ if (!this.elements.llmProfileSelect) return;
2913
+
2914
+ const select = this.elements.llmProfileSelect;
2915
+ select.innerHTML = ''; // Remove default option
2916
+
2917
+ if (this.state.llmProfiles.length === 0) {
2918
+ // Add placeholder option when no profiles available
2919
+ const placeholderOption = document.createElement('option');
2920
+ placeholderOption.value = '';
2921
+ placeholderOption.textContent = 'No LLM profiles configured - Go to Settings';
2922
+ placeholderOption.disabled = true;
2923
+ placeholderOption.selected = true;
2924
+ select.appendChild(placeholderOption);
2925
+ } else {
2926
+ // Add default empty option
2927
+ const emptyOption = document.createElement('option');
2928
+ emptyOption.value = '';
2929
+ emptyOption.textContent = 'Select LLM Profile...';
2930
+ emptyOption.disabled = true;
2931
+ select.appendChild(emptyOption);
2932
+
2933
+ // Add actual profiles
2934
+ this.state.llmProfiles.forEach(profile => {
2935
+ const option = document.createElement('option');
2936
+ option.value = profile.profile_name;
2937
+ option.textContent = profile.profile_name;
2938
+ if (profile.is_default) {
2939
+ option.selected = true;
2940
+ }
2941
+ select.appendChild(option);
2942
+ });
2943
+ }
2944
+
2945
+ // Update send button state if taskInput exists
2946
+ if (this.elements.taskInput) {
2947
+ this.handleTaskInputChange({ target: this.elements.taskInput });
2948
+ }
2949
+ }
2950
+
2951
+ showLLMProfileRequiredModal(action) {
2952
+ const isConfigureAction = action === 'configure';
2953
+ const title = isConfigureAction ? 'LLM Profile Required' : 'Please Select LLM Profile';
2954
+ const message = isConfigureAction
2955
+ ? 'No LLM profiles are configured. You need to configure at least one LLM profile before sending tasks.'
2956
+ : 'Please select an LLM profile from the dropdown to proceed with your task.';
2957
+
2958
+ const modal = this.createWarningModal({
2959
+ title,
2960
+ message,
2961
+ details: isConfigureAction
2962
+ ? 'LLM profiles contain the configuration for AI models (like OpenAI, Claude, etc.) that will process your tasks.'
2963
+ : null,
2964
+ buttons: isConfigureAction
2965
+ ? [
2966
+ {
2967
+ text: 'Open Settings',
2968
+ style: 'primary',
2969
+ action: () => {
2970
+ this.closeWarningModal();
2971
+ this.handleShowSettings();
2972
+ }
2973
+ },
2974
+ {
2975
+ text: 'Cancel',
2976
+ style: 'secondary',
2977
+ action: () => {
2978
+ this.closeWarningModal();
2979
+ }
2980
+ }
2981
+ ]
2982
+ : [
2983
+ {
2984
+ text: 'Open Settings',
2985
+ style: 'secondary',
2986
+ action: () => {
2987
+ this.closeWarningModal();
2988
+ this.handleShowSettings();
2989
+ }
2990
+ },
2991
+ {
2992
+ text: 'OK',
2993
+ style: 'primary',
2994
+ action: () => {
2995
+ this.closeWarningModal();
2996
+ this.elements.llmProfileSelect?.focus();
2997
+ }
2998
+ }
2999
+ ]
3000
+ });
3001
+
3002
+ document.body.appendChild(modal);
3003
+ }
3004
+
3005
+ updateSettingsDisplay() {
3006
+ // Update LLM profiles list in settings
3007
+ if (this.elements.llmProfilesList) {
3008
+ this.elements.llmProfilesList.innerHTML = '';
3009
+
3010
+ this.state.llmProfiles.forEach(profile => {
3011
+ const item = this.createProfileItem(profile, 'llm');
3012
+ this.elements.llmProfilesList.appendChild(item);
3013
+ });
3014
+ }
3015
+
3016
+ // Update MCP profiles list in settings
3017
+ if (this.elements.mcpFilesList) {
3018
+ this.elements.mcpFilesList.innerHTML = '';
3019
+
3020
+ this.state.mcpProfiles.forEach(profile => {
3021
+ const item = this.createProfileItem(profile, 'mcp');
3022
+ this.elements.mcpFilesList.appendChild(item);
3023
+ });
3024
+ }
3025
+ }
3026
+
3027
+ createProfileItem(profile, type) {
3028
+ const item = document.createElement('div');
3029
+ item.className = 'profile-item';
3030
+
3031
+ const isLLM = type === 'llm';
3032
+ const name = profile.profile_name;
3033
+ const details = isLLM ?
3034
+ `${profile.provider} - ${profile.model}` :
3035
+ `${profile.server_name || 'Unknown'}`;
3036
+
3037
+ // Add active status
3038
+ const activeStatus = profile.is_active ? 'Active' : 'Inactive';
3039
+ const activeClass = profile.is_active ? 'active' : 'inactive';
3040
+
3041
+ item.innerHTML = `
3042
+ <div class="profile-header">
3043
+ <span class="profile-name">${name}</span>
3044
+ <div class="profile-badges">
3045
+ ${isLLM && profile.is_default ? '<span class="profile-default">Default</span>' : ''}
3046
+ <span class="profile-status ${activeClass}">${activeStatus}</span>
3047
+ </div>
3048
+ <div class="profile-actions">
3049
+ <button class="profile-btn edit-btn" data-name="${name}" data-type="${type}">Edit</button>
3050
+ <button class="profile-btn danger delete-btn" data-name="${name}" data-type="${type}">Delete</button>
3051
+ </div>
3052
+ </div>
3053
+ <div class="profile-details">${details}</div>
3054
+ `;
3055
+
3056
+ // Add event listeners
3057
+ const editBtn = item.querySelector('.edit-btn');
3058
+ const deleteBtn = item.querySelector('.delete-btn');
3059
+
3060
+ editBtn?.addEventListener('click', () => {
3061
+ this.editProfile(name, type);
3062
+ });
3063
+
3064
+ deleteBtn?.addEventListener('click', () => {
3065
+ this.deleteProfile(name, type);
3066
+ });
3067
+
3068
+ return item;
3069
+ }
3070
+
3071
+ async editProfile(name, type) {
3072
+ this.showNotification(`Edit ${type.toUpperCase()} profile '${name}' coming soon...`, 'info');
3073
+ // TODO: Implement profile editing form
3074
+ console.log(`[UIManager] Edit ${type} profile:`, name);
3075
+ }
3076
+
3077
+ async deleteProfile(name, type) {
3078
+ if (!confirm(`Are you sure you want to delete the ${type.toUpperCase()} profile '${name}'?`)) {
3079
+ return;
3080
+ }
3081
+
3082
+ try {
3083
+ this.showLoading(`Deleting ${type} profile...`);
3084
+
3085
+ if (type === 'llm') {
3086
+ await this.apiClient.deleteLLMProfile(name);
3087
+ } else {
3088
+ await this.apiClient.deleteMCPProfile(name);
3089
+ }
3090
+
3091
+ // Refresh the settings data
3092
+ await this.loadSettingsData();
3093
+
3094
+ this.hideLoading();
3095
+ this.showNotification(`${type.toUpperCase()} profile '${name}' deleted successfully`, 'success');
3096
+ } catch (error) {
3097
+ this.hideLoading();
3098
+ this.showNotification(`Failed to delete ${type} profile: ${error.message}`, 'error');
3099
+ }
3100
+ }
3101
+
3102
+ openModal(modalName) {
3103
+ const modal = document.getElementById(`${modalName}-modal`);
3104
+ if (modal) {
3105
+ modal.classList.remove('hidden');
3106
+ modal.classList.add('scale-in');
3107
+ this.state.currentModal = modalName;
3108
+ }
3109
+ }
3110
+
3111
+ closeModal() {
3112
+ if (this.state.currentModal) {
3113
+ const modal = document.getElementById(`${this.state.currentModal}-modal`);
3114
+ if (modal) {
3115
+ modal.classList.add('hidden');
3116
+ modal.classList.remove('scale-in');
3117
+ }
3118
+ this.state.currentModal = null;
3119
+ }
3120
+ }
3121
+
3122
+ // History Modal Handlers
3123
+ setupHistoryModalHandlers() {
3124
+ // View More Tasks button
3125
+ this.elements.viewMoreTasksBtn?.addEventListener('click', this.handleViewMoreTasks.bind(this));
3126
+
3127
+ // Back to Recent button
3128
+ this.elements.backToRecentBtn?.addEventListener('click', this.handleBackToRecent.bind(this));
3129
+
3130
+ // Search and filter
3131
+ this.elements.sessionSearch?.addEventListener('input', this.handleSessionSearch.bind(this));
3132
+ this.elements.sessionFilter?.addEventListener('change', this.handleSessionFilter.bind(this));
3133
+
3134
+ // Pagination
3135
+ this.elements.prevPageBtn?.addEventListener('click', this.handlePrevPage.bind(this));
3136
+ this.elements.nextPageBtn?.addEventListener('click', this.handleNextPage.bind(this));
3137
+
3138
+ console.log('[UIManager] History modal handlers bound');
3139
+ }
3140
+
3141
+ async handleViewMoreTasks() {
3142
+ try {
3143
+ console.log('[UIManager] View More Tasks clicked');
3144
+ this.showLoading('Loading all sessions...');
3145
+
3146
+ // Switch to all sessions view
3147
+ this.state.historyMode = 'all';
3148
+ console.log('[UIManager] Set history mode to "all"');
3149
+
3150
+ await this.loadAllSessions();
3151
+ console.log('[UIManager] All sessions loaded, switching view');
3152
+
3153
+ this.displayAllSessionsView();
3154
+ console.log('[UIManager] All sessions view displayed');
3155
+
3156
+ this.hideLoading();
3157
+ } catch (error) {
3158
+ this.hideLoading();
3159
+ console.error('[UIManager] Error in handleViewMoreTasks:', error);
3160
+ this.showNotification(`Failed to load sessions: ${error.message}`, 'error');
3161
+ }
3162
+ }
3163
+
3164
+ handleBackToRecent() {
3165
+ this.state.historyMode = 'recent';
3166
+ this.displayRecentTasksView();
3167
+ }
3168
+
3169
+ handleSessionSearch(event) {
3170
+ this.state.searchQuery = event.target.value.trim().toLowerCase();
3171
+ this.filterAndDisplaySessions();
3172
+ }
3173
+
3174
+ handleSessionFilter(event) {
3175
+ this.state.statusFilter = event.target.value;
3176
+ this.filterAndDisplaySessions();
3177
+ }
3178
+
3179
+ handlePrevPage() {
3180
+ if (this.state.currentPage > 1) {
3181
+ this.state.currentPage--;
3182
+ this.filterAndDisplaySessions();
3183
+ }
3184
+ }
3185
+
3186
+ handleNextPage() {
3187
+ if (this.state.currentPage < this.state.totalPages) {
3188
+ this.state.currentPage++;
3189
+ this.filterAndDisplaySessions();
3190
+ }
3191
+ }
3192
+
3193
+ // History Data Loading Methods
3194
+ async loadRecentTasks() {
3195
+ try {
3196
+ console.log('[UIManager] Loading recent tasks...');
3197
+ const response = await this.apiClient.getRecentTasks();
3198
+
3199
+ // Handle API response structure: { tasks: [...], total_count: ..., limit: ... }
3200
+ let tasks = [];
3201
+ if (response && response.tasks && Array.isArray(response.tasks)) {
3202
+ tasks = response.tasks;
3203
+ } else if (response && Array.isArray(response)) {
3204
+ tasks = response;
3205
+ } else if (response && response.data && Array.isArray(response.data)) {
3206
+ tasks = response.data;
3207
+ }
3208
+
3209
+ // Take only the first 3 most recent tasks
3210
+ this.state.recentTasks = tasks.slice(0, 3);
3211
+ console.log('[UIManager] Recent tasks loaded:', this.state.recentTasks.length);
3212
+
3213
+ return this.state.recentTasks;
3214
+ } catch (error) {
3215
+ console.error('[UIManager] Failed to load recent tasks:', error);
3216
+ this.state.recentTasks = [];
3217
+ throw error;
3218
+ }
3219
+ }
3220
+
3221
+ async loadAllSessions() {
3222
+ try {
3223
+ console.log('[UIManager] Loading all sessions...');
3224
+ const response = await this.apiClient.getAllSessions();
3225
+
3226
+ // Handle API response structure: { sessions: [...], total_count: ..., limit: ..., offset: ... }
3227
+ let sessions = [];
3228
+ if (response && response.sessions && Array.isArray(response.sessions)) {
3229
+ sessions = response.sessions;
3230
+ } else if (response && Array.isArray(response)) {
3231
+ sessions = response;
3232
+ } else if (response && response.data && Array.isArray(response.data)) {
3233
+ sessions = response.data;
3234
+ }
3235
+
3236
+ this.state.allSessions = sessions;
3237
+ console.log('[UIManager] All sessions loaded:', this.state.allSessions.length);
3238
+
3239
+ return this.state.allSessions;
3240
+ } catch (error) {
3241
+ console.error('[UIManager] Failed to load all sessions:', error);
3242
+ this.state.allSessions = [];
3243
+ throw error;
3244
+ }
3245
+ }
3246
+
3247
+ // History Display Methods
3248
+ displayHistoryModal() {
3249
+ if (this.state.historyMode === 'recent') {
3250
+ this.displayRecentTasksView();
3251
+ } else {
3252
+ this.displayAllSessionsView();
3253
+ }
3254
+ this.openModal('history');
3255
+ }
3256
+
3257
+ displayRecentTasksView() {
3258
+ console.log('[UIManager] Switching to recent tasks view');
3259
+
3260
+ // Show recent tasks section and hide all sessions section
3261
+ if (this.elements.recentTasksList && this.elements.allSessionsSection) {
3262
+ const recentParent = this.elements.recentTasksList.parentElement;
3263
+ if (recentParent) {
3264
+ recentParent.classList.remove('hidden');
3265
+ recentParent.style.display = 'block';
3266
+ console.log('[UIManager] Showed recent tasks section');
3267
+ }
3268
+ this.elements.allSessionsSection.classList.add('hidden');
3269
+ this.elements.allSessionsSection.style.display = 'none';
3270
+ console.log('[UIManager] Hidden all sessions section');
3271
+ }
3272
+
3273
+ this.renderRecentTasks();
3274
+ }
3275
+
3276
+ displayAllSessionsView() {
3277
+ console.log('[UIManager] Switching to all sessions view');
3278
+ console.log('[UIManager] Elements check:', {
3279
+ recentTasksList: !!this.elements.recentTasksList,
3280
+ allSessionsSection: !!this.elements.allSessionsSection,
3281
+ recentTasksParent: !!this.elements.recentTasksList?.parentElement
3282
+ });
3283
+
3284
+ // Hide recent tasks section and show all sessions section
3285
+ if (this.elements.recentTasksList && this.elements.allSessionsSection) {
3286
+ const recentParent = this.elements.recentTasksList.parentElement;
3287
+ if (recentParent) {
3288
+ recentParent.style.display = 'none';
3289
+ recentParent.classList.add('hidden');
3290
+ console.log('[UIManager] Hidden recent tasks section');
3291
+ }
3292
+
3293
+ // Remove hidden class and set display block
3294
+ this.elements.allSessionsSection.classList.remove('hidden');
3295
+ this.elements.allSessionsSection.style.display = 'block';
3296
+ console.log('[UIManager] Showed all sessions section - removed hidden class and set display block');
3297
+
3298
+ // Debug: Check computed styles
3299
+ const computedStyle = window.getComputedStyle(this.elements.allSessionsSection);
3300
+ console.log('[UIManager] All sessions section computed display:', computedStyle.display);
3301
+ console.log('[UIManager] All sessions section classList:', this.elements.allSessionsSection.classList.toString());
3302
+
3303
+ } else {
3304
+ console.error('[UIManager] Missing elements for view switching:', {
3305
+ recentTasksList: !!this.elements.recentTasksList,
3306
+ allSessionsSection: !!this.elements.allSessionsSection
3307
+ });
3308
+ }
3309
+
3310
+ // Reset search and filter
3311
+ this.state.currentPage = 1;
3312
+ this.state.searchQuery = '';
3313
+ this.state.statusFilter = 'all';
3314
+
3315
+ if (this.elements.sessionSearch) {
3316
+ this.elements.sessionSearch.value = '';
3317
+ }
3318
+ if (this.elements.sessionFilter) {
3319
+ this.elements.sessionFilter.value = 'all';
3320
+ }
3321
+
3322
+ console.log('[UIManager] About to filter and display sessions');
3323
+ this.filterAndDisplaySessions();
3324
+ }
3325
+
3326
+ renderRecentTasks() {
3327
+ if (!this.elements.recentTasksList) return;
3328
+
3329
+ this.elements.recentTasksList.innerHTML = '';
3330
+
3331
+ if (this.state.recentTasks.length === 0) {
3332
+ this.elements.recentTasksList.innerHTML = `
3333
+ <div class="empty-state">
3334
+ <div class="empty-state-icon">📝</div>
3335
+ <div class="empty-state-title">No Recent Tasks</div>
3336
+ <div class="empty-state-description">Start a new task to see it here.</div>
3337
+ </div>
3338
+ `;
3339
+ return;
3340
+ }
3341
+
3342
+ this.state.recentTasks.forEach(task => {
3343
+ const taskItem = this.createTaskItem(task);
3344
+ this.elements.recentTasksList.appendChild(taskItem);
3345
+ });
3346
+ }
3347
+
3348
+ filterAndDisplaySessions() {
3349
+ if (!this.elements.allSessionsList) {
3350
+ console.error('[UIManager] allSessionsList element not found');
3351
+ return;
3352
+ }
3353
+
3354
+ console.log('[UIManager] Filtering sessions. Total sessions:', this.state.allSessions.length);
3355
+ console.log('[UIManager] Search query:', this.state.searchQuery);
3356
+ console.log('[UIManager] Status filter:', this.state.statusFilter);
3357
+
3358
+ let filteredSessions = [...this.state.allSessions]; // Create copy
3359
+
3360
+ // Apply search filter
3361
+ if (this.state.searchQuery) {
3362
+ filteredSessions = filteredSessions.filter(session =>
3363
+ session.session_id.toLowerCase().includes(this.state.searchQuery) ||
3364
+ (session.description && session.description.toLowerCase().includes(this.state.searchQuery))
3365
+ );
3366
+ }
3367
+
3368
+ // Apply status filter
3369
+ if (this.state.statusFilter !== 'all') {
3370
+ filteredSessions = filteredSessions.filter(session =>
3371
+ (session.status || 'active').toLowerCase() === this.state.statusFilter.toLowerCase()
3372
+ );
3373
+ }
3374
+
3375
+ console.log('[UIManager] Filtered sessions count:', filteredSessions.length);
3376
+
3377
+ // Calculate pagination
3378
+ const totalSessions = filteredSessions.length;
3379
+ this.state.totalPages = Math.ceil(totalSessions / this.state.pageSize);
3380
+
3381
+ // Ensure current page is valid
3382
+ if (this.state.currentPage > this.state.totalPages) {
3383
+ this.state.currentPage = Math.max(1, this.state.totalPages);
3384
+ }
3385
+
3386
+ // Get sessions for current page
3387
+ const startIndex = (this.state.currentPage - 1) * this.state.pageSize;
3388
+ const endIndex = startIndex + this.state.pageSize;
3389
+ const paginatedSessions = filteredSessions.slice(startIndex, endIndex);
3390
+
3391
+ console.log('[UIManager] Paginated sessions for display:', paginatedSessions.length);
3392
+
3393
+ // Render sessions
3394
+ this.renderSessionsList(paginatedSessions);
3395
+
3396
+ // Update pagination controls
3397
+ this.updatePaginationControls();
3398
+ }
3399
+
3400
+ renderSessionsList(sessions) {
3401
+ if (!this.elements.allSessionsList) {
3402
+ console.error('[UIManager] allSessionsList element not found for rendering');
3403
+ return;
3404
+ }
3405
+
3406
+ console.log('[UIManager] Rendering sessions list with', sessions.length, 'sessions');
3407
+
3408
+ this.elements.allSessionsList.innerHTML = '';
3409
+
3410
+ if (sessions.length === 0) {
3411
+ console.log('[UIManager] No sessions to display, showing empty state');
3412
+ this.elements.allSessionsList.innerHTML = `
3413
+ <div class="empty-state">
3414
+ <div class="empty-state-icon">🔍</div>
3415
+ <div class="empty-state-title">No Sessions Found</div>
3416
+ <div class="empty-state-description">Try adjusting your search or filter criteria.</div>
3417
+ </div>
3418
+ `;
3419
+ return;
3420
+ }
3421
+
3422
+ sessions.forEach((session, index) => {
3423
+ console.log(`[UIManager] Creating session item ${index + 1}:`, session.session_id);
3424
+ const sessionItem = this.createSessionItem(session);
3425
+ this.elements.allSessionsList.appendChild(sessionItem);
3426
+ });
3427
+
3428
+ console.log('[UIManager] Sessions list rendered successfully');
3429
+ }
3430
+
3431
+ updatePaginationControls() {
3432
+ // Update pagination buttons
3433
+ if (this.elements.prevPageBtn) {
3434
+ this.elements.prevPageBtn.disabled = this.state.currentPage <= 1;
3435
+ }
3436
+
3437
+ if (this.elements.nextPageBtn) {
3438
+ this.elements.nextPageBtn.disabled = this.state.currentPage >= this.state.totalPages;
3439
+ }
3440
+
3441
+ // Update page info
3442
+ if (this.elements.pageInfo) {
3443
+ if (this.state.totalPages === 0) {
3444
+ this.elements.pageInfo.textContent = 'No results';
3445
+ } else {
3446
+ this.elements.pageInfo.textContent = `Page ${this.state.currentPage} of ${this.state.totalPages}`;
3447
+ }
3448
+ }
3449
+ }
3450
+
3451
+ // Item Creation Methods
3452
+ createTaskItem(task) {
3453
+ const item = document.createElement('div');
3454
+ item.className = 'recent-task-item';
3455
+
3456
+ const sessionId = task.session_id || 'Unknown';
3457
+ const taskDesc = task.description || task.task_description || 'No description';
3458
+ const timestamp = new Date(task.created_at || task.timestamp || Date.now()).toLocaleString();
3459
+ const status = task.status || 'completed';
3460
+
3461
+ item.innerHTML = `
3462
+ <div class="task-item-header">
3463
+ <div class="task-session-id">${sessionId}</div>
3464
+ <div class="task-timestamp">${timestamp}</div>
3465
+ </div>
3466
+ <div class="task-description">${this.truncateText(taskDesc, 100)}</div>
3467
+ <div class="task-status">
3468
+ <span class="status-dot ${status}"></span>
3469
+ <span class="status-text">${status}</span>
3470
+ </div>
3471
+ `;
3472
+
3473
+ item.addEventListener('click', () => {
3474
+ this.handleTaskItemClick(task);
3475
+ });
3476
+
3477
+ return item;
3478
+ }
3479
+
3480
+ createSessionItem(session) {
3481
+ const item = document.createElement('div');
3482
+ item.className = 'session-item';
3483
+
3484
+ const sessionId = session.session_id || 'Unknown';
3485
+ const createdAt = new Date(session.created_at || session.timestamp || Date.now()).toLocaleString();
3486
+ const lastActivity = session.last_activity ? new Date(session.last_activity).toLocaleString() : 'No activity';
3487
+ const taskCount = session.task_count || 0;
3488
+ const status = session.status || 'active';
3489
+
3490
+ item.innerHTML = `
3491
+ <div class="session-item-header">
3492
+ <div class="session-id">${sessionId}</div>
3493
+ <div class="session-timestamp">${createdAt}</div>
3494
+ </div>
3495
+ <div class="session-details">
3496
+ <div class="session-info">
3497
+ <span class="session-task-count">${taskCount} task(s)</span>
3498
+ <span class="session-last-activity">Last: ${lastActivity}</span>
3499
+ </div>
3500
+ <div class="session-status">
3501
+ <span class="status-dot ${status}"></span>
3502
+ <span class="status-text">${status}</span>
3503
+ </div>
3504
+ </div>
3505
+ `;
3506
+
3507
+ // Add enhanced click handler with debugging
3508
+ item.addEventListener('click', (event) => {
3509
+ event.preventDefault();
3510
+ event.stopPropagation();
3511
+ this.handleSessionItemClick(session);
3512
+ });
3513
+
3514
+ // Add visual feedback for clickability
3515
+ item.style.cursor = 'pointer';
3516
+ item.setAttribute('title', `Click to load session: ${sessionId}`);
3517
+
3518
+ return item;
3519
+ }
3520
+
3521
+ // Click Handlers
3522
+ async handleTaskItemClick(task) {
3523
+ try {
3524
+
3525
+ const sessionId = task.session_id;
3526
+ if (!sessionId) {
3527
+ console.error('[UIManager] No session ID found in task data:', task);
3528
+ this.showNotification('Invalid task - no session ID found', 'error');
3529
+ return;
3530
+ }
3531
+
3532
+
3533
+ // Close the modal first
3534
+ this.closeModal();
3535
+
3536
+ // Load the session and show logs
3537
+ await this.loadSessionAndShowLogs(sessionId);
3538
+
3539
+ } catch (error) {
3540
+ console.error('[UIManager] Error in handleTaskItemClick:', error);
3541
+ this.showNotification(`Failed to load task session: ${error.message}`, 'error');
3542
+ }
3543
+ }
3544
+
3545
+ async handleSessionItemClick(session) {
3546
+ try {
3547
+
3548
+ const sessionId = session.session_id;
3549
+ if (!sessionId) {
3550
+ console.error('[UIManager] No session ID found in session data:', session);
3551
+ this.showNotification('Invalid session - no session ID found', 'error');
3552
+ return;
3553
+ }
3554
+
3555
+
3556
+ // Close the modal first
3557
+ this.closeModal();
3558
+
3559
+ // Load the session and show logs
3560
+ await this.loadSessionAndShowLogs(sessionId);
3561
+
3562
+ } catch (error) {
3563
+ console.error('[UIManager] Error in handleSessionItemClick:', error);
3564
+ this.showNotification(`Failed to load session: ${error.message}`, 'error');
3565
+ }
3566
+ }
3567
+
3568
+ async loadSessionAndShowLogs(sessionId) {
3569
+ // Check if task is running
3570
+ if (this.state.isTaskRunning) {
3571
+ const canProceed = await this.showTaskRunningWarning('load a different session');
3572
+ if (!canProceed) return;
3573
+ }
3574
+
3575
+ try {
3576
+ this.showLoading('Loading session...');
3577
+
3578
+
3579
+ // Load the session in session manager
3580
+ const success = await this.sessionManager.loadSession(sessionId);
3581
+
3582
+ if (success) {
3583
+ // Session manager will trigger the session loaded event
3584
+ // which will update the UI automatically
3585
+ this.showNotification('Session loaded successfully', 'success');
3586
+ } else {
3587
+ this.showNotification('Failed to load session - session may not exist', 'error');
3588
+ }
3589
+
3590
+ this.hideLoading();
3591
+ } catch (error) {
3592
+ console.error('[UIManager] Error in loadSessionAndShowLogs:', error);
3593
+ this.hideLoading();
3594
+ this.showNotification(`Failed to load session: ${error.message}`, 'error');
3595
+ }
3596
+ }
3597
+
3598
+ // Utility Methods
3599
+ truncateText(text, maxLength) {
3600
+ if (!text) return '';
3601
+ if (text.length <= maxLength) return text;
3602
+ return text.substring(0, maxLength) + '...';
3603
+ }
3604
+
3605
+ // Loading and notifications
3606
+ showLoading(message = 'Loading...') {
3607
+ this.state.isLoading = true;
3608
+
3609
+ if (this.elements.loadingOverlay) {
3610
+ const textElement = this.elements.loadingOverlay.querySelector('.loading-text');
3611
+ if (textElement) {
3612
+ textElement.textContent = message;
3613
+ }
3614
+ this.elements.loadingOverlay.classList.remove('hidden');
3615
+ }
3616
+ }
3617
+
3618
+ hideLoading() {
3619
+ this.state.isLoading = false;
3620
+
3621
+ if (this.elements.loadingOverlay) {
3622
+ this.elements.loadingOverlay.classList.add('hidden');
3623
+ }
3624
+ }
3625
+
3626
+ showNotification(message, type = 'info') {
3627
+ // Map UI notification types to valid Chrome notification types
3628
+ const validTypes = {
3629
+ 'info': 'basic',
3630
+ 'success': 'basic',
3631
+ 'warning': 'basic',
3632
+ 'error': 'basic',
3633
+ 'basic': 'basic',
3634
+ 'image': 'image',
3635
+ 'list': 'list',
3636
+ 'progress': 'progress'
3637
+ };
3638
+
3639
+ const chromeType = validTypes[type] || 'basic';
3640
+
3641
+ // Send notification to background script for display
3642
+ chrome.runtime.sendMessage({
3643
+ type: 'SHOW_NOTIFICATION',
3644
+ data: {
3645
+ title: 'VibeSurf',
3646
+ message,
3647
+ type: chromeType
3648
+ }
3649
+ });
3650
+
3651
+ }
3652
+
3653
+ // Initialization
3654
+ async initialize() {
3655
+ try {
3656
+ this.showLoading('Initializing VibeSurf...');
3657
+
3658
+ // Load settings data
3659
+ await this.loadSettingsData();
3660
+
3661
+ // Create initial session if none exists
3662
+ if (!this.sessionManager.getCurrentSession()) {
3663
+ await this.sessionManager.createSession();
3664
+ }
3665
+
3666
+ this.hideLoading();
3667
+ } catch (error) {
3668
+ this.hideLoading();
3669
+ console.error('[UIManager] Initialization failed:', error);
3670
+ this.showNotification(`Initialization failed: ${error.message}`, 'error');
3671
+ }
3672
+ }
3673
+
3674
+ // Cleanup
3675
+ destroy() {
3676
+ // Stop task status monitoring
3677
+ this.stopTaskStatusMonitoring();
3678
+
3679
+ // Remove event listeners if needed
3680
+ this.state.currentModal = null;
3681
+ }
3682
+ }
3683
+
3684
+ // Export for use in other modules
3685
+ if (typeof window !== 'undefined') {
3686
+ window.VibeSurfUIManager = VibeSurfUIManager;
3687
+ }