agentgui 1.0.146 → 1.0.147
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/lib/speech.js +5 -1
- package/package.json +1 -1
- package/server.js +3 -3
- package/static/index.html +20 -0
- package/static/js/client.js +18 -10
- package/static/js/streaming-renderer.js +21 -3
- package/static/js/voice.js +97 -23
package/lib/speech.js
CHANGED
|
@@ -167,7 +167,11 @@ async function transcribe(audioBuffer) {
|
|
|
167
167
|
const decoded = decodeWavToFloat32(buf);
|
|
168
168
|
audio = resampleTo16k(decoded.audio, decoded.sampleRate);
|
|
169
169
|
} else {
|
|
170
|
-
|
|
170
|
+
const sampleCount = Math.floor(buf.byteLength / 4);
|
|
171
|
+
if (sampleCount === 0) throw new Error('Audio buffer too small');
|
|
172
|
+
const aligned = new ArrayBuffer(sampleCount * 4);
|
|
173
|
+
new Uint8Array(aligned).set(buf.subarray(0, sampleCount * 4));
|
|
174
|
+
audio = new Float32Array(aligned);
|
|
171
175
|
}
|
|
172
176
|
const result = await stt(audio);
|
|
173
177
|
return result.text || '';
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -15,7 +15,7 @@ const express = require('express');
|
|
|
15
15
|
const Busboy = require('busboy');
|
|
16
16
|
const fsbrowse = require('fsbrowse');
|
|
17
17
|
|
|
18
|
-
const SYSTEM_PROMPT = `
|
|
18
|
+
const SYSTEM_PROMPT = `Write all responses as clean semantic HTML. Use tags like <h3>, <p>, <ul>, <li>, <ol>, <table>, <code>, <pre>, <strong>, <em>, <a>, <blockquote>, <details>, <summary>. Your HTML will be rendered directly in a styled container that already provides fonts, colors, spacing, and dark mode support. Do not include <html>, <head>, <body>, <style>, or <script> tags. Do not use inline styles unless necessary for layout like tables. Do not use CSS class names. Just write semantic HTML content.`;
|
|
19
19
|
|
|
20
20
|
const activeExecutions = new Map();
|
|
21
21
|
const messageQueues = new Map();
|
|
@@ -522,7 +522,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
522
522
|
const mimeTypes = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml' };
|
|
523
523
|
const contentType = mimeTypes[ext] || 'application/octet-stream';
|
|
524
524
|
const fileContent = fs.readFileSync(normalizedPath);
|
|
525
|
-
res.writeHead(200, { 'Content-Type': contentType, 'Cache-Control': '
|
|
525
|
+
res.writeHead(200, { 'Content-Type': contentType, 'Cache-Control': 'no-cache' });
|
|
526
526
|
res.end(fileContent);
|
|
527
527
|
} catch (err) {
|
|
528
528
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
@@ -574,7 +574,7 @@ function serveFile(filePath, res) {
|
|
|
574
574
|
res.writeHead(200, {
|
|
575
575
|
'Content-Type': contentType,
|
|
576
576
|
'Content-Length': stats.size,
|
|
577
|
-
'Cache-Control': '
|
|
577
|
+
'Cache-Control': 'no-cache, must-revalidate'
|
|
578
578
|
});
|
|
579
579
|
fs.createReadStream(filePath).pipe(res);
|
|
580
580
|
});
|
package/static/index.html
CHANGED
|
@@ -1106,6 +1106,26 @@
|
|
|
1106
1106
|
border-top: 1px solid var(--color-border);
|
|
1107
1107
|
}
|
|
1108
1108
|
|
|
1109
|
+
.voice-reread-btn {
|
|
1110
|
+
position: absolute;
|
|
1111
|
+
top: 0.5rem;
|
|
1112
|
+
right: 0.5rem;
|
|
1113
|
+
background: none;
|
|
1114
|
+
border: 1px solid var(--color-border);
|
|
1115
|
+
border-radius: 0.25rem;
|
|
1116
|
+
cursor: pointer;
|
|
1117
|
+
padding: 0.25rem;
|
|
1118
|
+
color: var(--color-text-secondary);
|
|
1119
|
+
opacity: 0;
|
|
1120
|
+
transition: opacity 0.15s, background-color 0.15s;
|
|
1121
|
+
display: flex;
|
|
1122
|
+
align-items: center;
|
|
1123
|
+
justify-content: center;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
.voice-block:hover .voice-reread-btn { opacity: 1; }
|
|
1127
|
+
.voice-reread-btn:hover { background: var(--color-bg-primary); color: var(--color-primary); }
|
|
1128
|
+
|
|
1109
1129
|
/* ===== RESPONSIVE: TABLET ===== */
|
|
1110
1130
|
@media (min-width: 769px) and (max-width: 1024px) {
|
|
1111
1131
|
:root { --sidebar-width: 260px; }
|
package/static/js/client.js
CHANGED
|
@@ -516,7 +516,7 @@ class AgentGUIClient {
|
|
|
516
516
|
if (block.type === 'text' && block.text) {
|
|
517
517
|
const text = block.text;
|
|
518
518
|
if (this.isHtmlContent(text)) {
|
|
519
|
-
return `<div class="html-content bg-white dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 overflow-x-auto">${text}</div>`;
|
|
519
|
+
return `<div class="html-content bg-white dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 overflow-x-auto">${this.sanitizeHtml(text)}</div>`;
|
|
520
520
|
}
|
|
521
521
|
const parts = this.parseMarkdownCodeBlocks(text);
|
|
522
522
|
if (parts.length === 1 && parts[0].type === 'text') {
|
|
@@ -524,7 +524,7 @@ class AgentGUIClient {
|
|
|
524
524
|
}
|
|
525
525
|
return parts.map(part => {
|
|
526
526
|
if (part.type === 'html') {
|
|
527
|
-
return `<div class="html-content bg-white dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 overflow-x-auto">${part.content}</div>`;
|
|
527
|
+
return `<div class="html-content bg-white dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 overflow-x-auto">${this.sanitizeHtml(part.content)}</div>`;
|
|
528
528
|
} else if (part.type === 'code') {
|
|
529
529
|
return this.renderCodeBlock(part.language, part.code);
|
|
530
530
|
}
|
|
@@ -682,9 +682,17 @@ class AgentGUIClient {
|
|
|
682
682
|
}
|
|
683
683
|
|
|
684
684
|
isHtmlContent(text) {
|
|
685
|
-
const
|
|
686
|
-
|
|
687
|
-
|
|
685
|
+
const htmlPattern = /<(?:div|table|section|article|ul|ol|dl|nav|header|footer|main|aside|figure|details|summary|h[1-6]|p|blockquote|pre|code|span|strong|em|a|img|br|hr|li|td|tr|th|thead|tbody|tfoot)\b[^>]*>/i;
|
|
686
|
+
return htmlPattern.test(text);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
sanitizeHtml(html) {
|
|
690
|
+
const dangerous = /<\s*\/?\s*(script|iframe|object|embed|applet|form|input|button|select|textarea)\b[^>]*>/gi;
|
|
691
|
+
let cleaned = html.replace(dangerous, '');
|
|
692
|
+
cleaned = cleaned.replace(/\s+on\w+\s*=\s*["'][^"']*["']/gi, '');
|
|
693
|
+
cleaned = cleaned.replace(/\s+on\w+\s*=\s*[^\s>]+/gi, '');
|
|
694
|
+
cleaned = cleaned.replace(/javascript\s*:/gi, '');
|
|
695
|
+
return cleaned;
|
|
688
696
|
}
|
|
689
697
|
|
|
690
698
|
parseMarkdownCodeBlocks(text) {
|
|
@@ -735,7 +743,7 @@ class AgentGUIClient {
|
|
|
735
743
|
Rendered HTML
|
|
736
744
|
</div>
|
|
737
745
|
<div class="html-content bg-white dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 overflow-x-auto">
|
|
738
|
-
${code}
|
|
746
|
+
${this.sanitizeHtml(code)}
|
|
739
747
|
</div>
|
|
740
748
|
</div>
|
|
741
749
|
`;
|
|
@@ -751,7 +759,7 @@ class AgentGUIClient {
|
|
|
751
759
|
renderMessageContent(content) {
|
|
752
760
|
if (typeof content === 'string') {
|
|
753
761
|
if (this.isHtmlContent(content)) {
|
|
754
|
-
return `<div class="message-text"><div class="html-content bg-white dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 overflow-x-auto">${content}</div></div>`;
|
|
762
|
+
return `<div class="message-text"><div class="html-content bg-white dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 overflow-x-auto">${this.sanitizeHtml(content)}</div></div>`;
|
|
755
763
|
}
|
|
756
764
|
return `<div class="message-text">${this.escapeHtml(content)}</div>`;
|
|
757
765
|
} else if (content && typeof content === 'object' && content.type === 'claude_execution') {
|
|
@@ -762,7 +770,7 @@ class AgentGUIClient {
|
|
|
762
770
|
const parts = this.parseMarkdownCodeBlocks(block.text);
|
|
763
771
|
parts.forEach(part => {
|
|
764
772
|
if (part.type === 'html') {
|
|
765
|
-
html += `<div class="message-text"><div class="html-content bg-white dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 overflow-x-auto">${part.content}</div></div>`;
|
|
773
|
+
html += `<div class="message-text"><div class="html-content bg-white dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 overflow-x-auto">${this.sanitizeHtml(part.content)}</div></div>`;
|
|
766
774
|
} else if (part.type === 'text') {
|
|
767
775
|
html += `<div class="message-text">${this.escapeHtml(part.content)}</div>`;
|
|
768
776
|
} else if (part.type === 'code') {
|
|
@@ -778,7 +786,7 @@ class AgentGUIClient {
|
|
|
778
786
|
Rendered HTML
|
|
779
787
|
</div>
|
|
780
788
|
<div class="html-content bg-white dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 overflow-x-auto">
|
|
781
|
-
${block.code}
|
|
789
|
+
${this.sanitizeHtml(block.code)}
|
|
782
790
|
</div>
|
|
783
791
|
</div>
|
|
784
792
|
`;
|
|
@@ -1399,7 +1407,7 @@ class AgentGUIClient {
|
|
|
1399
1407
|
|
|
1400
1408
|
if (typeof msg.content === 'string') {
|
|
1401
1409
|
if (this.isHtmlContent(msg.content)) {
|
|
1402
|
-
contentHtml = `<div class="message-text"><div class="html-content bg-white dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 overflow-x-auto">${msg.content}</div></div>`;
|
|
1410
|
+
contentHtml = `<div class="message-text"><div class="html-content bg-white dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 overflow-x-auto">${this.sanitizeHtml(msg.content)}</div></div>`;
|
|
1403
1411
|
} else {
|
|
1404
1412
|
contentHtml = `<div class="message-text">${this.escapeHtml(msg.content)}</div>`;
|
|
1405
1413
|
}
|
|
@@ -362,12 +362,30 @@ class StreamingRenderer {
|
|
|
362
362
|
div.className = 'block-text';
|
|
363
363
|
|
|
364
364
|
const text = block.text || '';
|
|
365
|
-
|
|
366
|
-
|
|
365
|
+
if (this.containsHtmlTags(text)) {
|
|
366
|
+
div.innerHTML = this.sanitizeHtml(text);
|
|
367
|
+
div.classList.add('html-content');
|
|
368
|
+
} else {
|
|
369
|
+
div.innerHTML = this.parseAndRenderMarkdown(text);
|
|
370
|
+
}
|
|
367
371
|
|
|
368
372
|
return div;
|
|
369
373
|
}
|
|
370
374
|
|
|
375
|
+
containsHtmlTags(text) {
|
|
376
|
+
const htmlPattern = /<(?:div|table|section|article|ul|ol|dl|nav|header|footer|main|aside|figure|details|summary|h[1-6]|p|blockquote|pre|code|span|strong|em|a|img|br|hr|li|td|tr|th|thead|tbody|tfoot)\b[^>]*>/i;
|
|
377
|
+
return htmlPattern.test(text);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
sanitizeHtml(html) {
|
|
381
|
+
const dangerous = /<\s*\/?\s*(script|iframe|object|embed|applet|form|input|button|select|textarea)\b[^>]*>/gi;
|
|
382
|
+
let cleaned = html.replace(dangerous, '');
|
|
383
|
+
cleaned = cleaned.replace(/\s+on\w+\s*=\s*["'][^"']*["']/gi, '');
|
|
384
|
+
cleaned = cleaned.replace(/\s+on\w+\s*=\s*[^\s>]+/gi, '');
|
|
385
|
+
cleaned = cleaned.replace(/javascript\s*:/gi, '');
|
|
386
|
+
return cleaned;
|
|
387
|
+
}
|
|
388
|
+
|
|
371
389
|
/**
|
|
372
390
|
* Parse markdown and render links, code, bold, italic
|
|
373
391
|
*/
|
|
@@ -1259,7 +1277,7 @@ class StreamingRenderer {
|
|
|
1259
1277
|
${cost ? `<div class="result-stat"><span class="stat-icon">💰</span><span class="stat-value">${this.escapeHtml(cost)}</span><span class="stat-label">cost</span></div>` : ''}
|
|
1260
1278
|
${turns ? `<div class="result-stat"><span class="stat-icon">🔄</span><span class="stat-value">${this.escapeHtml(String(turns))}</span><span class="stat-label">turns</span></div>` : ''}
|
|
1261
1279
|
</div>
|
|
1262
|
-
${block.result ? `<div class="result-content">${
|
|
1280
|
+
${block.result ? `<div class="result-content">${(() => { const r = typeof block.result === 'string' ? block.result : JSON.stringify(block.result, null, 2); return this.containsHtmlTags(r) ? '<div class="html-content">' + this.sanitizeHtml(r) + '</div>' : this.escapeHtml(r); })()}</div>` : ''}
|
|
1263
1281
|
`;
|
|
1264
1282
|
|
|
1265
1283
|
return div;
|
package/static/js/voice.js
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
var isRecording = false;
|
|
4
4
|
var ttsEnabled = true;
|
|
5
5
|
var voiceActive = false;
|
|
6
|
-
var lastSpokenBlockIndex = -1;
|
|
7
6
|
var currentConversationId = null;
|
|
8
7
|
var speechQueue = [];
|
|
9
8
|
var isSpeaking = false;
|
|
@@ -13,6 +12,8 @@
|
|
|
13
12
|
var scriptNode = null;
|
|
14
13
|
var recordedChunks = [];
|
|
15
14
|
var TARGET_SAMPLE_RATE = 16000;
|
|
15
|
+
var spokenChunks = new Set();
|
|
16
|
+
var isLoadingHistory = false;
|
|
16
17
|
|
|
17
18
|
function init() {
|
|
18
19
|
setupTTSToggle();
|
|
@@ -61,14 +62,28 @@
|
|
|
61
62
|
var micBtn = document.getElementById('voiceMicBtn');
|
|
62
63
|
if (micBtn) {
|
|
63
64
|
micBtn.removeAttribute('disabled');
|
|
64
|
-
micBtn.title = '
|
|
65
|
-
micBtn.addEventListener('
|
|
65
|
+
micBtn.title = 'Hold to record';
|
|
66
|
+
micBtn.addEventListener('mousedown', function(e) {
|
|
66
67
|
e.preventDefault();
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
startRecording();
|
|
69
|
+
});
|
|
70
|
+
micBtn.addEventListener('mouseup', function(e) {
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
stopRecording();
|
|
73
|
+
});
|
|
74
|
+
micBtn.addEventListener('mouseleave', function(e) {
|
|
75
|
+
if (isRecording) stopRecording();
|
|
76
|
+
});
|
|
77
|
+
micBtn.addEventListener('touchstart', function(e) {
|
|
78
|
+
e.preventDefault();
|
|
79
|
+
startRecording();
|
|
80
|
+
});
|
|
81
|
+
micBtn.addEventListener('touchend', function(e) {
|
|
82
|
+
e.preventDefault();
|
|
83
|
+
stopRecording();
|
|
84
|
+
});
|
|
85
|
+
micBtn.addEventListener('touchcancel', function(e) {
|
|
86
|
+
if (isRecording) stopRecording();
|
|
72
87
|
});
|
|
73
88
|
}
|
|
74
89
|
var sendBtn = document.getElementById('voiceSendBtn');
|
|
@@ -92,6 +107,35 @@
|
|
|
92
107
|
return result;
|
|
93
108
|
}
|
|
94
109
|
|
|
110
|
+
function encodeWav(float32Audio, sampleRate) {
|
|
111
|
+
var numSamples = float32Audio.length;
|
|
112
|
+
var bytesPerSample = 2;
|
|
113
|
+
var dataSize = numSamples * bytesPerSample;
|
|
114
|
+
var buffer = new ArrayBuffer(44 + dataSize);
|
|
115
|
+
var view = new DataView(buffer);
|
|
116
|
+
function writeStr(off, str) {
|
|
117
|
+
for (var i = 0; i < str.length; i++) view.setUint8(off + i, str.charCodeAt(i));
|
|
118
|
+
}
|
|
119
|
+
writeStr(0, 'RIFF');
|
|
120
|
+
view.setUint32(4, 36 + dataSize, true);
|
|
121
|
+
writeStr(8, 'WAVE');
|
|
122
|
+
writeStr(12, 'fmt ');
|
|
123
|
+
view.setUint32(16, 16, true);
|
|
124
|
+
view.setUint16(20, 1, true);
|
|
125
|
+
view.setUint16(22, 1, true);
|
|
126
|
+
view.setUint32(24, sampleRate, true);
|
|
127
|
+
view.setUint32(28, sampleRate * bytesPerSample, true);
|
|
128
|
+
view.setUint16(32, bytesPerSample, true);
|
|
129
|
+
view.setUint16(34, 16, true);
|
|
130
|
+
writeStr(36, 'data');
|
|
131
|
+
view.setUint32(40, dataSize, true);
|
|
132
|
+
for (var i = 0; i < numSamples; i++) {
|
|
133
|
+
var s = Math.max(-1, Math.min(1, float32Audio[i]));
|
|
134
|
+
view.setInt16(44 + i * 2, s < 0 ? s * 32768 : s * 32767, true);
|
|
135
|
+
}
|
|
136
|
+
return buffer;
|
|
137
|
+
}
|
|
138
|
+
|
|
95
139
|
async function startRecording() {
|
|
96
140
|
if (isRecording) return;
|
|
97
141
|
var el = document.getElementById('voiceTranscript');
|
|
@@ -146,11 +190,11 @@
|
|
|
146
190
|
var resampled = resampleBuffer(merged, sourceSampleRate, TARGET_SAMPLE_RATE);
|
|
147
191
|
if (el) el.textContent = 'Transcribing...';
|
|
148
192
|
try {
|
|
149
|
-
var
|
|
193
|
+
var wavBuffer = encodeWav(resampled, TARGET_SAMPLE_RATE);
|
|
150
194
|
var resp = await fetch(BASE + '/api/stt', {
|
|
151
195
|
method: 'POST',
|
|
152
|
-
headers: { 'Content-Type': '
|
|
153
|
-
body:
|
|
196
|
+
headers: { 'Content-Type': 'audio/wav' },
|
|
197
|
+
body: wavBuffer
|
|
154
198
|
});
|
|
155
199
|
var data = await resp.json();
|
|
156
200
|
if (data.text) {
|
|
@@ -240,6 +284,10 @@
|
|
|
240
284
|
}
|
|
241
285
|
}
|
|
242
286
|
|
|
287
|
+
function stripHtml(text) {
|
|
288
|
+
return text.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim();
|
|
289
|
+
}
|
|
290
|
+
|
|
243
291
|
function addVoiceBlock(text, isUser) {
|
|
244
292
|
var container = document.getElementById('voiceMessages');
|
|
245
293
|
if (!container) return;
|
|
@@ -247,13 +295,23 @@
|
|
|
247
295
|
if (emptyMsg) emptyMsg.remove();
|
|
248
296
|
var div = document.createElement('div');
|
|
249
297
|
div.className = 'voice-block' + (isUser ? ' voice-block-user' : '');
|
|
250
|
-
div.textContent = text;
|
|
298
|
+
div.textContent = isUser ? text : stripHtml(text);
|
|
299
|
+
if (!isUser) {
|
|
300
|
+
var rereadBtn = document.createElement('button');
|
|
301
|
+
rereadBtn.className = 'voice-reread-btn';
|
|
302
|
+
rereadBtn.title = 'Re-read aloud';
|
|
303
|
+
rereadBtn.innerHTML = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"/></svg>';
|
|
304
|
+
rereadBtn.addEventListener('click', function() {
|
|
305
|
+
speak(text);
|
|
306
|
+
});
|
|
307
|
+
div.appendChild(rereadBtn);
|
|
308
|
+
}
|
|
251
309
|
container.appendChild(div);
|
|
252
310
|
scrollVoiceToBottom();
|
|
253
311
|
return div;
|
|
254
312
|
}
|
|
255
313
|
|
|
256
|
-
function addVoiceResultBlock(block) {
|
|
314
|
+
function addVoiceResultBlock(block, autoSpeak) {
|
|
257
315
|
var container = document.getElementById('voiceMessages');
|
|
258
316
|
if (!container) return;
|
|
259
317
|
var emptyMsg = container.querySelector('.voice-empty');
|
|
@@ -267,9 +325,10 @@
|
|
|
267
325
|
if (block.result) {
|
|
268
326
|
resultText = typeof block.result === 'string' ? block.result : JSON.stringify(block.result);
|
|
269
327
|
}
|
|
328
|
+
var displayText = stripHtml(resultText);
|
|
270
329
|
var html = '';
|
|
271
|
-
if (
|
|
272
|
-
html += '<div>' + escapeHtml(
|
|
330
|
+
if (displayText) {
|
|
331
|
+
html += '<div>' + escapeHtml(displayText) + '</div>';
|
|
273
332
|
}
|
|
274
333
|
if (duration || cost) {
|
|
275
334
|
html += '<div class="voice-result-stats">';
|
|
@@ -282,9 +341,19 @@
|
|
|
282
341
|
html = isError ? 'Execution failed' : 'Execution complete';
|
|
283
342
|
}
|
|
284
343
|
div.innerHTML = html;
|
|
344
|
+
if (resultText) {
|
|
345
|
+
var rereadBtn = document.createElement('button');
|
|
346
|
+
rereadBtn.className = 'voice-reread-btn';
|
|
347
|
+
rereadBtn.title = 'Re-read aloud';
|
|
348
|
+
rereadBtn.innerHTML = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"/></svg>';
|
|
349
|
+
rereadBtn.addEventListener('click', function() {
|
|
350
|
+
speak(resultText);
|
|
351
|
+
});
|
|
352
|
+
div.appendChild(rereadBtn);
|
|
353
|
+
}
|
|
285
354
|
container.appendChild(div);
|
|
286
355
|
scrollVoiceToBottom();
|
|
287
|
-
if (ttsEnabled && resultText) {
|
|
356
|
+
if (autoSpeak && ttsEnabled && resultText) {
|
|
288
357
|
speak(resultText);
|
|
289
358
|
}
|
|
290
359
|
return div;
|
|
@@ -305,31 +374,33 @@
|
|
|
305
374
|
var data = e.detail;
|
|
306
375
|
if (!data) return;
|
|
307
376
|
if (data.type === 'streaming_progress' && data.block) {
|
|
308
|
-
handleVoiceBlock(data.block);
|
|
377
|
+
handleVoiceBlock(data.block, true);
|
|
309
378
|
}
|
|
310
379
|
if (data.type === 'streaming_start') {
|
|
311
|
-
|
|
380
|
+
spokenChunks = new Set();
|
|
312
381
|
}
|
|
313
382
|
});
|
|
314
383
|
window.addEventListener('conversation-selected', function(e) {
|
|
315
384
|
currentConversationId = e.detail.conversationId;
|
|
385
|
+
stopSpeaking();
|
|
386
|
+
spokenChunks = new Set();
|
|
316
387
|
if (voiceActive) {
|
|
317
388
|
loadVoiceBlocks(currentConversationId);
|
|
318
389
|
}
|
|
319
390
|
});
|
|
320
391
|
}
|
|
321
392
|
|
|
322
|
-
function handleVoiceBlock(block) {
|
|
393
|
+
function handleVoiceBlock(block, isNew) {
|
|
323
394
|
if (!block || !block.type) return;
|
|
324
395
|
if (block.type === 'text' && block.text) {
|
|
325
396
|
var div = addVoiceBlock(block.text, false);
|
|
326
|
-
if (div && ttsEnabled) {
|
|
397
|
+
if (div && isNew && ttsEnabled) {
|
|
327
398
|
div.classList.add('speaking');
|
|
328
399
|
speak(block.text);
|
|
329
400
|
setTimeout(function() { div.classList.remove('speaking'); }, 2000);
|
|
330
401
|
}
|
|
331
402
|
} else if (block.type === 'result') {
|
|
332
|
-
addVoiceResultBlock(block);
|
|
403
|
+
addVoiceResultBlock(block, isNew);
|
|
333
404
|
}
|
|
334
405
|
}
|
|
335
406
|
|
|
@@ -341,9 +412,11 @@
|
|
|
341
412
|
showVoiceEmpty(container);
|
|
342
413
|
return;
|
|
343
414
|
}
|
|
415
|
+
isLoadingHistory = true;
|
|
344
416
|
fetch(BASE + '/api/conversations/' + conversationId + '/chunks')
|
|
345
417
|
.then(function(res) { return res.json(); })
|
|
346
418
|
.then(function(data) {
|
|
419
|
+
isLoadingHistory = false;
|
|
347
420
|
if (!data.ok || !Array.isArray(data.chunks) || data.chunks.length === 0) {
|
|
348
421
|
showVoiceEmpty(container);
|
|
349
422
|
return;
|
|
@@ -356,19 +429,20 @@
|
|
|
356
429
|
addVoiceBlock(block.text, false);
|
|
357
430
|
hasContent = true;
|
|
358
431
|
} else if (block.type === 'result') {
|
|
359
|
-
addVoiceResultBlock(block);
|
|
432
|
+
addVoiceResultBlock(block, false);
|
|
360
433
|
hasContent = true;
|
|
361
434
|
}
|
|
362
435
|
});
|
|
363
436
|
if (!hasContent) showVoiceEmpty(container);
|
|
364
437
|
})
|
|
365
438
|
.catch(function() {
|
|
439
|
+
isLoadingHistory = false;
|
|
366
440
|
showVoiceEmpty(container);
|
|
367
441
|
});
|
|
368
442
|
}
|
|
369
443
|
|
|
370
444
|
function showVoiceEmpty(container) {
|
|
371
|
-
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>
|
|
445
|
+
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>Hold the microphone button to record.<br>Release to transcribe. Tap Send to submit.<br>New responses will be read aloud.</div></div>';
|
|
372
446
|
}
|
|
373
447
|
|
|
374
448
|
function activate() {
|