@chat21/chat21-web-widget 5.1.32-rc9 → 5.1.33-rc11
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 +61 -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.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 +63 -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 +242 -149
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.ts +7 -6
- 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 -61
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss +133 -16
- 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 +198 -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 +46 -18
- package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.scss +60 -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 +35 -3
- 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 +59 -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 +152 -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 +201 -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/pipe/marked.pipe.ts +51 -41
- 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 +59 -2
- package/src/app/providers/json-sources-parser.service.ts +175 -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 +702 -0
- package/src/app/providers/voice/voice-streaming.types.ts +112 -0
- package/src/app/providers/voice/voice.service.spec.ts +170 -3
- package/src/app/providers/voice/voice.service.ts +691 -17
- package/src/app/sass/_variables.scss +1 -1
- package/src/app/sass/animations.scss +19 -1
- package/src/app/utils/globals.ts +14 -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 +106 -100
- package/src/assets/i18n/es.json +107 -101
- package/src/assets/i18n/fr.json +107 -101
- package/src/assets/i18n/it.json +107 -99
- package/src/assets/sounds/keyboard.mp3 +0 -0
- 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/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,112 @@
|
|
|
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
|
+
| 'error';
|
|
90
|
+
|
|
91
|
+
/** Messaggio di controllo JSON dal proxy (`msg.event`); altri campi sono ignorati se non gestiti. */
|
|
92
|
+
export type VoiceWsControlMessage = {
|
|
93
|
+
event: VoiceWsServerEventName;
|
|
94
|
+
requestId?: string;
|
|
95
|
+
text?: string;
|
|
96
|
+
isFinal?: boolean;
|
|
97
|
+
message?: string;
|
|
98
|
+
} & Record<string, unknown>;
|
|
99
|
+
|
|
100
|
+
/** Single word with its karaoke highlight state. */
|
|
101
|
+
export interface VoiceTtsKaraokeWord {
|
|
102
|
+
text: string;
|
|
103
|
+
state: 'future' | 'active' | 'past';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Emitted on each word transition while TTS plays over WebSocket. */
|
|
107
|
+
export interface VoiceTtsKaraokeFrame {
|
|
108
|
+
/** Full text of the utterance being spoken. */
|
|
109
|
+
text: string;
|
|
110
|
+
words: ReadonlyArray<VoiceTtsKaraokeWord>;
|
|
111
|
+
activeIndex: number;
|
|
112
|
+
}
|
|
@@ -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,38 @@ 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', 'pauseRecording', 'resumeRecording'],
|
|
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);
|
|
68
|
+
spyOn(service as any, '_startKeyboardSound').and.stub();
|
|
69
|
+
spyOn(service as any, '_stopKeyboardSound').and.stub();
|
|
26
70
|
});
|
|
27
71
|
|
|
72
|
+
// ── Existing session lifecycle tests ──────────────────────────────────────
|
|
73
|
+
|
|
28
74
|
it('startSession should call ensureOnnxRuntimeEnv', async () => {
|
|
29
75
|
const stream = new MediaStream();
|
|
30
76
|
spyOn(navigator.mediaDevices, 'getUserMedia').and.returnValue(Promise.resolve(stream));
|
|
@@ -47,9 +93,22 @@ describe('VoiceService', () => {
|
|
|
47
93
|
expect(mockVad.start).toHaveBeenCalled();
|
|
48
94
|
});
|
|
49
95
|
|
|
96
|
+
it('startSession with voiceIngressStream should not use MicVAD', async () => {
|
|
97
|
+
const stream = createFakeMicStreamWithAudioTrack();
|
|
98
|
+
spyOn(navigator.mediaDevices, 'getUserMedia').and.returnValue(Promise.resolve(stream));
|
|
99
|
+
|
|
100
|
+
await service.startSession({
|
|
101
|
+
voiceIngressStream: { token: 'JWT x', sender: 'user1', recipient: 'support-group-p1-req1' },
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(vadService.ensureOnnxRuntimeEnv).not.toHaveBeenCalled();
|
|
105
|
+
expect(vadService.createMicVad).not.toHaveBeenCalled();
|
|
106
|
+
});
|
|
107
|
+
|
|
50
108
|
it('stopSession should destroy VAD and stop tracks', async () => {
|
|
51
|
-
const
|
|
52
|
-
const
|
|
109
|
+
const stream = createFakeMicStreamWithAudioTrack();
|
|
110
|
+
const track = stream.getAudioTracks()[0];
|
|
111
|
+
spyOn(track, 'stop').and.callThrough();
|
|
53
112
|
spyOn(navigator.mediaDevices, 'getUserMedia').and.returnValue(Promise.resolve(stream));
|
|
54
113
|
|
|
55
114
|
await service.startSession({ onRecordingComplete: () => {} });
|
|
@@ -57,4 +116,112 @@ describe('VoiceService', () => {
|
|
|
57
116
|
|
|
58
117
|
expect(track.stop).toHaveBeenCalled();
|
|
59
118
|
});
|
|
119
|
+
|
|
120
|
+
// ── Playback-gated listening re-enablement tests ──────────────────────────
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Start a WSS session and return a helper that tracks _isAcquisitionBlocked$ emissions.
|
|
124
|
+
*/
|
|
125
|
+
async function startWssSession(): Promise<boolean[]> {
|
|
126
|
+
const stream = createFakeMicStreamWithAudioTrack();
|
|
127
|
+
spyOn(navigator.mediaDevices, 'getUserMedia').and.returnValue(Promise.resolve(stream));
|
|
128
|
+
const blocked: boolean[] = [];
|
|
129
|
+
service.isAcquisitionBlocked$.subscribe((v) => blocked.push(v));
|
|
130
|
+
await service.startSession({
|
|
131
|
+
voiceIngressStream: { token: 'JWT x', sender: 'user1', recipient: 'support-group-p1-req1' },
|
|
132
|
+
});
|
|
133
|
+
return blocked;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
it('acquisition stays blocked after _flushTtsUnblock; unblocks only on "listening"', async () => {
|
|
137
|
+
const blocked = await startWssSession();
|
|
138
|
+
const initialLen = blocked.length;
|
|
139
|
+
|
|
140
|
+
// Simulate proxy sequence: speaking → binary audio → done
|
|
141
|
+
wsControl$.next({ event: 'speaking', text: 'hello' } as VoiceWsControlMessage);
|
|
142
|
+
// Emit a tiny audio buffer so _activeTtsSources increments
|
|
143
|
+
ttsBinaryChunk$.next(new ArrayBuffer(4));
|
|
144
|
+
wsControl$.next({ event: 'done' } as VoiceWsControlMessage);
|
|
145
|
+
|
|
146
|
+
// sendPlaybackComplete must NOT have been called yet (audio hasn't ended)
|
|
147
|
+
expect(voiceStreamingMock.sendPlaybackComplete).not.toHaveBeenCalled();
|
|
148
|
+
|
|
149
|
+
// _isAcquisitionBlocked$ must still be true — no premature unblock
|
|
150
|
+
const afterDone = blocked.slice(initialLen);
|
|
151
|
+
expect(afterDone.every((v) => v === true)).toBeTrue();
|
|
152
|
+
|
|
153
|
+
// Now simulate "listening" arriving from proxy
|
|
154
|
+
wsControl$.next({ event: 'listening' } as VoiceWsControlMessage);
|
|
155
|
+
|
|
156
|
+
const afterListening = blocked[blocked.length - 1];
|
|
157
|
+
expect(afterListening).toBeFalse();
|
|
158
|
+
expect(voiceStreamingMock.setAudioMuted).not.toHaveBeenCalled();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('empty-audio path: sendPlaybackComplete after flush but acquisition stays blocked until "listening"', async () => {
|
|
162
|
+
const blocked = await startWssSession();
|
|
163
|
+
const initialLen = blocked.length;
|
|
164
|
+
|
|
165
|
+
// done with no binary audio arms unblock; flush sends playback complete to proxy
|
|
166
|
+
wsControl$.next({ event: 'speaking', text: 'hello' } as VoiceWsControlMessage);
|
|
167
|
+
wsControl$.next({ event: 'done' } as VoiceWsControlMessage);
|
|
168
|
+
|
|
169
|
+
expect(voiceStreamingMock.sendPlaybackComplete).not.toHaveBeenCalled();
|
|
170
|
+
(service as any)._flushTtsUnblock(false);
|
|
171
|
+
expect(voiceStreamingMock.sendPlaybackComplete).toHaveBeenCalledTimes(1);
|
|
172
|
+
|
|
173
|
+
const afterDone = blocked.slice(initialLen);
|
|
174
|
+
expect(afterDone.every((v) => v === true)).toBeTrue();
|
|
175
|
+
|
|
176
|
+
wsControl$.next({ event: 'listening' } as VoiceWsControlMessage);
|
|
177
|
+
expect(blocked[blocked.length - 1]).toBeFalse();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('"listening" event unblocks acquisition without mic mute toggles (AEC keeps capture open)', async () => {
|
|
181
|
+
await startWssSession();
|
|
182
|
+
|
|
183
|
+
wsControl$.next({ event: 'speaking', text: 'hi' } as VoiceWsControlMessage);
|
|
184
|
+
wsControl$.next({ event: 'done' } as VoiceWsControlMessage);
|
|
185
|
+
wsControl$.next({ event: 'listening' } as VoiceWsControlMessage);
|
|
186
|
+
|
|
187
|
+
expect(voiceStreamingMock.setAudioMuted).not.toHaveBeenCalled();
|
|
188
|
+
expect((service as any)._isAcquisitionBlocked$.getValue()).toBe(false);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// ── Audio preemption tests (SPEC-002) ────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
it('second "speaking" cancels first audio: sendPlaybackComplete only after flush for the new turn', async () => {
|
|
194
|
+
await startWssSession();
|
|
195
|
+
voiceStreamingMock.sendPlaybackComplete.calls.reset();
|
|
196
|
+
|
|
197
|
+
wsControl$.next({ event: 'speaking', text: 'first' } as VoiceWsControlMessage);
|
|
198
|
+
ttsBinaryChunk$.next(new ArrayBuffer(4));
|
|
199
|
+
wsControl$.next({ event: 'done' } as VoiceWsControlMessage);
|
|
200
|
+
|
|
201
|
+
wsControl$.next({ event: 'speaking', text: 'second' } as VoiceWsControlMessage);
|
|
202
|
+
wsControl$.next({ event: 'done' } as VoiceWsControlMessage);
|
|
203
|
+
|
|
204
|
+
expect(voiceStreamingMock.sendPlaybackComplete).not.toHaveBeenCalled();
|
|
205
|
+
(service as any)._flushTtsUnblock(false);
|
|
206
|
+
expect(voiceStreamingMock.sendPlaybackComplete).toHaveBeenCalledTimes(1);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('second "speaking" resets counters so first audio ending does not trigger spurious sendPlaybackComplete', async () => {
|
|
210
|
+
await startWssSession();
|
|
211
|
+
voiceStreamingMock.sendPlaybackComplete.calls.reset();
|
|
212
|
+
|
|
213
|
+
wsControl$.next({ event: 'speaking', text: 'first' } as VoiceWsControlMessage);
|
|
214
|
+
ttsBinaryChunk$.next(new ArrayBuffer(4));
|
|
215
|
+
wsControl$.next({ event: 'done' } as VoiceWsControlMessage);
|
|
216
|
+
|
|
217
|
+
// Preempt
|
|
218
|
+
wsControl$.next({ event: 'speaking', text: 'second' } as VoiceWsControlMessage);
|
|
219
|
+
|
|
220
|
+
// Simulate first audio's onended firing AFTER the cancel (delayed Web Audio callback).
|
|
221
|
+
(service as any)._onTtsSourceEnded();
|
|
222
|
+
|
|
223
|
+
// _unblockAfterTts was cleared by cancel; no sendPlaybackComplete should fire
|
|
224
|
+
expect(voiceStreamingMock.sendPlaybackComplete).not.toHaveBeenCalled();
|
|
225
|
+
});
|
|
226
|
+
|
|
60
227
|
});
|