@chat21/chat21-web-widget 5.1.32-rc8 → 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.
Files changed (246) hide show
  1. package/.angular-mcp-cache/package.json +1 -0
  2. package/.cursor/angular18-accessibility-auditor-skill.md +442 -0
  3. package/.cursor/mcp.json +15 -0
  4. package/.github/workflows/playwright.yml +27 -0
  5. package/.playwright-mcp/console-2026-05-08T15-31-09-000Z.log +17 -0
  6. package/.playwright-mcp/console-2026-05-08T15-32-19-412Z.log +89 -0
  7. package/.playwright-mcp/console-2026-05-08T16-18-48-424Z.log +133 -0
  8. package/.playwright-mcp/console-2026-05-11T12-54-06-869Z.log +13 -0
  9. package/.playwright-mcp/console-2026-05-11T12-54-56-229Z.log +147 -0
  10. package/.playwright-mcp/console-2026-05-11T12-55-47-174Z.log +183 -0
  11. package/.playwright-mcp/console-2026-05-11T15-34-03-590Z.log +210 -0
  12. package/.playwright-mcp/console-2026-05-12T15-07-31-880Z.log +118 -0
  13. package/.playwright-mcp/page-2026-05-08T15-32-19-900Z.yml +851 -0
  14. package/.playwright-mcp/page-2026-05-08T15-32-47-264Z.yml +857 -0
  15. package/.playwright-mcp/page-2026-05-08T15-33-17-089Z.yml +1110 -0
  16. package/.playwright-mcp/page-2026-05-08T15-33-23-486Z.yml +1069 -0
  17. package/.playwright-mcp/page-2026-05-08T15-33-45-390Z.yml +1076 -0
  18. package/.playwright-mcp/page-2026-05-08T15-33-52-666Z.yml +1072 -0
  19. package/.playwright-mcp/page-2026-05-08T15-34-01-338Z.yml +1085 -0
  20. package/.playwright-mcp/page-2026-05-08T15-34-07-227Z.yml +1072 -0
  21. package/.playwright-mcp/page-2026-05-08T15-34-13-875Z.yml +1072 -0
  22. package/.playwright-mcp/page-2026-05-08T15-34-21-885Z.yml +1109 -0
  23. package/.playwright-mcp/page-2026-05-08T15-34-32-755Z.yml +1109 -0
  24. package/.playwright-mcp/page-2026-05-08T15-35-09-607Z.yml +1119 -0
  25. package/.playwright-mcp/page-2026-05-08T15-35-14-242Z.yml +1109 -0
  26. package/.playwright-mcp/page-2026-05-08T16-18-48-671Z.yml +44 -0
  27. package/.playwright-mcp/page-2026-05-08T16-18-52-753Z.png +0 -0
  28. package/.playwright-mcp/page-2026-05-08T16-19-13-919Z.yml +68 -0
  29. package/.playwright-mcp/page-2026-05-08T16-19-17-977Z.png +0 -0
  30. package/.playwright-mcp/page-2026-05-08T16-19-25-733Z.yml +120 -0
  31. package/.playwright-mcp/page-2026-05-08T16-19-29-252Z.png +0 -0
  32. package/.playwright-mcp/page-2026-05-08T16-19-39-269Z.yml +80 -0
  33. package/.playwright-mcp/page-2026-05-08T16-19-43-915Z.png +0 -0
  34. package/.playwright-mcp/page-2026-05-08T16-20-04-407Z.yml +81 -0
  35. package/.playwright-mcp/page-2026-05-08T16-20-08-984Z.png +0 -0
  36. package/.playwright-mcp/page-2026-05-08T16-20-32-397Z.png +0 -0
  37. package/.playwright-mcp/page-2026-05-08T16-20-58-658Z.png +0 -0
  38. package/.playwright-mcp/page-2026-05-08T16-21-12-320Z.yml +86 -0
  39. package/.playwright-mcp/page-2026-05-08T16-21-39-154Z.yml +91 -0
  40. package/.playwright-mcp/page-2026-05-08T16-21-45-420Z.png +0 -0
  41. package/.playwright-mcp/page-2026-05-08T16-22-21-062Z.yml +0 -0
  42. package/.playwright-mcp/page-2026-05-08T16-22-58-232Z.yml +91 -0
  43. package/.playwright-mcp/page-2026-05-08T16-23-36-520Z.yml +0 -0
  44. package/.playwright-mcp/page-2026-05-08T16-23-46-805Z.yml +100 -0
  45. package/.playwright-mcp/page-2026-05-08T16-23-55-169Z.png +0 -0
  46. package/.playwright-mcp/page-2026-05-08T16-24-26-574Z.yml +91 -0
  47. package/.playwright-mcp/page-2026-05-08T16-25-34-414Z.png +0 -0
  48. package/.playwright-mcp/page-2026-05-08T16-25-59-831Z.png +0 -0
  49. package/.playwright-mcp/page-2026-05-08T16-26-21-809Z.yml +91 -0
  50. package/.playwright-mcp/page-2026-05-08T16-26-47-443Z.yml +105 -0
  51. package/.playwright-mcp/page-2026-05-08T16-26-56-136Z.png +0 -0
  52. package/.playwright-mcp/page-2026-05-08T16-27-59-610Z.yml +48 -0
  53. package/.playwright-mcp/page-2026-05-11T12-54-07-180Z.yml +44 -0
  54. package/.playwright-mcp/page-2026-05-11T12-54-56-946Z.yml +4 -0
  55. package/.playwright-mcp/page-2026-05-11T12-55-47-503Z.yml +24 -0
  56. package/.playwright-mcp/page-2026-05-11T12-56-00-766Z.yml +28 -0
  57. package/.playwright-mcp/page-2026-05-11T12-56-06-438Z.yml +90 -0
  58. package/.playwright-mcp/page-2026-05-11T12-57-56-838Z.yml +106 -0
  59. package/.playwright-mcp/page-2026-05-11T12-58-00-124Z.yml +106 -0
  60. package/.playwright-mcp/page-2026-05-11T12-59-08-836Z.yml +61 -0
  61. package/.playwright-mcp/page-2026-05-11T12-59-12-088Z.yml +61 -0
  62. package/.playwright-mcp/page-2026-05-11T12-59-26-215Z.yml +69 -0
  63. package/.playwright-mcp/page-2026-05-11T12-59-29-519Z.yml +69 -0
  64. package/.playwright-mcp/page-2026-05-11T12-59-37-309Z.yml +0 -0
  65. package/.playwright-mcp/page-2026-05-11T12-59-39-968Z.yml +79 -0
  66. package/.playwright-mcp/page-2026-05-11T12-59-45-983Z.yml +78 -0
  67. package/.playwright-mcp/page-2026-05-11T12-59-49-951Z.yml +78 -0
  68. package/.playwright-mcp/page-2026-05-11T15-34-04-515Z.yml +0 -0
  69. package/.playwright-mcp/page-2026-05-12T15-07-32-171Z.yml +44 -0
  70. package/.playwright-mcp/page-2026-05-12T15-08-09-820Z.yml +119 -0
  71. package/CHANGELOG.md +61 -3
  72. package/angular.json +20 -3
  73. package/deploy_amazon_beta.sh +7 -17
  74. package/deploy_amazon_prod.sh +41 -0
  75. package/docs/TILEDESK_WIDGET_ACCESSIBILITY_STATEMENT_COMPLETE.md +379 -0
  76. package/env.sample +3 -2
  77. package/mocks/voice-websocket-mock/server.cjs +245 -0
  78. package/package.json +7 -3
  79. package/playwright-report/index.html +90 -0
  80. package/playwright.config.ts +41 -0
  81. package/src/app/app.component.html +2 -2
  82. package/src/app/app.component.scss +25 -14
  83. package/src/app/app.component.spec.ts +21 -6
  84. package/src/app/app.module.ts +4 -0
  85. package/src/app/component/conversation-detail/conversation/conversation.component.html +19 -11
  86. package/src/app/component/conversation-detail/conversation/conversation.component.scss +28 -0
  87. package/src/app/component/conversation-detail/conversation/conversation.component.spec.ts +644 -75
  88. package/src/app/component/conversation-detail/conversation/conversation.component.ts +61 -17
  89. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.html +25 -13
  90. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.spec.ts +123 -5
  91. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.ts +1 -0
  92. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.html +22 -9
  93. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.scss +23 -1
  94. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.spec.ts +249 -149
  95. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.ts +0 -1
  96. package/src/app/component/conversation-detail/conversation-emojii/conversation-emojii.component.spec.ts +53 -3
  97. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component copy.html +172 -0
  98. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.html +112 -62
  99. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss +133 -7
  100. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.spec.ts +452 -78
  101. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts +193 -79
  102. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.html +113 -53
  103. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.scss +12 -4
  104. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.spec.ts +274 -29
  105. package/src/app/component/conversation-detail/conversation-internal-frame/conversation-internal-frame.component.html +23 -9
  106. package/src/app/component/conversation-detail/conversation-internal-frame/conversation-internal-frame.component.spec.ts +80 -8
  107. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.html +29 -23
  108. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.spec.ts +185 -16
  109. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.ts +34 -14
  110. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.html +43 -19
  111. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.scss +63 -10
  112. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.ts +142 -12
  113. package/src/app/component/error-alert/error-alert.component.spec.ts +65 -5
  114. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.html +16 -7
  115. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.scss +21 -0
  116. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.spec.ts +89 -7
  117. package/src/app/component/form/form-builder/form-builder.component.html +1 -1
  118. package/src/app/component/form/form-builder/form-builder.component.spec.ts +163 -21
  119. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.html +8 -4
  120. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.scss +10 -5
  121. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.spec.ts +90 -16
  122. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.ts +26 -0
  123. package/src/app/component/form/inputs/form-label/form-label.component.spec.ts +45 -11
  124. package/src/app/component/form/inputs/form-radio-button/form-radio-button.component.spec.ts +24 -6
  125. package/src/app/component/form/inputs/form-select/form-select.component.spec.ts +14 -5
  126. package/src/app/component/form/inputs/form-text/form-text.component.html +14 -12
  127. package/src/app/component/form/inputs/form-text/form-text.component.scss +11 -1
  128. package/src/app/component/form/inputs/form-text/form-text.component.spec.ts +113 -17
  129. package/src/app/component/form/inputs/form-text/form-text.component.ts +26 -0
  130. package/src/app/component/form/inputs/form-textarea/form-textarea.component.html +13 -11
  131. package/src/app/component/form/inputs/form-textarea/form-textarea.component.scss +6 -5
  132. package/src/app/component/form/inputs/form-textarea/form-textarea.component.spec.ts +149 -13
  133. package/src/app/component/form/inputs/form-textarea/form-textarea.component.ts +26 -0
  134. package/src/app/component/form/prechat-form/prechat-form.component.html +14 -11
  135. package/src/app/component/form/prechat-form/prechat-form.component.spec.ts +102 -10
  136. package/src/app/component/form/prechat-form/prechat-form.component.ts +8 -1
  137. package/src/app/component/form/prechat-form-test-mock.ts +35 -0
  138. package/src/app/component/home/home.component.html +38 -31
  139. package/src/app/component/home/home.component.scss +4 -2
  140. package/src/app/component/home/home.component.spec.ts +226 -11
  141. package/src/app/component/home-conversations/home-conversations.component.html +30 -26
  142. package/src/app/component/home-conversations/home-conversations.component.scss +3 -0
  143. package/src/app/component/home-conversations/home-conversations.component.spec.ts +212 -36
  144. package/src/app/component/last-message/last-message.component.html +15 -9
  145. package/src/app/component/last-message/last-message.component.scss +16 -2
  146. package/src/app/component/last-message/last-message.component.spec.ts +204 -23
  147. package/src/app/component/launcher-button/launcher-button.component.html +8 -13
  148. package/src/app/component/launcher-button/launcher-button.component.spec.ts +104 -8
  149. package/src/app/component/list-all-conversations/list-all-conversations.component.html +12 -17
  150. package/src/app/component/list-all-conversations/list-all-conversations.component.scss +2 -0
  151. package/src/app/component/list-conversations/list-conversations.component.html +22 -22
  152. package/src/app/component/menu-options/menu-options.component.html +30 -20
  153. package/src/app/component/menu-options/menu-options.component.spec.ts +125 -9
  154. package/src/app/component/message/audio/audio.component.html +13 -15
  155. package/src/app/component/message/audio/audio.component.spec.ts +140 -5
  156. package/src/app/component/message/audio/audio.component.ts +1 -0
  157. package/src/app/component/message/audio-sync/audio-sync.component.scss +1 -1
  158. package/src/app/component/message/audio-sync/audio-sync.component.spec.ts +81 -1
  159. package/src/app/component/message/audio-sync/audio-sync.component.ts +134 -24
  160. package/src/app/component/message/avatar/avatar.component.html +2 -2
  161. package/src/app/component/message/avatar/avatar.component.spec.ts +99 -7
  162. package/src/app/component/message/bubble-message/bubble-message.component.html +39 -52
  163. package/src/app/component/message/bubble-message/bubble-message.component.scss +54 -1
  164. package/src/app/component/message/bubble-message/bubble-message.component.spec.ts +154 -57
  165. package/src/app/component/message/bubble-message/bubble-message.component.ts +138 -110
  166. package/src/app/component/message/buttons/action-button/action-button.component.html +3 -4
  167. package/src/app/component/message/buttons/action-button/action-button.component.spec.ts +49 -5
  168. package/src/app/component/message/buttons/link-button/link-button.component.scss +5 -8
  169. package/src/app/component/message/buttons/link-button/link-button.component.spec.ts +50 -5
  170. package/src/app/component/message/buttons/text-button/text-button.component.spec.ts +44 -5
  171. package/src/app/component/message/carousel/carousel.component.html +29 -16
  172. package/src/app/component/message/carousel/carousel.component.scss +20 -8
  173. package/src/app/component/message/carousel/carousel.component.spec.ts +80 -3
  174. package/src/app/component/message/carousel/carousel.component.ts +16 -0
  175. package/src/app/component/message/frame/frame.component.html +9 -4
  176. package/src/app/component/message/frame/frame.component.spec.ts +34 -15
  177. package/src/app/component/message/frame/frame.component.ts +7 -2
  178. package/src/app/component/message/html/html.component.html +1 -1
  179. package/src/app/component/message/html/html.component.scss +1 -1
  180. package/src/app/component/message/html/html.component.spec.ts +24 -7
  181. package/src/app/component/message/image/image.component.html +12 -10
  182. package/src/app/component/message/image/image.component.scss +16 -0
  183. package/src/app/component/message/image/image.component.spec.ts +101 -15
  184. package/src/app/component/message/image/image.component.ts +90 -51
  185. package/src/app/component/message/info-message/info-message.component.spec.ts +26 -14
  186. package/src/app/component/message/json-sources/json-sources.component.html +38 -0
  187. package/src/app/component/message/json-sources/json-sources.component.scss +197 -0
  188. package/src/app/component/message/json-sources/json-sources.component.ts +89 -0
  189. package/src/app/component/message/like-unlike/like-unlike.component.html +7 -9
  190. package/src/app/component/message/like-unlike/like-unlike.component.spec.ts +31 -3
  191. package/src/app/component/message/return-receipt/return-receipt.component.spec.ts +38 -17
  192. package/src/app/component/message/text/text.component.html +3 -3
  193. package/src/app/component/message/text/text.component.scss +80 -86
  194. package/src/app/component/message/text/text.component.spec.ts +106 -13
  195. package/src/app/component/message-attachment/message-attachment.component.spec.ts +134 -13
  196. package/src/app/component/selection-department/selection-department.component.html +21 -23
  197. package/src/app/component/selection-department/selection-department.component.spec.ts +159 -14
  198. package/src/app/component/selection-department/selection-department.component.ts +8 -1
  199. package/src/app/component/send-button/send-button.component.html +5 -13
  200. package/src/app/component/send-button/send-button.component.spec.ts +2 -2
  201. package/src/app/component/star-rating-widget/star-rating-widget.component.html +51 -81
  202. package/src/app/directives/tooltip.directive.spec.ts +8 -4
  203. package/src/app/modals/confirm-close/confirm-close.component.html +20 -8
  204. package/src/app/modals/confirm-close/confirm-close.component.scss +3 -0
  205. package/src/app/modals/confirm-close/confirm-close.component.spec.ts +13 -4
  206. package/src/app/modals/confirm-close/confirm-close.component.ts +8 -1
  207. package/src/app/pipe/html-entites-encode.pipe.spec.ts +35 -2
  208. package/src/app/pipe/marked.pipe.spec.ts +38 -2
  209. package/src/app/providers/app-config.service.ts +4 -2
  210. package/src/app/providers/brand.service.spec.ts +23 -2
  211. package/src/app/providers/brand.service.ts +1 -1
  212. package/src/app/providers/global-settings.service.spec.ts +1009 -14
  213. package/src/app/providers/global-settings.service.ts +30 -2
  214. package/src/app/providers/json-sources-parser.service.ts +182 -0
  215. package/src/app/providers/translator.service.ts +24 -6
  216. package/src/app/providers/tts-audio-playback-coordinator.service.spec.ts +117 -0
  217. package/src/app/providers/tts-audio-playback-coordinator.service.ts +45 -7
  218. package/src/app/providers/url-preview.service.ts +82 -0
  219. package/src/app/providers/voice/audio.types.ts +6 -0
  220. package/src/app/providers/voice/voice-streaming.service.spec.ts +23 -0
  221. package/src/app/providers/voice/voice-streaming.service.ts +710 -0
  222. package/src/app/providers/voice/voice-streaming.types.ts +113 -0
  223. package/src/app/providers/voice/voice.service.spec.ts +203 -3
  224. package/src/app/providers/voice/voice.service.ts +521 -12
  225. package/src/app/sass/_variables.scss +1 -1
  226. package/src/app/sass/animations.scss +19 -1
  227. package/src/app/utils/globals.ts +4 -0
  228. package/src/app/utils/json-sources-utils.ts +27 -0
  229. package/src/app/utils/url-utils.ts +98 -0
  230. package/src/app/utils/utils-resources.ts +1 -1
  231. package/src/assets/i18n/en.json +26 -1
  232. package/src/assets/i18n/es.json +106 -101
  233. package/src/assets/i18n/fr.json +106 -101
  234. package/src/assets/i18n/it.json +106 -99
  235. package/src/assets/twp/index-dev.html +18 -0
  236. package/src/assets/twp/tiledesk_widget_files/widget-css-override-example.css +14 -0
  237. package/src/chat21-core/providers/chat-manager.spec.ts +72 -0
  238. package/src/chat21-core/providers/scripts/script.service.spec.ts +12 -2
  239. package/src/chat21-core/utils/constants.ts +4 -0
  240. package/src/chat21-core/utils/utils-message.ts +23 -1
  241. package/src/widget-config-template.json +3 -1
  242. package/src/widget-config.json +28 -27
  243. package/test-results/.last-run.json +4 -0
  244. package/tests/widget-form-rich.spec.ts +67 -0
  245. package/tests/widget-index-dev-settings.spec.ts +52 -0
  246. 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: [VoiceService, { provide: VadService, useValue: vadService }],
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 track = jasmine.createSpyObj<MediaStreamTrack>('MediaStreamTrack', ['stop']);
52
- const stream = new MediaStream([track]);
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
  });