vibesurf 0.1.9a6__py3-none-any.whl → 0.1.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vibe_surf/_version.py +2 -2
- vibe_surf/agents/vibe_surf_agent.py +25 -15
- vibe_surf/backend/api/browser.py +66 -0
- vibe_surf/backend/api/task.py +2 -1
- vibe_surf/backend/main.py +76 -1
- vibe_surf/backend/shared_state.py +2 -0
- vibe_surf/browser/agent_browser_session.py +312 -62
- vibe_surf/browser/browser_manager.py +57 -92
- vibe_surf/browser/watchdogs/dom_watchdog.py +43 -43
- vibe_surf/chrome_extension/background.js +84 -0
- vibe_surf/chrome_extension/manifest.json +3 -1
- vibe_surf/chrome_extension/scripts/file-manager.js +526 -0
- vibe_surf/chrome_extension/scripts/history-manager.js +658 -0
- vibe_surf/chrome_extension/scripts/modal-manager.js +487 -0
- vibe_surf/chrome_extension/scripts/session-manager.js +31 -8
- vibe_surf/chrome_extension/scripts/settings-manager.js +1214 -0
- vibe_surf/chrome_extension/scripts/ui-manager.js +770 -3186
- vibe_surf/chrome_extension/sidepanel.html +27 -4
- vibe_surf/chrome_extension/styles/activity.css +574 -0
- vibe_surf/chrome_extension/styles/base.css +76 -0
- vibe_surf/chrome_extension/styles/history-modal.css +791 -0
- vibe_surf/chrome_extension/styles/input.css +429 -0
- vibe_surf/chrome_extension/styles/layout.css +186 -0
- vibe_surf/chrome_extension/styles/responsive.css +454 -0
- vibe_surf/chrome_extension/styles/settings-environment.css +165 -0
- vibe_surf/chrome_extension/styles/settings-forms.css +389 -0
- vibe_surf/chrome_extension/styles/settings-modal.css +141 -0
- vibe_surf/chrome_extension/styles/settings-profiles.css +244 -0
- vibe_surf/chrome_extension/styles/settings-responsive.css +144 -0
- vibe_surf/chrome_extension/styles/settings-utilities.css +25 -0
- vibe_surf/chrome_extension/styles/variables.css +54 -0
- vibe_surf/cli.py +1 -0
- vibe_surf/controller/vibesurf_tools.py +0 -2
- {vibesurf-0.1.9a6.dist-info → vibesurf-0.1.10.dist-info}/METADATA +18 -2
- {vibesurf-0.1.9a6.dist-info → vibesurf-0.1.10.dist-info}/RECORD +39 -23
- vibe_surf/chrome_extension/styles/main.css +0 -2338
- vibe_surf/chrome_extension/styles/settings.css +0 -1100
- {vibesurf-0.1.9a6.dist-info → vibesurf-0.1.10.dist-info}/WHEEL +0 -0
- {vibesurf-0.1.9a6.dist-info → vibesurf-0.1.10.dist-info}/entry_points.txt +0 -0
- {vibesurf-0.1.9a6.dist-info → vibesurf-0.1.10.dist-info}/licenses/LICENSE +0 -0
- {vibesurf-0.1.9a6.dist-info → vibesurf-0.1.10.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
// File Manager - Handles file uploads, file links, and file operations
|
|
2
|
+
// Manages file selection, upload, display, and file:// protocol handling
|
|
3
|
+
|
|
4
|
+
class VibeSurfFileManager {
|
|
5
|
+
constructor(sessionManager) {
|
|
6
|
+
this.sessionManager = sessionManager;
|
|
7
|
+
this.state = {
|
|
8
|
+
uploadedFiles: [],
|
|
9
|
+
isHandlingFileLink: false
|
|
10
|
+
};
|
|
11
|
+
this.elements = {};
|
|
12
|
+
this.eventListeners = new Map();
|
|
13
|
+
|
|
14
|
+
this.bindElements();
|
|
15
|
+
this.bindEvents();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
bindElements() {
|
|
19
|
+
this.elements = {
|
|
20
|
+
// File input and buttons
|
|
21
|
+
attachFileBtn: document.getElementById('attach-file-btn'),
|
|
22
|
+
fileInput: document.getElementById('file-input'),
|
|
23
|
+
|
|
24
|
+
// File list container (created dynamically)
|
|
25
|
+
uploadedFilesList: null
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
bindEvents() {
|
|
30
|
+
// File attachment
|
|
31
|
+
this.elements.attachFileBtn?.addEventListener('click', this.handleAttachFiles.bind(this));
|
|
32
|
+
this.elements.fileInput?.addEventListener('change', this.handleFileSelection.bind(this));
|
|
33
|
+
|
|
34
|
+
// File link handling with delegation
|
|
35
|
+
document.addEventListener('click', this.handleFileLinkClick.bind(this));
|
|
36
|
+
|
|
37
|
+
// Listen for files uploaded event from session manager
|
|
38
|
+
if (this.sessionManager) {
|
|
39
|
+
this.sessionManager.on('filesUploaded', this.handleFilesUploaded.bind(this));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Event system for communicating with main UI manager
|
|
44
|
+
on(event, callback) {
|
|
45
|
+
if (!this.eventListeners.has(event)) {
|
|
46
|
+
this.eventListeners.set(event, []);
|
|
47
|
+
}
|
|
48
|
+
this.eventListeners.get(event).push(callback);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
emit(event, data) {
|
|
52
|
+
if (this.eventListeners.has(event)) {
|
|
53
|
+
this.eventListeners.get(event).forEach(callback => {
|
|
54
|
+
try {
|
|
55
|
+
callback(data);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error(`[FileManager] Event callback error for ${event}:`, error);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// File Upload Handling
|
|
64
|
+
handleAttachFiles() {
|
|
65
|
+
this.elements.fileInput?.click();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async handleFileSelection(event) {
|
|
69
|
+
const files = Array.from(event.target.files);
|
|
70
|
+
|
|
71
|
+
if (files.length === 0) return;
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
this.emit('loading', { message: `Uploading ${files.length} file(s)...` });
|
|
75
|
+
|
|
76
|
+
const response = await this.sessionManager.uploadFiles(files);
|
|
77
|
+
|
|
78
|
+
console.log('[FileManager] File upload response:', response);
|
|
79
|
+
|
|
80
|
+
// If SessionManager doesn't trigger the event, handle it directly
|
|
81
|
+
if (response && response.files) {
|
|
82
|
+
console.log('[FileManager] Manually handling uploaded files');
|
|
83
|
+
this.handleFilesUploaded({
|
|
84
|
+
sessionId: this.sessionManager.getCurrentSessionId(),
|
|
85
|
+
files: response.files
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this.emit('loading', { hide: true });
|
|
90
|
+
this.emit('notification', {
|
|
91
|
+
message: `${files.length} file(s) uploaded successfully`,
|
|
92
|
+
type: 'success'
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Clear file input
|
|
96
|
+
event.target.value = '';
|
|
97
|
+
} catch (error) {
|
|
98
|
+
this.emit('loading', { hide: true });
|
|
99
|
+
this.emit('notification', {
|
|
100
|
+
message: `File upload failed: ${error.message}`,
|
|
101
|
+
type: 'error'
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
handleFilesUploaded(data) {
|
|
107
|
+
console.log('[FileManager] Files uploaded event received:', data);
|
|
108
|
+
|
|
109
|
+
// Ensure data.files is always an array - handle both single file and array cases
|
|
110
|
+
let filesArray = [];
|
|
111
|
+
if (data.files) {
|
|
112
|
+
if (Array.isArray(data.files)) {
|
|
113
|
+
filesArray = data.files;
|
|
114
|
+
} else {
|
|
115
|
+
// If single file object, wrap in array
|
|
116
|
+
filesArray = [data.files];
|
|
117
|
+
console.log('[FileManager] Single file detected, wrapping in array');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
console.log('[FileManager] Processing files array:', filesArray);
|
|
122
|
+
|
|
123
|
+
if (filesArray.length > 0) {
|
|
124
|
+
// Append new files to existing uploaded files (for multiple uploads)
|
|
125
|
+
const newFiles = filesArray.map(file => ({
|
|
126
|
+
id: file.file_id,
|
|
127
|
+
name: file.original_filename,
|
|
128
|
+
path: file.file_path, // Updated to use file_path field
|
|
129
|
+
size: file.file_size,
|
|
130
|
+
type: file.mime_type,
|
|
131
|
+
stored_filename: file.stored_filename,
|
|
132
|
+
file_path: file.file_path // Add file_path for backward compatibility
|
|
133
|
+
}));
|
|
134
|
+
|
|
135
|
+
console.log('[FileManager] Mapped new files:', newFiles);
|
|
136
|
+
|
|
137
|
+
// Add to existing files instead of replacing
|
|
138
|
+
this.state.uploadedFiles = [...this.state.uploadedFiles, ...newFiles];
|
|
139
|
+
|
|
140
|
+
console.log('[FileManager] Updated uploaded files state:', this.state.uploadedFiles);
|
|
141
|
+
|
|
142
|
+
// Update the visual file list
|
|
143
|
+
this.updateFilesList();
|
|
144
|
+
} else {
|
|
145
|
+
console.warn('[FileManager] No files to process in uploaded data');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// File List Management
|
|
150
|
+
updateFilesList() {
|
|
151
|
+
const container = this.getOrCreateFilesListContainer();
|
|
152
|
+
|
|
153
|
+
// Debug logging to identify the issue
|
|
154
|
+
console.log('[FileManager] updateFilesList called');
|
|
155
|
+
console.log('[FileManager] uploadedFiles type:', typeof this.state.uploadedFiles);
|
|
156
|
+
console.log('[FileManager] uploadedFiles isArray:', Array.isArray(this.state.uploadedFiles));
|
|
157
|
+
console.log('[FileManager] uploadedFiles value:', this.state.uploadedFiles);
|
|
158
|
+
|
|
159
|
+
// Ensure uploadedFiles is always an array
|
|
160
|
+
if (!Array.isArray(this.state.uploadedFiles)) {
|
|
161
|
+
console.error('[FileManager] uploadedFiles is not an array, resetting to empty array');
|
|
162
|
+
this.state.uploadedFiles = [];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (this.state.uploadedFiles.length === 0) {
|
|
166
|
+
container.style.display = 'none';
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
container.style.display = 'block';
|
|
171
|
+
|
|
172
|
+
// Build HTML safely with proper validation
|
|
173
|
+
let filesHTML = '';
|
|
174
|
+
try {
|
|
175
|
+
filesHTML = this.state.uploadedFiles.map((file, index) => {
|
|
176
|
+
console.log(`[FileManager] Processing file ${index}:`, file);
|
|
177
|
+
|
|
178
|
+
// Validate file object structure
|
|
179
|
+
if (!file || typeof file !== 'object') {
|
|
180
|
+
console.error(`[FileManager] Invalid file object at index ${index}:`, file);
|
|
181
|
+
return '';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Extract properties safely with fallbacks
|
|
185
|
+
const fileId = file.id || file.file_id || `file_${index}`;
|
|
186
|
+
const fileName = file.name || file.original_filename || 'Unknown file';
|
|
187
|
+
const filePath = file.path || file.file_path || file.stored_filename || 'Unknown path';
|
|
188
|
+
|
|
189
|
+
console.log(`[FileManager] File display data: id=${fileId}, name=${fileName}, path=${filePath}`);
|
|
190
|
+
|
|
191
|
+
return `
|
|
192
|
+
<div class="file-item" data-file-id="${fileId}">
|
|
193
|
+
<span class="file-name" title="${filePath}">${fileName}</span>
|
|
194
|
+
<button class="file-remove-btn" title="Remove file" data-file-id="${fileId}">×</button>
|
|
195
|
+
</div>
|
|
196
|
+
`;
|
|
197
|
+
}).join('');
|
|
198
|
+
} catch (error) {
|
|
199
|
+
console.error('[FileManager] Error generating files HTML:', error);
|
|
200
|
+
filesHTML = '<div class="error-message">Error displaying files</div>';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
container.innerHTML = `
|
|
204
|
+
<div class="files-items">
|
|
205
|
+
${filesHTML}
|
|
206
|
+
</div>
|
|
207
|
+
`;
|
|
208
|
+
|
|
209
|
+
// Add event listeners for remove buttons
|
|
210
|
+
container.querySelectorAll('.file-remove-btn').forEach(btn => {
|
|
211
|
+
btn.addEventListener('click', (e) => {
|
|
212
|
+
e.preventDefault();
|
|
213
|
+
const fileId = btn.dataset.fileId;
|
|
214
|
+
this.removeUploadedFile(fileId);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
getOrCreateFilesListContainer() {
|
|
220
|
+
let container = document.getElementById('uploaded-files-list');
|
|
221
|
+
|
|
222
|
+
if (!container) {
|
|
223
|
+
container = document.createElement('div');
|
|
224
|
+
container.id = 'uploaded-files-list';
|
|
225
|
+
container.className = 'uploaded-files-container';
|
|
226
|
+
|
|
227
|
+
// Insert after the textarea-container to avoid affecting button layout
|
|
228
|
+
const taskInput = document.getElementById('task-input');
|
|
229
|
+
if (taskInput) {
|
|
230
|
+
const textareaContainer = taskInput.closest('.textarea-container');
|
|
231
|
+
if (textareaContainer && textareaContainer.parentElement) {
|
|
232
|
+
// Insert after the textarea-container but before the input-footer
|
|
233
|
+
const inputFooter = textareaContainer.parentElement.querySelector('.input-footer');
|
|
234
|
+
if (inputFooter) {
|
|
235
|
+
textareaContainer.parentElement.insertBefore(container, inputFooter);
|
|
236
|
+
} else {
|
|
237
|
+
textareaContainer.parentElement.insertBefore(container, textareaContainer.nextSibling);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return container;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
removeUploadedFile(fileId) {
|
|
247
|
+
console.log('[FileManager] Removing uploaded file:', fileId);
|
|
248
|
+
|
|
249
|
+
// Remove from state
|
|
250
|
+
this.state.uploadedFiles = this.state.uploadedFiles.filter(file => file.id !== fileId);
|
|
251
|
+
|
|
252
|
+
// Update visual list
|
|
253
|
+
this.updateFilesList();
|
|
254
|
+
|
|
255
|
+
this.emit('notification', {
|
|
256
|
+
message: 'File removed from upload list',
|
|
257
|
+
type: 'info'
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
clearUploadedFiles() {
|
|
262
|
+
this.state.uploadedFiles = [];
|
|
263
|
+
this.updateFilesList();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// File Link Handling
|
|
267
|
+
handleFileLinkClick(event) {
|
|
268
|
+
const target = event.target;
|
|
269
|
+
|
|
270
|
+
// Check if clicked element is a file link
|
|
271
|
+
if (target.matches('a.file-link') || target.closest('a.file-link')) {
|
|
272
|
+
event.preventDefault();
|
|
273
|
+
|
|
274
|
+
const fileLink = target.matches('a.file-link') ? target : target.closest('a.file-link');
|
|
275
|
+
const filePath = fileLink.getAttribute('data-file-path');
|
|
276
|
+
|
|
277
|
+
this.handleFileLink(filePath);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async handleFileLink(filePath) {
|
|
282
|
+
// Prevent multiple simultaneous calls
|
|
283
|
+
if (this.state.isHandlingFileLink) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
this.state.isHandlingFileLink = true;
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
// First decode the URL-encoded path
|
|
291
|
+
let decodedPath = decodeURIComponent(filePath);
|
|
292
|
+
|
|
293
|
+
// Remove file:// protocol prefix and normalize
|
|
294
|
+
let cleanPath = decodedPath.replace(/^file:\/\/\//, '').replace(/^file:\/\//, '');
|
|
295
|
+
|
|
296
|
+
// Ensure path starts with / for Unix paths if not Windows drive
|
|
297
|
+
if (!cleanPath.startsWith('/') && !cleanPath.match(/^[A-Za-z]:/)) {
|
|
298
|
+
cleanPath = '/' + cleanPath;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Convert all backslashes to forward slashes
|
|
302
|
+
cleanPath = cleanPath.replace(/\\/g, '/');
|
|
303
|
+
|
|
304
|
+
// Create proper file URL - always use triple slash for proper format
|
|
305
|
+
const fileUrl = cleanPath.match(/^[A-Za-z]:/) ?
|
|
306
|
+
`file:///${cleanPath}` :
|
|
307
|
+
`file:///${cleanPath.replace(/^\//, '')}`; // Remove leading slash and add triple slash
|
|
308
|
+
|
|
309
|
+
// Create Windows format path for system open
|
|
310
|
+
const windowsPath = cleanPath.replace(/\//g, '\\');
|
|
311
|
+
|
|
312
|
+
// Show user notification about the action
|
|
313
|
+
this.emit('notification', {
|
|
314
|
+
message: `Opening file: ${cleanPath}`,
|
|
315
|
+
type: 'info'
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// Use setTimeout to prevent UI blocking
|
|
319
|
+
setTimeout(async () => {
|
|
320
|
+
try {
|
|
321
|
+
// Primary strategy: Try browser open first for HTML files (more reliable)
|
|
322
|
+
if (fileUrl.toLowerCase().endsWith('.html') || fileUrl.toLowerCase().endsWith('.htm')) {
|
|
323
|
+
try {
|
|
324
|
+
const opened = window.open(fileUrl, '_blank', 'noopener,noreferrer');
|
|
325
|
+
if (opened) {
|
|
326
|
+
this.emit('notification', {
|
|
327
|
+
message: 'File opened in browser',
|
|
328
|
+
type: 'success'
|
|
329
|
+
});
|
|
330
|
+
return;
|
|
331
|
+
} else {
|
|
332
|
+
// If browser is blocked, try system open
|
|
333
|
+
await this.trySystemOpen(windowsPath, fileUrl);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
} catch (browserError) {
|
|
337
|
+
await this.trySystemOpen(windowsPath, fileUrl);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
} else {
|
|
341
|
+
// For non-HTML files, try system open first
|
|
342
|
+
const systemSuccess = await this.trySystemOpen(windowsPath, fileUrl);
|
|
343
|
+
if (systemSuccess) return;
|
|
344
|
+
|
|
345
|
+
// Fallback to browser if system open fails
|
|
346
|
+
try {
|
|
347
|
+
const opened = window.open(fileUrl, '_blank', 'noopener,noreferrer');
|
|
348
|
+
if (opened) {
|
|
349
|
+
this.emit('notification', {
|
|
350
|
+
message: 'File opened in browser',
|
|
351
|
+
type: 'success'
|
|
352
|
+
});
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
} catch (browserError) {
|
|
356
|
+
console.error('[FileManager] Failed to open file:', browserError);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Last resort: Copy path to clipboard
|
|
361
|
+
this.copyToClipboardFallback(fileUrl);
|
|
362
|
+
|
|
363
|
+
} catch (error) {
|
|
364
|
+
console.error('[FileManager] Error in async file handling:', error);
|
|
365
|
+
this.emit('notification', {
|
|
366
|
+
message: `Unable to open file: ${error.message}`,
|
|
367
|
+
type: 'error'
|
|
368
|
+
});
|
|
369
|
+
} finally {
|
|
370
|
+
this.state.isHandlingFileLink = false;
|
|
371
|
+
}
|
|
372
|
+
}, 50); // Small delay to prevent UI blocking
|
|
373
|
+
|
|
374
|
+
} catch (error) {
|
|
375
|
+
console.error('[FileManager] Error handling file link:', error);
|
|
376
|
+
this.emit('notification', {
|
|
377
|
+
message: `Unable to open file: ${error.message}`,
|
|
378
|
+
type: 'error'
|
|
379
|
+
});
|
|
380
|
+
this.state.isHandlingFileLink = false;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async trySystemOpen(windowsPath, fileUrl) {
|
|
385
|
+
try {
|
|
386
|
+
const systemOpenPromise = chrome.runtime.sendMessage({
|
|
387
|
+
type: 'OPEN_FILE_SYSTEM',
|
|
388
|
+
data: { filePath: windowsPath }
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// Add timeout to prevent hanging
|
|
392
|
+
const systemOpenResponse = await Promise.race([
|
|
393
|
+
systemOpenPromise,
|
|
394
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 3000))
|
|
395
|
+
]);
|
|
396
|
+
|
|
397
|
+
if (systemOpenResponse && systemOpenResponse.success) {
|
|
398
|
+
this.emit('notification', {
|
|
399
|
+
message: 'File opened with system default application',
|
|
400
|
+
type: 'success'
|
|
401
|
+
});
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
return false;
|
|
405
|
+
} catch (systemError) {
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async copyToClipboardFallback(fileUrl) {
|
|
411
|
+
try {
|
|
412
|
+
await navigator.clipboard.writeText(fileUrl);
|
|
413
|
+
this.emit('notification', {
|
|
414
|
+
message: 'File URL copied to clipboard - paste in browser address bar',
|
|
415
|
+
type: 'info'
|
|
416
|
+
});
|
|
417
|
+
} catch (clipboardError) {
|
|
418
|
+
console.error('[FileManager] Clipboard failed:', clipboardError);
|
|
419
|
+
this.emit('notification', {
|
|
420
|
+
message: 'Unable to open file. URL: ' + fileUrl,
|
|
421
|
+
type: 'warning'
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Utility Methods
|
|
427
|
+
formatFileSize(bytes) {
|
|
428
|
+
if (!bytes) return '0 B';
|
|
429
|
+
|
|
430
|
+
const k = 1024;
|
|
431
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
432
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
433
|
+
|
|
434
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
escapeHtml(text) {
|
|
438
|
+
if (typeof text !== 'string') return '';
|
|
439
|
+
const div = document.createElement('div');
|
|
440
|
+
div.textContent = text;
|
|
441
|
+
return div.innerHTML;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Public interface
|
|
445
|
+
getUploadedFiles() {
|
|
446
|
+
return [...this.state.uploadedFiles]; // Return copy
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
getUploadedFilesForTask() {
|
|
450
|
+
if (this.state.uploadedFiles.length === 0) {
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
console.log('[FileManager] Raw uploaded files state:', this.state.uploadedFiles);
|
|
455
|
+
|
|
456
|
+
// Extract the first file path (backend expects single string)
|
|
457
|
+
const firstFile = this.state.uploadedFiles[0];
|
|
458
|
+
let filePath = null;
|
|
459
|
+
|
|
460
|
+
if (typeof firstFile === 'string') {
|
|
461
|
+
filePath = firstFile;
|
|
462
|
+
} else if (firstFile && typeof firstFile === 'object') {
|
|
463
|
+
// Extract path and normalize
|
|
464
|
+
filePath = firstFile.file_path || firstFile.path || firstFile.stored_filename || firstFile.file_path;
|
|
465
|
+
if (filePath) {
|
|
466
|
+
filePath = filePath.replace(/\\/g, '/');
|
|
467
|
+
console.log('[FileManager] Normalized file path:', filePath);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (filePath) {
|
|
472
|
+
// Show info if multiple files uploaded but only first will be processed
|
|
473
|
+
if (this.state.uploadedFiles.length > 1) {
|
|
474
|
+
console.warn('[FileManager] Multiple files uploaded, but backend only supports single file. Using first file:', filePath);
|
|
475
|
+
this.emit('notification', {
|
|
476
|
+
message: `Multiple files uploaded. Only the first file "${firstFile.name || filePath}" will be processed.`,
|
|
477
|
+
type: 'warning'
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return filePath;
|
|
482
|
+
} else {
|
|
483
|
+
console.error('[FileManager] Could not extract file path from uploaded file:', firstFile);
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
hasUploadedFiles() {
|
|
489
|
+
return this.state.uploadedFiles.length > 0;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Enable/disable file attachment based on task running state
|
|
493
|
+
setEnabled(enabled) {
|
|
494
|
+
if (this.elements.attachFileBtn) {
|
|
495
|
+
this.elements.attachFileBtn.disabled = !enabled;
|
|
496
|
+
|
|
497
|
+
if (enabled) {
|
|
498
|
+
this.elements.attachFileBtn.classList.remove('task-running-disabled');
|
|
499
|
+
this.elements.attachFileBtn.removeAttribute('title');
|
|
500
|
+
} else {
|
|
501
|
+
this.elements.attachFileBtn.classList.add('task-running-disabled');
|
|
502
|
+
this.elements.attachFileBtn.setAttribute('title', 'Disabled while task is running');
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Get current state
|
|
508
|
+
getState() {
|
|
509
|
+
return { ...this.state };
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Update session manager reference
|
|
513
|
+
setSessionManager(sessionManager) {
|
|
514
|
+
this.sessionManager = sessionManager;
|
|
515
|
+
|
|
516
|
+
// Re-bind files uploaded event
|
|
517
|
+
if (this.sessionManager) {
|
|
518
|
+
this.sessionManager.on('filesUploaded', this.handleFilesUploaded.bind(this));
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Export for use in other modules
|
|
524
|
+
if (typeof window !== 'undefined') {
|
|
525
|
+
window.VibeSurfFileManager = VibeSurfFileManager;
|
|
526
|
+
}
|