daedalus-cli 3.72.0 → 3.73.0
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/CHANGELOG.md +28 -0
- package/README.md +30 -3
- package/dist/agents/roles.js +8 -8
- package/dist/agents/roles.js.map +1 -1
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +36 -5
- package/dist/model.js.map +1 -1
- package/dist/repl.d.ts.map +1 -1
- package/dist/repl.js +176 -19
- package/dist/repl.js.map +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/webui/public/index.html +210 -24
- package/dist/webui/public/marked.min.js +69 -0
- package/dist/webui/public/script.js +1006 -6
- package/dist/webui/public/styles.css +1589 -84
- package/dist/webui/server.d.ts +38 -0
- package/dist/webui/server.d.ts.map +1 -1
- package/dist/webui/server.js +345 -32
- package/dist/webui/server.js.map +1 -1
- package/dist/webui/server.test.js +257 -3
- package/dist/webui/server.test.js.map +1 -1
- package/dist/webui/types.d.ts +22 -0
- package/dist/webui/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -25,15 +25,711 @@ if (clearBtn) {
|
|
|
25
25
|
});
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
const chatMessages = document.getElementById('chat-messages');
|
|
29
|
+
const chatForm = document.getElementById('chat-form');
|
|
30
|
+
const chatInput = document.getElementById('chat-input');
|
|
31
|
+
const chatStatusBadge = document.getElementById('chat-status-badge');
|
|
32
|
+
|
|
33
|
+
function highlightSyntax(code) {
|
|
34
|
+
const escaped = code
|
|
35
|
+
.replace(/&/g, '&')
|
|
36
|
+
.replace(/</g, '<')
|
|
37
|
+
.replace(/>/g, '>');
|
|
38
|
+
|
|
39
|
+
return escaped
|
|
40
|
+
.replace(/\b(const|let|var|function|return|if|else|for|while|import|export|from|async|await|class|type|interface|def|self|pub|fn|struct|match|case|switch|try|catch|throw|finally|yield)\b/g, '<span class="hl-keyword">$1</span>')
|
|
41
|
+
.replace(/\b(true|false|null|undefined|None|True|False|nil)\b/g, '<span class="hl-boolean">$1</span>')
|
|
42
|
+
.replace(/\b(\d+(\.\d+)?)\b/g, '<span class="hl-number">$1</span>')
|
|
43
|
+
.replace(/(["'`])(.*?)\1/g, '<span class="hl-string">$1$2$1</span>')
|
|
44
|
+
.replace(/(\/\/.*|\/\*[\s\S]*?\*\/|#.*)/g, '<span class="hl-comment">$1</span>');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function renderMarkdown(text) {
|
|
48
|
+
let html = '';
|
|
49
|
+
if (typeof window.marked !== 'undefined' && typeof window.marked.parse === 'function') {
|
|
50
|
+
try {
|
|
51
|
+
html = window.marked.parse(text, { breaks: true, gfm: true });
|
|
52
|
+
} catch {
|
|
53
|
+
html = text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br>');
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
html = text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br>');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const tempDiv = document.createElement('div');
|
|
60
|
+
tempDiv.innerHTML = html;
|
|
61
|
+
|
|
62
|
+
tempDiv.querySelectorAll('pre').forEach(pre => {
|
|
63
|
+
const code = pre.querySelector('code');
|
|
64
|
+
const rawCode = code ? (code.textContent || '') : (pre.textContent || '');
|
|
65
|
+
const langMatch = code?.className?.match(/language-([a-zA-Z0-9_-]+)/);
|
|
66
|
+
const lang = langMatch ? langMatch[1] : 'CODE';
|
|
67
|
+
|
|
68
|
+
const wrapper = document.createElement('div');
|
|
69
|
+
wrapper.className = 'code-block-wrapper';
|
|
70
|
+
|
|
71
|
+
const header = document.createElement('div');
|
|
72
|
+
header.className = 'code-block-header';
|
|
73
|
+
|
|
74
|
+
const langSpan = document.createElement('span');
|
|
75
|
+
langSpan.className = 'code-lang';
|
|
76
|
+
langSpan.textContent = lang;
|
|
77
|
+
|
|
78
|
+
const copyBtn = document.createElement('button');
|
|
79
|
+
copyBtn.type = 'button';
|
|
80
|
+
copyBtn.className = 'code-copy-btn';
|
|
81
|
+
copyBtn.textContent = 'COPY CODE';
|
|
82
|
+
copyBtn.addEventListener('click', (e) => {
|
|
83
|
+
e.stopPropagation();
|
|
84
|
+
navigator.clipboard.writeText(rawCode).then(() => {
|
|
85
|
+
copyBtn.textContent = 'COPIED!';
|
|
86
|
+
copyBtn.classList.add('copied');
|
|
87
|
+
setTimeout(() => {
|
|
88
|
+
copyBtn.textContent = 'COPY CODE';
|
|
89
|
+
copyBtn.classList.remove('copied');
|
|
90
|
+
}, 1800);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
header.appendChild(langSpan);
|
|
95
|
+
header.appendChild(copyBtn);
|
|
96
|
+
|
|
97
|
+
if (code) {
|
|
98
|
+
code.innerHTML = highlightSyntax(rawCode);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
pre.parentNode.insertBefore(wrapper, pre);
|
|
102
|
+
wrapper.appendChild(header);
|
|
103
|
+
wrapper.appendChild(pre);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return tempDiv.innerHTML;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function addChatMessage(role, text, roleBadge = null, imageBase64 = null, timestamp = null) {
|
|
110
|
+
if (!chatMessages) return;
|
|
111
|
+
const msgEl = document.createElement('div');
|
|
112
|
+
msgEl.className = `chat-msg ${role}`;
|
|
113
|
+
|
|
114
|
+
const header = document.createElement('div');
|
|
115
|
+
header.className = 'msg-header';
|
|
116
|
+
|
|
117
|
+
const sender = document.createElement('span');
|
|
118
|
+
sender.className = 'sender';
|
|
119
|
+
sender.textContent = role === 'user' ? 'YOU' : 'DAEDALUS';
|
|
120
|
+
header.appendChild(sender);
|
|
121
|
+
|
|
122
|
+
if (roleBadge) {
|
|
123
|
+
const badge = document.createElement('span');
|
|
124
|
+
badge.className = 'badge-role';
|
|
125
|
+
badge.textContent = roleBadge;
|
|
126
|
+
header.appendChild(badge);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const timeStr = new Date(timestamp || Date.now()).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
|
|
130
|
+
const timeSpan = document.createElement('span');
|
|
131
|
+
timeSpan.className = 'msg-timestamp';
|
|
132
|
+
timeSpan.textContent = timeStr;
|
|
133
|
+
header.appendChild(timeSpan);
|
|
134
|
+
|
|
135
|
+
if (role === 'assistant') {
|
|
136
|
+
const copyBtn = document.createElement('button');
|
|
137
|
+
copyBtn.className = 'copy-btn';
|
|
138
|
+
copyBtn.textContent = 'COPY';
|
|
139
|
+
copyBtn.addEventListener('click', () => {
|
|
140
|
+
const raw = body.dataset.raw || body.textContent || '';
|
|
141
|
+
navigator.clipboard.writeText(raw).then(() => {
|
|
142
|
+
copyBtn.textContent = 'COPIED!';
|
|
143
|
+
copyBtn.classList.add('copied');
|
|
144
|
+
setTimeout(() => {
|
|
145
|
+
copyBtn.textContent = 'COPY';
|
|
146
|
+
copyBtn.classList.remove('copied');
|
|
147
|
+
}, 1800);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
header.appendChild(copyBtn);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const body = document.createElement('div');
|
|
154
|
+
body.className = 'msg-body';
|
|
155
|
+
body.dataset.raw = text;
|
|
156
|
+
|
|
157
|
+
if (imageBase64) {
|
|
158
|
+
const img = document.createElement('img');
|
|
159
|
+
img.className = 'chat-msg-img';
|
|
160
|
+
img.src = imageBase64.startsWith('data:') ? imageBase64 : `data:image/png;base64,${imageBase64}`;
|
|
161
|
+
img.alt = 'Uploaded image';
|
|
162
|
+
body.appendChild(img);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (role === 'assistant') {
|
|
166
|
+
const textContainer = document.createElement('div');
|
|
167
|
+
textContainer.innerHTML = renderMarkdown(text);
|
|
168
|
+
body.appendChild(textContainer);
|
|
169
|
+
} else {
|
|
170
|
+
const textSpan = document.createElement('div');
|
|
171
|
+
textSpan.textContent = text;
|
|
172
|
+
body.appendChild(textSpan);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
msgEl.appendChild(header);
|
|
176
|
+
msgEl.appendChild(body);
|
|
177
|
+
chatMessages.appendChild(msgEl);
|
|
178
|
+
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
179
|
+
return body;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let activeAssistantBody = null;
|
|
183
|
+
let thinkingEl = null;
|
|
184
|
+
let currentRunningToolEl = null;
|
|
185
|
+
|
|
186
|
+
function renderToolStart(toolName) {
|
|
187
|
+
if (!chatMessages) return null;
|
|
188
|
+
const accordion = document.createElement('div');
|
|
189
|
+
accordion.className = 'tool-accordion running';
|
|
190
|
+
|
|
191
|
+
const header = document.createElement('div');
|
|
192
|
+
header.className = 'tool-accordion-header';
|
|
193
|
+
|
|
194
|
+
const left = document.createElement('div');
|
|
195
|
+
left.className = 'tool-accordion-left';
|
|
196
|
+
|
|
197
|
+
const icon = document.createElement('span');
|
|
198
|
+
icon.className = 'tool-accordion-icon';
|
|
199
|
+
icon.textContent = '⚡';
|
|
200
|
+
|
|
201
|
+
const name = document.createElement('span');
|
|
202
|
+
name.className = 'tool-accordion-name';
|
|
203
|
+
name.textContent = toolName;
|
|
204
|
+
|
|
205
|
+
left.appendChild(icon);
|
|
206
|
+
left.appendChild(name);
|
|
207
|
+
|
|
208
|
+
const right = document.createElement('div');
|
|
209
|
+
right.style.display = 'flex';
|
|
210
|
+
right.style.alignItems = 'center';
|
|
211
|
+
|
|
212
|
+
const status = document.createElement('span');
|
|
213
|
+
status.className = 'tool-accordion-status running';
|
|
214
|
+
status.textContent = 'RUNNING';
|
|
215
|
+
|
|
216
|
+
const chevron = document.createElement('span');
|
|
217
|
+
chevron.className = 'tool-accordion-chevron';
|
|
218
|
+
chevron.textContent = '▶';
|
|
219
|
+
|
|
220
|
+
right.appendChild(status);
|
|
221
|
+
right.appendChild(chevron);
|
|
222
|
+
|
|
223
|
+
header.appendChild(left);
|
|
224
|
+
header.appendChild(right);
|
|
225
|
+
|
|
226
|
+
const body = document.createElement('div');
|
|
227
|
+
body.className = 'tool-accordion-body hidden';
|
|
228
|
+
body.textContent = `Executing tool: ${toolName}...`;
|
|
229
|
+
|
|
230
|
+
header.addEventListener('click', () => {
|
|
231
|
+
accordion.classList.toggle('open');
|
|
232
|
+
body.classList.toggle('hidden');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
accordion.appendChild(header);
|
|
236
|
+
accordion.appendChild(body);
|
|
237
|
+
chatMessages.appendChild(accordion);
|
|
238
|
+
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
239
|
+
currentRunningToolEl = accordion;
|
|
240
|
+
return accordion;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function renderToolResult(toolName, resultSummary) {
|
|
244
|
+
let accordion = currentRunningToolEl;
|
|
245
|
+
if (!accordion) {
|
|
246
|
+
accordion = renderToolStart(toolName);
|
|
247
|
+
}
|
|
248
|
+
if (accordion) {
|
|
249
|
+
accordion.classList.remove('running');
|
|
250
|
+
accordion.classList.add('completed');
|
|
251
|
+
|
|
252
|
+
const status = accordion.querySelector('.tool-accordion-status');
|
|
253
|
+
if (status) {
|
|
254
|
+
status.className = 'tool-accordion-status completed';
|
|
255
|
+
status.textContent = 'COMPLETED';
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const body = accordion.querySelector('.tool-accordion-body');
|
|
259
|
+
if (body) {
|
|
260
|
+
body.textContent = resultSummary || `Tool ${toolName} execution finished.`;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
currentRunningToolEl = null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function showThinkingSpinner() {
|
|
267
|
+
removeThinkingSpinner();
|
|
268
|
+
thinkingEl = document.createElement('div');
|
|
269
|
+
thinkingEl.className = 'chat-msg thinking';
|
|
270
|
+
thinkingEl.innerHTML = `
|
|
271
|
+
<div class="thinking-dots"><span></span><span></span><span></span></div>
|
|
272
|
+
<span class="thinking-text">Daedalus is consulting the labyrinth...</span>
|
|
273
|
+
`;
|
|
274
|
+
chatMessages.appendChild(thinkingEl);
|
|
275
|
+
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function removeThinkingSpinner() {
|
|
279
|
+
if (thinkingEl && thinkingEl.parentNode) {
|
|
280
|
+
thinkingEl.parentNode.removeChild(thinkingEl);
|
|
281
|
+
}
|
|
282
|
+
thinkingEl = null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function updateThinkingSpinner(text) {
|
|
286
|
+
if (thinkingEl && chatMessages) {
|
|
287
|
+
const label = thinkingEl.querySelector('.thinking-text');
|
|
288
|
+
if (label) label.textContent = text;
|
|
289
|
+
chatMessages.appendChild(thinkingEl);
|
|
290
|
+
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const fileInput = document.getElementById('file-input');
|
|
295
|
+
const attachBtn = document.getElementById('attach-btn');
|
|
296
|
+
const attachmentPreviewEl = document.getElementById('attachment-preview');
|
|
297
|
+
const chatPanel = document.querySelector('.chat-panel');
|
|
298
|
+
|
|
299
|
+
let currentAttachment = null;
|
|
300
|
+
|
|
301
|
+
function setAttachment(attachment) {
|
|
302
|
+
currentAttachment = attachment;
|
|
303
|
+
if (!attachmentPreviewEl) return;
|
|
304
|
+
attachmentPreviewEl.innerHTML = '';
|
|
305
|
+
if (!attachment) {
|
|
306
|
+
attachmentPreviewEl.classList.add('hidden');
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
attachmentPreviewEl.classList.remove('hidden');
|
|
310
|
+
|
|
311
|
+
const chip = document.createElement('div');
|
|
312
|
+
chip.className = 'attachment-chip';
|
|
313
|
+
|
|
314
|
+
if (attachment.isImage && attachment.dataUrl) {
|
|
315
|
+
const thumb = document.createElement('img');
|
|
316
|
+
thumb.className = 'attachment-thumb';
|
|
317
|
+
thumb.src = attachment.dataUrl;
|
|
318
|
+
chip.appendChild(thumb);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const name = document.createElement('span');
|
|
322
|
+
name.className = 'attachment-name';
|
|
323
|
+
name.textContent = attachment.name;
|
|
324
|
+
chip.appendChild(name);
|
|
325
|
+
|
|
326
|
+
const removeBtn = document.createElement('button');
|
|
327
|
+
removeBtn.className = 'attachment-remove';
|
|
328
|
+
removeBtn.textContent = '×';
|
|
329
|
+
removeBtn.title = 'Remove attachment';
|
|
330
|
+
removeBtn.addEventListener('click', () => setAttachment(null));
|
|
331
|
+
chip.appendChild(removeBtn);
|
|
332
|
+
|
|
333
|
+
attachmentPreviewEl.appendChild(chip);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function processFile(file) {
|
|
337
|
+
if (!file) return;
|
|
338
|
+
const isImage = file.type.startsWith('image/');
|
|
339
|
+
const reader = new FileReader();
|
|
340
|
+
if (isImage) {
|
|
341
|
+
reader.onload = (e) => {
|
|
342
|
+
const dataUrl = e.target.result;
|
|
343
|
+
const base64 = typeof dataUrl === 'string' ? dataUrl.split(',')[1] : '';
|
|
344
|
+
setAttachment({
|
|
345
|
+
name: file.name || 'image.png',
|
|
346
|
+
isImage: true,
|
|
347
|
+
dataUrl: typeof dataUrl === 'string' ? dataUrl : '',
|
|
348
|
+
base64,
|
|
349
|
+
});
|
|
350
|
+
};
|
|
351
|
+
reader.readAsDataURL(file);
|
|
352
|
+
} else {
|
|
353
|
+
reader.onload = (e) => {
|
|
354
|
+
const textContent = typeof e.target.result === 'string' ? e.target.result : '';
|
|
355
|
+
setAttachment({
|
|
356
|
+
name: file.name,
|
|
357
|
+
isImage: false,
|
|
358
|
+
textContent,
|
|
359
|
+
});
|
|
360
|
+
};
|
|
361
|
+
reader.readAsText(file);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (attachBtn && fileInput) {
|
|
366
|
+
attachBtn.addEventListener('click', () => fileInput.click());
|
|
367
|
+
fileInput.addEventListener('change', (e) => {
|
|
368
|
+
if (e.target.files?.[0]) {
|
|
369
|
+
processFile(e.target.files[0]);
|
|
370
|
+
fileInput.value = '';
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
document.addEventListener('paste', (e) => {
|
|
376
|
+
if (!e.clipboardData?.items) return;
|
|
377
|
+
for (const item of e.clipboardData.items) {
|
|
378
|
+
if (item.type.startsWith('image/')) {
|
|
379
|
+
const file = item.getAsFile();
|
|
380
|
+
if (file) {
|
|
381
|
+
processFile(file);
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
if (chatPanel) {
|
|
389
|
+
chatPanel.addEventListener('dragover', (e) => {
|
|
390
|
+
e.preventDefault();
|
|
391
|
+
chatPanel.classList.add('drag-over');
|
|
392
|
+
});
|
|
393
|
+
chatPanel.addEventListener('dragleave', (e) => {
|
|
394
|
+
if (!chatPanel.contains(e.relatedTarget)) {
|
|
395
|
+
chatPanel.classList.remove('drag-over');
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
chatPanel.addEventListener('drop', (e) => {
|
|
399
|
+
e.preventDefault();
|
|
400
|
+
chatPanel.classList.remove('drag-over');
|
|
401
|
+
if (e.dataTransfer?.files?.[0]) {
|
|
402
|
+
processFile(e.dataTransfer.files[0]);
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const sentHistory = [];
|
|
408
|
+
let sentIndex = -1;
|
|
409
|
+
|
|
410
|
+
function autoResizeInput() {
|
|
411
|
+
if (!chatInput) return;
|
|
412
|
+
chatInput.style.height = 'auto';
|
|
413
|
+
chatInput.style.height = Math.min(chatInput.scrollHeight, 180) + 'px';
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (chatForm && chatInput) {
|
|
417
|
+
chatInput.addEventListener('input', autoResizeInput);
|
|
418
|
+
|
|
419
|
+
chatInput.addEventListener('keydown', (e) => {
|
|
420
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
421
|
+
e.preventDefault();
|
|
422
|
+
chatForm.dispatchEvent(new Event('submit', { cancelable: true }));
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (e.key === 'Enter' && e.shiftKey) {
|
|
427
|
+
requestAnimationFrame(autoResizeInput);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (e.key === 'Escape') {
|
|
432
|
+
chatInput.value = '';
|
|
433
|
+
chatInput.style.height = 'auto';
|
|
434
|
+
sentIndex = -1;
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (e.key === 'ArrowUp') {
|
|
439
|
+
if (chatInput.selectionStart === 0 && chatInput.selectionEnd === 0 && sentHistory.length > 0) {
|
|
440
|
+
if (sentIndex === -1) {
|
|
441
|
+
sentIndex = sentHistory.length - 1;
|
|
442
|
+
} else if (sentIndex > 0) {
|
|
443
|
+
sentIndex--;
|
|
444
|
+
}
|
|
445
|
+
chatInput.value = sentHistory[sentIndex] || '';
|
|
446
|
+
autoResizeInput();
|
|
447
|
+
e.preventDefault();
|
|
448
|
+
}
|
|
449
|
+
} else if (e.key === 'ArrowDown') {
|
|
450
|
+
if (sentIndex !== -1) {
|
|
451
|
+
if (sentIndex < sentHistory.length - 1) {
|
|
452
|
+
sentIndex++;
|
|
453
|
+
chatInput.value = sentHistory[sentIndex] || '';
|
|
454
|
+
} else {
|
|
455
|
+
sentIndex = -1;
|
|
456
|
+
chatInput.value = '';
|
|
457
|
+
}
|
|
458
|
+
autoResizeInput();
|
|
459
|
+
e.preventDefault();
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
chatForm.addEventListener('submit', async (e) => {
|
|
465
|
+
e.preventDefault();
|
|
466
|
+
const text = chatInput.value.trim();
|
|
467
|
+
if (!text && !currentAttachment) return;
|
|
468
|
+
|
|
469
|
+
let effectiveText = text;
|
|
470
|
+
if (currentAttachment && !currentAttachment.isImage && currentAttachment.textContent) {
|
|
471
|
+
effectiveText = effectiveText
|
|
472
|
+
? `${effectiveText}\n\n[Attached file: ${currentAttachment.name}]\n\`\`\`\n${currentAttachment.textContent}\n\`\`\``
|
|
473
|
+
: `[Attached file: ${currentAttachment.name}]\n\`\`\`\n${currentAttachment.textContent}\n\`\`\``;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const imageBase64 = currentAttachment?.isImage ? currentAttachment.base64 : undefined;
|
|
477
|
+
const attachmentSnap = currentAttachment;
|
|
478
|
+
setAttachment(null);
|
|
479
|
+
|
|
480
|
+
sentHistory.push(text || (attachmentSnap ? `[Attached ${attachmentSnap.name}]` : ''));
|
|
481
|
+
sentIndex = -1;
|
|
482
|
+
chatInput.value = '';
|
|
483
|
+
chatInput.style.height = 'auto';
|
|
484
|
+
if (chatStatusBadge) chatStatusBadge.textContent = 'THINKING...';
|
|
485
|
+
|
|
486
|
+
addChatMessage('user', effectiveText || 'Attached image', null, imageBase64);
|
|
487
|
+
activeAssistantBody = null;
|
|
488
|
+
showThinkingSpinner();
|
|
489
|
+
|
|
490
|
+
try {
|
|
491
|
+
const res = await fetch('/api/chat', {
|
|
492
|
+
method: 'POST',
|
|
493
|
+
headers: { 'Content-Type': 'application/json' },
|
|
494
|
+
body: JSON.stringify({ message: effectiveText, imageBase64 })
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
if (!res.ok) {
|
|
498
|
+
removeThinkingSpinner();
|
|
499
|
+
const errData = await res.json().catch(() => ({}));
|
|
500
|
+
addChatMessage('error', `Error: ${errData.error || res.statusText}`);
|
|
501
|
+
if (chatStatusBadge) chatStatusBadge.textContent = 'ERROR';
|
|
502
|
+
}
|
|
503
|
+
} catch (err) {
|
|
504
|
+
removeThinkingSpinner();
|
|
505
|
+
addChatMessage('error', `Network Error: ${err.message}`);
|
|
506
|
+
if (chatStatusBadge) chatStatusBadge.textContent = 'ERROR';
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Sidebar Tab Switching
|
|
512
|
+
document.querySelectorAll('.sidebar-tab').forEach(tab => {
|
|
513
|
+
tab.addEventListener('click', () => {
|
|
514
|
+
document.querySelectorAll('.sidebar-tab').forEach(t => t.classList.remove('active'));
|
|
515
|
+
document.querySelectorAll('.sidebar-tab-content').forEach(c => c.classList.add('hidden'));
|
|
516
|
+
tab.classList.add('active');
|
|
517
|
+
const tabName = tab.getAttribute('data-tab');
|
|
518
|
+
const target = document.getElementById('tab-' + tabName);
|
|
519
|
+
if (target) target.classList.remove('hidden');
|
|
520
|
+
if (tabName === 'files') loadFileTree();
|
|
521
|
+
if (tabName === 'context') loadContextFiles();
|
|
522
|
+
if (tabName === 'sessions') loadSessions();
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
// Cheat Sheet Click-to-Insert Handler
|
|
527
|
+
document.querySelectorAll('.cmd-chip').forEach(chip => {
|
|
528
|
+
chip.addEventListener('click', () => {
|
|
529
|
+
const cmd = chip.getAttribute('data-cmd');
|
|
530
|
+
if (!cmd || !chatInput) return;
|
|
531
|
+
chatInput.value = cmd;
|
|
532
|
+
chatInput.focus();
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
// File Tree
|
|
537
|
+
const fileTreeEl = document.getElementById('file-tree');
|
|
538
|
+
const fileTreeCwdEl = document.getElementById('file-tree-cwd');
|
|
539
|
+
const fileTreeRefreshBtn = document.getElementById('file-tree-refresh');
|
|
540
|
+
let fileTreeLoaded = false;
|
|
541
|
+
|
|
542
|
+
function fileIcon(node) {
|
|
543
|
+
if (node.type === 'dir') return '▶';
|
|
544
|
+
const ext = node.name.split('.').pop() || '';
|
|
545
|
+
const icons = { ts: '𝑇', js: '𝑱', json: '{}', md: '≡', css: '⌗', html: '◇', txt: '·', yml: '⚙', yaml: '⚙', sh: '$', py: '𝑃', go: 'G', rs: '⚙' };
|
|
546
|
+
return icons[ext] || '·';
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function renderTreeNodes(nodes, depth) {
|
|
550
|
+
const ul = document.createElement('ul');
|
|
551
|
+
ul.className = 'tree-list';
|
|
552
|
+
for (const node of nodes) {
|
|
553
|
+
const li = document.createElement('li');
|
|
554
|
+
li.className = 'tree-node';
|
|
555
|
+
li.style.paddingLeft = (depth * 12) + 'px';
|
|
556
|
+
|
|
557
|
+
if (node.type === 'dir') {
|
|
558
|
+
const toggle = document.createElement('span');
|
|
559
|
+
toggle.className = 'tree-icon tree-dir-icon';
|
|
560
|
+
toggle.textContent = fileIcon(node);
|
|
561
|
+
|
|
562
|
+
const label = document.createElement('span');
|
|
563
|
+
label.className = 'tree-label tree-dir';
|
|
564
|
+
label.textContent = node.name;
|
|
565
|
+
|
|
566
|
+
let childrenEl = null;
|
|
567
|
+
let expanded = false;
|
|
568
|
+
|
|
569
|
+
const expand = () => {
|
|
570
|
+
expanded = !expanded;
|
|
571
|
+
toggle.classList.toggle('expanded', expanded);
|
|
572
|
+
if (expanded && !childrenEl && node.children?.length) {
|
|
573
|
+
childrenEl = renderTreeNodes(node.children, 0);
|
|
574
|
+
li.appendChild(childrenEl);
|
|
575
|
+
}
|
|
576
|
+
if (childrenEl) childrenEl.classList.toggle('hidden', !expanded);
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
toggle.addEventListener('click', expand);
|
|
580
|
+
label.addEventListener('click', expand);
|
|
581
|
+
li.appendChild(toggle);
|
|
582
|
+
li.appendChild(label);
|
|
583
|
+
} else {
|
|
584
|
+
const icon = document.createElement('span');
|
|
585
|
+
icon.className = 'tree-icon tree-file-icon';
|
|
586
|
+
icon.textContent = fileIcon(node);
|
|
587
|
+
|
|
588
|
+
const label = document.createElement('span');
|
|
589
|
+
label.className = 'tree-label tree-file';
|
|
590
|
+
label.textContent = node.name;
|
|
591
|
+
label.title = node.path;
|
|
592
|
+
|
|
593
|
+
label.addEventListener('click', () => {
|
|
594
|
+
if (!chatInput) return;
|
|
595
|
+
const current = chatInput.value;
|
|
596
|
+
chatInput.value = current ? current + ' ' + node.path : node.path;
|
|
597
|
+
chatInput.focus();
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
li.appendChild(icon);
|
|
601
|
+
li.appendChild(label);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
ul.appendChild(li);
|
|
605
|
+
}
|
|
606
|
+
return ul;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
async function loadFileTree() {
|
|
610
|
+
if (!fileTreeEl) return;
|
|
611
|
+
if (fileTreeLoaded) return;
|
|
612
|
+
fileTreeEl.innerHTML = '<div class="file-tree-loading">Loading...</div>';
|
|
613
|
+
try {
|
|
614
|
+
const res = await fetch('/api/files');
|
|
615
|
+
if (!res.ok) throw new Error('Failed to load');
|
|
616
|
+
const data = await res.json();
|
|
617
|
+
if (fileTreeCwdEl) fileTreeCwdEl.textContent = data.cwd || '—';
|
|
618
|
+
fileTreeEl.innerHTML = '';
|
|
619
|
+
if (!data.tree?.length) {
|
|
620
|
+
fileTreeEl.innerHTML = '<div class="file-tree-loading">No files found.</div>';
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
fileTreeEl.appendChild(renderTreeNodes(data.tree, 0));
|
|
624
|
+
fileTreeLoaded = true;
|
|
625
|
+
} catch {
|
|
626
|
+
fileTreeEl.innerHTML = '<div class="file-tree-loading">Failed to load tree.</div>';
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (fileTreeRefreshBtn) {
|
|
631
|
+
fileTreeRefreshBtn.addEventListener('click', () => {
|
|
632
|
+
fileTreeLoaded = false;
|
|
633
|
+
loadFileTree();
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Active Context Files
|
|
638
|
+
const contextFilesListEl = document.getElementById('context-files-list');
|
|
639
|
+
const contextRefreshBtn = document.getElementById('context-refresh-btn');
|
|
640
|
+
|
|
641
|
+
async function loadContextFiles() {
|
|
642
|
+
if (!contextFilesListEl) return;
|
|
643
|
+
contextFilesListEl.innerHTML = '<div class="file-tree-loading">Loading context...</div>';
|
|
644
|
+
try {
|
|
645
|
+
const res = await fetch('/api/context');
|
|
646
|
+
if (!res.ok) throw new Error('Failed to load context');
|
|
647
|
+
const data = await res.json();
|
|
648
|
+
contextFilesListEl.innerHTML = '';
|
|
649
|
+
if (!data.files || data.files.length === 0) {
|
|
650
|
+
contextFilesListEl.innerHTML = '<div class="file-tree-loading">No active files in context.</div>';
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
data.files.forEach(file => {
|
|
654
|
+
const chip = document.createElement('div');
|
|
655
|
+
chip.className = 'context-file-chip';
|
|
656
|
+
|
|
657
|
+
const name = document.createElement('span');
|
|
658
|
+
name.className = 'context-file-name';
|
|
659
|
+
name.textContent = file;
|
|
660
|
+
name.title = `Click to insert ${file}`;
|
|
661
|
+
name.addEventListener('click', () => {
|
|
662
|
+
if (!chatInput) return;
|
|
663
|
+
const current = chatInput.value;
|
|
664
|
+
chatInput.value = current ? current + ' ' + file : file;
|
|
665
|
+
chatInput.focus();
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
const removeBtn = document.createElement('button');
|
|
669
|
+
removeBtn.className = 'context-file-remove';
|
|
670
|
+
removeBtn.textContent = '×';
|
|
671
|
+
removeBtn.title = `Remove ${file} from context`;
|
|
672
|
+
removeBtn.addEventListener('click', async (e) => {
|
|
673
|
+
e.stopPropagation();
|
|
674
|
+
try {
|
|
675
|
+
await fetch('/api/context', {
|
|
676
|
+
method: 'DELETE',
|
|
677
|
+
headers: { 'Content-Type': 'application/json' },
|
|
678
|
+
body: JSON.stringify({ file })
|
|
679
|
+
});
|
|
680
|
+
loadContextFiles();
|
|
681
|
+
} catch {}
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
chip.appendChild(name);
|
|
685
|
+
chip.appendChild(removeBtn);
|
|
686
|
+
contextFilesListEl.appendChild(chip);
|
|
687
|
+
});
|
|
688
|
+
} catch {
|
|
689
|
+
contextFilesListEl.innerHTML = '<div class="file-tree-loading">Failed to load context files.</div>';
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (contextRefreshBtn) {
|
|
694
|
+
contextRefreshBtn.addEventListener('click', () => {
|
|
695
|
+
loadContextFiles();
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Chat History Loading
|
|
700
|
+
let historyLoaded = false;
|
|
701
|
+
async function loadChatHistory() {
|
|
702
|
+
if (historyLoaded) return;
|
|
703
|
+
try {
|
|
704
|
+
const res = await fetch('/api/history');
|
|
705
|
+
if (!res.ok) return;
|
|
706
|
+
const data = await res.json();
|
|
707
|
+
if (data.history && Array.isArray(data.history)) {
|
|
708
|
+
const validHistory = data.history.filter(item => item.role === 'user' || item.role === 'assistant');
|
|
709
|
+
if (validHistory.length > 0) {
|
|
710
|
+
chatMessages.innerHTML = '';
|
|
711
|
+
validHistory.forEach(item => {
|
|
712
|
+
addChatMessage(item.role === 'assistant' ? 'assistant' : 'user', item.text, null, null, item.timestamp);
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
historyLoaded = true;
|
|
717
|
+
} catch {}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
let reconnectAttempts = 0;
|
|
721
|
+
|
|
28
722
|
function connectSSE() {
|
|
29
723
|
const eventSource = new EventSource('/telemetry');
|
|
30
724
|
|
|
31
725
|
eventSource.onopen = () => {
|
|
726
|
+
reconnectAttempts = 0;
|
|
32
727
|
if (statusEl) {
|
|
33
728
|
statusEl.className = 'status connected';
|
|
34
729
|
statusEl.textContent = '● CONNECTED';
|
|
35
730
|
}
|
|
36
731
|
addLog('Connected to Daedalus telemetry stream.');
|
|
732
|
+
loadChatHistory();
|
|
37
733
|
};
|
|
38
734
|
|
|
39
735
|
eventSource.onmessage = (event) => {
|
|
@@ -47,16 +743,67 @@ function connectSSE() {
|
|
|
47
743
|
return;
|
|
48
744
|
}
|
|
49
745
|
|
|
746
|
+
if (data.type === 'chat_token') {
|
|
747
|
+
if (data.role === 'user') {
|
|
748
|
+
// already added locally or by another client
|
|
749
|
+
} else {
|
|
750
|
+
removeThinkingSpinner();
|
|
751
|
+
if (!activeAssistantBody) {
|
|
752
|
+
activeAssistantBody = addChatMessage('assistant', data.text || '', 'STREAM', null, data.timestamp);
|
|
753
|
+
} else if (data.text) {
|
|
754
|
+
const raw = (activeAssistantBody.dataset.raw || '') + data.text;
|
|
755
|
+
activeAssistantBody.dataset.raw = raw;
|
|
756
|
+
activeAssistantBody.innerHTML = renderMarkdown(raw);
|
|
757
|
+
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
758
|
+
}
|
|
759
|
+
if (chatStatusBadge) chatStatusBadge.textContent = 'STREAMING...';
|
|
760
|
+
}
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
if (data.type === 'chat_tool_start') {
|
|
765
|
+
addLog(`⚡ Agent tool started: <strong>${data.tool}</strong>`);
|
|
766
|
+
if (chatStatusBadge) chatStatusBadge.textContent = `TOOL: ${data.tool}`;
|
|
767
|
+
updateThinkingSpinner(`Running: ${data.tool}...`);
|
|
768
|
+
renderToolStart(data.tool);
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (data.type === 'chat_tool_result') {
|
|
773
|
+
addLog(`✔ Agent tool finished: <strong>${data.tool}</strong>`);
|
|
774
|
+
updateThinkingSpinner('Daedalus is processing results...');
|
|
775
|
+
renderToolResult(data.tool, data.content || data.result);
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
if (data.type === 'chat_done') {
|
|
780
|
+
removeThinkingSpinner();
|
|
781
|
+
if (activeAssistantBody && activeAssistantBody.dataset.raw) {
|
|
782
|
+
activeAssistantBody.innerHTML = renderMarkdown(activeAssistantBody.dataset.raw);
|
|
783
|
+
}
|
|
784
|
+
activeAssistantBody = null;
|
|
785
|
+
if (chatStatusBadge) chatStatusBadge.textContent = 'READY';
|
|
786
|
+
addLog('Chat completion finished.');
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
if (data.type === 'chat_error') {
|
|
791
|
+
removeThinkingSpinner();
|
|
792
|
+
addChatMessage('error', `Execution error: ${data.content}`, null, null, data.timestamp);
|
|
793
|
+
if (chatStatusBadge) chatStatusBadge.textContent = 'ERROR';
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
|
|
50
797
|
if (data.metric && metrics[data.metric]) {
|
|
51
|
-
metrics[data.metric].value.textContent = data.value;
|
|
798
|
+
metrics[data.metric].value.textContent = data.value + '%';
|
|
52
799
|
metrics[data.metric].status.textContent = data.value > 80 ? 'HIGH' : (data.value > 40 ? 'BUSY' : 'NORMAL');
|
|
53
800
|
addLog(`Received metric update: <strong>${data.metric.toUpperCase()}</strong> = ${data.value}%`);
|
|
54
801
|
} else if (data.cpu !== undefined || data.memory !== undefined) {
|
|
55
802
|
if (data.cpu !== undefined && metrics.cpu) {
|
|
56
|
-
metrics.cpu.value.textContent = data.cpu;
|
|
803
|
+
metrics.cpu.value.textContent = data.cpu + '%';
|
|
57
804
|
}
|
|
58
805
|
if (data.memory !== undefined && metrics.memory) {
|
|
59
|
-
metrics.memory.value.textContent = data.memory;
|
|
806
|
+
metrics.memory.value.textContent = data.memory + '%';
|
|
60
807
|
}
|
|
61
808
|
addLog(`Telemetry update: CPU=${data.cpu || '--'}%, MEM=${data.memory || '--'}%`);
|
|
62
809
|
}
|
|
@@ -66,13 +813,266 @@ function connectSSE() {
|
|
|
66
813
|
};
|
|
67
814
|
|
|
68
815
|
eventSource.onerror = () => {
|
|
816
|
+
reconnectAttempts++;
|
|
817
|
+
const delay = Math.min(16000, Math.pow(2, reconnectAttempts - 1) * 1000);
|
|
69
818
|
if (statusEl) {
|
|
70
|
-
statusEl.className = 'status
|
|
71
|
-
statusEl.textContent =
|
|
819
|
+
statusEl.className = 'status reconnecting';
|
|
820
|
+
statusEl.textContent = `● RECONNECTING (${Math.round(delay / 1000)}s)...`;
|
|
72
821
|
}
|
|
73
822
|
eventSource.close();
|
|
74
|
-
setTimeout(connectSSE,
|
|
823
|
+
setTimeout(connectSSE, delay);
|
|
75
824
|
};
|
|
76
825
|
}
|
|
77
826
|
|
|
827
|
+
// Chronicles (Saved Sessions) Management
|
|
828
|
+
const sessionsListEl = document.getElementById('sessions-list');
|
|
829
|
+
const sessionNewBtn = document.getElementById('session-new-btn');
|
|
830
|
+
const sessionRefreshBtn = document.getElementById('session-refresh-btn');
|
|
831
|
+
|
|
832
|
+
async function loadSessions() {
|
|
833
|
+
if (!sessionsListEl) return;
|
|
834
|
+
sessionsListEl.innerHTML = '<div class="file-tree-loading">Accessing session archives...</div>';
|
|
835
|
+
try {
|
|
836
|
+
const res = await fetch('/api/sessions');
|
|
837
|
+
if (!res.ok) throw new Error('Failed to load sessions');
|
|
838
|
+
const data = await res.json();
|
|
839
|
+
sessionsListEl.innerHTML = '';
|
|
840
|
+
if (!data.sessions || data.sessions.length === 0) {
|
|
841
|
+
sessionsListEl.innerHTML = '<div class="file-tree-loading">No saved sessions found.</div>';
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
data.sessions.forEach(sess => {
|
|
846
|
+
const card = document.createElement('div');
|
|
847
|
+
card.className = 'session-card';
|
|
848
|
+
|
|
849
|
+
const header = document.createElement('div');
|
|
850
|
+
header.className = 'session-card-header';
|
|
851
|
+
|
|
852
|
+
const title = document.createElement('span');
|
|
853
|
+
title.className = 'session-card-title';
|
|
854
|
+
title.textContent = sess.title || sess.id.slice(0, 16);
|
|
855
|
+
title.title = sess.title || sess.id;
|
|
856
|
+
|
|
857
|
+
const date = document.createElement('span');
|
|
858
|
+
date.className = 'session-card-date';
|
|
859
|
+
date.textContent = new Date(sess.updated_at || sess.created_at).toLocaleDateString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
|
860
|
+
|
|
861
|
+
header.appendChild(title);
|
|
862
|
+
header.appendChild(date);
|
|
863
|
+
|
|
864
|
+
const meta = document.createElement('div');
|
|
865
|
+
meta.className = 'session-card-meta';
|
|
866
|
+
|
|
867
|
+
const turns = document.createElement('span');
|
|
868
|
+
turns.className = 'session-card-date';
|
|
869
|
+
turns.textContent = `${sess.turns_count || 0} turns`;
|
|
870
|
+
|
|
871
|
+
const actions = document.createElement('div');
|
|
872
|
+
actions.className = 'session-actions';
|
|
873
|
+
|
|
874
|
+
const resumeBtn = document.createElement('button');
|
|
875
|
+
resumeBtn.className = 'session-btn';
|
|
876
|
+
resumeBtn.textContent = 'RESUME';
|
|
877
|
+
resumeBtn.title = 'Resume this session';
|
|
878
|
+
resumeBtn.addEventListener('click', async (e) => {
|
|
879
|
+
e.stopPropagation();
|
|
880
|
+
resumeBtn.textContent = '...';
|
|
881
|
+
try {
|
|
882
|
+
const r = await fetch('/api/sessions/resume', {
|
|
883
|
+
method: 'POST',
|
|
884
|
+
headers: { 'Content-Type': 'application/json' },
|
|
885
|
+
body: JSON.stringify({ sessionId: sess.id }),
|
|
886
|
+
});
|
|
887
|
+
if (r.ok) {
|
|
888
|
+
historyLoaded = false;
|
|
889
|
+
await loadChatHistory();
|
|
890
|
+
addLog(`Resumed session: <strong>${sess.title || sess.id}</strong>`);
|
|
891
|
+
loadSessions();
|
|
892
|
+
}
|
|
893
|
+
} catch {
|
|
894
|
+
resumeBtn.textContent = 'RESUME';
|
|
895
|
+
}
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
const delBtn = document.createElement('button');
|
|
899
|
+
delBtn.className = 'session-btn delete';
|
|
900
|
+
delBtn.textContent = 'DEL';
|
|
901
|
+
delBtn.title = 'Delete session';
|
|
902
|
+
delBtn.addEventListener('click', async (e) => {
|
|
903
|
+
e.stopPropagation();
|
|
904
|
+
if (!confirm(`Delete session "${sess.title || sess.id}"?`)) return;
|
|
905
|
+
try {
|
|
906
|
+
await fetch('/api/sessions', {
|
|
907
|
+
method: 'DELETE',
|
|
908
|
+
headers: { 'Content-Type': 'application/json' },
|
|
909
|
+
body: JSON.stringify({ sessionId: sess.id }),
|
|
910
|
+
});
|
|
911
|
+
loadSessions();
|
|
912
|
+
} catch {}
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
actions.appendChild(resumeBtn);
|
|
916
|
+
actions.appendChild(delBtn);
|
|
917
|
+
|
|
918
|
+
meta.appendChild(turns);
|
|
919
|
+
meta.appendChild(actions);
|
|
920
|
+
|
|
921
|
+
card.appendChild(header);
|
|
922
|
+
card.appendChild(meta);
|
|
923
|
+
sessionsListEl.appendChild(card);
|
|
924
|
+
});
|
|
925
|
+
} catch {
|
|
926
|
+
sessionsListEl.innerHTML = '<div class="file-tree-loading">Failed to load session archives.</div>';
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
if (sessionNewBtn) {
|
|
931
|
+
sessionNewBtn.addEventListener('click', async () => {
|
|
932
|
+
try {
|
|
933
|
+
const res = await fetch('/api/sessions/new', { method: 'POST' });
|
|
934
|
+
if (res.ok) {
|
|
935
|
+
historyLoaded = false;
|
|
936
|
+
chatMessages.innerHTML = '';
|
|
937
|
+
addChatMessage('assistant', 'New session initialized. Sanctum is ready for instructions.', 'ARCHITECT');
|
|
938
|
+
loadSessions();
|
|
939
|
+
loadContextFiles();
|
|
940
|
+
}
|
|
941
|
+
} catch {}
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
if (sessionRefreshBtn) {
|
|
946
|
+
sessionRefreshBtn.addEventListener('click', () => loadSessions());
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// New Chat / New Rite Button Handler
|
|
950
|
+
const newChatBtn = document.getElementById('new-chat-btn');
|
|
951
|
+
if (newChatBtn) {
|
|
952
|
+
newChatBtn.addEventListener('click', async () => {
|
|
953
|
+
chatMessages.innerHTML = '';
|
|
954
|
+
activeAssistantBody = null;
|
|
955
|
+
addChatMessage('assistant', 'Sanctum console refreshed. Ready for consultation.', 'ARCHITECT');
|
|
956
|
+
if (chatStatusBadge) chatStatusBadge.textContent = 'SANCTUM READY';
|
|
957
|
+
try {
|
|
958
|
+
await fetch('/api/sessions/new', { method: 'POST' });
|
|
959
|
+
loadSessions();
|
|
960
|
+
loadContextFiles();
|
|
961
|
+
} catch {}
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// Model Switcher Modal & Active Model Badge
|
|
966
|
+
const activeModelBadge = document.getElementById('active-model-badge');
|
|
967
|
+
const modelLabel = document.getElementById('model-label');
|
|
968
|
+
const modelModal = document.getElementById('model-modal');
|
|
969
|
+
const modelModalClose = document.getElementById('model-modal-close');
|
|
970
|
+
const modelOptionsList = document.getElementById('model-options-list');
|
|
971
|
+
|
|
972
|
+
const DEFAULT_MODELS = [
|
|
973
|
+
{ id: 'auto', name: 'Auto / Smart Router', desc: 'Dynamically routes based on task complexity and health' },
|
|
974
|
+
{ id: 'claude-3-5-sonnet', name: 'Claude 3.5 Sonnet', desc: 'Anthropic — Superior reasoning & code generation' },
|
|
975
|
+
{ id: 'gpt-4o', name: 'GPT-4o', desc: 'OpenAI — High-speed multimodal intelligence' },
|
|
976
|
+
{ id: 'gemini-1.5-pro', name: 'Gemini 1.5 Pro', desc: 'Google — Vast context window & deep analysis' },
|
|
977
|
+
{ id: 'gemini-1.5-flash', name: 'Gemini 1.5 Flash', desc: 'Google — Ultra-low latency responses' },
|
|
978
|
+
{ id: 'deepseek-chat', name: 'DeepSeek V3', desc: 'DeepSeek — High-efficiency coding & logic' },
|
|
979
|
+
{ id: 'ollama/llama3.1', name: 'Llama 3.1 / Local', desc: 'Local-first offline execution via Ollama' },
|
|
980
|
+
];
|
|
981
|
+
|
|
982
|
+
async function loadModels() {
|
|
983
|
+
try {
|
|
984
|
+
const res = await fetch('/api/models');
|
|
985
|
+
if (!res.ok) return;
|
|
986
|
+
const data = await res.json();
|
|
987
|
+
const currentModel = data.activeModel || 'auto';
|
|
988
|
+
if (modelLabel) {
|
|
989
|
+
modelLabel.textContent = `MODEL: ${currentModel.toUpperCase()}`;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
if (modelOptionsList) {
|
|
993
|
+
const models = (data.availableModels && data.availableModels.length > 0)
|
|
994
|
+
? data.availableModels
|
|
995
|
+
: DEFAULT_MODELS;
|
|
996
|
+
|
|
997
|
+
modelOptionsList.innerHTML = '';
|
|
998
|
+
models.forEach(m => {
|
|
999
|
+
const card = document.createElement('div');
|
|
1000
|
+
card.className = `model-option-card ${m.id === currentModel ? 'active' : ''}`;
|
|
1001
|
+
|
|
1002
|
+
const header = document.createElement('div');
|
|
1003
|
+
header.className = 'model-option-header';
|
|
1004
|
+
|
|
1005
|
+
const name = document.createElement('span');
|
|
1006
|
+
name.className = 'model-option-name';
|
|
1007
|
+
name.textContent = m.name || m.id;
|
|
1008
|
+
|
|
1009
|
+
const badge = document.createElement('span');
|
|
1010
|
+
badge.className = 'model-option-badge';
|
|
1011
|
+
badge.textContent = m.provider ? m.provider.toUpperCase() : (m.id === currentModel ? 'ACTIVE' : 'SELECT');
|
|
1012
|
+
|
|
1013
|
+
header.appendChild(name);
|
|
1014
|
+
header.appendChild(badge);
|
|
1015
|
+
|
|
1016
|
+
const desc = document.createElement('div');
|
|
1017
|
+
desc.className = 'model-option-desc';
|
|
1018
|
+
desc.textContent = m.desc || `Switch active engine to ${m.id}`;
|
|
1019
|
+
|
|
1020
|
+
card.appendChild(header);
|
|
1021
|
+
card.appendChild(desc);
|
|
1022
|
+
|
|
1023
|
+
card.addEventListener('click', async () => {
|
|
1024
|
+
try {
|
|
1025
|
+
await fetch('/api/models/switch', {
|
|
1026
|
+
method: 'POST',
|
|
1027
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1028
|
+
body: JSON.stringify({ model: m.id })
|
|
1029
|
+
});
|
|
1030
|
+
if (modelLabel) modelLabel.textContent = `MODEL: ${m.id.toUpperCase()}`;
|
|
1031
|
+
addLog(`Switched active model to <strong>${m.name || m.id}</strong>`);
|
|
1032
|
+
closeModelModal();
|
|
1033
|
+
loadModels();
|
|
1034
|
+
} catch {}
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
modelOptionsList.appendChild(card);
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
} catch {}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function openModelModal() {
|
|
1044
|
+
if (modelModal) {
|
|
1045
|
+
modelModal.classList.remove('hidden');
|
|
1046
|
+
loadModels();
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
function closeModelModal() {
|
|
1051
|
+
if (modelModal) {
|
|
1052
|
+
modelModal.classList.add('hidden');
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
if (activeModelBadge) {
|
|
1057
|
+
activeModelBadge.addEventListener('click', openModelModal);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
if (modelModalClose) {
|
|
1061
|
+
modelModalClose.addEventListener('click', closeModelModal);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
if (modelModal) {
|
|
1065
|
+
modelModal.addEventListener('click', (e) => {
|
|
1066
|
+
if (e.target === modelModal) closeModelModal();
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
|
|
78
1070
|
connectSSE();
|
|
1071
|
+
loadModels();
|
|
1072
|
+
|
|
1073
|
+
setInterval(() => {
|
|
1074
|
+
if (lastUpdateEl) {
|
|
1075
|
+
lastUpdateEl.textContent = new Date().toLocaleTimeString();
|
|
1076
|
+
}
|
|
1077
|
+
}, 1000);
|
|
1078
|
+
|