agentgui 1.0.130 → 1.0.138
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 +46 -0
- package/bin/gmgui.cjs +1 -1
- package/database.js +11 -2
- package/package.json +2 -1
- package/server.js +77 -10
- package/setup-npm-token.sh +68 -0
- package/static/app.js +5 -3
- package/static/index.html +245 -3
- package/static/js/client.js +66 -8
- package/static/js/conversations.js +34 -6
- package/static/js/features.js +17 -17
- package/static/js/streaming-renderer.js +48 -24
- package/static/js/syntax-highlighter.js +1 -1
- package/static/js/voice.js +430 -0
- package/static/styles.css +86 -0
|
@@ -432,18 +432,24 @@ class StreamingRenderer {
|
|
|
432
432
|
|
|
433
433
|
const code = block.code || '';
|
|
434
434
|
const language = (block.language || 'plaintext').toLowerCase();
|
|
435
|
+
const lineCount = code.split('\n').length;
|
|
435
436
|
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
437
|
+
const details = document.createElement('details');
|
|
438
|
+
details.className = 'collapsible-code';
|
|
439
|
+
|
|
440
|
+
const summary = document.createElement('summary');
|
|
441
|
+
summary.className = 'collapsible-code-summary';
|
|
442
|
+
summary.innerHTML = `
|
|
443
|
+
<span class="collapsible-code-label">${this.escapeHtml(language)} - ${lineCount} line${lineCount !== 1 ? 's' : ''}</span>
|
|
440
444
|
<button class="copy-code-btn" title="Copy code">
|
|
441
445
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
|
|
442
446
|
</button>
|
|
443
447
|
`;
|
|
444
448
|
|
|
445
|
-
const copyBtn =
|
|
446
|
-
copyBtn.addEventListener('click', () => {
|
|
449
|
+
const copyBtn = summary.querySelector('.copy-code-btn');
|
|
450
|
+
copyBtn.addEventListener('click', (e) => {
|
|
451
|
+
e.preventDefault();
|
|
452
|
+
e.stopPropagation();
|
|
447
453
|
navigator.clipboard.writeText(code).then(() => {
|
|
448
454
|
const orig = copyBtn.innerHTML;
|
|
449
455
|
copyBtn.innerHTML = '<svg viewBox="0 0 20 20" fill="#34d399"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>';
|
|
@@ -451,13 +457,18 @@ class StreamingRenderer {
|
|
|
451
457
|
});
|
|
452
458
|
});
|
|
453
459
|
|
|
454
|
-
|
|
455
|
-
const highlightedHTML = StreamingRenderer.renderCodeWithHighlight(code, this.escapeHtml.bind(this));
|
|
460
|
+
const preStyle = "background:#1e293b;padding:1rem;border-radius:0 0 0.375rem 0.375rem;overflow-x:auto;font-family:'Monaco','Menlo','Ubuntu Mono',monospace;font-size:0.875rem;line-height:1.6;color:#e2e8f0;border:1px solid #334155;border-top:none;margin:0";
|
|
456
461
|
const codeContainer = document.createElement('div');
|
|
457
|
-
|
|
462
|
+
if (typeof hljs !== 'undefined') {
|
|
463
|
+
const result = hljs.highlightAuto(code);
|
|
464
|
+
codeContainer.innerHTML = `<pre style="${preStyle}"><code class="hljs">${result.value}</code></pre>`;
|
|
465
|
+
} else {
|
|
466
|
+
codeContainer.innerHTML = `<pre style="${preStyle}"><code>${this.escapeHtml(code)}</code></pre>`;
|
|
467
|
+
}
|
|
458
468
|
|
|
459
|
-
|
|
460
|
-
|
|
469
|
+
details.appendChild(summary);
|
|
470
|
+
details.appendChild(codeContainer);
|
|
471
|
+
div.appendChild(details);
|
|
461
472
|
|
|
462
473
|
return div;
|
|
463
474
|
}
|
|
@@ -991,12 +1002,18 @@ class StreamingRenderer {
|
|
|
991
1002
|
* Render code with basic syntax highlighting
|
|
992
1003
|
*/
|
|
993
1004
|
static renderCodeWithHighlight(code, esc) {
|
|
994
|
-
const preStyle = "background:#1e293b;padding:1rem;border-radius:0.375rem;overflow-x:auto;font-family:'Monaco','Menlo','Ubuntu Mono',monospace;font-size:0.875rem;line-height:1.6;color:#e2e8f0;border:1px solid #334155";
|
|
1005
|
+
const preStyle = "background:#1e293b;padding:1rem;border-radius:0 0 0.375rem 0.375rem;overflow-x:auto;font-family:'Monaco','Menlo','Ubuntu Mono',monospace;font-size:0.875rem;line-height:1.6;color:#e2e8f0;border:1px solid #334155;border-top:none;margin:0";
|
|
1006
|
+
const lineCount = code.split('\n').length;
|
|
1007
|
+
const lang = (typeof hljs !== 'undefined') ? (hljs.highlightAuto(code).language || 'code') : 'code';
|
|
1008
|
+
const summaryLabel = `${lang} - ${lineCount} line${lineCount !== 1 ? 's' : ''}`;
|
|
1009
|
+
let codeHtml;
|
|
995
1010
|
if (typeof hljs !== 'undefined') {
|
|
996
1011
|
const result = hljs.highlightAuto(code);
|
|
997
|
-
|
|
1012
|
+
codeHtml = `<pre style="${preStyle}"><code class="hljs">${result.value}</code></pre>`;
|
|
1013
|
+
} else {
|
|
1014
|
+
codeHtml = `<pre style="${preStyle}">${esc(code)}</pre>`;
|
|
998
1015
|
}
|
|
999
|
-
return `<
|
|
1016
|
+
return `<details class="collapsible-code"><summary class="collapsible-code-summary">${summaryLabel}</summary>${codeHtml}</details>`;
|
|
1000
1017
|
}
|
|
1001
1018
|
|
|
1002
1019
|
/**
|
|
@@ -1521,16 +1538,19 @@ class StreamingRenderer {
|
|
|
1521
1538
|
<div class="html-content bg-white dark:bg-gray-800 p-4 overflow-x-auto">${part.code}</div>
|
|
1522
1539
|
</div>`;
|
|
1523
1540
|
} else {
|
|
1541
|
+
const partLineCount = part.code.split('\n').length;
|
|
1524
1542
|
html += `<div class="mb-3 rounded-lg overflow-hidden border border-gray-200 dark:border-gray-800">
|
|
1525
|
-
<
|
|
1526
|
-
<
|
|
1527
|
-
|
|
1528
|
-
<
|
|
1529
|
-
<
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1543
|
+
<details class="collapsible-code">
|
|
1544
|
+
<summary class="collapsible-code-summary">
|
|
1545
|
+
<span>${this.escapeHtml(part.language)} - ${partLineCount} line${partLineCount !== 1 ? 's' : ''}</span>
|
|
1546
|
+
<button class="copy-code-btn text-gray-400 hover:text-gray-200 transition-colors p-1 rounded hover:bg-gray-800" title="Copy code" onclick="event.preventDefault();event.stopPropagation();navigator.clipboard.writeText(this.closest('.collapsible-code').querySelector('code').textContent)">
|
|
1547
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
1548
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
|
|
1549
|
+
</svg>
|
|
1550
|
+
</button>
|
|
1551
|
+
</summary>
|
|
1552
|
+
<pre class="bg-gray-900 text-gray-100 p-4 overflow-x-auto" style="margin:0;border-radius:0 0 0.375rem 0.375rem"><code class="language-${this.escapeHtml(part.language)}">${this.escapeHtml(part.code)}</code></pre>
|
|
1553
|
+
</details>
|
|
1534
1554
|
</div>`;
|
|
1535
1555
|
}
|
|
1536
1556
|
}
|
|
@@ -1578,8 +1598,12 @@ class StreamingRenderer {
|
|
|
1578
1598
|
</div>
|
|
1579
1599
|
`;
|
|
1580
1600
|
} else {
|
|
1601
|
+
const codeLineCount = code.split('\n').length;
|
|
1581
1602
|
div.innerHTML = `
|
|
1582
|
-
<
|
|
1603
|
+
<details class="collapsible-code">
|
|
1604
|
+
<summary class="collapsible-code-summary">${this.escapeHtml(language)} - ${codeLineCount} line${codeLineCount !== 1 ? 's' : ''}</summary>
|
|
1605
|
+
<pre class="bg-gray-900 text-gray-100 p-4 overflow-x-auto" style="margin:0;border-radius:0 0 0.375rem 0.375rem"><code class="language-${this.escapeHtml(language)}">${this.escapeHtml(code)}</code></pre>
|
|
1606
|
+
</details>
|
|
1583
1607
|
`;
|
|
1584
1608
|
}
|
|
1585
1609
|
return div;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
class SyntaxHighlighter {
|
|
7
7
|
constructor(config = {}) {
|
|
8
8
|
this.config = {
|
|
9
|
-
cdnUrl: config.cdnUrl || 'https://
|
|
9
|
+
cdnUrl: config.cdnUrl || 'https://cdn.jsdelivr.net/npm/prismjs@1.29.0',
|
|
10
10
|
lazyLoad: config.lazyLoad !== false,
|
|
11
11
|
enableCache: config.enableCache !== false,
|
|
12
12
|
maxCacheSize: config.maxCacheSize || 500,
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import { STT, TTS } from 'webtalk-sdk';
|
|
2
|
+
|
|
3
|
+
(function() {
|
|
4
|
+
const BASE = window.__BASE_URL || '';
|
|
5
|
+
let stt = null;
|
|
6
|
+
let tts = null;
|
|
7
|
+
let isRecording = false;
|
|
8
|
+
let ttsEnabled = true;
|
|
9
|
+
let voiceActive = false;
|
|
10
|
+
let lastSpokenBlockIndex = -1;
|
|
11
|
+
let currentConversationId = null;
|
|
12
|
+
let sttReady = false;
|
|
13
|
+
let ttsReady = false;
|
|
14
|
+
let speechQueue = [];
|
|
15
|
+
let isSpeaking = false;
|
|
16
|
+
|
|
17
|
+
async function init() {
|
|
18
|
+
setupTTSToggle();
|
|
19
|
+
setupUI();
|
|
20
|
+
setupStreamingListener();
|
|
21
|
+
setupAgentSelector();
|
|
22
|
+
initSTT();
|
|
23
|
+
initTTS();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
var sttLoadPhase = 'starting';
|
|
27
|
+
|
|
28
|
+
async function initSTT() {
|
|
29
|
+
try {
|
|
30
|
+
stt = new STT({
|
|
31
|
+
basePath: BASE + '/webtalk',
|
|
32
|
+
onTranscript: function(text) {
|
|
33
|
+
var el = document.getElementById('voiceTranscript');
|
|
34
|
+
if (el) {
|
|
35
|
+
el.textContent = text;
|
|
36
|
+
el.setAttribute('data-final', text);
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
onPartial: function(text) {
|
|
40
|
+
var el = document.getElementById('voiceTranscript');
|
|
41
|
+
if (el) {
|
|
42
|
+
var existing = el.getAttribute('data-final') || '';
|
|
43
|
+
el.textContent = existing + text;
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
onStatus: function(status) {
|
|
47
|
+
var micBtn = document.getElementById('voiceMicBtn');
|
|
48
|
+
if (!micBtn) return;
|
|
49
|
+
if (status === 'recording') {
|
|
50
|
+
micBtn.classList.add('recording');
|
|
51
|
+
} else {
|
|
52
|
+
micBtn.classList.remove('recording');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
var origInit = stt.init.bind(stt);
|
|
57
|
+
var initPromise = new Promise(function(resolve, reject) {
|
|
58
|
+
origInit().then(resolve).catch(reject);
|
|
59
|
+
if (stt.worker) {
|
|
60
|
+
var origHandler = stt.worker.onmessage;
|
|
61
|
+
stt.worker.onmessage = function(e) {
|
|
62
|
+
var msg = e.data;
|
|
63
|
+
if (msg && msg.status) {
|
|
64
|
+
if (msg.status === 'progress' || msg.status === 'download') {
|
|
65
|
+
if (sttLoadPhase !== 'downloading') {
|
|
66
|
+
sttLoadPhase = 'downloading';
|
|
67
|
+
updateMicState();
|
|
68
|
+
}
|
|
69
|
+
} else if (msg.status === 'done' && msg.file && msg.file.endsWith('.onnx')) {
|
|
70
|
+
sttLoadPhase = 'compiling';
|
|
71
|
+
updateMicState();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (origHandler) origHandler.call(stt.worker, e);
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
await initPromise;
|
|
79
|
+
sttReady = true;
|
|
80
|
+
updateMicState();
|
|
81
|
+
} catch (e) {
|
|
82
|
+
console.warn('STT init failed:', e.message);
|
|
83
|
+
sttLoadPhase = 'failed';
|
|
84
|
+
updateMicState();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function updateMicState() {
|
|
89
|
+
var micBtn = document.getElementById('voiceMicBtn');
|
|
90
|
+
if (!micBtn) return;
|
|
91
|
+
if (sttReady) {
|
|
92
|
+
micBtn.removeAttribute('disabled');
|
|
93
|
+
micBtn.title = 'Click to record';
|
|
94
|
+
micBtn.classList.remove('loading');
|
|
95
|
+
} else if (sttLoadPhase === 'failed') {
|
|
96
|
+
micBtn.setAttribute('disabled', 'true');
|
|
97
|
+
micBtn.title = 'Speech recognition failed to load';
|
|
98
|
+
micBtn.classList.remove('loading');
|
|
99
|
+
} else {
|
|
100
|
+
micBtn.setAttribute('disabled', 'true');
|
|
101
|
+
micBtn.classList.add('loading');
|
|
102
|
+
if (sttLoadPhase === 'downloading') {
|
|
103
|
+
micBtn.title = 'Downloading speech models...';
|
|
104
|
+
} else if (sttLoadPhase === 'compiling') {
|
|
105
|
+
micBtn.title = 'Compiling speech models (may take a minute)...';
|
|
106
|
+
} else {
|
|
107
|
+
micBtn.title = 'Loading speech recognition...';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function initTTS() {
|
|
113
|
+
try {
|
|
114
|
+
tts = new TTS({
|
|
115
|
+
basePath: BASE + '/webtalk',
|
|
116
|
+
apiBasePath: BASE,
|
|
117
|
+
onStatus: function() {},
|
|
118
|
+
onAudioReady: function(url) {
|
|
119
|
+
var audio = new Audio(url);
|
|
120
|
+
audio.onended = function() {
|
|
121
|
+
isSpeaking = false;
|
|
122
|
+
processQueue();
|
|
123
|
+
};
|
|
124
|
+
audio.onerror = function() {
|
|
125
|
+
isSpeaking = false;
|
|
126
|
+
processQueue();
|
|
127
|
+
};
|
|
128
|
+
audio.play().catch(function() {
|
|
129
|
+
isSpeaking = false;
|
|
130
|
+
processQueue();
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
await tts.init();
|
|
135
|
+
ttsReady = true;
|
|
136
|
+
} catch (e) {
|
|
137
|
+
console.warn('TTS init failed:', e.message);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function setupAgentSelector() {
|
|
142
|
+
var voiceSelector = document.querySelector('[data-voice-agent-selector]');
|
|
143
|
+
if (!voiceSelector) return;
|
|
144
|
+
var mainSelector = document.querySelector('[data-agent-selector]');
|
|
145
|
+
if (mainSelector) {
|
|
146
|
+
voiceSelector.innerHTML = mainSelector.innerHTML;
|
|
147
|
+
voiceSelector.value = mainSelector.value;
|
|
148
|
+
mainSelector.addEventListener('change', function() {
|
|
149
|
+
voiceSelector.value = mainSelector.value;
|
|
150
|
+
});
|
|
151
|
+
voiceSelector.addEventListener('change', function() {
|
|
152
|
+
mainSelector.value = voiceSelector.value;
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function setupTTSToggle() {
|
|
158
|
+
var toggle = document.getElementById('voiceTTSToggle');
|
|
159
|
+
if (toggle) {
|
|
160
|
+
var saved = localStorage.getItem('voice-tts-enabled');
|
|
161
|
+
if (saved !== null) {
|
|
162
|
+
ttsEnabled = saved === 'true';
|
|
163
|
+
toggle.checked = ttsEnabled;
|
|
164
|
+
}
|
|
165
|
+
toggle.addEventListener('change', function() {
|
|
166
|
+
ttsEnabled = toggle.checked;
|
|
167
|
+
localStorage.setItem('voice-tts-enabled', ttsEnabled);
|
|
168
|
+
if (!ttsEnabled) stopSpeaking();
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
var stopBtn = document.getElementById('voiceStopSpeaking');
|
|
172
|
+
if (stopBtn) {
|
|
173
|
+
stopBtn.addEventListener('click', stopSpeaking);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function setupUI() {
|
|
178
|
+
var micBtn = document.getElementById('voiceMicBtn');
|
|
179
|
+
if (micBtn) {
|
|
180
|
+
micBtn.addEventListener('click', function(e) {
|
|
181
|
+
e.preventDefault();
|
|
182
|
+
if (!isRecording) {
|
|
183
|
+
startRecording();
|
|
184
|
+
} else {
|
|
185
|
+
stopRecording();
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
var sendBtn = document.getElementById('voiceSendBtn');
|
|
190
|
+
if (sendBtn) {
|
|
191
|
+
sendBtn.addEventListener('click', sendVoiceMessage);
|
|
192
|
+
}
|
|
193
|
+
updateMicState();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function startRecording() {
|
|
197
|
+
if (isRecording) return;
|
|
198
|
+
var el = document.getElementById('voiceTranscript');
|
|
199
|
+
if (!stt || !sttReady) {
|
|
200
|
+
if (el) el.textContent = 'Speech recognition still loading, please wait...';
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (el) {
|
|
204
|
+
el.textContent = '';
|
|
205
|
+
el.setAttribute('data-final', '');
|
|
206
|
+
}
|
|
207
|
+
isRecording = true;
|
|
208
|
+
try {
|
|
209
|
+
await stt.startRecording();
|
|
210
|
+
} catch (e) {
|
|
211
|
+
isRecording = false;
|
|
212
|
+
if (el) el.textContent = 'Mic access denied or unavailable: ' + e.message;
|
|
213
|
+
console.warn('Recording start failed:', e.message);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function stopRecording() {
|
|
218
|
+
if (!stt || !isRecording) return;
|
|
219
|
+
isRecording = false;
|
|
220
|
+
try {
|
|
221
|
+
await stt.stopRecording();
|
|
222
|
+
} catch (e) {}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function sendVoiceMessage() {
|
|
226
|
+
var el = document.getElementById('voiceTranscript');
|
|
227
|
+
if (!el) return;
|
|
228
|
+
var text = el.textContent.trim();
|
|
229
|
+
if (!text) return;
|
|
230
|
+
addVoiceBlock(text, true);
|
|
231
|
+
el.textContent = '';
|
|
232
|
+
el.setAttribute('data-final', '');
|
|
233
|
+
if (typeof agentGUIClient !== 'undefined' && agentGUIClient) {
|
|
234
|
+
var input = agentGUIClient.ui.messageInput;
|
|
235
|
+
if (input) {
|
|
236
|
+
input.value = text;
|
|
237
|
+
agentGUIClient.startExecution();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function speak(text) {
|
|
243
|
+
if (!ttsEnabled || !tts || !ttsReady) return;
|
|
244
|
+
var clean = text.replace(/<[^>]*>/g, '').trim();
|
|
245
|
+
if (!clean) return;
|
|
246
|
+
speechQueue.push(clean);
|
|
247
|
+
processQueue();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function processQueue() {
|
|
251
|
+
if (isSpeaking || speechQueue.length === 0) return;
|
|
252
|
+
isSpeaking = true;
|
|
253
|
+
var text = speechQueue.shift();
|
|
254
|
+
tts.generate(text).catch(function() {
|
|
255
|
+
isSpeaking = false;
|
|
256
|
+
processQueue();
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function stopSpeaking() {
|
|
261
|
+
speechQueue = [];
|
|
262
|
+
isSpeaking = false;
|
|
263
|
+
if (tts) tts.stop();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function addVoiceBlock(text, isUser) {
|
|
267
|
+
var container = document.getElementById('voiceMessages');
|
|
268
|
+
if (!container) return;
|
|
269
|
+
var emptyMsg = container.querySelector('.voice-empty');
|
|
270
|
+
if (emptyMsg) emptyMsg.remove();
|
|
271
|
+
var div = document.createElement('div');
|
|
272
|
+
div.className = 'voice-block' + (isUser ? ' voice-block-user' : '');
|
|
273
|
+
div.textContent = text;
|
|
274
|
+
container.appendChild(div);
|
|
275
|
+
scrollVoiceToBottom();
|
|
276
|
+
return div;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function addVoiceResultBlock(block) {
|
|
280
|
+
var container = document.getElementById('voiceMessages');
|
|
281
|
+
if (!container) return;
|
|
282
|
+
var emptyMsg = container.querySelector('.voice-empty');
|
|
283
|
+
if (emptyMsg) emptyMsg.remove();
|
|
284
|
+
var div = document.createElement('div');
|
|
285
|
+
div.className = 'voice-block';
|
|
286
|
+
var isError = block.is_error || false;
|
|
287
|
+
var duration = block.duration_ms ? (block.duration_ms / 1000).toFixed(1) + 's' : '';
|
|
288
|
+
var cost = block.total_cost_usd ? '$' + block.total_cost_usd.toFixed(4) : '';
|
|
289
|
+
var resultText = '';
|
|
290
|
+
if (block.result) {
|
|
291
|
+
resultText = typeof block.result === 'string' ? block.result : JSON.stringify(block.result);
|
|
292
|
+
}
|
|
293
|
+
var html = '';
|
|
294
|
+
if (resultText) {
|
|
295
|
+
html += '<div>' + escapeHtml(resultText) + '</div>';
|
|
296
|
+
}
|
|
297
|
+
if (duration || cost) {
|
|
298
|
+
html += '<div class="voice-result-stats">';
|
|
299
|
+
if (duration) html += duration;
|
|
300
|
+
if (duration && cost) html += ' | ';
|
|
301
|
+
if (cost) html += cost;
|
|
302
|
+
html += '</div>';
|
|
303
|
+
}
|
|
304
|
+
if (!html) {
|
|
305
|
+
html = isError ? 'Execution failed' : 'Execution complete';
|
|
306
|
+
}
|
|
307
|
+
div.innerHTML = html;
|
|
308
|
+
container.appendChild(div);
|
|
309
|
+
scrollVoiceToBottom();
|
|
310
|
+
if (ttsEnabled && resultText) {
|
|
311
|
+
speak(resultText);
|
|
312
|
+
}
|
|
313
|
+
return div;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function scrollVoiceToBottom() {
|
|
317
|
+
var scroll = document.getElementById('voiceScroll');
|
|
318
|
+
if (scroll) {
|
|
319
|
+
requestAnimationFrame(function() {
|
|
320
|
+
scroll.scrollTop = scroll.scrollHeight;
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function setupStreamingListener() {
|
|
326
|
+
window.addEventListener('ws-message', function(e) {
|
|
327
|
+
if (!voiceActive) return;
|
|
328
|
+
var data = e.detail;
|
|
329
|
+
if (!data) return;
|
|
330
|
+
if (data.type === 'streaming_progress' && data.block) {
|
|
331
|
+
handleVoiceBlock(data.block);
|
|
332
|
+
}
|
|
333
|
+
if (data.type === 'streaming_start') {
|
|
334
|
+
lastSpokenBlockIndex = -1;
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
window.addEventListener('conversation-selected', function(e) {
|
|
338
|
+
currentConversationId = e.detail.conversationId;
|
|
339
|
+
if (voiceActive) {
|
|
340
|
+
loadVoiceBlocks(currentConversationId);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function handleVoiceBlock(block) {
|
|
346
|
+
if (!block || !block.type) return;
|
|
347
|
+
if (block.type === 'text' && block.text) {
|
|
348
|
+
var div = addVoiceBlock(block.text, false);
|
|
349
|
+
if (div && ttsEnabled) {
|
|
350
|
+
div.classList.add('speaking');
|
|
351
|
+
speak(block.text);
|
|
352
|
+
setTimeout(function() { div.classList.remove('speaking'); }, 2000);
|
|
353
|
+
}
|
|
354
|
+
} else if (block.type === 'result') {
|
|
355
|
+
addVoiceResultBlock(block);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function loadVoiceBlocks(conversationId) {
|
|
360
|
+
var container = document.getElementById('voiceMessages');
|
|
361
|
+
if (!container) return;
|
|
362
|
+
container.innerHTML = '';
|
|
363
|
+
if (!conversationId) {
|
|
364
|
+
showVoiceEmpty(container);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
fetch(BASE + '/api/conversations/' + conversationId + '/chunks')
|
|
368
|
+
.then(function(res) { return res.json(); })
|
|
369
|
+
.then(function(data) {
|
|
370
|
+
if (!data.ok || !Array.isArray(data.chunks) || data.chunks.length === 0) {
|
|
371
|
+
showVoiceEmpty(container);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
var hasContent = false;
|
|
375
|
+
data.chunks.forEach(function(chunk) {
|
|
376
|
+
var block = typeof chunk.data === 'string' ? JSON.parse(chunk.data) : chunk.data;
|
|
377
|
+
if (!block) return;
|
|
378
|
+
if (block.type === 'text' && block.text) {
|
|
379
|
+
addVoiceBlock(block.text, false);
|
|
380
|
+
hasContent = true;
|
|
381
|
+
} else if (block.type === 'result') {
|
|
382
|
+
addVoiceResultBlock(block);
|
|
383
|
+
hasContent = true;
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
if (!hasContent) showVoiceEmpty(container);
|
|
387
|
+
})
|
|
388
|
+
.catch(function() {
|
|
389
|
+
showVoiceEmpty(container);
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function showVoiceEmpty(container) {
|
|
394
|
+
container.innerHTML = '<div class="voice-empty"><div class="voice-empty-icon"><svg viewBox="0 0 24 24" width="64" height="64" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg></div><div>Tap the microphone and speak to send a message.<br>Responses will be read aloud.</div></div>';
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function activate() {
|
|
398
|
+
voiceActive = true;
|
|
399
|
+
if (currentConversationId) {
|
|
400
|
+
loadVoiceBlocks(currentConversationId);
|
|
401
|
+
} else {
|
|
402
|
+
var container = document.getElementById('voiceMessages');
|
|
403
|
+
if (container && !container.hasChildNodes()) {
|
|
404
|
+
showVoiceEmpty(container);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function deactivate() {
|
|
410
|
+
voiceActive = false;
|
|
411
|
+
stopSpeaking();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function escapeHtml(text) {
|
|
415
|
+
var map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
|
416
|
+
return text.replace(/[&<>"']/g, function(c) { return map[c]; });
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
window.voiceModule = {
|
|
420
|
+
activate: activate,
|
|
421
|
+
deactivate: deactivate,
|
|
422
|
+
handleBlock: handleVoiceBlock
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
if (document.readyState === 'loading') {
|
|
426
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
427
|
+
} else {
|
|
428
|
+
init();
|
|
429
|
+
}
|
|
430
|
+
})();
|
package/static/styles.css
CHANGED
|
@@ -825,7 +825,93 @@ html, body {
|
|
|
825
825
|
color: var(--text-secondary);
|
|
826
826
|
}
|
|
827
827
|
|
|
828
|
+
.collapsible-code {
|
|
829
|
+
border-radius: 0.375rem;
|
|
830
|
+
overflow: hidden;
|
|
831
|
+
margin: 0.5rem 0;
|
|
832
|
+
border: 1px solid #334155;
|
|
833
|
+
background: #1e293b;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
.collapsible-code-summary {
|
|
837
|
+
display: flex;
|
|
838
|
+
align-items: center;
|
|
839
|
+
justify-content: space-between;
|
|
840
|
+
padding: 0.5rem 1rem;
|
|
841
|
+
background: #0f172a;
|
|
842
|
+
color: #94a3b8;
|
|
843
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
844
|
+
font-size: 0.75rem;
|
|
845
|
+
font-weight: 600;
|
|
846
|
+
text-transform: uppercase;
|
|
847
|
+
letter-spacing: 0.05em;
|
|
848
|
+
cursor: pointer;
|
|
849
|
+
user-select: none;
|
|
850
|
+
list-style: none;
|
|
851
|
+
border-bottom: 1px solid #334155;
|
|
852
|
+
transition: background 0.15s ease;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
.collapsible-code-summary::-webkit-details-marker {
|
|
856
|
+
display: none;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
.collapsible-code-summary::before {
|
|
860
|
+
content: '';
|
|
861
|
+
display: inline-block;
|
|
862
|
+
width: 0;
|
|
863
|
+
height: 0;
|
|
864
|
+
border-style: solid;
|
|
865
|
+
border-width: 5px 0 5px 8px;
|
|
866
|
+
border-color: transparent transparent transparent #64748b;
|
|
867
|
+
margin-right: 0.5rem;
|
|
868
|
+
transition: transform 0.15s ease;
|
|
869
|
+
flex-shrink: 0;
|
|
870
|
+
}
|
|
828
871
|
|
|
872
|
+
.collapsible-code[open] > .collapsible-code-summary::before {
|
|
873
|
+
transform: rotate(90deg);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
.collapsible-code-summary:hover {
|
|
877
|
+
background: #1e293b;
|
|
878
|
+
color: #e2e8f0;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
.collapsible-code-summary:hover::before {
|
|
882
|
+
border-left-color: #e2e8f0;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
.collapsible-code-summary .copy-code-btn {
|
|
886
|
+
background: none;
|
|
887
|
+
border: none;
|
|
888
|
+
color: inherit;
|
|
889
|
+
cursor: pointer;
|
|
890
|
+
padding: 0.25rem;
|
|
891
|
+
border-radius: 0.25rem;
|
|
892
|
+
display: flex;
|
|
893
|
+
align-items: center;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
.collapsible-code-summary .copy-code-btn:hover {
|
|
897
|
+
color: #e2e8f0;
|
|
898
|
+
background: rgba(255, 255, 255, 0.1);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
.collapsible-code > pre {
|
|
902
|
+
margin: 0;
|
|
903
|
+
border-radius: 0 0 0.375rem 0.375rem;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
.collapsible-code > div > pre {
|
|
907
|
+
margin: 0;
|
|
908
|
+
border-radius: 0 0 0.375rem 0.375rem;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
.collapsible-code .code-block {
|
|
912
|
+
margin: 0;
|
|
913
|
+
border-radius: 0 0 0.375rem 0.375rem;
|
|
914
|
+
}
|
|
829
915
|
|
|
830
916
|
.image-block {
|
|
831
917
|
flex: 0 1 100%;
|