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