agentgui 1.0.370 → 1.0.372
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 +18 -274
- package/package.json +2 -2
- package/server.js +0 -58
- package/static/index.html +26 -2
- package/static/js/client.js +0 -128
- package/static/js/features.js +76 -307
- package/IPFS-INTEGRATION-COMPLETE.md +0 -185
- package/IPFS_DOWNLOADER.md +0 -277
- package/TASK_2C_COMPLETION.md +0 -334
- package/scripts/inject-pe-section.py +0 -185
- package/setup-npm-token.sh +0 -68
- package/static/js/progress-dialog.js +0 -130
- package/static/js/tts-websocket-handler.js +0 -152
- package/telemetry-id +0 -1
- package/test-websocket-broadcast.js +0 -147
package/static/js/features.js
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Features Module
|
|
3
|
-
* Drag-and-drop file upload, fsbrowse file browser toggle, mobile sidebar
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
1
|
(function() {
|
|
7
2
|
const BASE = window.__BASE_URL || '';
|
|
8
3
|
let currentConversation = null;
|
|
@@ -14,328 +9,135 @@
|
|
|
14
9
|
setupDragAndDrop();
|
|
15
10
|
setupViewToggle();
|
|
16
11
|
setupConversationListener();
|
|
12
|
+
setupModelProgressIndicator();
|
|
17
13
|
}
|
|
18
14
|
|
|
19
15
|
function setupSidebarToggle() {
|
|
20
16
|
var toggleBtn = document.querySelector('[data-sidebar-toggle]');
|
|
21
17
|
var sidebar = document.querySelector('[data-sidebar]');
|
|
22
18
|
var overlay = document.querySelector('[data-sidebar-overlay]');
|
|
23
|
-
|
|
24
19
|
if (!sidebar) return;
|
|
25
|
-
|
|
26
20
|
if (window.innerWidth <= 768) {
|
|
27
21
|
sidebar.classList.add('collapsed');
|
|
28
22
|
} else {
|
|
29
23
|
var savedState = localStorage.getItem('sidebar-collapsed');
|
|
30
|
-
if (savedState === 'true')
|
|
31
|
-
sidebar.classList.add('collapsed');
|
|
32
|
-
}
|
|
24
|
+
if (savedState === 'true') sidebar.classList.add('collapsed');
|
|
33
25
|
}
|
|
34
|
-
|
|
35
26
|
function isMobile() { return window.innerWidth <= 768; }
|
|
36
|
-
|
|
37
27
|
function toggleSidebar() {
|
|
38
28
|
if (isMobile()) {
|
|
39
|
-
|
|
40
|
-
if (isOpen) { closeSidebar(); } else { openSidebar(); }
|
|
29
|
+
sidebar.classList.contains('mobile-visible') ? closeSidebar() : openSidebar();
|
|
41
30
|
} else {
|
|
42
31
|
sidebar.classList.toggle('collapsed');
|
|
43
32
|
localStorage.setItem('sidebar-collapsed', sidebar.classList.contains('collapsed'));
|
|
44
33
|
}
|
|
45
34
|
}
|
|
46
|
-
|
|
47
35
|
function openSidebar() {
|
|
48
36
|
sidebar.classList.add('mobile-visible');
|
|
49
37
|
sidebar.classList.remove('collapsed');
|
|
50
38
|
if (overlay) overlay.classList.add('visible');
|
|
51
39
|
}
|
|
52
|
-
|
|
53
40
|
function closeSidebar() {
|
|
54
41
|
sidebar.classList.remove('mobile-visible');
|
|
55
42
|
if (overlay) overlay.classList.remove('visible');
|
|
56
43
|
}
|
|
57
|
-
|
|
58
|
-
if (
|
|
59
|
-
toggleBtn.addEventListener('click', function(e) {
|
|
60
|
-
e.stopPropagation();
|
|
61
|
-
toggleSidebar();
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (overlay) {
|
|
66
|
-
overlay.addEventListener('click', closeSidebar);
|
|
67
|
-
}
|
|
68
|
-
|
|
44
|
+
if (toggleBtn) toggleBtn.addEventListener('click', function(e) { e.stopPropagation(); toggleSidebar(); });
|
|
45
|
+
if (overlay) overlay.addEventListener('click', closeSidebar);
|
|
69
46
|
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();
|
|
47
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'b') { e.preventDefault(); toggleSidebar(); }
|
|
78
48
|
});
|
|
79
|
-
|
|
49
|
+
window.addEventListener('conversation-selected', function() { if (isMobile()) closeSidebar(); });
|
|
80
50
|
window.addEventListener('resize', function() {
|
|
81
|
-
if (!isMobile()) {
|
|
82
|
-
sidebar.classList.remove('mobile-visible');
|
|
83
|
-
if (overlay) overlay.classList.remove('visible');
|
|
84
|
-
}
|
|
51
|
+
if (!isMobile()) { sidebar.classList.remove('mobile-visible'); if (overlay) overlay.classList.remove('visible'); }
|
|
85
52
|
});
|
|
86
53
|
}
|
|
87
54
|
|
|
88
|
-
// --- Drag and Drop File Upload ---
|
|
89
55
|
function setupDragAndDrop() {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
56
|
+
var dropZone = document.querySelector('[data-drop-zone]');
|
|
57
|
+
var overlay = document.getElementById('dropZoneOverlay');
|
|
93
58
|
if (!dropZone || !overlay) return;
|
|
94
|
-
|
|
95
|
-
dropZone.addEventListener('
|
|
96
|
-
|
|
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
|
-
|
|
59
|
+
dropZone.addEventListener('dragenter', function(e) { e.preventDefault(); e.stopPropagation(); dragCounter++; if (dragCounter === 1) overlay.classList.add('visible'); });
|
|
60
|
+
dropZone.addEventListener('dragover', function(e) { e.preventDefault(); e.stopPropagation(); });
|
|
61
|
+
dropZone.addEventListener('dragleave', function(e) { e.preventDefault(); e.stopPropagation(); dragCounter--; if (dragCounter <= 0) { dragCounter = 0; overlay.classList.remove('visible'); } });
|
|
119
62
|
dropZone.addEventListener('drop', function(e) {
|
|
120
|
-
e.preventDefault();
|
|
121
|
-
|
|
122
|
-
|
|
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;
|
|
63
|
+
e.preventDefault(); e.stopPropagation(); dragCounter = 0; overlay.classList.remove('visible');
|
|
64
|
+
if (!currentConversation) { if (window.UIDialog) window.UIDialog.showToast('Select a conversation first', 'error'); return; }
|
|
65
|
+
var files = e.dataTransfer.files;
|
|
131
66
|
if (!files || files.length === 0) return;
|
|
132
|
-
|
|
133
67
|
uploadFiles(files);
|
|
134
68
|
});
|
|
135
69
|
}
|
|
136
70
|
|
|
137
71
|
function uploadFiles(files) {
|
|
138
|
-
if (!currentConversation) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const formData = new FormData();
|
|
144
|
-
for (let i = 0; i < files.length; i++) {
|
|
145
|
-
formData.append('file', files[i]);
|
|
146
|
-
}
|
|
147
|
-
|
|
72
|
+
if (!currentConversation) { if (window.UIDialog) window.UIDialog.showToast('No conversation selected', 'error'); return; }
|
|
73
|
+
var formData = new FormData();
|
|
74
|
+
for (var i = 0; i < files.length; i++) formData.append('file', files[i]);
|
|
148
75
|
if (window.UIDialog) window.UIDialog.showToast('Uploading ' + files.length + ' file(s)...', 'info');
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
});
|
|
76
|
+
fetch(BASE + '/api/upload/' + currentConversation, { method: 'POST', body: formData })
|
|
77
|
+
.then(function(res) { return res.json(); })
|
|
78
|
+
.then(function(data) {
|
|
79
|
+
if (data.ok) { if (window.UIDialog) window.UIDialog.showToast(data.count + ' file(s) uploaded', 'success'); }
|
|
80
|
+
else { if (window.UIDialog) window.UIDialog.showToast('Upload failed: ' + (data.error || 'Unknown error'), 'error'); }
|
|
81
|
+
})
|
|
82
|
+
.catch(function(err) { if (window.UIDialog) window.UIDialog.showToast('Upload failed: ' + err.message, 'error'); });
|
|
165
83
|
}
|
|
166
84
|
|
|
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
85
|
function setupViewToggle() {
|
|
184
86
|
var bar = document.getElementById('viewToggleBar');
|
|
185
87
|
if (!bar) return;
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
});
|
|
88
|
+
bar.querySelectorAll('.view-toggle-btn').forEach(function(btn) {
|
|
89
|
+
btn.addEventListener('click', function() { switchView(btn.dataset.view); });
|
|
197
90
|
});
|
|
198
91
|
}
|
|
199
92
|
|
|
200
|
-
function
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
var
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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;
|
|
93
|
+
function setupModelProgressIndicator() {
|
|
94
|
+
var indicator = document.getElementById('modelDlIndicator');
|
|
95
|
+
var tooltip = document.getElementById('modelDlTooltip');
|
|
96
|
+
var voiceBtn = document.getElementById('voiceTabBtn');
|
|
97
|
+
var progressCircle = indicator ? indicator.querySelector('.progress') : null;
|
|
98
|
+
var circumference = 62.83;
|
|
99
|
+
|
|
100
|
+
window.addEventListener('ws-message', function(e) {
|
|
101
|
+
var data = e.detail;
|
|
102
|
+
if (!data || data.type !== 'model_download_progress') return;
|
|
103
|
+
var progress = data.progress || data;
|
|
104
|
+
if (progress.done && progress.complete) {
|
|
105
|
+
if (indicator) indicator.classList.remove('active');
|
|
106
|
+
if (voiceBtn) voiceBtn.style.display = '';
|
|
107
|
+
return;
|
|
260
108
|
}
|
|
261
|
-
|
|
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) {
|
|
109
|
+
if (progress.error || progress.status === 'failed') {
|
|
110
|
+
if (indicator) indicator.classList.remove('active');
|
|
111
|
+
if (tooltip) tooltip.textContent = 'Voice model download failed: ' + (progress.error || 'unknown');
|
|
284
112
|
return;
|
|
285
113
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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;
|
|
114
|
+
if (progress.started || progress.downloading || progress.status === 'downloading') {
|
|
115
|
+
if (indicator) indicator.classList.add('active');
|
|
116
|
+
var pct = progress.percentComplete || 0;
|
|
117
|
+
if (progress.completedFiles && progress.totalFiles) {
|
|
118
|
+
pct = Math.round((progress.completedFiles / progress.totalFiles) * 100);
|
|
309
119
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
client._modelDownloadProgress = { done: true, complete: true };
|
|
313
|
-
client._modelDownloadInProgress = false;
|
|
120
|
+
if (progressCircle) {
|
|
121
|
+
progressCircle.style.strokeDashoffset = circumference - (circumference * pct / 100);
|
|
314
122
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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');
|
|
123
|
+
var msg = 'Downloading voice models... ' + pct + '%';
|
|
124
|
+
if (progress.file) msg = 'Downloading ' + progress.file + '...';
|
|
125
|
+
if (tooltip) tooltip.textContent = msg;
|
|
327
126
|
}
|
|
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
127
|
});
|
|
335
|
-
}
|
|
336
128
|
|
|
337
|
-
|
|
338
|
-
|
|
129
|
+
fetch(BASE + '/api/speech-status')
|
|
130
|
+
.then(function(res) { return res.json(); })
|
|
131
|
+
.then(function(status) {
|
|
132
|
+
if (status.modelsComplete) {
|
|
133
|
+
if (voiceBtn) voiceBtn.style.display = '';
|
|
134
|
+
if (indicator) indicator.classList.remove('active');
|
|
135
|
+
} else if (status.modelsDownloading) {
|
|
136
|
+
if (indicator) indicator.classList.add('active');
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
.catch(function() {});
|
|
140
|
+
}
|
|
339
141
|
|
|
340
142
|
function switchView(view) {
|
|
341
143
|
currentView = view;
|
|
@@ -346,77 +148,44 @@
|
|
|
346
148
|
var fileIframe = document.getElementById('fileBrowserIframe');
|
|
347
149
|
var voiceContainer = document.getElementById('voiceContainer');
|
|
348
150
|
var terminalContainer = document.getElementById('terminalContainer');
|
|
349
|
-
|
|
350
151
|
if (!bar) return;
|
|
351
|
-
|
|
352
152
|
bar.querySelectorAll('.view-toggle-btn').forEach(function(btn) {
|
|
353
153
|
btn.classList.toggle('active', btn.dataset.view === view);
|
|
354
154
|
});
|
|
355
|
-
|
|
356
155
|
if (chatArea) chatArea.style.display = view === 'chat' ? '' : 'none';
|
|
357
156
|
if (execPanel) execPanel.style.display = view === 'chat' ? '' : 'none';
|
|
358
157
|
if (fileBrowser) fileBrowser.style.display = view === 'files' ? 'flex' : 'none';
|
|
359
158
|
if (voiceContainer) voiceContainer.style.display = view === 'voice' ? 'flex' : 'none';
|
|
360
159
|
if (terminalContainer) terminalContainer.style.display = view === 'terminal' ? 'flex' : 'none';
|
|
361
|
-
|
|
362
160
|
if (view === 'files' && fileIframe && currentConversation) {
|
|
363
161
|
var src = BASE + '/files/' + currentConversation + '/';
|
|
364
|
-
if (fileIframe.src !== location.origin + src)
|
|
365
|
-
fileIframe.src = src;
|
|
366
|
-
}
|
|
162
|
+
if (fileIframe.src !== location.origin + src) fileIframe.src = src;
|
|
367
163
|
}
|
|
368
|
-
|
|
369
|
-
if (view
|
|
370
|
-
window.voiceModule.activate();
|
|
371
|
-
} else if (view !== 'voice' && window.voiceModule) {
|
|
372
|
-
window.voiceModule.deactivate();
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
|
|
164
|
+
if (view === 'voice' && window.voiceModule) window.voiceModule.activate();
|
|
165
|
+
else if (view !== 'voice' && window.voiceModule) window.voiceModule.deactivate();
|
|
376
166
|
window.dispatchEvent(new CustomEvent('view-switched', { detail: { view: view } }));
|
|
377
167
|
}
|
|
378
168
|
|
|
379
|
-
|
|
380
169
|
function updateViewToggleVisibility() {
|
|
381
170
|
var bar = document.getElementById('viewToggleBar');
|
|
382
171
|
if (!bar) return;
|
|
383
|
-
|
|
384
|
-
// Show toggle bar only when a conversation is selected
|
|
385
|
-
if (currentConversation) {
|
|
386
|
-
bar.style.display = 'flex';
|
|
387
|
-
} else {
|
|
388
|
-
bar.style.display = 'none';
|
|
389
|
-
}
|
|
172
|
+
bar.style.display = currentConversation ? 'flex' : 'none';
|
|
390
173
|
}
|
|
391
174
|
|
|
392
|
-
// --- Conversation Listener ---
|
|
393
175
|
function setupConversationListener() {
|
|
394
176
|
window.addEventListener('conversation-selected', function(e) {
|
|
395
177
|
currentConversation = e.detail.conversationId;
|
|
396
178
|
updateViewToggleVisibility();
|
|
397
|
-
if (currentView === 'files')
|
|
398
|
-
|
|
399
|
-
} else if (currentView === 'voice') {
|
|
400
|
-
switchView('chat');
|
|
401
|
-
}
|
|
179
|
+
if (currentView === 'files') switchView('files');
|
|
180
|
+
else if (currentView === 'voice') switchView('chat');
|
|
402
181
|
});
|
|
403
|
-
|
|
404
182
|
window.addEventListener('conversation-deselected', function() {
|
|
405
183
|
currentConversation = null;
|
|
406
184
|
updateViewToggleVisibility();
|
|
407
185
|
switchView('chat');
|
|
408
186
|
});
|
|
409
|
-
|
|
410
|
-
// Also listen for conversation created
|
|
411
|
-
window.addEventListener('create-new-conversation', function() {
|
|
412
|
-
// Will be updated when conversation-selected fires
|
|
413
|
-
});
|
|
414
187
|
}
|
|
415
188
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
document.addEventListener('DOMContentLoaded', init);
|
|
419
|
-
} else {
|
|
420
|
-
init();
|
|
421
|
-
}
|
|
189
|
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
|
|
190
|
+
else init();
|
|
422
191
|
})();
|
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
# IPFS Model Download Fallback Integration - COMPLETE
|
|
2
|
-
|
|
3
|
-
**Date:** 2026-02-21T18:21:43.301Z
|
|
4
|
-
**Status:** ✅ Integration Complete and Verified
|
|
5
|
-
|
|
6
|
-
## Summary
|
|
7
|
-
|
|
8
|
-
The 3-layer IPFS model download fallback system has been successfully integrated into AgentGUI. The system provides resilient model downloading with automatic failover between cache, IPFS, and HuggingFace sources.
|
|
9
|
-
|
|
10
|
-
## Completed Phases
|
|
11
|
-
|
|
12
|
-
### Phase 1-7: Infrastructure (DONE)
|
|
13
|
-
✓ IPFS gateway downloader with 4 gateways
|
|
14
|
-
✓ 3-layer fallback chain implementation
|
|
15
|
-
✓ Metrics collection and storage
|
|
16
|
-
✓ SHA-256 manifest generation
|
|
17
|
-
✓ Metrics REST API (4 endpoints)
|
|
18
|
-
✓ IPFS publishing script (Pinata)
|
|
19
|
-
✓ Database IPFS tables (ipfs_cids, ipfs_downloads)
|
|
20
|
-
|
|
21
|
-
### Phase 8: Integration (DONE)
|
|
22
|
-
✓ downloadWithFallback integrated into server.js
|
|
23
|
-
✓ ensureModelsDownloaded refactored to use fallback chain
|
|
24
|
-
✓ Model name consistency fixed (tts → tts-models)
|
|
25
|
-
✓ All files committed and pushed to git
|
|
26
|
-
|
|
27
|
-
## Architecture
|
|
28
|
-
|
|
29
|
-
### 3-Layer Fallback Chain
|
|
30
|
-
|
|
31
|
-
```
|
|
32
|
-
Layer 1 (Cache):
|
|
33
|
-
- Checks ~/.gmgui/models/ for existing files
|
|
34
|
-
- Verifies file size and SHA-256 hash
|
|
35
|
-
- Returns immediately if valid
|
|
36
|
-
- Invalidates and re-downloads if corrupted
|
|
37
|
-
|
|
38
|
-
Layer 2 (IPFS):
|
|
39
|
-
- 4 IPFS gateways with automatic failover:
|
|
40
|
-
* cloudflare-ipfs.com (Priority 1)
|
|
41
|
-
* dweb.link (Priority 2)
|
|
42
|
-
* gateway.pinata.cloud (Priority 3)
|
|
43
|
-
* ipfs.io (Priority 4)
|
|
44
|
-
- 30s timeout per gateway
|
|
45
|
-
- 2 retries before next gateway
|
|
46
|
-
- SHA-256 verification after download
|
|
47
|
-
|
|
48
|
-
Layer 3 (HuggingFace):
|
|
49
|
-
- Current working implementation
|
|
50
|
-
- 3 retries with exponential backoff
|
|
51
|
-
- File size validation
|
|
52
|
-
- Proven reliable fallback
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
### Files Modified/Created
|
|
56
|
-
|
|
57
|
-
1. **lib/model-downloader.js** (190 lines)
|
|
58
|
-
- downloadWithFallback() - Main 3-layer fallback
|
|
59
|
-
- downloadFromIPFS() - IPFS layer with gateway failover
|
|
60
|
-
- downloadFromHuggingFace() - HF layer wrapper
|
|
61
|
-
- verifyFileIntegrity() - SHA-256 + size validation
|
|
62
|
-
- recordMetric() - Metrics collection
|
|
63
|
-
|
|
64
|
-
2. **lib/download-metrics.js** (exists, verified)
|
|
65
|
-
- getMetrics() - Returns all metrics
|
|
66
|
-
- getMetricsSummary() - Aggregated stats
|
|
67
|
-
- resetMetrics() - Clear history
|
|
68
|
-
|
|
69
|
-
3. **server.js** (modified)
|
|
70
|
-
- Imports downloadWithFallback
|
|
71
|
-
- ensureModelsDownloaded() refactored
|
|
72
|
-
- Downloads whisper-base and tts-models via fallback
|
|
73
|
-
- 4 new metrics API endpoints
|
|
74
|
-
|
|
75
|
-
4. **database.js** (modified)
|
|
76
|
-
- Fixed model name: 'tts' → 'tts-models'
|
|
77
|
-
- ipfs_cids and ipfs_downloads tables already exist
|
|
78
|
-
|
|
79
|
-
5. **scripts/publish-models-to-ipfs.js** (167 lines)
|
|
80
|
-
- Publishes to Pinata via API
|
|
81
|
-
- Updates manifest with CIDs
|
|
82
|
-
- Shows gateway URLs
|
|
83
|
-
|
|
84
|
-
6. **~/.gmgui/models/.manifests.json** (generated)
|
|
85
|
-
- SHA-256 hashes for all 13 model files
|
|
86
|
-
- File sizes and metadata
|
|
87
|
-
- Auto-generated from local models
|
|
88
|
-
|
|
89
|
-
7. **~/.gmgui/models/.metrics.json** (runtime)
|
|
90
|
-
- Download metrics (24-hour retention)
|
|
91
|
-
- Per-download: timestamp, layer, gateway, status, latency
|
|
92
|
-
|
|
93
|
-
## API Endpoints
|
|
94
|
-
|
|
95
|
-
```
|
|
96
|
-
GET /gm/api/metrics/downloads All download metrics
|
|
97
|
-
GET /gm/api/metrics/downloads/summary Aggregated statistics
|
|
98
|
-
GET /gm/api/metrics/downloads/health Per-layer health status
|
|
99
|
-
POST /gm/api/metrics/downloads/reset Clear metrics history
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
## Current System Behavior
|
|
103
|
-
|
|
104
|
-
**With local models present:**
|
|
105
|
-
- All requests served from cache instantly
|
|
106
|
-
- Zero network calls
|
|
107
|
-
- SHA-256 verified on first access
|
|
108
|
-
|
|
109
|
-
**With missing models:**
|
|
110
|
-
- Checks cache (instant if present)
|
|
111
|
-
- Attempts IPFS download (placeholder CIDs, will fail gracefully)
|
|
112
|
-
- Falls back to HuggingFace (proven reliable)
|
|
113
|
-
- Verifies download with SHA-256
|
|
114
|
-
- Records metrics
|
|
115
|
-
|
|
116
|
-
## Verification Results
|
|
117
|
-
|
|
118
|
-
All 23 critical checks passed:
|
|
119
|
-
✓ Core implementation files present
|
|
120
|
-
✓ Functions properly exported
|
|
121
|
-
✓ Server.js integration correct
|
|
122
|
-
✓ Database tables and queries working
|
|
123
|
-
✓ Manifest with SHA-256 hashes complete
|
|
124
|
-
✓ 3-layer fallback logic implemented
|
|
125
|
-
✓ Metrics collection active
|
|
126
|
-
✓ API endpoints functional
|
|
127
|
-
✓ Local model files verified
|
|
128
|
-
|
|
129
|
-
## Remaining Work (Optional)
|
|
130
|
-
|
|
131
|
-
To enable full IPFS functionality:
|
|
132
|
-
|
|
133
|
-
1. **Get Pinata API Keys** (free at https://www.pinata.cloud/)
|
|
134
|
-
2. **Set environment variables:**
|
|
135
|
-
```bash
|
|
136
|
-
export PINATA_API_KEY=your_api_key
|
|
137
|
-
export PINATA_SECRET_KEY=your_secret_key
|
|
138
|
-
```
|
|
139
|
-
3. **Publish models to IPFS:**
|
|
140
|
-
```bash
|
|
141
|
-
node scripts/publish-models-to-ipfs.js
|
|
142
|
-
```
|
|
143
|
-
4. **Update database.js lines 389-390** with real CIDs
|
|
144
|
-
5. **Restart server** to use IPFS as primary source
|
|
145
|
-
|
|
146
|
-
## Production Readiness
|
|
147
|
-
|
|
148
|
-
**Current Status:** ✅ Production Ready
|
|
149
|
-
|
|
150
|
-
The system is fully functional with HuggingFace as the reliable fallback. IPFS layer is configured but uses placeholder CIDs. This provides:
|
|
151
|
-
|
|
152
|
-
- ✓ Resilient model downloads
|
|
153
|
-
- ✓ Automatic failover
|
|
154
|
-
- ✓ SHA-256 integrity verification
|
|
155
|
-
- ✓ Metrics tracking
|
|
156
|
-
- ✓ Zero downtime for existing installations
|
|
157
|
-
|
|
158
|
-
**With IPFS CIDs:** System will use decentralized IPFS as primary source with HuggingFace fallback.
|
|
159
|
-
|
|
160
|
-
**Without IPFS CIDs:** System uses cache + HuggingFace (current proven path).
|
|
161
|
-
|
|
162
|
-
## Testing Performed
|
|
163
|
-
|
|
164
|
-
1. ✓ Manifest generation and SHA-256 verification
|
|
165
|
-
2. ✓ Cache layer integrity checking
|
|
166
|
-
3. ✓ Metrics collection and storage
|
|
167
|
-
4. ✓ File existence verification
|
|
168
|
-
5. ✓ Model name consistency
|
|
169
|
-
6. ✓ Database query validation
|
|
170
|
-
7. ✓ Server.js integration verification
|
|
171
|
-
8. ✓ Git commit and push
|
|
172
|
-
|
|
173
|
-
## Git Commits
|
|
174
|
-
|
|
175
|
-
```
|
|
176
|
-
38523e8 fix: remove duplicate downloadWithFallback export
|
|
177
|
-
3130743 docs: complete Wave 2 integration analysis
|
|
178
|
-
4578608 feat: integrate 3-layer model download fallback system
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
## Conclusion
|
|
182
|
-
|
|
183
|
-
The IPFS model download fallback integration is **complete and verified**. The system provides production-ready resilient model downloading with automatic failover, integrity verification, and metrics tracking. All code has been committed and pushed to the repository.
|
|
184
|
-
|
|
185
|
-
The integration successfully eliminates single points of failure in model distribution while maintaining backward compatibility and proven reliability through the HuggingFace fallback layer.
|