agentgui 1.0.234 → 1.0.236
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/claude-runner.js +147 -20
- package/lib/speech.js +1 -4
- package/package.json +1 -1
- package/server.js +48 -11
- package/static/js/streaming-renderer.js +95 -0
- package/static/js/voice.js +36 -14
package/lib/claude-runner.js
CHANGED
|
@@ -561,11 +561,33 @@ registry.register({
|
|
|
561
561
|
|
|
562
562
|
// Agent message chunk (text response)
|
|
563
563
|
if (update.sessionUpdate === 'agent_message_chunk' && update.content) {
|
|
564
|
+
let contentBlock;
|
|
565
|
+
|
|
566
|
+
// Handle different content formats
|
|
567
|
+
if (typeof update.content === 'string') {
|
|
568
|
+
contentBlock = { type: 'text', text: update.content };
|
|
569
|
+
} else if (update.content.type === 'text' && update.content.text) {
|
|
570
|
+
contentBlock = update.content;
|
|
571
|
+
} else if (update.content.text) {
|
|
572
|
+
contentBlock = { type: 'text', text: update.content.text };
|
|
573
|
+
} else if (update.content.content) {
|
|
574
|
+
const inner = update.content.content;
|
|
575
|
+
if (typeof inner === 'string') {
|
|
576
|
+
contentBlock = { type: 'text', text: inner };
|
|
577
|
+
} else if (inner.type === 'text' && inner.text) {
|
|
578
|
+
contentBlock = inner;
|
|
579
|
+
} else {
|
|
580
|
+
contentBlock = { type: 'text', text: JSON.stringify(inner) };
|
|
581
|
+
}
|
|
582
|
+
} else {
|
|
583
|
+
contentBlock = { type: 'text', text: JSON.stringify(update.content) };
|
|
584
|
+
}
|
|
585
|
+
|
|
564
586
|
return {
|
|
565
587
|
type: 'assistant',
|
|
566
588
|
message: {
|
|
567
589
|
role: 'assistant',
|
|
568
|
-
content: [
|
|
590
|
+
content: [contentBlock]
|
|
569
591
|
},
|
|
570
592
|
session_id: params.sessionId
|
|
571
593
|
};
|
|
@@ -580,26 +602,63 @@ registry.register({
|
|
|
580
602
|
content: [{
|
|
581
603
|
type: 'tool_use',
|
|
582
604
|
id: update.toolCallId,
|
|
583
|
-
name: update.title || 'tool',
|
|
584
|
-
input: update.input || {}
|
|
605
|
+
name: update.title || update.kind || 'tool',
|
|
606
|
+
input: update.rawInput || update.input || {}
|
|
585
607
|
}]
|
|
586
608
|
},
|
|
587
609
|
session_id: params.sessionId
|
|
588
610
|
};
|
|
589
611
|
}
|
|
590
612
|
|
|
591
|
-
// Tool call update (result)
|
|
592
|
-
if (update.sessionUpdate === 'tool_call_update'
|
|
593
|
-
const
|
|
613
|
+
// Tool call update (result) - handle all statuses
|
|
614
|
+
if (update.sessionUpdate === 'tool_call_update') {
|
|
615
|
+
const status = update.status;
|
|
616
|
+
const isError = status === 'failed';
|
|
617
|
+
const isCompleted = status === 'completed';
|
|
618
|
+
|
|
619
|
+
if (!isCompleted && !isError) {
|
|
620
|
+
return {
|
|
621
|
+
type: 'tool_status',
|
|
622
|
+
tool_use_id: update.toolCallId,
|
|
623
|
+
status: status,
|
|
624
|
+
session_id: params.sessionId
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const contentParts = [];
|
|
629
|
+
if (update.content && Array.isArray(update.content)) {
|
|
630
|
+
for (const item of update.content) {
|
|
631
|
+
if (item.type === 'content' && item.content) {
|
|
632
|
+
const innerContent = item.content;
|
|
633
|
+
if (innerContent.type === 'text' && innerContent.text) {
|
|
634
|
+
contentParts.push(innerContent.text);
|
|
635
|
+
} else if (innerContent.type === 'resource' && innerContent.resource) {
|
|
636
|
+
contentParts.push(innerContent.resource.text || JSON.stringify(innerContent.resource));
|
|
637
|
+
} else {
|
|
638
|
+
contentParts.push(JSON.stringify(innerContent));
|
|
639
|
+
}
|
|
640
|
+
} else if (item.type === 'diff') {
|
|
641
|
+
const diffText = item.oldText
|
|
642
|
+
? `--- ${item.path}\n+++ ${item.path}\n${item.oldText}\n---\n${item.newText}`
|
|
643
|
+
: `+++ ${item.path}\n${item.newText}`;
|
|
644
|
+
contentParts.push(diffText);
|
|
645
|
+
} else if (item.type === 'terminal') {
|
|
646
|
+
contentParts.push(`[Terminal: ${item.terminalId}]`);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const combinedContent = contentParts.join('\n') || (update.rawOutput ? JSON.stringify(update.rawOutput) : '');
|
|
652
|
+
|
|
594
653
|
return {
|
|
595
|
-
type: '
|
|
654
|
+
type: 'user',
|
|
596
655
|
message: {
|
|
597
|
-
role: '
|
|
656
|
+
role: 'user',
|
|
598
657
|
content: [{
|
|
599
658
|
type: 'tool_result',
|
|
600
659
|
tool_use_id: update.toolCallId,
|
|
601
|
-
content:
|
|
602
|
-
is_error:
|
|
660
|
+
content: combinedContent,
|
|
661
|
+
is_error: isError
|
|
603
662
|
}]
|
|
604
663
|
},
|
|
605
664
|
session_id: params.sessionId
|
|
@@ -668,11 +727,33 @@ function createACPProtocolHandler() {
|
|
|
668
727
|
|
|
669
728
|
// Agent message chunk (text response)
|
|
670
729
|
if (update.sessionUpdate === 'agent_message_chunk' && update.content) {
|
|
730
|
+
let contentBlock;
|
|
731
|
+
|
|
732
|
+
// Handle different content formats
|
|
733
|
+
if (typeof update.content === 'string') {
|
|
734
|
+
contentBlock = { type: 'text', text: update.content };
|
|
735
|
+
} else if (update.content.type === 'text' && update.content.text) {
|
|
736
|
+
contentBlock = update.content;
|
|
737
|
+
} else if (update.content.text) {
|
|
738
|
+
contentBlock = { type: 'text', text: update.content.text };
|
|
739
|
+
} else if (update.content.content) {
|
|
740
|
+
const inner = update.content.content;
|
|
741
|
+
if (typeof inner === 'string') {
|
|
742
|
+
contentBlock = { type: 'text', text: inner };
|
|
743
|
+
} else if (inner.type === 'text' && inner.text) {
|
|
744
|
+
contentBlock = inner;
|
|
745
|
+
} else {
|
|
746
|
+
contentBlock = { type: 'text', text: JSON.stringify(inner) };
|
|
747
|
+
}
|
|
748
|
+
} else {
|
|
749
|
+
contentBlock = { type: 'text', text: JSON.stringify(update.content) };
|
|
750
|
+
}
|
|
751
|
+
|
|
671
752
|
return {
|
|
672
753
|
type: 'assistant',
|
|
673
754
|
message: {
|
|
674
755
|
role: 'assistant',
|
|
675
|
-
content: [
|
|
756
|
+
content: [contentBlock]
|
|
676
757
|
},
|
|
677
758
|
session_id: params.sessionId
|
|
678
759
|
};
|
|
@@ -687,26 +768,63 @@ function createACPProtocolHandler() {
|
|
|
687
768
|
content: [{
|
|
688
769
|
type: 'tool_use',
|
|
689
770
|
id: update.toolCallId,
|
|
690
|
-
name: update.title || 'tool',
|
|
691
|
-
input: update.input || {}
|
|
771
|
+
name: update.title || update.kind || 'tool',
|
|
772
|
+
input: update.rawInput || update.input || {}
|
|
692
773
|
}]
|
|
693
774
|
},
|
|
694
775
|
session_id: params.sessionId
|
|
695
776
|
};
|
|
696
777
|
}
|
|
697
778
|
|
|
698
|
-
// Tool call update (result)
|
|
699
|
-
if (update.sessionUpdate === 'tool_call_update'
|
|
700
|
-
const
|
|
779
|
+
// Tool call update (result) - handle all statuses
|
|
780
|
+
if (update.sessionUpdate === 'tool_call_update') {
|
|
781
|
+
const status = update.status;
|
|
782
|
+
const isError = status === 'failed';
|
|
783
|
+
const isCompleted = status === 'completed';
|
|
784
|
+
|
|
785
|
+
if (!isCompleted && !isError) {
|
|
786
|
+
return {
|
|
787
|
+
type: 'tool_status',
|
|
788
|
+
tool_use_id: update.toolCallId,
|
|
789
|
+
status: status,
|
|
790
|
+
session_id: params.sessionId
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const contentParts = [];
|
|
795
|
+
if (update.content && Array.isArray(update.content)) {
|
|
796
|
+
for (const item of update.content) {
|
|
797
|
+
if (item.type === 'content' && item.content) {
|
|
798
|
+
const innerContent = item.content;
|
|
799
|
+
if (innerContent.type === 'text' && innerContent.text) {
|
|
800
|
+
contentParts.push(innerContent.text);
|
|
801
|
+
} else if (innerContent.type === 'resource' && innerContent.resource) {
|
|
802
|
+
contentParts.push(innerContent.resource.text || JSON.stringify(innerContent.resource));
|
|
803
|
+
} else {
|
|
804
|
+
contentParts.push(JSON.stringify(innerContent));
|
|
805
|
+
}
|
|
806
|
+
} else if (item.type === 'diff') {
|
|
807
|
+
const diffText = item.oldText
|
|
808
|
+
? `--- ${item.path}\n+++ ${item.path}\n${item.oldText}\n---\n${item.newText}`
|
|
809
|
+
: `+++ ${item.path}\n${item.newText}`;
|
|
810
|
+
contentParts.push(diffText);
|
|
811
|
+
} else if (item.type === 'terminal') {
|
|
812
|
+
contentParts.push(`[Terminal: ${item.terminalId}]`);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const combinedContent = contentParts.join('\n') || (update.rawOutput ? JSON.stringify(update.rawOutput) : '');
|
|
818
|
+
|
|
701
819
|
return {
|
|
702
|
-
type: '
|
|
820
|
+
type: 'user',
|
|
703
821
|
message: {
|
|
704
|
-
role: '
|
|
822
|
+
role: 'user',
|
|
705
823
|
content: [{
|
|
706
824
|
type: 'tool_result',
|
|
707
825
|
tool_use_id: update.toolCallId,
|
|
708
|
-
content:
|
|
709
|
-
is_error:
|
|
826
|
+
content: combinedContent,
|
|
827
|
+
is_error: isError
|
|
710
828
|
}]
|
|
711
829
|
},
|
|
712
830
|
session_id: params.sessionId
|
|
@@ -726,6 +844,15 @@ function createACPProtocolHandler() {
|
|
|
726
844
|
};
|
|
727
845
|
}
|
|
728
846
|
|
|
847
|
+
// Plan update
|
|
848
|
+
if (update.sessionUpdate === 'plan') {
|
|
849
|
+
return {
|
|
850
|
+
type: 'plan',
|
|
851
|
+
entries: update.entries || [],
|
|
852
|
+
session_id: params.sessionId
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
|
|
729
856
|
return null;
|
|
730
857
|
}
|
|
731
858
|
|
package/lib/speech.js
CHANGED
|
@@ -89,10 +89,7 @@ function synthesize(text, voiceId) {
|
|
|
89
89
|
function synthesizeStream(text, voiceId) {
|
|
90
90
|
if (needsPatch && voiceId && PREDEFINED_IDS.has(voiceId)) {
|
|
91
91
|
return (async function* () {
|
|
92
|
-
|
|
93
|
-
for (const sentence of sentences) {
|
|
94
|
-
yield await synthesizeDirect(sentence, voiceId);
|
|
95
|
-
}
|
|
92
|
+
yield await synthesizeDirect(text, voiceId);
|
|
96
93
|
})();
|
|
97
94
|
}
|
|
98
95
|
return serverTTS.synthesizeStream(text, voiceId, EXTRA_VOICE_DIRS);
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -120,19 +120,16 @@ function flushTTSaccumulator(key, conversationId, sessionId) {
|
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
if (voices.size === 0) return;
|
|
123
|
-
const
|
|
123
|
+
const cacheKey = speech.ttsCacheKey(text, vid);
|
|
124
124
|
for (const vid of voices) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
pushTTSAudio(cacheKey, cached, conversationId, sessionId, vid);
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
|
-
speech.synthesize(sentence, vid).then(wav => {
|
|
133
|
-
pushTTSAudio(cacheKey, wav, conversationId, sessionId, vid);
|
|
134
|
-
}).catch(() => {});
|
|
125
|
+
const cached = speech.ttsCacheGet(cacheKey);
|
|
126
|
+
if (cached) {
|
|
127
|
+
pushTTSAudio(cacheKey, cached, conversationId, sessionId, vid);
|
|
128
|
+
continue;
|
|
135
129
|
}
|
|
130
|
+
speech.synthesize(text, vid).then(wav => {
|
|
131
|
+
pushTTSAudio(cacheKey, wav, conversationId, sessionId, vid);
|
|
132
|
+
}).catch(() => {});
|
|
136
133
|
}
|
|
137
134
|
}).catch(() => {});
|
|
138
135
|
}
|
|
@@ -2069,6 +2066,46 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
2069
2066
|
if (parsed.result && allBlocks.length === 0) {
|
|
2070
2067
|
allBlocks.push({ type: 'text', text: String(parsed.result) });
|
|
2071
2068
|
}
|
|
2069
|
+
} else if (parsed.type === 'tool_status') {
|
|
2070
|
+
// Handle ACP tool status updates (in_progress, pending)
|
|
2071
|
+
broadcastSync({
|
|
2072
|
+
type: 'streaming_progress',
|
|
2073
|
+
sessionId,
|
|
2074
|
+
conversationId,
|
|
2075
|
+
block: {
|
|
2076
|
+
type: 'tool_status',
|
|
2077
|
+
tool_use_id: parsed.tool_use_id,
|
|
2078
|
+
status: parsed.status
|
|
2079
|
+
},
|
|
2080
|
+
seq: currentSequence,
|
|
2081
|
+
timestamp: Date.now()
|
|
2082
|
+
});
|
|
2083
|
+
} else if (parsed.type === 'usage') {
|
|
2084
|
+
// Handle ACP usage updates
|
|
2085
|
+
broadcastSync({
|
|
2086
|
+
type: 'streaming_progress',
|
|
2087
|
+
sessionId,
|
|
2088
|
+
conversationId,
|
|
2089
|
+
block: {
|
|
2090
|
+
type: 'usage',
|
|
2091
|
+
usage: parsed.usage
|
|
2092
|
+
},
|
|
2093
|
+
seq: currentSequence,
|
|
2094
|
+
timestamp: Date.now()
|
|
2095
|
+
});
|
|
2096
|
+
} else if (parsed.type === 'plan') {
|
|
2097
|
+
// Handle ACP plan updates
|
|
2098
|
+
broadcastSync({
|
|
2099
|
+
type: 'streaming_progress',
|
|
2100
|
+
sessionId,
|
|
2101
|
+
conversationId,
|
|
2102
|
+
block: {
|
|
2103
|
+
type: 'plan',
|
|
2104
|
+
entries: parsed.entries
|
|
2105
|
+
},
|
|
2106
|
+
seq: currentSequence,
|
|
2107
|
+
timestamp: Date.now()
|
|
2108
|
+
});
|
|
2072
2109
|
}
|
|
2073
2110
|
};
|
|
2074
2111
|
|
|
@@ -351,6 +351,12 @@ class StreamingRenderer {
|
|
|
351
351
|
return this.renderBlockSystem(block, context);
|
|
352
352
|
case 'result':
|
|
353
353
|
return this.renderBlockResult(block, context);
|
|
354
|
+
case 'tool_status':
|
|
355
|
+
return this.renderBlockToolStatus(block, context);
|
|
356
|
+
case 'usage':
|
|
357
|
+
return this.renderBlockUsage(block, context);
|
|
358
|
+
case 'plan':
|
|
359
|
+
return this.renderBlockPlan(block, context);
|
|
354
360
|
default:
|
|
355
361
|
return this.renderBlockGeneric(block, context);
|
|
356
362
|
}
|
|
@@ -1320,6 +1326,95 @@ class StreamingRenderer {
|
|
|
1320
1326
|
return details;
|
|
1321
1327
|
}
|
|
1322
1328
|
|
|
1329
|
+
/**
|
|
1330
|
+
* Render tool status block (ACP in_progress/pending updates)
|
|
1331
|
+
*/
|
|
1332
|
+
renderBlockToolStatus(block, context) {
|
|
1333
|
+
const status = block.status || 'pending';
|
|
1334
|
+
const statusIcons = {
|
|
1335
|
+
pending: '<svg viewBox="0 0 20 20" fill="currentColor" style="color:var(--color-text-secondary)"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"/></svg>',
|
|
1336
|
+
in_progress: '<svg viewBox="0 0 20 20" fill="currentColor" class="animate-spin" style="color:var(--color-info)"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"/></svg>'
|
|
1337
|
+
};
|
|
1338
|
+
const statusLabels = {
|
|
1339
|
+
pending: 'Pending',
|
|
1340
|
+
in_progress: 'Running...'
|
|
1341
|
+
};
|
|
1342
|
+
|
|
1343
|
+
const div = document.createElement('div');
|
|
1344
|
+
div.className = 'block-tool-status';
|
|
1345
|
+
div.dataset.toolUseId = block.tool_use_id || '';
|
|
1346
|
+
div.innerHTML = `
|
|
1347
|
+
<div style="display:flex;align-items:center;gap:0.5rem;padding:0.25rem 0.5rem;font-size:0.75rem;color:var(--color-text-secondary)">
|
|
1348
|
+
${statusIcons[status] || statusIcons.pending}
|
|
1349
|
+
<span>${statusLabels[status] || status}</span>
|
|
1350
|
+
</div>
|
|
1351
|
+
`;
|
|
1352
|
+
return div;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
/**
|
|
1356
|
+
* Render usage block (ACP usage updates)
|
|
1357
|
+
*/
|
|
1358
|
+
renderBlockUsage(block, context) {
|
|
1359
|
+
const usage = block.usage || {};
|
|
1360
|
+
const used = usage.used || 0;
|
|
1361
|
+
const size = usage.size || 0;
|
|
1362
|
+
const cost = usage.cost ? '$' + usage.cost.toFixed(4) : '';
|
|
1363
|
+
|
|
1364
|
+
const div = document.createElement('div');
|
|
1365
|
+
div.className = 'block-usage';
|
|
1366
|
+
div.innerHTML = `
|
|
1367
|
+
<div style="display:flex;gap:1rem;padding:0.25rem 0.5rem;font-size:0.7rem;color:var(--color-text-secondary);background:var(--color-bg-secondary);border-radius:0.25rem">
|
|
1368
|
+
${used ? `<span><strong>Used:</strong> ${used.toLocaleString()}</span>` : ''}
|
|
1369
|
+
${size ? `<span><strong>Context:</strong> ${size.toLocaleString()}</span>` : ''}
|
|
1370
|
+
${cost ? `<span><strong>Cost:</strong> ${cost}</span>` : ''}
|
|
1371
|
+
</div>
|
|
1372
|
+
`;
|
|
1373
|
+
return div;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
/**
|
|
1377
|
+
* Render plan block (ACP plan updates)
|
|
1378
|
+
*/
|
|
1379
|
+
renderBlockPlan(block, context) {
|
|
1380
|
+
const entries = block.entries || [];
|
|
1381
|
+
if (entries.length === 0) return null;
|
|
1382
|
+
|
|
1383
|
+
const priorityColors = {
|
|
1384
|
+
high: '#ef4444',
|
|
1385
|
+
medium: '#f59e0b',
|
|
1386
|
+
low: '#6b7280'
|
|
1387
|
+
};
|
|
1388
|
+
const statusIcons = {
|
|
1389
|
+
pending: '○',
|
|
1390
|
+
in_progress: '◐',
|
|
1391
|
+
completed: '●'
|
|
1392
|
+
};
|
|
1393
|
+
|
|
1394
|
+
const div = document.createElement('div');
|
|
1395
|
+
div.className = 'block-plan';
|
|
1396
|
+
div.innerHTML = `
|
|
1397
|
+
<details class="folded-tool folded-tool-info">
|
|
1398
|
+
<summary class="folded-tool-bar">
|
|
1399
|
+
<span class="folded-tool-icon"><svg viewBox="0 0 20 20" fill="currentColor"><path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"/><path fill-rule="evenodd" d="M4 5a2 2 0 012-2 3 3 0 003 3h2a3 3 0 003-3 2 2 0 012 2v11a2 2 0 01-2 2H6a2 2 0 01-2-2V5zm3 4a1 1 0 000 2h.01a1 1 0 100-2H7zm3 0a1 1 0 000 2h3a1 1 0 100-2h-3zm-3 4a1 1 0 100 2h.01a1 1 0 100-2H7zm3 0a1 1 0 100 2h3a1 1 0 100-2h-3z" clip-rule="evenodd"/></svg></span>
|
|
1400
|
+
<span class="folded-tool-name">Plan</span>
|
|
1401
|
+
<span class="folded-tool-desc">${entries.length} tasks</span>
|
|
1402
|
+
</summary>
|
|
1403
|
+
<div class="folded-tool-body">
|
|
1404
|
+
<div style="display:flex;flex-direction:column;gap:0.375rem">
|
|
1405
|
+
${entries.map(e => `
|
|
1406
|
+
<div style="display:flex;align-items:center;gap:0.5rem;font-size:0.8rem">
|
|
1407
|
+
<span style="color:${priorityColors[e.priority] || priorityColors.low}">${statusIcons[e.status] || statusIcons.pending}</span>
|
|
1408
|
+
<span style="${e.status === 'completed' ? 'text-decoration:line-through;opacity:0.6' : ''}">${this.escapeHtml(e.content || '')}</span>
|
|
1409
|
+
</div>
|
|
1410
|
+
`).join('')}
|
|
1411
|
+
</div>
|
|
1412
|
+
</div>
|
|
1413
|
+
</details>
|
|
1414
|
+
`;
|
|
1415
|
+
return div;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1323
1418
|
/**
|
|
1324
1419
|
* Render generic block with formatted key-value pairs
|
|
1325
1420
|
*/
|
package/static/js/voice.js
CHANGED
|
@@ -317,9 +317,23 @@
|
|
|
317
317
|
}
|
|
318
318
|
|
|
319
319
|
function splitSentences(text) {
|
|
320
|
+
if (!text) return [text];
|
|
320
321
|
var raw = text.match(/[^.!?]+[.!?]+[\s]?|[^.!?]+$/g);
|
|
321
322
|
if (!raw) return [text];
|
|
322
|
-
|
|
323
|
+
var sentences = raw.map(function(s) { return s.trim(); }).filter(function(s) { return s.length > 0; });
|
|
324
|
+
var result = [];
|
|
325
|
+
for (var i = 0; i < sentences.length; i++) {
|
|
326
|
+
var s = sentences[i];
|
|
327
|
+
if (result.length > 0) {
|
|
328
|
+
var prev = result[result.length - 1];
|
|
329
|
+
if (s.match(/^(\d+[\.\)]|\d+\s)/) || prev.match(/\d+[\.\)]$/)) {
|
|
330
|
+
result[result.length - 1] = prev + ' ' + s;
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
result.push(s);
|
|
335
|
+
}
|
|
336
|
+
return result;
|
|
323
337
|
}
|
|
324
338
|
|
|
325
339
|
var audioChunkQueue = [];
|
|
@@ -382,17 +396,9 @@
|
|
|
382
396
|
return;
|
|
383
397
|
}
|
|
384
398
|
|
|
385
|
-
var sentences =
|
|
399
|
+
var sentences = [text];
|
|
386
400
|
var cachedSentences = [];
|
|
387
|
-
var uncachedText = [];
|
|
388
|
-
for (var i = 0; i < sentences.length; i++) {
|
|
389
|
-
var blob = getCachedTTSBlob(sentences[i]);
|
|
390
|
-
if (blob) {
|
|
391
|
-
cachedSentences.push({ idx: i, blob: blob });
|
|
392
|
-
} else {
|
|
393
|
-
uncachedText.push(sentences[i]);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
401
|
+
var uncachedText = [text];
|
|
396
402
|
|
|
397
403
|
if (cachedSentences.length === sentences.length) {
|
|
398
404
|
ttsConsecutiveFailures = 0;
|
|
@@ -530,16 +536,32 @@
|
|
|
530
536
|
if (!container) return;
|
|
531
537
|
var emptyMsg = container.querySelector('.voice-empty');
|
|
532
538
|
if (emptyMsg) emptyMsg.remove();
|
|
539
|
+
var lastChild = container.lastElementChild;
|
|
540
|
+
if (!isUser && lastChild && lastChild.classList.contains('voice-block') && !lastChild.classList.contains('voice-block-user')) {
|
|
541
|
+
var contentSpan = lastChild.querySelector('.voice-block-content');
|
|
542
|
+
if (contentSpan) {
|
|
543
|
+
contentSpan.textContent += '\n' + stripHtml(text);
|
|
544
|
+
lastChild._fullText = (lastChild._fullText || contentSpan.textContent) + '\n' + text;
|
|
545
|
+
scrollVoiceToBottom();
|
|
546
|
+
return lastChild;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
533
549
|
var div = document.createElement('div');
|
|
534
550
|
div.className = 'voice-block' + (isUser ? ' voice-block-user' : '');
|
|
535
|
-
|
|
536
|
-
|
|
551
|
+
if (isUser) {
|
|
552
|
+
div.textContent = text;
|
|
553
|
+
} else {
|
|
554
|
+
var contentSpan = document.createElement('span');
|
|
555
|
+
contentSpan.className = 'voice-block-content';
|
|
556
|
+
contentSpan.textContent = stripHtml(text);
|
|
557
|
+
div.appendChild(contentSpan);
|
|
558
|
+
div._fullText = text;
|
|
537
559
|
var rereadBtn = document.createElement('button');
|
|
538
560
|
rereadBtn.className = 'voice-reread-btn';
|
|
539
561
|
rereadBtn.title = 'Re-read aloud';
|
|
540
562
|
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>';
|
|
541
563
|
rereadBtn.addEventListener('click', function() {
|
|
542
|
-
speak(
|
|
564
|
+
speak(div._fullText || contentSpan.textContent);
|
|
543
565
|
});
|
|
544
566
|
div.appendChild(rereadBtn);
|
|
545
567
|
}
|