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.
@@ -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 header = document.createElement('div');
437
- header.className = 'code-header';
438
- header.innerHTML = `
439
- <span class="lang-badge">${this.escapeHtml(language)}</span>
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 = header.querySelector('.copy-code-btn');
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
- // Use syntax highlighting instead of just escaping
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
- codeContainer.innerHTML = highlightedHTML;
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
- div.appendChild(header);
460
- div.appendChild(codeContainer);
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
- return `<pre style="${preStyle}"><code class="hljs">${result.value}</code></pre>`;
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 `<pre style="${preStyle}">${esc(code)}</pre>`;
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
- <div class="flex items-center justify-between gap-2 bg-gray-900 dark:bg-gray-950 px-4 py-2 border-b border-gray-800">
1526
- <span class="text-xs font-mono text-gray-400 uppercase">${this.escapeHtml(part.language)}</span>
1527
- <button class="copy-code-btn text-gray-400 hover:text-gray-200 transition-colors p-1 rounded hover:bg-gray-800" title="Copy code">
1528
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1529
- <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>
1530
- </svg>
1531
- </button>
1532
- </div>
1533
- <pre class="bg-gray-900 text-gray-100 p-4 overflow-x-auto"><code class="language-${this.escapeHtml(part.language)}">${this.escapeHtml(part.code)}</code></pre>
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
- <pre class="bg-gray-900 text-gray-100 p-4 rounded overflow-x-auto"><code class="language-${this.escapeHtml(language)}">${this.escapeHtml(code)}</code></pre>
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://unpkg.com/prism@1.29.0',
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 = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
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%;