@chat21/chat21-web-widget 5.1.33-rc9 → 5.1.34-rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/package.json +1 -1
- package/src/app/component/conversation-detail/conversation/conversation.component.ts +3 -1
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.spec.ts +0 -7
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.ts +7 -5
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.html +4 -3
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss +18 -18
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts +6 -0
- package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.html +8 -5
- package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.scss +5 -1
- package/src/app/component/form/inputs/form-text/form-text.component.ts +9 -3
- package/src/app/component/message/bubble-message/bubble-message.component.scss +5 -0
- package/src/app/component/message/bubble-message/bubble-message.component.ts +14 -0
- package/src/app/component/message/json-sources/json-sources.component.scss +12 -8
- package/src/app/pipe/marked.pipe.ts +51 -41
- package/src/app/providers/global-settings.service.ts +31 -0
- package/src/app/providers/json-sources-parser.service.ts +25 -32
- package/src/app/providers/voice/voice-streaming.service.ts +11 -19
- package/src/app/providers/voice/voice-streaming.types.ts +0 -1
- package/src/app/providers/voice/voice.service.spec.ts +12 -45
- package/src/app/providers/voice/voice.service.ts +215 -45
- package/src/app/utils/globals.ts +10 -0
- package/src/assets/i18n/en.json +106 -125
- package/src/assets/i18n/es.json +1 -0
- package/src/assets/i18n/fr.json +1 -0
- package/src/assets/i18n/it.json +1 -0
- package/src/assets/sounds/keyboard.mp3 +0 -0
- package/src/assets/twp/chatbot-panel.html +3 -1
- package/src/chat21-core/utils/utils-message.ts +15 -5
- package/src/widget-config-template.json +1 -0
- package/src/widget-config.json +30 -28
- package/.playwright-mcp/console-2026-05-08T15-31-09-000Z.log +0 -17
- package/.playwright-mcp/console-2026-05-08T15-32-19-412Z.log +0 -89
- package/.playwright-mcp/console-2026-05-08T16-18-48-424Z.log +0 -133
- package/.playwright-mcp/console-2026-05-11T12-54-06-869Z.log +0 -13
- package/.playwright-mcp/console-2026-05-11T12-54-56-229Z.log +0 -147
- package/.playwright-mcp/console-2026-05-11T12-55-47-174Z.log +0 -183
- package/.playwright-mcp/console-2026-05-11T15-34-03-590Z.log +0 -210
- package/.playwright-mcp/console-2026-05-12T15-07-31-880Z.log +0 -118
- package/.playwright-mcp/page-2026-05-08T15-32-19-900Z.yml +0 -851
- package/.playwright-mcp/page-2026-05-08T15-32-47-264Z.yml +0 -857
- package/.playwright-mcp/page-2026-05-08T15-33-17-089Z.yml +0 -1110
- package/.playwright-mcp/page-2026-05-08T15-33-23-486Z.yml +0 -1069
- package/.playwright-mcp/page-2026-05-08T15-33-45-390Z.yml +0 -1076
- package/.playwright-mcp/page-2026-05-08T15-33-52-666Z.yml +0 -1072
- package/.playwright-mcp/page-2026-05-08T15-34-01-338Z.yml +0 -1085
- package/.playwright-mcp/page-2026-05-08T15-34-07-227Z.yml +0 -1072
- package/.playwright-mcp/page-2026-05-08T15-34-13-875Z.yml +0 -1072
- package/.playwright-mcp/page-2026-05-08T15-34-21-885Z.yml +0 -1109
- package/.playwright-mcp/page-2026-05-08T15-34-32-755Z.yml +0 -1109
- package/.playwright-mcp/page-2026-05-08T15-35-09-607Z.yml +0 -1119
- package/.playwright-mcp/page-2026-05-08T15-35-14-242Z.yml +0 -1109
- package/.playwright-mcp/page-2026-05-08T16-18-48-671Z.yml +0 -44
- package/.playwright-mcp/page-2026-05-08T16-18-52-753Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-19-13-919Z.yml +0 -68
- package/.playwright-mcp/page-2026-05-08T16-19-17-977Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-19-25-733Z.yml +0 -120
- package/.playwright-mcp/page-2026-05-08T16-19-29-252Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-19-39-269Z.yml +0 -80
- package/.playwright-mcp/page-2026-05-08T16-19-43-915Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-20-04-407Z.yml +0 -81
- package/.playwright-mcp/page-2026-05-08T16-20-08-984Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-20-32-397Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-20-58-658Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-21-12-320Z.yml +0 -86
- package/.playwright-mcp/page-2026-05-08T16-21-39-154Z.yml +0 -91
- package/.playwright-mcp/page-2026-05-08T16-21-45-420Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-22-21-062Z.yml +0 -0
- package/.playwright-mcp/page-2026-05-08T16-22-58-232Z.yml +0 -91
- package/.playwright-mcp/page-2026-05-08T16-23-36-520Z.yml +0 -0
- package/.playwright-mcp/page-2026-05-08T16-23-46-805Z.yml +0 -100
- package/.playwright-mcp/page-2026-05-08T16-23-55-169Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-24-26-574Z.yml +0 -91
- package/.playwright-mcp/page-2026-05-08T16-25-34-414Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-25-59-831Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-26-21-809Z.yml +0 -91
- package/.playwright-mcp/page-2026-05-08T16-26-47-443Z.yml +0 -105
- package/.playwright-mcp/page-2026-05-08T16-26-56-136Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-27-59-610Z.yml +0 -48
- package/.playwright-mcp/page-2026-05-11T12-54-07-180Z.yml +0 -44
- package/.playwright-mcp/page-2026-05-11T12-54-56-946Z.yml +0 -4
- package/.playwright-mcp/page-2026-05-11T12-55-47-503Z.yml +0 -24
- package/.playwright-mcp/page-2026-05-11T12-56-00-766Z.yml +0 -28
- package/.playwright-mcp/page-2026-05-11T12-56-06-438Z.yml +0 -90
- package/.playwright-mcp/page-2026-05-11T12-57-56-838Z.yml +0 -106
- package/.playwright-mcp/page-2026-05-11T12-58-00-124Z.yml +0 -106
- package/.playwright-mcp/page-2026-05-11T12-59-08-836Z.yml +0 -61
- package/.playwright-mcp/page-2026-05-11T12-59-12-088Z.yml +0 -61
- package/.playwright-mcp/page-2026-05-11T12-59-26-215Z.yml +0 -69
- package/.playwright-mcp/page-2026-05-11T12-59-29-519Z.yml +0 -69
- package/.playwright-mcp/page-2026-05-11T12-59-37-309Z.yml +0 -0
- package/.playwright-mcp/page-2026-05-11T12-59-39-968Z.yml +0 -79
- package/.playwright-mcp/page-2026-05-11T12-59-45-983Z.yml +0 -78
- package/.playwright-mcp/page-2026-05-11T12-59-49-951Z.yml +0 -78
- package/.playwright-mcp/page-2026-05-11T15-34-04-515Z.yml +0 -0
- package/.playwright-mcp/page-2026-05-12T15-07-32-171Z.yml +0 -44
- package/.playwright-mcp/page-2026-05-12T15-08-09-820Z.yml +0 -119
- package/docs/TILEDESK_WIDGET_ACCESSIBILITY_STATEMENT_COMPLETE.md +0 -379
- package/playwright-report/index.html +0 -90
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component copy.html +0 -172
- package/test-results/.last-run.json +0 -4
|
@@ -7,25 +7,19 @@ import { mergeJsonSourcesMissingFields } from 'src/app/utils/json-sources-utils'
|
|
|
7
7
|
|
|
8
8
|
export type UrlPreviewMessage = {
|
|
9
9
|
type?: string; // "url_preview"
|
|
10
|
-
activeMode?: 'form' | 'list' | 'text' | string;
|
|
11
|
-
form?: { sources?: any[] };
|
|
12
|
-
list?: string;
|
|
13
10
|
text?: string;
|
|
14
11
|
};
|
|
15
12
|
|
|
16
13
|
/**
|
|
17
14
|
* Parse and enrich "url_preview" messages into `JsonSourceItem[]`.
|
|
18
15
|
*
|
|
19
|
-
* This service is intentionally isolated so it can be replaced/removed easily.
|
|
20
|
-
*
|
|
21
16
|
* Rules:
|
|
22
|
-
* -
|
|
23
|
-
* - `
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* and merges the missing fields only (never overwriting existing values).
|
|
17
|
+
* - The payload is always read from `msg.text`, regardless of `activeMode`.
|
|
18
|
+
* - `msg.text` may be either:
|
|
19
|
+
* - a JSON array of source objects (`{source_name, source_file_name, ...}`), or
|
|
20
|
+
* - a plain string from which URLs are extracted (split by whitespace/punctuation).
|
|
21
|
+
* - After building the initial array, `url-preview` is called only for items that miss
|
|
22
|
+
* title or description, and missing fields are merged in (never overwriting).
|
|
29
23
|
*/
|
|
30
24
|
@Injectable({ providedIn: 'root' })
|
|
31
25
|
export class JsonSourcesParserService {
|
|
@@ -98,23 +92,11 @@ export class JsonSourcesParserService {
|
|
|
98
92
|
private parseBaseJsonSources(msg?: UrlPreviewMessage | null): JsonSourceItem[] | null {
|
|
99
93
|
if (!msg || msg.type !== 'url_preview') return null;
|
|
100
94
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
if (normalizedMode === 'list') {
|
|
108
|
-
return this.mapListToSources(msg.list);
|
|
109
|
-
}
|
|
110
|
-
if (normalizedMode === 'text') {
|
|
111
|
-
return this.mapTextToSources(msg.text);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// best-effort fallback order
|
|
115
|
-
return this.mapSourcesArray(msg.form?.sources)
|
|
116
|
-
|| this.mapTextToSources(msg.text)
|
|
117
|
-
|| this.mapListToSources(msg.list);
|
|
95
|
+
// Regardless of `activeMode`, the payload is always read from `msg.text`.
|
|
96
|
+
// It can be either a JSON array of source objects, or a plain string with URLs.
|
|
97
|
+
return this.isJsonArrayOfObjects(msg.text)
|
|
98
|
+
? this.mapTextToSources(msg.text)
|
|
99
|
+
: this.mapListToSources(msg.text);
|
|
118
100
|
}
|
|
119
101
|
|
|
120
102
|
private mapListToSources(listValue?: string): JsonSourceItem[] | null {
|
|
@@ -122,6 +104,16 @@ export class JsonSourcesParserService {
|
|
|
122
104
|
return urls.length ? urls.map(u => ({ link: u, title: u })) : null;
|
|
123
105
|
}
|
|
124
106
|
|
|
107
|
+
private isJsonArrayOfObjects(text?: string): boolean {
|
|
108
|
+
if (!text) return false;
|
|
109
|
+
try {
|
|
110
|
+
const parsed = this.parseJsonLenient(text);
|
|
111
|
+
return Array.isArray(parsed) && parsed.some(it => it && typeof it === 'object' && !Array.isArray(it));
|
|
112
|
+
} catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
125
117
|
private mapTextToSources(text?: string): JsonSourceItem[] | null {
|
|
126
118
|
if (!text) return null;
|
|
127
119
|
try {
|
|
@@ -137,9 +129,10 @@ export class JsonSourcesParserService {
|
|
|
137
129
|
if (!arr || arr.length === 0) return null;
|
|
138
130
|
const mapped = arr
|
|
139
131
|
.filter((s: any) => s && typeof s === 'object' && typeof s[JSON_SOURCE_FIELD_URL] === 'string')
|
|
140
|
-
.map((s: any): JsonSourceItem => {
|
|
132
|
+
.map((s: any): JsonSourceItem | null => {
|
|
141
133
|
const rawUrl = (s[JSON_SOURCE_FIELD_URL] || '').toString().trim();
|
|
142
|
-
const normalized = extractUrlsFromText(rawUrl, 1)[0]
|
|
134
|
+
const normalized = extractUrlsFromText(rawUrl, 1)[0];
|
|
135
|
+
if (!normalized) return null;
|
|
143
136
|
return {
|
|
144
137
|
link: normalized,
|
|
145
138
|
title: (s[JSON_SOURCE_FIELD_TITLE] || rawUrl).toString(),
|
|
@@ -147,7 +140,7 @@ export class JsonSourcesParserService {
|
|
|
147
140
|
image: typeof s.source_image === 'string' ? s.source_image : undefined
|
|
148
141
|
};
|
|
149
142
|
})
|
|
150
|
-
.filter((x: JsonSourceItem) => !!x.link);
|
|
143
|
+
.filter((x: JsonSourceItem | null): x is JsonSourceItem => !!x && !!x.link);
|
|
151
144
|
return mapped.length ? mapped : null;
|
|
152
145
|
}
|
|
153
146
|
|
|
@@ -13,10 +13,11 @@ import {
|
|
|
13
13
|
VoiceWsControlMessage,
|
|
14
14
|
} from './voice-streaming.types';
|
|
15
15
|
|
|
16
|
-
// Flux docs recommend 80ms chunks for optimal latency;
|
|
17
|
-
// balance for WebM containerization overhead in the browser
|
|
16
|
+
// Flux docs recommend 80ms chunks for optimal latency; 160ms is a practical
|
|
17
|
+
// balance for WebM containerization overhead in the browser while providing
|
|
18
|
+
// good STT accuracy.
|
|
18
19
|
// Source: https://developers.deepgram.com/docs/flux/quickstart
|
|
19
|
-
const DEFAULT_TIMESLICE_MS =
|
|
20
|
+
const DEFAULT_TIMESLICE_MS = 160;
|
|
20
21
|
const READY_TIMEOUT_MS = 10_000;
|
|
21
22
|
const SESSION_STARTED_TIMEOUT_MS = 10_000;
|
|
22
23
|
|
|
@@ -258,6 +259,12 @@ export class VoiceStreamingService {
|
|
|
258
259
|
this.mediaStream = shared
|
|
259
260
|
? shared
|
|
260
261
|
: await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
262
|
+
const tracks = this.mediaStream.getAudioTracks();
|
|
263
|
+
this.logger.info('[VoiceStreaming] microphone acquired', {
|
|
264
|
+
shared: !!shared,
|
|
265
|
+
tracks: tracks.length,
|
|
266
|
+
label: tracks[0]?.label ?? '(unknown)',
|
|
267
|
+
});
|
|
261
268
|
const recorderOpts: MediaRecorderOptions = {};
|
|
262
269
|
if (mime) {
|
|
263
270
|
recorderOpts.mimeType = mime;
|
|
@@ -571,7 +578,7 @@ export class VoiceStreamingService {
|
|
|
571
578
|
|
|
572
579
|
/**
|
|
573
580
|
* Send `{ event: "tts_playback_complete" }` to the proxy, signalling that TTS
|
|
574
|
-
* playback has finished and the microphone is
|
|
581
|
+
* playback has finished and the microphone is now safe to receive user speech.
|
|
575
582
|
*/
|
|
576
583
|
sendPlaybackComplete(): void {
|
|
577
584
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
@@ -580,21 +587,6 @@ export class VoiceStreamingService {
|
|
|
580
587
|
}
|
|
581
588
|
}
|
|
582
589
|
|
|
583
|
-
/**
|
|
584
|
-
* Send `{ event: "barge_in" }` to the proxy, requesting an immediate interruption
|
|
585
|
-
* of the ongoing TTS playback. Use when the user explicitly wants to speak while
|
|
586
|
-
* the bot is talking (e.g. via a UI button or a client-side VAD onset).
|
|
587
|
-
*
|
|
588
|
-
* The proxy will stop the TTS stream and transition to LISTENING; the widget should
|
|
589
|
-
* handle the server-sent `barge_in` and `listening` events to update local state.
|
|
590
|
-
*/
|
|
591
|
-
sendBargeIn(): void {
|
|
592
|
-
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
593
|
-
this.ws.send(JSON.stringify({ event: 'barge_in' }));
|
|
594
|
-
this.logger.info('[VoiceStreaming] barge_in sent');
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
|
|
598
590
|
private cleanup(): void {
|
|
599
591
|
this.logger.info('[VoiceStreaming] cleanup', { state: this._currentState, sessionId: this.currentSessionId });
|
|
600
592
|
this.audioChunkCount = 0;
|
|
@@ -44,7 +44,7 @@ describe('VoiceService', () => {
|
|
|
44
44
|
|
|
45
45
|
voiceStreamingMock = jasmine.createSpyObj<VoiceStreamingService>(
|
|
46
46
|
'VoiceStreamingService',
|
|
47
|
-
['start', 'stop', 'setAudioMuted', 'sendPlaybackComplete', '
|
|
47
|
+
['start', 'stop', 'setAudioMuted', 'sendPlaybackComplete', 'pauseRecording', 'resumeRecording'],
|
|
48
48
|
);
|
|
49
49
|
voiceStreamingMock.start.and.returnValue(Promise.resolve());
|
|
50
50
|
voiceStreamingMock.stop.and.returnValue(
|
|
@@ -65,6 +65,8 @@ describe('VoiceService', () => {
|
|
|
65
65
|
],
|
|
66
66
|
});
|
|
67
67
|
service = TestBed.inject(VoiceService);
|
|
68
|
+
spyOn(service as any, '_startKeyboardSound').and.stub();
|
|
69
|
+
spyOn(service as any, '_stopKeyboardSound').and.stub();
|
|
68
70
|
});
|
|
69
71
|
|
|
70
72
|
// ── Existing session lifecycle tests ──────────────────────────────────────
|
|
@@ -156,22 +158,21 @@ describe('VoiceService', () => {
|
|
|
156
158
|
expect(voiceStreamingMock.setAudioMuted).not.toHaveBeenCalled();
|
|
157
159
|
});
|
|
158
160
|
|
|
159
|
-
it('empty-audio path: sendPlaybackComplete
|
|
161
|
+
it('empty-audio path: sendPlaybackComplete after flush but acquisition stays blocked until "listening"', async () => {
|
|
160
162
|
const blocked = await startWssSession();
|
|
161
163
|
const initialLen = blocked.length;
|
|
162
164
|
|
|
163
|
-
//
|
|
165
|
+
// done with no binary audio arms unblock; flush sends playback complete to proxy
|
|
164
166
|
wsControl$.next({ event: 'speaking', text: 'hello' } as VoiceWsControlMessage);
|
|
165
167
|
wsControl$.next({ event: 'done' } as VoiceWsControlMessage);
|
|
166
168
|
|
|
167
|
-
|
|
169
|
+
expect(voiceStreamingMock.sendPlaybackComplete).not.toHaveBeenCalled();
|
|
170
|
+
(service as any)._flushTtsUnblock(false);
|
|
168
171
|
expect(voiceStreamingMock.sendPlaybackComplete).toHaveBeenCalledTimes(1);
|
|
169
172
|
|
|
170
|
-
// Acquisition must still be blocked — proxy hasn't confirmed LISTENING yet
|
|
171
173
|
const afterDone = blocked.slice(initialLen);
|
|
172
174
|
expect(afterDone.every((v) => v === true)).toBeTrue();
|
|
173
175
|
|
|
174
|
-
// Unblock only after proxy confirms
|
|
175
176
|
wsControl$.next({ event: 'listening' } as VoiceWsControlMessage);
|
|
176
177
|
expect(blocked[blocked.length - 1]).toBeFalse();
|
|
177
178
|
});
|
|
@@ -189,22 +190,19 @@ describe('VoiceService', () => {
|
|
|
189
190
|
|
|
190
191
|
// ── Audio preemption tests (SPEC-002) ────────────────────────────────────
|
|
191
192
|
|
|
192
|
-
it('second "speaking" cancels first audio: sendPlaybackComplete
|
|
193
|
+
it('second "speaking" cancels first audio: sendPlaybackComplete only after flush for the new turn', async () => {
|
|
193
194
|
await startWssSession();
|
|
194
195
|
voiceStreamingMock.sendPlaybackComplete.calls.reset();
|
|
195
196
|
|
|
196
|
-
// First turn: audio chunk arrives → _activeTtsSources = 1 (sync) → done sets _unblockAfterTts
|
|
197
197
|
wsControl$.next({ event: 'speaking', text: 'first' } as VoiceWsControlMessage);
|
|
198
|
-
ttsBinaryChunk$.next(new ArrayBuffer(4));
|
|
199
|
-
wsControl$.next({ event: 'done' } as VoiceWsControlMessage);
|
|
198
|
+
ttsBinaryChunk$.next(new ArrayBuffer(4));
|
|
199
|
+
wsControl$.next({ event: 'done' } as VoiceWsControlMessage);
|
|
200
200
|
|
|
201
|
-
// Second turn preempts while first audio is still "playing"
|
|
202
201
|
wsControl$.next({ event: 'speaking', text: 'second' } as VoiceWsControlMessage);
|
|
203
|
-
// _cancelAllTtsAudio() resets _activeTtsSources=0, _unblockAfterTts=false
|
|
204
|
-
|
|
205
|
-
// done with no audio → sendPlaybackComplete immediately (new turn, _activeTtsSources = 0)
|
|
206
202
|
wsControl$.next({ event: 'done' } as VoiceWsControlMessage);
|
|
207
203
|
|
|
204
|
+
expect(voiceStreamingMock.sendPlaybackComplete).not.toHaveBeenCalled();
|
|
205
|
+
(service as any)._flushTtsUnblock(false);
|
|
208
206
|
expect(voiceStreamingMock.sendPlaybackComplete).toHaveBeenCalledTimes(1);
|
|
209
207
|
});
|
|
210
208
|
|
|
@@ -226,35 +224,4 @@ describe('VoiceService', () => {
|
|
|
226
224
|
expect(voiceStreamingMock.sendPlaybackComplete).not.toHaveBeenCalled();
|
|
227
225
|
});
|
|
228
226
|
|
|
229
|
-
// ── Barge-in ──────────────────────────────────────────────────────────────
|
|
230
|
-
|
|
231
|
-
it('barge_in event cancels TTS audio and unblocks acquisition without sending tts_playback_complete', async () => {
|
|
232
|
-
await startWssSession();
|
|
233
|
-
voiceStreamingMock.sendPlaybackComplete.calls.reset();
|
|
234
|
-
|
|
235
|
-
// Simulate bot speaking with audio in flight
|
|
236
|
-
wsControl$.next({ event: 'speaking', text: 'hello' } as VoiceWsControlMessage);
|
|
237
|
-
ttsBinaryChunk$.next(new ArrayBuffer(4)); // _activeTtsSources++ synchronously
|
|
238
|
-
wsControl$.next({ event: 'done' } as VoiceWsControlMessage); // _unblockAfterTts = true
|
|
239
|
-
|
|
240
|
-
// Proxy detects user speech and sends barge_in
|
|
241
|
-
wsControl$.next({ event: 'barge_in' } as VoiceWsControlMessage);
|
|
242
|
-
|
|
243
|
-
// tts_playback_complete must NOT be sent — it was an interruption, not a completion
|
|
244
|
-
expect(voiceStreamingMock.sendPlaybackComplete).not.toHaveBeenCalled();
|
|
245
|
-
expect(voiceStreamingMock.setAudioMuted).not.toHaveBeenCalled();
|
|
246
|
-
expect((service as any)._isAcquisitionBlocked$.getValue()).toBe(false);
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
it('barge_in while no TTS is active does not throw and still unblocks acquisition', async () => {
|
|
250
|
-
await startWssSession();
|
|
251
|
-
voiceStreamingMock.sendPlaybackComplete.calls.reset();
|
|
252
|
-
|
|
253
|
-
// No speaking event — mic was never muted
|
|
254
|
-
expect(() => {
|
|
255
|
-
wsControl$.next({ event: 'barge_in' } as VoiceWsControlMessage);
|
|
256
|
-
}).not.toThrow();
|
|
257
|
-
|
|
258
|
-
expect(voiceStreamingMock.sendPlaybackComplete).not.toHaveBeenCalled();
|
|
259
|
-
});
|
|
260
227
|
});
|