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.
- vibe_surf/__init__.py +12 -0
- vibe_surf/_version.py +34 -0
- vibe_surf/agents/__init__.py +0 -0
- vibe_surf/agents/browser_use_agent.py +1106 -0
- vibe_surf/agents/prompts/__init__.py +1 -0
- vibe_surf/agents/prompts/vibe_surf_prompt.py +176 -0
- vibe_surf/agents/report_writer_agent.py +360 -0
- vibe_surf/agents/vibe_surf_agent.py +1632 -0
- vibe_surf/backend/__init__.py +0 -0
- vibe_surf/backend/api/__init__.py +3 -0
- vibe_surf/backend/api/activity.py +243 -0
- vibe_surf/backend/api/config.py +740 -0
- vibe_surf/backend/api/files.py +322 -0
- vibe_surf/backend/api/models.py +257 -0
- vibe_surf/backend/api/task.py +300 -0
- vibe_surf/backend/database/__init__.py +13 -0
- vibe_surf/backend/database/manager.py +129 -0
- vibe_surf/backend/database/models.py +164 -0
- vibe_surf/backend/database/queries.py +922 -0
- vibe_surf/backend/database/schemas.py +100 -0
- vibe_surf/backend/llm_config.py +182 -0
- vibe_surf/backend/main.py +137 -0
- vibe_surf/backend/migrations/__init__.py +16 -0
- vibe_surf/backend/migrations/init_db.py +303 -0
- vibe_surf/backend/migrations/seed_data.py +236 -0
- vibe_surf/backend/shared_state.py +601 -0
- vibe_surf/backend/utils/__init__.py +7 -0
- vibe_surf/backend/utils/encryption.py +164 -0
- vibe_surf/backend/utils/llm_factory.py +225 -0
- vibe_surf/browser/__init__.py +8 -0
- vibe_surf/browser/agen_browser_profile.py +130 -0
- vibe_surf/browser/agent_browser_session.py +416 -0
- vibe_surf/browser/browser_manager.py +296 -0
- vibe_surf/browser/utils.py +790 -0
- vibe_surf/browser/watchdogs/__init__.py +0 -0
- vibe_surf/browser/watchdogs/action_watchdog.py +291 -0
- vibe_surf/browser/watchdogs/dom_watchdog.py +954 -0
- vibe_surf/chrome_extension/background.js +558 -0
- vibe_surf/chrome_extension/config.js +48 -0
- vibe_surf/chrome_extension/content.js +284 -0
- vibe_surf/chrome_extension/dev-reload.js +47 -0
- vibe_surf/chrome_extension/icons/convert-svg.js +33 -0
- vibe_surf/chrome_extension/icons/logo-preview.html +187 -0
- vibe_surf/chrome_extension/icons/logo.png +0 -0
- vibe_surf/chrome_extension/manifest.json +53 -0
- vibe_surf/chrome_extension/popup.html +134 -0
- vibe_surf/chrome_extension/scripts/api-client.js +473 -0
- vibe_surf/chrome_extension/scripts/main.js +491 -0
- vibe_surf/chrome_extension/scripts/markdown-it.min.js +3 -0
- vibe_surf/chrome_extension/scripts/session-manager.js +599 -0
- vibe_surf/chrome_extension/scripts/ui-manager.js +3687 -0
- vibe_surf/chrome_extension/sidepanel.html +347 -0
- vibe_surf/chrome_extension/styles/animations.css +471 -0
- vibe_surf/chrome_extension/styles/components.css +670 -0
- vibe_surf/chrome_extension/styles/main.css +2307 -0
- vibe_surf/chrome_extension/styles/settings.css +1100 -0
- vibe_surf/cli.py +357 -0
- vibe_surf/controller/__init__.py +0 -0
- vibe_surf/controller/file_system.py +53 -0
- vibe_surf/controller/mcp_client.py +68 -0
- vibe_surf/controller/vibesurf_controller.py +616 -0
- vibe_surf/controller/views.py +37 -0
- vibe_surf/llm/__init__.py +21 -0
- vibe_surf/llm/openai_compatible.py +237 -0
- vibesurf-0.1.0.dist-info/METADATA +97 -0
- vibesurf-0.1.0.dist-info/RECORD +70 -0
- vibesurf-0.1.0.dist-info/WHEEL +5 -0
- vibesurf-0.1.0.dist-info/entry_points.txt +2 -0
- vibesurf-0.1.0.dist-info/licenses/LICENSE +201 -0
- 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
|
+
}
|