agentgui 1.0.274 → 1.0.276
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.
- package/CLAUDE.md +280 -280
- package/IPFS_DOWNLOADER.md +277 -277
- package/TASK_2C_COMPLETION.md +334 -334
- package/agentgui.ico +0 -0
- package/bin/gmgui.cjs +54 -54
- package/build-portable.js +13 -42
- package/database.js +1422 -1406
- package/lib/claude-runner.js +1130 -1130
- package/lib/ipfs-downloader.js +459 -459
- package/lib/speech.js +159 -152
- package/package.json +1 -1
- package/readme.md +76 -76
- package/server.js +3787 -3794
- package/setup-npm-token.sh +68 -68
- package/static/app.js +773 -773
- package/static/event-rendering-showcase.html +708 -708
- package/static/index.html +3178 -3180
- package/static/js/agent-auth.js +298 -298
- package/static/js/audio-recorder-processor.js +18 -18
- package/static/js/client.js +2656 -2656
- package/static/js/conversations.js +583 -583
- package/static/js/dialogs.js +267 -267
- package/static/js/event-consolidator.js +101 -101
- package/static/js/event-filter.js +311 -311
- package/static/js/event-processor.js +452 -452
- package/static/js/features.js +413 -413
- package/static/js/kalman-filter.js +67 -67
- package/static/js/progress-dialog.js +130 -130
- package/static/js/script-runner.js +219 -219
- package/static/js/streaming-renderer.js +2123 -2120
- package/static/js/syntax-highlighter.js +269 -269
- package/static/js/tts-websocket-handler.js +152 -152
- package/static/js/ui-components.js +431 -431
- package/static/js/voice.js +849 -849
- package/static/js/websocket-manager.js +596 -596
- package/static/templates/INDEX.html +465 -465
- package/static/templates/README.md +190 -190
- package/static/templates/agent-capabilities.html +56 -56
- package/static/templates/agent-metadata-panel.html +44 -44
- package/static/templates/agent-status-badge.html +30 -30
- package/static/templates/code-annotation-panel.html +155 -155
- package/static/templates/code-suggestion-panel.html +184 -184
- package/static/templates/command-header.html +77 -77
- package/static/templates/command-output-scrollable.html +118 -118
- package/static/templates/elapsed-time.html +54 -54
- package/static/templates/error-alert.html +106 -106
- package/static/templates/error-history-timeline.html +160 -160
- package/static/templates/error-recovery-options.html +109 -109
- package/static/templates/error-stack-trace.html +95 -95
- package/static/templates/error-summary.html +80 -80
- package/static/templates/event-counter.html +48 -48
- package/static/templates/execution-actions.html +97 -97
- package/static/templates/execution-progress-bar.html +80 -80
- package/static/templates/execution-stepper.html +120 -120
- package/static/templates/file-breadcrumb.html +118 -118
- package/static/templates/file-diff-viewer.html +121 -121
- package/static/templates/file-metadata.html +133 -133
- package/static/templates/file-read-panel.html +66 -66
- package/static/templates/file-write-panel.html +120 -120
- package/static/templates/git-branch-remote.html +107 -107
- package/static/templates/git-diff-list.html +101 -101
- package/static/templates/git-log-visualization.html +153 -153
- package/static/templates/git-status-panel.html +115 -115
- package/static/templates/quality-metrics-display.html +170 -170
- package/static/templates/terminal-output-panel.html +87 -87
- package/static/templates/test-results-display.html +144 -144
- package/static/theme.js +72 -72
- package/test-download-progress.js +223 -223
- package/test-websocket-broadcast.js +147 -147
- package/tests/ipfs-downloader.test.js +370 -370
package/static/js/features.js
CHANGED
|
@@ -1,413 +1,413 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Features Module
|
|
3
|
-
* Drag-and-drop file upload, fsbrowse file browser toggle, mobile sidebar
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
(function() {
|
|
7
|
-
const BASE = window.__BASE_URL || '';
|
|
8
|
-
let currentConversation = null;
|
|
9
|
-
let currentView = 'chat';
|
|
10
|
-
let dragCounter = 0;
|
|
11
|
-
|
|
12
|
-
function init() {
|
|
13
|
-
setupSidebarToggle();
|
|
14
|
-
setupDragAndDrop();
|
|
15
|
-
setupViewToggle();
|
|
16
|
-
setupConversationListener();
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function setupSidebarToggle() {
|
|
20
|
-
var toggleBtn = document.querySelector('[data-sidebar-toggle]');
|
|
21
|
-
var sidebar = document.querySelector('[data-sidebar]');
|
|
22
|
-
var overlay = document.querySelector('[data-sidebar-overlay]');
|
|
23
|
-
|
|
24
|
-
if (!sidebar) return;
|
|
25
|
-
|
|
26
|
-
if (window.innerWidth <= 768) {
|
|
27
|
-
sidebar.classList.add('collapsed');
|
|
28
|
-
} else {
|
|
29
|
-
var savedState = localStorage.getItem('sidebar-collapsed');
|
|
30
|
-
if (savedState === 'true') {
|
|
31
|
-
sidebar.classList.add('collapsed');
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function isMobile() { return window.innerWidth <= 768; }
|
|
36
|
-
|
|
37
|
-
function toggleSidebar() {
|
|
38
|
-
if (isMobile()) {
|
|
39
|
-
var isOpen = sidebar.classList.contains('mobile-visible');
|
|
40
|
-
if (isOpen) { closeSidebar(); } else { openSidebar(); }
|
|
41
|
-
} else {
|
|
42
|
-
sidebar.classList.toggle('collapsed');
|
|
43
|
-
localStorage.setItem('sidebar-collapsed', sidebar.classList.contains('collapsed'));
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function openSidebar() {
|
|
48
|
-
sidebar.classList.add('mobile-visible');
|
|
49
|
-
sidebar.classList.remove('collapsed');
|
|
50
|
-
if (overlay) overlay.classList.add('visible');
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function closeSidebar() {
|
|
54
|
-
sidebar.classList.remove('mobile-visible');
|
|
55
|
-
if (overlay) overlay.classList.remove('visible');
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (toggleBtn) {
|
|
59
|
-
toggleBtn.addEventListener('click', function(e) {
|
|
60
|
-
e.stopPropagation();
|
|
61
|
-
toggleSidebar();
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (overlay) {
|
|
66
|
-
overlay.addEventListener('click', closeSidebar);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
document.addEventListener('keydown', function(e) {
|
|
70
|
-
if ((e.ctrlKey || e.metaKey) && e.key === 'b') {
|
|
71
|
-
e.preventDefault();
|
|
72
|
-
toggleSidebar();
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
window.addEventListener('conversation-selected', function() {
|
|
77
|
-
if (isMobile()) closeSidebar();
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
window.addEventListener('resize', function() {
|
|
81
|
-
if (!isMobile()) {
|
|
82
|
-
sidebar.classList.remove('mobile-visible');
|
|
83
|
-
if (overlay) overlay.classList.remove('visible');
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// --- Drag and Drop File Upload ---
|
|
89
|
-
function setupDragAndDrop() {
|
|
90
|
-
const dropZone = document.querySelector('[data-drop-zone]');
|
|
91
|
-
const overlay = document.getElementById('dropZoneOverlay');
|
|
92
|
-
|
|
93
|
-
if (!dropZone || !overlay) return;
|
|
94
|
-
|
|
95
|
-
dropZone.addEventListener('dragenter', function(e) {
|
|
96
|
-
e.preventDefault();
|
|
97
|
-
e.stopPropagation();
|
|
98
|
-
dragCounter++;
|
|
99
|
-
if (dragCounter === 1) {
|
|
100
|
-
overlay.classList.add('visible');
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
dropZone.addEventListener('dragover', function(e) {
|
|
105
|
-
e.preventDefault();
|
|
106
|
-
e.stopPropagation();
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
dropZone.addEventListener('dragleave', function(e) {
|
|
110
|
-
e.preventDefault();
|
|
111
|
-
e.stopPropagation();
|
|
112
|
-
dragCounter--;
|
|
113
|
-
if (dragCounter <= 0) {
|
|
114
|
-
dragCounter = 0;
|
|
115
|
-
overlay.classList.remove('visible');
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
dropZone.addEventListener('drop', function(e) {
|
|
120
|
-
e.preventDefault();
|
|
121
|
-
e.stopPropagation();
|
|
122
|
-
dragCounter = 0;
|
|
123
|
-
overlay.classList.remove('visible');
|
|
124
|
-
|
|
125
|
-
if (!currentConversation) {
|
|
126
|
-
if (window.UIDialog) window.UIDialog.showToast('Select a conversation first', 'error');
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const files = e.dataTransfer.files;
|
|
131
|
-
if (!files || files.length === 0) return;
|
|
132
|
-
|
|
133
|
-
uploadFiles(files);
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function uploadFiles(files) {
|
|
138
|
-
if (!currentConversation) {
|
|
139
|
-
if (window.UIDialog) window.UIDialog.showToast('No conversation selected', 'error');
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const formData = new FormData();
|
|
144
|
-
for (let i = 0; i < files.length; i++) {
|
|
145
|
-
formData.append('file', files[i]);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (window.UIDialog) window.UIDialog.showToast('Uploading ' + files.length + ' file(s)...', 'info');
|
|
149
|
-
|
|
150
|
-
fetch(BASE + '/api/upload/' + currentConversation, {
|
|
151
|
-
method: 'POST',
|
|
152
|
-
body: formData
|
|
153
|
-
})
|
|
154
|
-
.then(function(res) { return res.json(); })
|
|
155
|
-
.then(function(data) {
|
|
156
|
-
if (data.ok) {
|
|
157
|
-
if (window.UIDialog) window.UIDialog.showToast(data.count + ' file(s) uploaded', 'success');
|
|
158
|
-
} else {
|
|
159
|
-
if (window.UIDialog) window.UIDialog.showToast('Upload failed: ' + (data.error || 'Unknown error'), 'error');
|
|
160
|
-
}
|
|
161
|
-
})
|
|
162
|
-
.catch(function(err) {
|
|
163
|
-
if (window.UIDialog) window.UIDialog.showToast('Upload failed: ' + err.message, 'error');
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function showToast(message, type) {
|
|
168
|
-
var existing = document.querySelector('.upload-toast');
|
|
169
|
-
if (existing) existing.remove();
|
|
170
|
-
|
|
171
|
-
var toast = document.createElement('div');
|
|
172
|
-
toast.className = 'upload-toast ' + (type || 'info');
|
|
173
|
-
toast.textContent = message;
|
|
174
|
-
document.body.appendChild(toast);
|
|
175
|
-
|
|
176
|
-
setTimeout(function() {
|
|
177
|
-
toast.style.opacity = '0';
|
|
178
|
-
setTimeout(function() { toast.remove(); }, 300);
|
|
179
|
-
}, 3000);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// --- View Toggle (Chat / Files) ---
|
|
183
|
-
function setupViewToggle() {
|
|
184
|
-
var bar = document.getElementById('viewToggleBar');
|
|
185
|
-
if (!bar) return;
|
|
186
|
-
|
|
187
|
-
var buttons = bar.querySelectorAll('.view-toggle-btn');
|
|
188
|
-
buttons.forEach(function(btn) {
|
|
189
|
-
btn.addEventListener('click', function() {
|
|
190
|
-
var view = btn.dataset.view;
|
|
191
|
-
if (view === 'voice') {
|
|
192
|
-
handleVoiceTabClick();
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
switchView(view);
|
|
196
|
-
});
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function handleVoiceTabClick() {
|
|
201
|
-
if (isVoiceReady()) {
|
|
202
|
-
switchView('voice');
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
var client = window.agentGUIClient;
|
|
206
|
-
if (client && client._modelDownloadProgress == null) {
|
|
207
|
-
fetch((window.__BASE_URL || '') + '/api/speech-status')
|
|
208
|
-
.then(function(res) { return res.json(); })
|
|
209
|
-
.then(function(status) {
|
|
210
|
-
if (status.modelsComplete) {
|
|
211
|
-
if (client) {
|
|
212
|
-
client._modelDownloadProgress = { done: true, complete: true };
|
|
213
|
-
client._modelDownloadInProgress = false;
|
|
214
|
-
}
|
|
215
|
-
switchView('voice');
|
|
216
|
-
} else if (status.modelsDownloading) {
|
|
217
|
-
if (client) {
|
|
218
|
-
client._modelDownloadProgress = status.modelsProgress || { downloading: true };
|
|
219
|
-
client._modelDownloadInProgress = true;
|
|
220
|
-
}
|
|
221
|
-
showVoiceDownloadProgress();
|
|
222
|
-
} else {
|
|
223
|
-
triggerVoiceModelDownload();
|
|
224
|
-
}
|
|
225
|
-
})
|
|
226
|
-
.catch(function() { triggerVoiceModelDownload(); });
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
if (client && client._modelDownloadInProgress) {
|
|
230
|
-
showVoiceDownloadProgress();
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
triggerVoiceModelDownload();
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function showVoiceDownloadProgress() {
|
|
237
|
-
if (window._voiceProgressDialog) return;
|
|
238
|
-
|
|
239
|
-
window._voiceProgressDialog = window.UIDialog.showProgress({
|
|
240
|
-
title: 'Downloading Voice Models',
|
|
241
|
-
message: 'Preparing speech recognition and synthesis models...'
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
var checkInterval = setInterval(function() {
|
|
245
|
-
if (isVoiceReady()) {
|
|
246
|
-
clearInterval(checkInterval);
|
|
247
|
-
if (window._voiceProgressDialog) {
|
|
248
|
-
window._voiceProgressDialog.close();
|
|
249
|
-
window._voiceProgressDialog = null;
|
|
250
|
-
}
|
|
251
|
-
switchView('voice');
|
|
252
|
-
}
|
|
253
|
-
}, 500);
|
|
254
|
-
|
|
255
|
-
setTimeout(function() {
|
|
256
|
-
clearInterval(checkInterval);
|
|
257
|
-
if (window._voiceProgressDialog) {
|
|
258
|
-
window._voiceProgressDialog.close();
|
|
259
|
-
window._voiceProgressDialog = null;
|
|
260
|
-
}
|
|
261
|
-
}, 120000);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function updateVoiceProgress(percent, message) {
|
|
265
|
-
if (window._voiceProgressDialog) {
|
|
266
|
-
window._voiceProgressDialog.update(percent, message);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
window.__updateVoiceProgress = updateVoiceProgress;
|
|
271
|
-
|
|
272
|
-
function isVoiceReady() {
|
|
273
|
-
var client = window.agentGUIClient;
|
|
274
|
-
if (!client) return false;
|
|
275
|
-
if (client._modelDownloadInProgress) return false;
|
|
276
|
-
var p = client._modelDownloadProgress;
|
|
277
|
-
return p != null && (p.done === true || p.complete === true);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function triggerVoiceModelDownload() {
|
|
281
|
-
var client = window.agentGUIClient;
|
|
282
|
-
if (client && client._modelDownloadInProgress) {
|
|
283
|
-
if (window._voiceProgressDialog) {
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
showToast('Voice models downloading... please wait', 'info');
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
if (!window._voiceProgressDialog) {
|
|
291
|
-
window._voiceProgressDialog = new ProgressDialog({
|
|
292
|
-
title: 'Voice Models',
|
|
293
|
-
message: 'Preparing to download voice models...',
|
|
294
|
-
percentage: 0,
|
|
295
|
-
cancellable: false
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
fetch((window.__BASE_URL || '') + '/api/speech-status', {
|
|
300
|
-
method: 'POST',
|
|
301
|
-
headers: { 'Content-Type': 'application/json' },
|
|
302
|
-
body: JSON.stringify({ forceDownload: true })
|
|
303
|
-
}).then(function(res) { return res.json(); })
|
|
304
|
-
.then(function(data) {
|
|
305
|
-
if (data.ok && data.modelsComplete) {
|
|
306
|
-
if (window._voiceProgressDialog) {
|
|
307
|
-
window._voiceProgressDialog.close();
|
|
308
|
-
window._voiceProgressDialog = null;
|
|
309
|
-
}
|
|
310
|
-
var client = window.agentGUIClient;
|
|
311
|
-
if (client) {
|
|
312
|
-
client._modelDownloadProgress = { done: true, complete: true };
|
|
313
|
-
client._modelDownloadInProgress = false;
|
|
314
|
-
}
|
|
315
|
-
switchView('voice');
|
|
316
|
-
} else if (data.ok) {
|
|
317
|
-
window._voiceTabPendingOpen = true;
|
|
318
|
-
if (window._voiceProgressDialog) {
|
|
319
|
-
window._voiceProgressDialog.update(0, 'Starting download...');
|
|
320
|
-
}
|
|
321
|
-
} else {
|
|
322
|
-
if (window._voiceProgressDialog) {
|
|
323
|
-
window._voiceProgressDialog.close();
|
|
324
|
-
window._voiceProgressDialog = null;
|
|
325
|
-
}
|
|
326
|
-
showToast('Failed to start download: ' + (data.error || 'unknown'), 'error');
|
|
327
|
-
}
|
|
328
|
-
}).catch(function(err) {
|
|
329
|
-
if (window._voiceProgressDialog) {
|
|
330
|
-
window._voiceProgressDialog.close();
|
|
331
|
-
window._voiceProgressDialog = null;
|
|
332
|
-
}
|
|
333
|
-
showToast('Download request failed: ' + err.message, 'error');
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
window.__checkVoiceReady = isVoiceReady;
|
|
338
|
-
window.__showVoiceDownloadProgress = showVoiceDownloadProgress;
|
|
339
|
-
|
|
340
|
-
function switchView(view) {
|
|
341
|
-
currentView = view;
|
|
342
|
-
var bar = document.getElementById('viewToggleBar');
|
|
343
|
-
var chatArea = document.getElementById('output-scroll');
|
|
344
|
-
var execPanel = document.querySelector('.input-section');
|
|
345
|
-
var fileBrowser = document.getElementById('fileBrowserContainer');
|
|
346
|
-
var fileIframe = document.getElementById('fileBrowserIframe');
|
|
347
|
-
var voiceContainer = document.getElementById('voiceContainer');
|
|
348
|
-
var terminalContainer = document.getElementById('terminalContainer');
|
|
349
|
-
|
|
350
|
-
if (!bar) return;
|
|
351
|
-
|
|
352
|
-
bar.querySelectorAll('.view-toggle-btn').forEach(function(btn) {
|
|
353
|
-
btn.classList.toggle('active', btn.dataset.view === view);
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
if (chatArea) chatArea.style.display = view === 'chat' ? '' : 'none';
|
|
357
|
-
if (execPanel) execPanel.style.display = view === 'chat' ? '' : 'none';
|
|
358
|
-
if (fileBrowser) fileBrowser.style.display = view === 'files' ? 'flex' : 'none';
|
|
359
|
-
if (voiceContainer) voiceContainer.style.display = view === 'voice' ? 'flex' : 'none';
|
|
360
|
-
if (terminalContainer) terminalContainer.style.display = view === 'terminal' ? 'flex' : 'none';
|
|
361
|
-
|
|
362
|
-
if (view === 'files' && fileIframe && currentConversation) {
|
|
363
|
-
var src = BASE + '/files/' + currentConversation + '/';
|
|
364
|
-
if (fileIframe.src !== location.origin + src) {
|
|
365
|
-
fileIframe.src = src;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
if (view === 'voice' && window.voiceModule) {
|
|
370
|
-
window.voiceModule.activate();
|
|
371
|
-
} else if (view !== 'voice' && window.voiceModule) {
|
|
372
|
-
window.voiceModule.deactivate();
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
window.dispatchEvent(new CustomEvent('view-switched', { detail: { view: view } }));
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function updateViewToggleVisibility() {
|
|
379
|
-
var bar = document.getElementById('viewToggleBar');
|
|
380
|
-
if (!bar) return;
|
|
381
|
-
|
|
382
|
-
// Show toggle bar only when a conversation is selected
|
|
383
|
-
if (currentConversation) {
|
|
384
|
-
bar.style.display = 'flex';
|
|
385
|
-
} else {
|
|
386
|
-
bar.style.display = 'none';
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// --- Conversation Listener ---
|
|
391
|
-
function setupConversationListener() {
|
|
392
|
-
window.addEventListener('conversation-selected', function(e) {
|
|
393
|
-
currentConversation = e.detail.conversationId;
|
|
394
|
-
updateViewToggleVisibility();
|
|
395
|
-
// If currently in files view, reload the iframe
|
|
396
|
-
if (currentView === 'files') {
|
|
397
|
-
switchView('files');
|
|
398
|
-
}
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
// Also listen for conversation created
|
|
402
|
-
window.addEventListener('create-new-conversation', function() {
|
|
403
|
-
// Will be updated when conversation-selected fires
|
|
404
|
-
});
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// Initialize when DOM is ready
|
|
408
|
-
if (document.readyState === 'loading') {
|
|
409
|
-
document.addEventListener('DOMContentLoaded', init);
|
|
410
|
-
} else {
|
|
411
|
-
init();
|
|
412
|
-
}
|
|
413
|
-
})();
|
|
1
|
+
/**
|
|
2
|
+
* Features Module
|
|
3
|
+
* Drag-and-drop file upload, fsbrowse file browser toggle, mobile sidebar
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
(function() {
|
|
7
|
+
const BASE = window.__BASE_URL || '';
|
|
8
|
+
let currentConversation = null;
|
|
9
|
+
let currentView = 'chat';
|
|
10
|
+
let dragCounter = 0;
|
|
11
|
+
|
|
12
|
+
function init() {
|
|
13
|
+
setupSidebarToggle();
|
|
14
|
+
setupDragAndDrop();
|
|
15
|
+
setupViewToggle();
|
|
16
|
+
setupConversationListener();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function setupSidebarToggle() {
|
|
20
|
+
var toggleBtn = document.querySelector('[data-sidebar-toggle]');
|
|
21
|
+
var sidebar = document.querySelector('[data-sidebar]');
|
|
22
|
+
var overlay = document.querySelector('[data-sidebar-overlay]');
|
|
23
|
+
|
|
24
|
+
if (!sidebar) return;
|
|
25
|
+
|
|
26
|
+
if (window.innerWidth <= 768) {
|
|
27
|
+
sidebar.classList.add('collapsed');
|
|
28
|
+
} else {
|
|
29
|
+
var savedState = localStorage.getItem('sidebar-collapsed');
|
|
30
|
+
if (savedState === 'true') {
|
|
31
|
+
sidebar.classList.add('collapsed');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isMobile() { return window.innerWidth <= 768; }
|
|
36
|
+
|
|
37
|
+
function toggleSidebar() {
|
|
38
|
+
if (isMobile()) {
|
|
39
|
+
var isOpen = sidebar.classList.contains('mobile-visible');
|
|
40
|
+
if (isOpen) { closeSidebar(); } else { openSidebar(); }
|
|
41
|
+
} else {
|
|
42
|
+
sidebar.classList.toggle('collapsed');
|
|
43
|
+
localStorage.setItem('sidebar-collapsed', sidebar.classList.contains('collapsed'));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function openSidebar() {
|
|
48
|
+
sidebar.classList.add('mobile-visible');
|
|
49
|
+
sidebar.classList.remove('collapsed');
|
|
50
|
+
if (overlay) overlay.classList.add('visible');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function closeSidebar() {
|
|
54
|
+
sidebar.classList.remove('mobile-visible');
|
|
55
|
+
if (overlay) overlay.classList.remove('visible');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (toggleBtn) {
|
|
59
|
+
toggleBtn.addEventListener('click', function(e) {
|
|
60
|
+
e.stopPropagation();
|
|
61
|
+
toggleSidebar();
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (overlay) {
|
|
66
|
+
overlay.addEventListener('click', closeSidebar);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
document.addEventListener('keydown', function(e) {
|
|
70
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'b') {
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
toggleSidebar();
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
window.addEventListener('conversation-selected', function() {
|
|
77
|
+
if (isMobile()) closeSidebar();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
window.addEventListener('resize', function() {
|
|
81
|
+
if (!isMobile()) {
|
|
82
|
+
sidebar.classList.remove('mobile-visible');
|
|
83
|
+
if (overlay) overlay.classList.remove('visible');
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// --- Drag and Drop File Upload ---
|
|
89
|
+
function setupDragAndDrop() {
|
|
90
|
+
const dropZone = document.querySelector('[data-drop-zone]');
|
|
91
|
+
const overlay = document.getElementById('dropZoneOverlay');
|
|
92
|
+
|
|
93
|
+
if (!dropZone || !overlay) return;
|
|
94
|
+
|
|
95
|
+
dropZone.addEventListener('dragenter', function(e) {
|
|
96
|
+
e.preventDefault();
|
|
97
|
+
e.stopPropagation();
|
|
98
|
+
dragCounter++;
|
|
99
|
+
if (dragCounter === 1) {
|
|
100
|
+
overlay.classList.add('visible');
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
dropZone.addEventListener('dragover', function(e) {
|
|
105
|
+
e.preventDefault();
|
|
106
|
+
e.stopPropagation();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
dropZone.addEventListener('dragleave', function(e) {
|
|
110
|
+
e.preventDefault();
|
|
111
|
+
e.stopPropagation();
|
|
112
|
+
dragCounter--;
|
|
113
|
+
if (dragCounter <= 0) {
|
|
114
|
+
dragCounter = 0;
|
|
115
|
+
overlay.classList.remove('visible');
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
dropZone.addEventListener('drop', function(e) {
|
|
120
|
+
e.preventDefault();
|
|
121
|
+
e.stopPropagation();
|
|
122
|
+
dragCounter = 0;
|
|
123
|
+
overlay.classList.remove('visible');
|
|
124
|
+
|
|
125
|
+
if (!currentConversation) {
|
|
126
|
+
if (window.UIDialog) window.UIDialog.showToast('Select a conversation first', 'error');
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const files = e.dataTransfer.files;
|
|
131
|
+
if (!files || files.length === 0) return;
|
|
132
|
+
|
|
133
|
+
uploadFiles(files);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function uploadFiles(files) {
|
|
138
|
+
if (!currentConversation) {
|
|
139
|
+
if (window.UIDialog) window.UIDialog.showToast('No conversation selected', 'error');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const formData = new FormData();
|
|
144
|
+
for (let i = 0; i < files.length; i++) {
|
|
145
|
+
formData.append('file', files[i]);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (window.UIDialog) window.UIDialog.showToast('Uploading ' + files.length + ' file(s)...', 'info');
|
|
149
|
+
|
|
150
|
+
fetch(BASE + '/api/upload/' + currentConversation, {
|
|
151
|
+
method: 'POST',
|
|
152
|
+
body: formData
|
|
153
|
+
})
|
|
154
|
+
.then(function(res) { return res.json(); })
|
|
155
|
+
.then(function(data) {
|
|
156
|
+
if (data.ok) {
|
|
157
|
+
if (window.UIDialog) window.UIDialog.showToast(data.count + ' file(s) uploaded', 'success');
|
|
158
|
+
} else {
|
|
159
|
+
if (window.UIDialog) window.UIDialog.showToast('Upload failed: ' + (data.error || 'Unknown error'), 'error');
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
.catch(function(err) {
|
|
163
|
+
if (window.UIDialog) window.UIDialog.showToast('Upload failed: ' + err.message, 'error');
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function showToast(message, type) {
|
|
168
|
+
var existing = document.querySelector('.upload-toast');
|
|
169
|
+
if (existing) existing.remove();
|
|
170
|
+
|
|
171
|
+
var toast = document.createElement('div');
|
|
172
|
+
toast.className = 'upload-toast ' + (type || 'info');
|
|
173
|
+
toast.textContent = message;
|
|
174
|
+
document.body.appendChild(toast);
|
|
175
|
+
|
|
176
|
+
setTimeout(function() {
|
|
177
|
+
toast.style.opacity = '0';
|
|
178
|
+
setTimeout(function() { toast.remove(); }, 300);
|
|
179
|
+
}, 3000);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// --- View Toggle (Chat / Files) ---
|
|
183
|
+
function setupViewToggle() {
|
|
184
|
+
var bar = document.getElementById('viewToggleBar');
|
|
185
|
+
if (!bar) return;
|
|
186
|
+
|
|
187
|
+
var buttons = bar.querySelectorAll('.view-toggle-btn');
|
|
188
|
+
buttons.forEach(function(btn) {
|
|
189
|
+
btn.addEventListener('click', function() {
|
|
190
|
+
var view = btn.dataset.view;
|
|
191
|
+
if (view === 'voice') {
|
|
192
|
+
handleVoiceTabClick();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
switchView(view);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function handleVoiceTabClick() {
|
|
201
|
+
if (isVoiceReady()) {
|
|
202
|
+
switchView('voice');
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
var client = window.agentGUIClient;
|
|
206
|
+
if (client && client._modelDownloadProgress == null) {
|
|
207
|
+
fetch((window.__BASE_URL || '') + '/api/speech-status')
|
|
208
|
+
.then(function(res) { return res.json(); })
|
|
209
|
+
.then(function(status) {
|
|
210
|
+
if (status.modelsComplete) {
|
|
211
|
+
if (client) {
|
|
212
|
+
client._modelDownloadProgress = { done: true, complete: true };
|
|
213
|
+
client._modelDownloadInProgress = false;
|
|
214
|
+
}
|
|
215
|
+
switchView('voice');
|
|
216
|
+
} else if (status.modelsDownloading) {
|
|
217
|
+
if (client) {
|
|
218
|
+
client._modelDownloadProgress = status.modelsProgress || { downloading: true };
|
|
219
|
+
client._modelDownloadInProgress = true;
|
|
220
|
+
}
|
|
221
|
+
showVoiceDownloadProgress();
|
|
222
|
+
} else {
|
|
223
|
+
triggerVoiceModelDownload();
|
|
224
|
+
}
|
|
225
|
+
})
|
|
226
|
+
.catch(function() { triggerVoiceModelDownload(); });
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (client && client._modelDownloadInProgress) {
|
|
230
|
+
showVoiceDownloadProgress();
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
triggerVoiceModelDownload();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function showVoiceDownloadProgress() {
|
|
237
|
+
if (window._voiceProgressDialog) return;
|
|
238
|
+
|
|
239
|
+
window._voiceProgressDialog = window.UIDialog.showProgress({
|
|
240
|
+
title: 'Downloading Voice Models',
|
|
241
|
+
message: 'Preparing speech recognition and synthesis models...'
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
var checkInterval = setInterval(function() {
|
|
245
|
+
if (isVoiceReady()) {
|
|
246
|
+
clearInterval(checkInterval);
|
|
247
|
+
if (window._voiceProgressDialog) {
|
|
248
|
+
window._voiceProgressDialog.close();
|
|
249
|
+
window._voiceProgressDialog = null;
|
|
250
|
+
}
|
|
251
|
+
switchView('voice');
|
|
252
|
+
}
|
|
253
|
+
}, 500);
|
|
254
|
+
|
|
255
|
+
setTimeout(function() {
|
|
256
|
+
clearInterval(checkInterval);
|
|
257
|
+
if (window._voiceProgressDialog) {
|
|
258
|
+
window._voiceProgressDialog.close();
|
|
259
|
+
window._voiceProgressDialog = null;
|
|
260
|
+
}
|
|
261
|
+
}, 120000);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function updateVoiceProgress(percent, message) {
|
|
265
|
+
if (window._voiceProgressDialog) {
|
|
266
|
+
window._voiceProgressDialog.update(percent, message);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
window.__updateVoiceProgress = updateVoiceProgress;
|
|
271
|
+
|
|
272
|
+
function isVoiceReady() {
|
|
273
|
+
var client = window.agentGUIClient;
|
|
274
|
+
if (!client) return false;
|
|
275
|
+
if (client._modelDownloadInProgress) return false;
|
|
276
|
+
var p = client._modelDownloadProgress;
|
|
277
|
+
return p != null && (p.done === true || p.complete === true);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function triggerVoiceModelDownload() {
|
|
281
|
+
var client = window.agentGUIClient;
|
|
282
|
+
if (client && client._modelDownloadInProgress) {
|
|
283
|
+
if (window._voiceProgressDialog) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
showToast('Voice models downloading... please wait', 'info');
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (!window._voiceProgressDialog) {
|
|
291
|
+
window._voiceProgressDialog = new ProgressDialog({
|
|
292
|
+
title: 'Voice Models',
|
|
293
|
+
message: 'Preparing to download voice models...',
|
|
294
|
+
percentage: 0,
|
|
295
|
+
cancellable: false
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
fetch((window.__BASE_URL || '') + '/api/speech-status', {
|
|
300
|
+
method: 'POST',
|
|
301
|
+
headers: { 'Content-Type': 'application/json' },
|
|
302
|
+
body: JSON.stringify({ forceDownload: true })
|
|
303
|
+
}).then(function(res) { return res.json(); })
|
|
304
|
+
.then(function(data) {
|
|
305
|
+
if (data.ok && data.modelsComplete) {
|
|
306
|
+
if (window._voiceProgressDialog) {
|
|
307
|
+
window._voiceProgressDialog.close();
|
|
308
|
+
window._voiceProgressDialog = null;
|
|
309
|
+
}
|
|
310
|
+
var client = window.agentGUIClient;
|
|
311
|
+
if (client) {
|
|
312
|
+
client._modelDownloadProgress = { done: true, complete: true };
|
|
313
|
+
client._modelDownloadInProgress = false;
|
|
314
|
+
}
|
|
315
|
+
switchView('voice');
|
|
316
|
+
} else if (data.ok) {
|
|
317
|
+
window._voiceTabPendingOpen = true;
|
|
318
|
+
if (window._voiceProgressDialog) {
|
|
319
|
+
window._voiceProgressDialog.update(0, 'Starting download...');
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
if (window._voiceProgressDialog) {
|
|
323
|
+
window._voiceProgressDialog.close();
|
|
324
|
+
window._voiceProgressDialog = null;
|
|
325
|
+
}
|
|
326
|
+
showToast('Failed to start download: ' + (data.error || 'unknown'), 'error');
|
|
327
|
+
}
|
|
328
|
+
}).catch(function(err) {
|
|
329
|
+
if (window._voiceProgressDialog) {
|
|
330
|
+
window._voiceProgressDialog.close();
|
|
331
|
+
window._voiceProgressDialog = null;
|
|
332
|
+
}
|
|
333
|
+
showToast('Download request failed: ' + err.message, 'error');
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
window.__checkVoiceReady = isVoiceReady;
|
|
338
|
+
window.__showVoiceDownloadProgress = showVoiceDownloadProgress;
|
|
339
|
+
|
|
340
|
+
function switchView(view) {
|
|
341
|
+
currentView = view;
|
|
342
|
+
var bar = document.getElementById('viewToggleBar');
|
|
343
|
+
var chatArea = document.getElementById('output-scroll');
|
|
344
|
+
var execPanel = document.querySelector('.input-section');
|
|
345
|
+
var fileBrowser = document.getElementById('fileBrowserContainer');
|
|
346
|
+
var fileIframe = document.getElementById('fileBrowserIframe');
|
|
347
|
+
var voiceContainer = document.getElementById('voiceContainer');
|
|
348
|
+
var terminalContainer = document.getElementById('terminalContainer');
|
|
349
|
+
|
|
350
|
+
if (!bar) return;
|
|
351
|
+
|
|
352
|
+
bar.querySelectorAll('.view-toggle-btn').forEach(function(btn) {
|
|
353
|
+
btn.classList.toggle('active', btn.dataset.view === view);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
if (chatArea) chatArea.style.display = view === 'chat' ? '' : 'none';
|
|
357
|
+
if (execPanel) execPanel.style.display = view === 'chat' ? '' : 'none';
|
|
358
|
+
if (fileBrowser) fileBrowser.style.display = view === 'files' ? 'flex' : 'none';
|
|
359
|
+
if (voiceContainer) voiceContainer.style.display = view === 'voice' ? 'flex' : 'none';
|
|
360
|
+
if (terminalContainer) terminalContainer.style.display = view === 'terminal' ? 'flex' : 'none';
|
|
361
|
+
|
|
362
|
+
if (view === 'files' && fileIframe && currentConversation) {
|
|
363
|
+
var src = BASE + '/files/' + currentConversation + '/';
|
|
364
|
+
if (fileIframe.src !== location.origin + src) {
|
|
365
|
+
fileIframe.src = src;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (view === 'voice' && window.voiceModule) {
|
|
370
|
+
window.voiceModule.activate();
|
|
371
|
+
} else if (view !== 'voice' && window.voiceModule) {
|
|
372
|
+
window.voiceModule.deactivate();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
window.dispatchEvent(new CustomEvent('view-switched', { detail: { view: view } }));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function updateViewToggleVisibility() {
|
|
379
|
+
var bar = document.getElementById('viewToggleBar');
|
|
380
|
+
if (!bar) return;
|
|
381
|
+
|
|
382
|
+
// Show toggle bar only when a conversation is selected
|
|
383
|
+
if (currentConversation) {
|
|
384
|
+
bar.style.display = 'flex';
|
|
385
|
+
} else {
|
|
386
|
+
bar.style.display = 'none';
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// --- Conversation Listener ---
|
|
391
|
+
function setupConversationListener() {
|
|
392
|
+
window.addEventListener('conversation-selected', function(e) {
|
|
393
|
+
currentConversation = e.detail.conversationId;
|
|
394
|
+
updateViewToggleVisibility();
|
|
395
|
+
// If currently in files view, reload the iframe
|
|
396
|
+
if (currentView === 'files') {
|
|
397
|
+
switchView('files');
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// Also listen for conversation created
|
|
402
|
+
window.addEventListener('create-new-conversation', function() {
|
|
403
|
+
// Will be updated when conversation-selected fires
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Initialize when DOM is ready
|
|
408
|
+
if (document.readyState === 'loading') {
|
|
409
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
410
|
+
} else {
|
|
411
|
+
init();
|
|
412
|
+
}
|
|
413
|
+
})();
|