@chat21/chat21-web-widget 5.1.32-rc9 → 5.1.33-rc8
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/.angular-mcp-cache/package.json +1 -0
- package/.cursor/angular18-accessibility-auditor-skill.md +442 -0
- package/.cursor/mcp.json +15 -0
- package/.github/workflows/playwright.yml +27 -0
- package/.playwright-mcp/console-2026-05-08T15-31-09-000Z.log +17 -0
- package/.playwright-mcp/console-2026-05-08T15-32-19-412Z.log +89 -0
- package/.playwright-mcp/console-2026-05-08T16-18-48-424Z.log +133 -0
- package/.playwright-mcp/console-2026-05-11T12-54-06-869Z.log +13 -0
- package/.playwright-mcp/console-2026-05-11T12-54-56-229Z.log +147 -0
- package/.playwright-mcp/console-2026-05-11T12-55-47-174Z.log +183 -0
- package/.playwright-mcp/console-2026-05-11T15-34-03-590Z.log +210 -0
- package/.playwright-mcp/console-2026-05-12T15-07-31-880Z.log +118 -0
- package/.playwright-mcp/page-2026-05-08T15-32-19-900Z.yml +851 -0
- package/.playwright-mcp/page-2026-05-08T15-32-47-264Z.yml +857 -0
- package/.playwright-mcp/page-2026-05-08T15-33-17-089Z.yml +1110 -0
- package/.playwright-mcp/page-2026-05-08T15-33-23-486Z.yml +1069 -0
- package/.playwright-mcp/page-2026-05-08T15-33-45-390Z.yml +1076 -0
- package/.playwright-mcp/page-2026-05-08T15-33-52-666Z.yml +1072 -0
- package/.playwright-mcp/page-2026-05-08T15-34-01-338Z.yml +1085 -0
- package/.playwright-mcp/page-2026-05-08T15-34-07-227Z.yml +1072 -0
- package/.playwright-mcp/page-2026-05-08T15-34-13-875Z.yml +1072 -0
- package/.playwright-mcp/page-2026-05-08T15-34-21-885Z.yml +1109 -0
- package/.playwright-mcp/page-2026-05-08T15-34-32-755Z.yml +1109 -0
- package/.playwright-mcp/page-2026-05-08T15-35-09-607Z.yml +1119 -0
- package/.playwright-mcp/page-2026-05-08T15-35-14-242Z.yml +1109 -0
- package/.playwright-mcp/page-2026-05-08T16-18-48-671Z.yml +44 -0
- package/.playwright-mcp/page-2026-05-08T16-18-52-753Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-19-13-919Z.yml +68 -0
- package/.playwright-mcp/page-2026-05-08T16-19-17-977Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-19-25-733Z.yml +120 -0
- package/.playwright-mcp/page-2026-05-08T16-19-29-252Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-19-39-269Z.yml +80 -0
- package/.playwright-mcp/page-2026-05-08T16-19-43-915Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-20-04-407Z.yml +81 -0
- 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 +86 -0
- package/.playwright-mcp/page-2026-05-08T16-21-39-154Z.yml +91 -0
- 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 +91 -0
- package/.playwright-mcp/page-2026-05-08T16-23-36-520Z.yml +0 -0
- package/.playwright-mcp/page-2026-05-08T16-23-46-805Z.yml +100 -0
- package/.playwright-mcp/page-2026-05-08T16-23-55-169Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-24-26-574Z.yml +91 -0
- 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 +91 -0
- package/.playwright-mcp/page-2026-05-08T16-26-47-443Z.yml +105 -0
- package/.playwright-mcp/page-2026-05-08T16-26-56-136Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-27-59-610Z.yml +48 -0
- package/.playwright-mcp/page-2026-05-11T12-54-07-180Z.yml +44 -0
- package/.playwright-mcp/page-2026-05-11T12-54-56-946Z.yml +4 -0
- package/.playwright-mcp/page-2026-05-11T12-55-47-503Z.yml +24 -0
- package/.playwright-mcp/page-2026-05-11T12-56-00-766Z.yml +28 -0
- package/.playwright-mcp/page-2026-05-11T12-56-06-438Z.yml +90 -0
- package/.playwright-mcp/page-2026-05-11T12-57-56-838Z.yml +106 -0
- package/.playwright-mcp/page-2026-05-11T12-58-00-124Z.yml +106 -0
- package/.playwright-mcp/page-2026-05-11T12-59-08-836Z.yml +61 -0
- package/.playwright-mcp/page-2026-05-11T12-59-12-088Z.yml +61 -0
- package/.playwright-mcp/page-2026-05-11T12-59-26-215Z.yml +69 -0
- package/.playwright-mcp/page-2026-05-11T12-59-29-519Z.yml +69 -0
- package/.playwright-mcp/page-2026-05-11T12-59-37-309Z.yml +0 -0
- package/.playwright-mcp/page-2026-05-11T12-59-39-968Z.yml +79 -0
- package/.playwright-mcp/page-2026-05-11T12-59-45-983Z.yml +78 -0
- package/.playwright-mcp/page-2026-05-11T12-59-49-951Z.yml +78 -0
- package/.playwright-mcp/page-2026-05-11T15-34-04-515Z.yml +0 -0
- package/.playwright-mcp/page-2026-05-12T15-07-32-171Z.yml +44 -0
- package/.playwright-mcp/page-2026-05-12T15-08-09-820Z.yml +119 -0
- package/CHANGELOG.md +54 -4
- package/angular.json +20 -3
- package/deploy_amazon_beta.sh +7 -17
- package/deploy_amazon_prod.sh +41 -0
- package/docs/TILEDESK_WIDGET_ACCESSIBILITY_STATEMENT_COMPLETE.md +379 -0
- package/env.sample +3 -2
- package/mocks/voice-websocket-mock/server.cjs +245 -0
- package/package.json +7 -3
- package/playwright-report/index.html +90 -0
- package/playwright.config.ts +41 -0
- package/src/app/app.component.html +2 -2
- package/src/app/app.component.scss +25 -14
- package/src/app/app.component.spec.ts +21 -6
- package/src/app/app.module.ts +4 -0
- package/src/app/component/conversation-detail/conversation/conversation.component.html +19 -11
- package/src/app/component/conversation-detail/conversation/conversation.component.scss +28 -0
- package/src/app/component/conversation-detail/conversation/conversation.component.spec.ts +644 -75
- package/src/app/component/conversation-detail/conversation/conversation.component.ts +61 -17
- package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.html +25 -13
- package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.spec.ts +123 -5
- package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.ts +1 -0
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.html +17 -7
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.scss +15 -3
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.spec.ts +249 -149
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.ts +0 -1
- package/src/app/component/conversation-detail/conversation-emojii/conversation-emojii.component.spec.ts +53 -3
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component copy.html +172 -0
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.html +112 -62
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss +133 -7
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.spec.ts +452 -78
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts +192 -84
- package/src/app/component/conversation-detail/conversation-header/conversation-header.component.html +113 -53
- package/src/app/component/conversation-detail/conversation-header/conversation-header.component.scss +12 -4
- package/src/app/component/conversation-detail/conversation-header/conversation-header.component.spec.ts +274 -29
- package/src/app/component/conversation-detail/conversation-internal-frame/conversation-internal-frame.component.html +23 -9
- package/src/app/component/conversation-detail/conversation-internal-frame/conversation-internal-frame.component.spec.ts +80 -8
- package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.html +29 -23
- package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.spec.ts +185 -16
- package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.ts +34 -14
- package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.html +43 -18
- package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.scss +56 -2
- package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.ts +135 -5
- package/src/app/component/error-alert/error-alert.component.spec.ts +65 -5
- package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.html +16 -7
- package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.scss +21 -0
- package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.spec.ts +89 -7
- package/src/app/component/form/form-builder/form-builder.component.html +1 -1
- package/src/app/component/form/form-builder/form-builder.component.spec.ts +163 -21
- package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.html +8 -4
- package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.scss +10 -5
- package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.spec.ts +90 -16
- package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.ts +26 -0
- package/src/app/component/form/inputs/form-label/form-label.component.spec.ts +45 -11
- package/src/app/component/form/inputs/form-radio-button/form-radio-button.component.spec.ts +24 -6
- package/src/app/component/form/inputs/form-select/form-select.component.spec.ts +14 -5
- package/src/app/component/form/inputs/form-text/form-text.component.html +14 -12
- package/src/app/component/form/inputs/form-text/form-text.component.scss +11 -1
- package/src/app/component/form/inputs/form-text/form-text.component.spec.ts +113 -17
- package/src/app/component/form/inputs/form-text/form-text.component.ts +26 -0
- package/src/app/component/form/inputs/form-textarea/form-textarea.component.html +13 -11
- package/src/app/component/form/inputs/form-textarea/form-textarea.component.scss +6 -5
- package/src/app/component/form/inputs/form-textarea/form-textarea.component.spec.ts +149 -13
- package/src/app/component/form/inputs/form-textarea/form-textarea.component.ts +26 -0
- package/src/app/component/form/prechat-form/prechat-form.component.html +14 -11
- package/src/app/component/form/prechat-form/prechat-form.component.spec.ts +102 -10
- package/src/app/component/form/prechat-form/prechat-form.component.ts +8 -1
- package/src/app/component/form/prechat-form-test-mock.ts +35 -0
- package/src/app/component/home/home.component.html +38 -31
- package/src/app/component/home/home.component.scss +4 -2
- package/src/app/component/home/home.component.spec.ts +226 -11
- package/src/app/component/home-conversations/home-conversations.component.html +30 -26
- package/src/app/component/home-conversations/home-conversations.component.scss +3 -0
- package/src/app/component/home-conversations/home-conversations.component.spec.ts +212 -36
- package/src/app/component/last-message/last-message.component.html +15 -9
- package/src/app/component/last-message/last-message.component.scss +16 -2
- package/src/app/component/last-message/last-message.component.spec.ts +204 -23
- package/src/app/component/launcher-button/launcher-button.component.html +8 -13
- package/src/app/component/launcher-button/launcher-button.component.spec.ts +104 -8
- package/src/app/component/list-all-conversations/list-all-conversations.component.html +12 -17
- package/src/app/component/list-all-conversations/list-all-conversations.component.scss +2 -0
- package/src/app/component/list-conversations/list-conversations.component.html +22 -22
- package/src/app/component/menu-options/menu-options.component.html +30 -20
- package/src/app/component/menu-options/menu-options.component.spec.ts +125 -9
- package/src/app/component/message/audio/audio.component.html +13 -15
- package/src/app/component/message/audio/audio.component.spec.ts +140 -5
- package/src/app/component/message/audio/audio.component.ts +1 -0
- package/src/app/component/message/audio-sync/audio-sync.component.scss +1 -0
- package/src/app/component/message/audio-sync/audio-sync.component.spec.ts +81 -1
- package/src/app/component/message/audio-sync/audio-sync.component.ts +133 -86
- package/src/app/component/message/avatar/avatar.component.html +2 -2
- package/src/app/component/message/avatar/avatar.component.spec.ts +99 -7
- package/src/app/component/message/bubble-message/bubble-message.component.html +39 -52
- package/src/app/component/message/bubble-message/bubble-message.component.scss +54 -1
- package/src/app/component/message/bubble-message/bubble-message.component.spec.ts +154 -57
- package/src/app/component/message/bubble-message/bubble-message.component.ts +138 -110
- package/src/app/component/message/buttons/action-button/action-button.component.html +3 -4
- package/src/app/component/message/buttons/action-button/action-button.component.spec.ts +49 -5
- package/src/app/component/message/buttons/link-button/link-button.component.scss +5 -8
- package/src/app/component/message/buttons/link-button/link-button.component.spec.ts +50 -5
- package/src/app/component/message/buttons/text-button/text-button.component.spec.ts +44 -5
- package/src/app/component/message/carousel/carousel.component.html +29 -16
- package/src/app/component/message/carousel/carousel.component.scss +20 -8
- package/src/app/component/message/carousel/carousel.component.spec.ts +80 -3
- package/src/app/component/message/carousel/carousel.component.ts +16 -0
- package/src/app/component/message/frame/frame.component.html +9 -4
- package/src/app/component/message/frame/frame.component.spec.ts +34 -15
- package/src/app/component/message/frame/frame.component.ts +7 -2
- package/src/app/component/message/html/html.component.html +1 -1
- package/src/app/component/message/html/html.component.scss +1 -1
- package/src/app/component/message/html/html.component.spec.ts +24 -7
- package/src/app/component/message/image/image.component.html +12 -10
- package/src/app/component/message/image/image.component.scss +16 -0
- package/src/app/component/message/image/image.component.spec.ts +101 -15
- package/src/app/component/message/image/image.component.ts +90 -51
- package/src/app/component/message/info-message/info-message.component.spec.ts +26 -14
- package/src/app/component/message/json-sources/json-sources.component.html +38 -0
- package/src/app/component/message/json-sources/json-sources.component.scss +197 -0
- package/src/app/component/message/json-sources/json-sources.component.ts +89 -0
- package/src/app/component/message/like-unlike/like-unlike.component.html +7 -9
- package/src/app/component/message/like-unlike/like-unlike.component.spec.ts +31 -3
- package/src/app/component/message/return-receipt/return-receipt.component.spec.ts +38 -17
- package/src/app/component/message/text/text.component.html +3 -3
- package/src/app/component/message/text/text.component.scss +80 -86
- package/src/app/component/message/text/text.component.spec.ts +106 -13
- package/src/app/component/message-attachment/message-attachment.component.spec.ts +134 -13
- package/src/app/component/selection-department/selection-department.component.html +21 -23
- package/src/app/component/selection-department/selection-department.component.spec.ts +159 -14
- package/src/app/component/selection-department/selection-department.component.ts +8 -1
- package/src/app/component/send-button/send-button.component.html +5 -13
- package/src/app/component/send-button/send-button.component.spec.ts +2 -2
- package/src/app/component/star-rating-widget/star-rating-widget.component.html +51 -81
- package/src/app/directives/tooltip.directive.spec.ts +8 -4
- package/src/app/modals/confirm-close/confirm-close.component.html +20 -8
- package/src/app/modals/confirm-close/confirm-close.component.scss +3 -0
- package/src/app/modals/confirm-close/confirm-close.component.spec.ts +13 -4
- package/src/app/modals/confirm-close/confirm-close.component.ts +8 -1
- package/src/app/pipe/html-entites-encode.pipe.spec.ts +35 -2
- package/src/app/pipe/marked.pipe.spec.ts +38 -2
- package/src/app/providers/app-config.service.ts +4 -2
- package/src/app/providers/brand.service.spec.ts +23 -2
- package/src/app/providers/brand.service.ts +1 -1
- package/src/app/providers/global-settings.service.spec.ts +1009 -14
- package/src/app/providers/global-settings.service.ts +30 -2
- package/src/app/providers/json-sources-parser.service.ts +182 -0
- package/src/app/providers/translator.service.ts +24 -6
- package/src/app/providers/tts-audio-playback-coordinator.service.spec.ts +117 -0
- package/src/app/providers/tts-audio-playback-coordinator.service.ts +39 -16
- package/src/app/providers/url-preview.service.ts +82 -0
- package/src/app/providers/voice/audio.types.ts +6 -0
- package/src/app/providers/voice/voice-streaming.service.spec.ts +23 -0
- package/src/app/providers/voice/voice-streaming.service.ts +710 -0
- package/src/app/providers/voice/voice-streaming.types.ts +113 -0
- package/src/app/providers/voice/voice.service.spec.ts +203 -3
- package/src/app/providers/voice/voice.service.ts +517 -13
- package/src/app/sass/_variables.scss +1 -1
- package/src/app/sass/animations.scss +19 -1
- package/src/app/utils/globals.ts +4 -0
- package/src/app/utils/json-sources-utils.ts +27 -0
- package/src/app/utils/url-utils.ts +98 -0
- package/src/app/utils/utils-resources.ts +1 -1
- package/src/assets/i18n/en.json +26 -1
- package/src/assets/i18n/es.json +106 -101
- package/src/assets/i18n/fr.json +106 -101
- package/src/assets/i18n/it.json +106 -99
- package/src/assets/twp/index-dev.html +18 -0
- package/src/assets/twp/tiledesk_widget_files/widget-css-override-example.css +14 -0
- package/src/chat21-core/providers/chat-manager.spec.ts +72 -0
- package/src/chat21-core/providers/scripts/script.service.spec.ts +12 -2
- package/src/chat21-core/utils/constants.ts +4 -0
- package/src/chat21-core/utils/utils-message.ts +23 -1
- package/src/widget-config-template.json +3 -1
- package/src/widget-config.json +28 -27
- package/test-results/.last-run.json +4 -0
- package/tests/widget-form-rich.spec.ts +67 -0
- package/tests/widget-index-dev-settings.spec.ts +52 -0
- package/tests/widget-twp-iframe.spec.ts +39 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configurazione sessione WSS /ws/voice.
|
|
3
|
+
*
|
|
4
|
+
* L'URL di connessione porta solo: token, mimeType, sttProvider, ttsProvider (ADR-002).
|
|
5
|
+
* I campi di identità di sessione viaggiano nel config frame JSON inviato subito dopo onopen.
|
|
6
|
+
*
|
|
7
|
+
* Il config frame fonde i campi di routing Chat21 (`sender`, `recipient`, `lang`) con la struttura
|
|
8
|
+
* prodotta da `chat21client.js#sendMessage` (`text`, `type`, `recipient_fullname`, `sender_fullname`,
|
|
9
|
+
* `attributes`, `metadata`, `channel_type`), così il proxy riceve lo stesso payload di un normale
|
|
10
|
+
* messaggio Chat21.
|
|
11
|
+
*/
|
|
12
|
+
export interface VoiceStreamingSessionConfig {
|
|
13
|
+
/** JWT auth token — finisce in `?token=` nell'URL. */
|
|
14
|
+
token: string;
|
|
15
|
+
/** Chat21 userId — campo `sender` del config frame. */
|
|
16
|
+
sender: string;
|
|
17
|
+
/** Chat21 conversationId, es. `support-group-<projectId>-<requestId>` — campo `recipient` del config frame. */
|
|
18
|
+
recipient: string;
|
|
19
|
+
/** Codice lingua BCP-47, default `'en'` — campo `lang` del config frame. */
|
|
20
|
+
lang?: string;
|
|
21
|
+
sttProvider?: string;
|
|
22
|
+
ttsProvider?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Base URL del WebSocket *senza* query, incluso path.
|
|
25
|
+
* Esempio: `wss://proxy.example.com/ws/voice` o `ws://127.0.0.1:4587/ws/voice` (mock locale)
|
|
26
|
+
* Se assente, si usa `voiceProxyWsBaseUrl` dal widget config caricato con `AppConfigService.getConfig()`.
|
|
27
|
+
*/
|
|
28
|
+
wsBaseUrl?: string;
|
|
29
|
+
/** Default 1000 — intervallo `MediaRecorder.start(timeslice)` in ms */
|
|
30
|
+
timesliceMs?: number;
|
|
31
|
+
/** Se valorizzato, ha precedenza sulle euristiche (es. `audio/webm;codecs=opus`) */
|
|
32
|
+
mimeType?: string;
|
|
33
|
+
|
|
34
|
+
// ── Campi sendMessage (chat21client.js#sendMessage outgoing_message) ──────────────────────────
|
|
35
|
+
/** Testo del messaggio — default `""` per il config frame. Corrisponde a `text` in `sendMessage`. */
|
|
36
|
+
text?: string;
|
|
37
|
+
/** Tipo del messaggio — default `"text"`. Corrisponde a `type` in `sendMessage`. */
|
|
38
|
+
type?: string;
|
|
39
|
+
/** Nome completo del destinatario. Corrisponde a `recipient_fullname` in `sendMessage`. */
|
|
40
|
+
recipient_fullname?: string;
|
|
41
|
+
/** Nome completo del mittente. Corrisponde a `sender_fullname` in `sendMessage`. */
|
|
42
|
+
sender_fullname?: string;
|
|
43
|
+
/** Attributi del messaggio (es. lingua, info utente). Corrisponde a `attributes` in `sendMessage`. */
|
|
44
|
+
attributes?: Record<string, unknown>;
|
|
45
|
+
/** Metadata del messaggio — default `""`. Corrisponde a `metadata` in `sendMessage`. */
|
|
46
|
+
metadata?: unknown;
|
|
47
|
+
/** Tipo di canale (es. `"direct"`). Corrisponde a `channel_type` in `sendMessage`. */
|
|
48
|
+
channel_type?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type VoiceStreamingConnectionState =
|
|
52
|
+
| 'idle'
|
|
53
|
+
| 'connecting'
|
|
54
|
+
| 'open'
|
|
55
|
+
| 'streaming'
|
|
56
|
+
| 'stopping'
|
|
57
|
+
| 'closed'
|
|
58
|
+
| 'error';
|
|
59
|
+
|
|
60
|
+
export interface VoiceStreamingServerMessage {
|
|
61
|
+
/** Originale: text JSON o testo, binary come ArrayBuffer */
|
|
62
|
+
data: string | ArrayBuffer;
|
|
63
|
+
isBinary: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface VoiceStreamingStopOptions {
|
|
67
|
+
discard?: boolean;
|
|
68
|
+
/** Dopo `MediaRecorder.stop`, attende un messaggio testuale dal server con l’URL (JSON `url` / `audioUrl` / …) prima di chiudere il socket */
|
|
69
|
+
awaitServerResultUrl?: boolean;
|
|
70
|
+
serverResultTimeoutMs?: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface VoiceStreamingStopResult {
|
|
74
|
+
blob: Blob | null;
|
|
75
|
+
mimeType: string;
|
|
76
|
+
/** Estratto dal messaggio testuale del server al termine dello stream, se `awaitServerResultUrl` e protocollo coerente */
|
|
77
|
+
resultUrl: string | null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Messaggio di controllo JSON dal proxy voce (`msg.event`). */
|
|
81
|
+
export type VoiceWsServerEventName =
|
|
82
|
+
| 'ready'
|
|
83
|
+
| 'session_started'
|
|
84
|
+
| 'listening'
|
|
85
|
+
| 'transcript'
|
|
86
|
+
| 'thinking'
|
|
87
|
+
| 'speaking'
|
|
88
|
+
| 'done'
|
|
89
|
+
| 'barge_in'
|
|
90
|
+
| 'error';
|
|
91
|
+
|
|
92
|
+
/** Messaggio di controllo JSON dal proxy (`msg.event`); altri campi sono ignorati se non gestiti. */
|
|
93
|
+
export type VoiceWsControlMessage = {
|
|
94
|
+
event: VoiceWsServerEventName;
|
|
95
|
+
requestId?: string;
|
|
96
|
+
text?: string;
|
|
97
|
+
isFinal?: boolean;
|
|
98
|
+
message?: string;
|
|
99
|
+
} & Record<string, unknown>;
|
|
100
|
+
|
|
101
|
+
/** Single word with its karaoke highlight state. */
|
|
102
|
+
export interface VoiceTtsKaraokeWord {
|
|
103
|
+
text: string;
|
|
104
|
+
state: 'future' | 'active' | 'past';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Emitted on each word transition while TTS plays over WebSocket. */
|
|
108
|
+
export interface VoiceTtsKaraokeFrame {
|
|
109
|
+
/** Full text of the utterance being spoken. */
|
|
110
|
+
text: string;
|
|
111
|
+
words: ReadonlyArray<VoiceTtsKaraokeWord>;
|
|
112
|
+
activeIndex: number;
|
|
113
|
+
}
|
|
@@ -1,11 +1,31 @@
|
|
|
1
1
|
import { TestBed } from '@angular/core/testing';
|
|
2
|
+
import { Subject } from 'rxjs';
|
|
2
3
|
|
|
3
4
|
import { VoiceService } from './voice.service';
|
|
4
5
|
import { VadService } from './vad.service';
|
|
6
|
+
import { VoiceStreamingService } from './voice-streaming.service';
|
|
7
|
+
import { TtsAudioPlaybackCoordinator } from '../tts-audio-playback-coordinator.service';
|
|
8
|
+
import { VoiceWsControlMessage } from './voice-streaming.types';
|
|
9
|
+
|
|
10
|
+
/** Stream con traccia audio reale (Web Audio), richiesto da `createMediaStreamSource` nei test WSS/legacy. */
|
|
11
|
+
function createFakeMicStreamWithAudioTrack(): MediaStream {
|
|
12
|
+
const ctx = new AudioContext();
|
|
13
|
+
const dest = ctx.createMediaStreamDestination();
|
|
14
|
+
const osc = ctx.createOscillator();
|
|
15
|
+
const gain = ctx.createGain();
|
|
16
|
+
gain.gain.value = 0.00001;
|
|
17
|
+
osc.connect(gain);
|
|
18
|
+
gain.connect(dest);
|
|
19
|
+
osc.start(0);
|
|
20
|
+
return dest.stream;
|
|
21
|
+
}
|
|
5
22
|
|
|
6
23
|
describe('VoiceService', () => {
|
|
7
24
|
let service: VoiceService;
|
|
8
25
|
let vadService: jasmine.SpyObj<VadService>;
|
|
26
|
+
let wsControl$: Subject<VoiceWsControlMessage>;
|
|
27
|
+
let ttsBinaryChunk$: Subject<ArrayBuffer>;
|
|
28
|
+
let voiceStreamingMock: jasmine.SpyObj<VoiceStreamingService>;
|
|
9
29
|
|
|
10
30
|
let mockVad: { start: jasmine.Spy; pause: jasmine.Spy; destroy: jasmine.Spy };
|
|
11
31
|
|
|
@@ -19,12 +39,36 @@ describe('VoiceService', () => {
|
|
|
19
39
|
vadService.ensureOnnxRuntimeEnv.and.returnValue(Promise.resolve());
|
|
20
40
|
vadService.createMicVad.and.returnValue(Promise.resolve(mockVad as any));
|
|
21
41
|
|
|
42
|
+
wsControl$ = new Subject<VoiceWsControlMessage>();
|
|
43
|
+
ttsBinaryChunk$ = new Subject<ArrayBuffer>();
|
|
44
|
+
|
|
45
|
+
voiceStreamingMock = jasmine.createSpyObj<VoiceStreamingService>(
|
|
46
|
+
'VoiceStreamingService',
|
|
47
|
+
['start', 'stop', 'setAudioMuted', 'sendPlaybackComplete', 'sendBargeIn'],
|
|
48
|
+
);
|
|
49
|
+
voiceStreamingMock.start.and.returnValue(Promise.resolve());
|
|
50
|
+
voiceStreamingMock.stop.and.returnValue(
|
|
51
|
+
Promise.resolve({ blob: null, mimeType: '', resultUrl: null }),
|
|
52
|
+
);
|
|
53
|
+
// Expose the subjects as readonly observables via Object.defineProperty
|
|
54
|
+
Object.defineProperty(voiceStreamingMock, 'wsControl$', { get: () => wsControl$.asObservable() });
|
|
55
|
+
Object.defineProperty(voiceStreamingMock, 'ttsBinaryChunk$', { get: () => ttsBinaryChunk$.asObservable() });
|
|
56
|
+
|
|
57
|
+
const ttsMock = { isTTSPlaying$: { subscribe: () => ({ unsubscribe: () => undefined }) } };
|
|
58
|
+
|
|
22
59
|
TestBed.configureTestingModule({
|
|
23
|
-
providers: [
|
|
60
|
+
providers: [
|
|
61
|
+
VoiceService,
|
|
62
|
+
{ provide: VadService, useValue: vadService },
|
|
63
|
+
{ provide: VoiceStreamingService, useValue: voiceStreamingMock },
|
|
64
|
+
{ provide: TtsAudioPlaybackCoordinator, useValue: ttsMock },
|
|
65
|
+
],
|
|
24
66
|
});
|
|
25
67
|
service = TestBed.inject(VoiceService);
|
|
26
68
|
});
|
|
27
69
|
|
|
70
|
+
// ── Existing session lifecycle tests ──────────────────────────────────────
|
|
71
|
+
|
|
28
72
|
it('startSession should call ensureOnnxRuntimeEnv', async () => {
|
|
29
73
|
const stream = new MediaStream();
|
|
30
74
|
spyOn(navigator.mediaDevices, 'getUserMedia').and.returnValue(Promise.resolve(stream));
|
|
@@ -47,9 +91,22 @@ describe('VoiceService', () => {
|
|
|
47
91
|
expect(mockVad.start).toHaveBeenCalled();
|
|
48
92
|
});
|
|
49
93
|
|
|
94
|
+
it('startSession with voiceIngressStream should not use MicVAD', async () => {
|
|
95
|
+
const stream = createFakeMicStreamWithAudioTrack();
|
|
96
|
+
spyOn(navigator.mediaDevices, 'getUserMedia').and.returnValue(Promise.resolve(stream));
|
|
97
|
+
|
|
98
|
+
await service.startSession({
|
|
99
|
+
voiceIngressStream: { token: 'JWT x', sender: 'user1', recipient: 'support-group-p1-req1' },
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(vadService.ensureOnnxRuntimeEnv).not.toHaveBeenCalled();
|
|
103
|
+
expect(vadService.createMicVad).not.toHaveBeenCalled();
|
|
104
|
+
});
|
|
105
|
+
|
|
50
106
|
it('stopSession should destroy VAD and stop tracks', async () => {
|
|
51
|
-
const
|
|
52
|
-
const
|
|
107
|
+
const stream = createFakeMicStreamWithAudioTrack();
|
|
108
|
+
const track = stream.getAudioTracks()[0];
|
|
109
|
+
spyOn(track, 'stop').and.callThrough();
|
|
53
110
|
spyOn(navigator.mediaDevices, 'getUserMedia').and.returnValue(Promise.resolve(stream));
|
|
54
111
|
|
|
55
112
|
await service.startSession({ onRecordingComplete: () => {} });
|
|
@@ -57,4 +114,147 @@ describe('VoiceService', () => {
|
|
|
57
114
|
|
|
58
115
|
expect(track.stop).toHaveBeenCalled();
|
|
59
116
|
});
|
|
117
|
+
|
|
118
|
+
// ── Playback-gated listening re-enablement tests ──────────────────────────
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Start a WSS session and return a helper that tracks _isAcquisitionBlocked$ emissions.
|
|
122
|
+
*/
|
|
123
|
+
async function startWssSession(): Promise<boolean[]> {
|
|
124
|
+
const stream = createFakeMicStreamWithAudioTrack();
|
|
125
|
+
spyOn(navigator.mediaDevices, 'getUserMedia').and.returnValue(Promise.resolve(stream));
|
|
126
|
+
const blocked: boolean[] = [];
|
|
127
|
+
service.isAcquisitionBlocked$.subscribe((v) => blocked.push(v));
|
|
128
|
+
await service.startSession({
|
|
129
|
+
voiceIngressStream: { token: 'JWT x', sender: 'user1', recipient: 'support-group-p1-req1' },
|
|
130
|
+
});
|
|
131
|
+
return blocked;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
it('acquisition stays blocked after _flushTtsUnblock; unblocks only on "listening"', async () => {
|
|
135
|
+
const blocked = await startWssSession();
|
|
136
|
+
const initialLen = blocked.length;
|
|
137
|
+
|
|
138
|
+
// Simulate proxy sequence: speaking → binary audio → done
|
|
139
|
+
wsControl$.next({ event: 'speaking', text: 'hello' } as VoiceWsControlMessage);
|
|
140
|
+
// Emit a tiny audio buffer so _activeTtsSources increments
|
|
141
|
+
ttsBinaryChunk$.next(new ArrayBuffer(4));
|
|
142
|
+
wsControl$.next({ event: 'done' } as VoiceWsControlMessage);
|
|
143
|
+
|
|
144
|
+
// sendPlaybackComplete must NOT have been called yet (audio hasn't ended)
|
|
145
|
+
expect(voiceStreamingMock.sendPlaybackComplete).not.toHaveBeenCalled();
|
|
146
|
+
|
|
147
|
+
// _isAcquisitionBlocked$ must still be true — no premature unblock
|
|
148
|
+
const afterDone = blocked.slice(initialLen);
|
|
149
|
+
expect(afterDone.every((v) => v === true)).toBeTrue();
|
|
150
|
+
|
|
151
|
+
// Now simulate "listening" arriving from proxy
|
|
152
|
+
wsControl$.next({ event: 'listening' } as VoiceWsControlMessage);
|
|
153
|
+
|
|
154
|
+
const afterListening = blocked[blocked.length - 1];
|
|
155
|
+
expect(afterListening).toBeFalse();
|
|
156
|
+
expect(voiceStreamingMock.setAudioMuted).not.toHaveBeenCalled();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('empty-audio path: sendPlaybackComplete immediately but acquisition stays blocked until "listening"', async () => {
|
|
160
|
+
const blocked = await startWssSession();
|
|
161
|
+
const initialLen = blocked.length;
|
|
162
|
+
|
|
163
|
+
// Simulate done arriving with NO binary audio (_activeTtsSources === 0)
|
|
164
|
+
wsControl$.next({ event: 'speaking', text: 'hello' } as VoiceWsControlMessage);
|
|
165
|
+
wsControl$.next({ event: 'done' } as VoiceWsControlMessage);
|
|
166
|
+
|
|
167
|
+
// Proxy signalled immediately
|
|
168
|
+
expect(voiceStreamingMock.sendPlaybackComplete).toHaveBeenCalledTimes(1);
|
|
169
|
+
|
|
170
|
+
// Acquisition must still be blocked — proxy hasn't confirmed LISTENING yet
|
|
171
|
+
const afterDone = blocked.slice(initialLen);
|
|
172
|
+
expect(afterDone.every((v) => v === true)).toBeTrue();
|
|
173
|
+
|
|
174
|
+
// Unblock only after proxy confirms
|
|
175
|
+
wsControl$.next({ event: 'listening' } as VoiceWsControlMessage);
|
|
176
|
+
expect(blocked[blocked.length - 1]).toBeFalse();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('"listening" event unblocks acquisition without mic mute toggles (AEC keeps capture open)', async () => {
|
|
180
|
+
await startWssSession();
|
|
181
|
+
|
|
182
|
+
wsControl$.next({ event: 'speaking', text: 'hi' } as VoiceWsControlMessage);
|
|
183
|
+
wsControl$.next({ event: 'done' } as VoiceWsControlMessage);
|
|
184
|
+
wsControl$.next({ event: 'listening' } as VoiceWsControlMessage);
|
|
185
|
+
|
|
186
|
+
expect(voiceStreamingMock.setAudioMuted).not.toHaveBeenCalled();
|
|
187
|
+
expect((service as any)._isAcquisitionBlocked$.getValue()).toBe(false);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// ── Audio preemption tests (SPEC-002) ────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
it('second "speaking" cancels first audio: sendPlaybackComplete called exactly once for the new turn', async () => {
|
|
193
|
+
await startWssSession();
|
|
194
|
+
voiceStreamingMock.sendPlaybackComplete.calls.reset();
|
|
195
|
+
|
|
196
|
+
// First turn: audio chunk arrives → _activeTtsSources = 1 (sync) → done sets _unblockAfterTts
|
|
197
|
+
wsControl$.next({ event: 'speaking', text: 'first' } as VoiceWsControlMessage);
|
|
198
|
+
ttsBinaryChunk$.next(new ArrayBuffer(4)); // _activeTtsSources++ synchronously
|
|
199
|
+
wsControl$.next({ event: 'done' } as VoiceWsControlMessage); // _unblockAfterTts = true
|
|
200
|
+
|
|
201
|
+
// Second turn preempts while first audio is still "playing"
|
|
202
|
+
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
|
+
wsControl$.next({ event: 'done' } as VoiceWsControlMessage);
|
|
207
|
+
|
|
208
|
+
expect(voiceStreamingMock.sendPlaybackComplete).toHaveBeenCalledTimes(1);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('second "speaking" resets counters so first audio ending does not trigger spurious sendPlaybackComplete', async () => {
|
|
212
|
+
await startWssSession();
|
|
213
|
+
voiceStreamingMock.sendPlaybackComplete.calls.reset();
|
|
214
|
+
|
|
215
|
+
wsControl$.next({ event: 'speaking', text: 'first' } as VoiceWsControlMessage);
|
|
216
|
+
ttsBinaryChunk$.next(new ArrayBuffer(4));
|
|
217
|
+
wsControl$.next({ event: 'done' } as VoiceWsControlMessage);
|
|
218
|
+
|
|
219
|
+
// Preempt
|
|
220
|
+
wsControl$.next({ event: 'speaking', text: 'second' } as VoiceWsControlMessage);
|
|
221
|
+
|
|
222
|
+
// Simulate first audio's onended firing AFTER the cancel (delayed Web Audio callback).
|
|
223
|
+
(service as any)._onTtsSourceEnded();
|
|
224
|
+
|
|
225
|
+
// _unblockAfterTts was cleared by cancel; no sendPlaybackComplete should fire
|
|
226
|
+
expect(voiceStreamingMock.sendPlaybackComplete).not.toHaveBeenCalled();
|
|
227
|
+
});
|
|
228
|
+
|
|
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
|
+
});
|
|
60
260
|
});
|