@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.
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 -4
  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.config.ts +41 -0
  80. package/src/app/app.component.html +2 -2
  81. package/src/app/app.component.scss +25 -14
  82. package/src/app/app.component.spec.ts +21 -6
  83. package/src/app/app.module.ts +4 -0
  84. package/src/app/component/conversation-detail/conversation/conversation.component.html +19 -11
  85. package/src/app/component/conversation-detail/conversation/conversation.component.scss +28 -0
  86. package/src/app/component/conversation-detail/conversation/conversation.component.spec.ts +644 -75
  87. package/src/app/component/conversation-detail/conversation/conversation.component.ts +63 -17
  88. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.html +25 -13
  89. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.spec.ts +123 -5
  90. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.ts +1 -0
  91. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.html +17 -7
  92. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.scss +15 -3
  93. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.spec.ts +242 -149
  94. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.ts +7 -6
  95. package/src/app/component/conversation-detail/conversation-emojii/conversation-emojii.component.spec.ts +53 -3
  96. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component copy.html +172 -0
  97. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.html +112 -61
  98. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss +133 -16
  99. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.spec.ts +452 -78
  100. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts +198 -84
  101. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.html +113 -53
  102. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.scss +12 -4
  103. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.spec.ts +274 -29
  104. package/src/app/component/conversation-detail/conversation-internal-frame/conversation-internal-frame.component.html +23 -9
  105. package/src/app/component/conversation-detail/conversation-internal-frame/conversation-internal-frame.component.spec.ts +80 -8
  106. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.html +29 -23
  107. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.spec.ts +185 -16
  108. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.ts +34 -14
  109. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.html +46 -18
  110. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.scss +60 -2
  111. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.ts +135 -5
  112. package/src/app/component/error-alert/error-alert.component.spec.ts +65 -5
  113. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.html +16 -7
  114. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.scss +21 -0
  115. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.spec.ts +89 -7
  116. package/src/app/component/form/form-builder/form-builder.component.html +1 -1
  117. package/src/app/component/form/form-builder/form-builder.component.spec.ts +163 -21
  118. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.html +8 -4
  119. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.scss +10 -5
  120. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.spec.ts +90 -16
  121. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.ts +26 -0
  122. package/src/app/component/form/inputs/form-label/form-label.component.spec.ts +45 -11
  123. package/src/app/component/form/inputs/form-radio-button/form-radio-button.component.spec.ts +24 -6
  124. package/src/app/component/form/inputs/form-select/form-select.component.spec.ts +14 -5
  125. package/src/app/component/form/inputs/form-text/form-text.component.html +14 -12
  126. package/src/app/component/form/inputs/form-text/form-text.component.scss +11 -1
  127. package/src/app/component/form/inputs/form-text/form-text.component.spec.ts +113 -17
  128. package/src/app/component/form/inputs/form-text/form-text.component.ts +35 -3
  129. package/src/app/component/form/inputs/form-textarea/form-textarea.component.html +13 -11
  130. package/src/app/component/form/inputs/form-textarea/form-textarea.component.scss +6 -5
  131. package/src/app/component/form/inputs/form-textarea/form-textarea.component.spec.ts +149 -13
  132. package/src/app/component/form/inputs/form-textarea/form-textarea.component.ts +26 -0
  133. package/src/app/component/form/prechat-form/prechat-form.component.html +14 -11
  134. package/src/app/component/form/prechat-form/prechat-form.component.spec.ts +102 -10
  135. package/src/app/component/form/prechat-form/prechat-form.component.ts +8 -1
  136. package/src/app/component/form/prechat-form-test-mock.ts +35 -0
  137. package/src/app/component/home/home.component.html +38 -31
  138. package/src/app/component/home/home.component.scss +4 -2
  139. package/src/app/component/home/home.component.spec.ts +226 -11
  140. package/src/app/component/home-conversations/home-conversations.component.html +30 -26
  141. package/src/app/component/home-conversations/home-conversations.component.scss +3 -0
  142. package/src/app/component/home-conversations/home-conversations.component.spec.ts +212 -36
  143. package/src/app/component/last-message/last-message.component.html +15 -9
  144. package/src/app/component/last-message/last-message.component.scss +16 -2
  145. package/src/app/component/last-message/last-message.component.spec.ts +204 -23
  146. package/src/app/component/launcher-button/launcher-button.component.html +8 -13
  147. package/src/app/component/launcher-button/launcher-button.component.spec.ts +104 -8
  148. package/src/app/component/list-all-conversations/list-all-conversations.component.html +12 -17
  149. package/src/app/component/list-all-conversations/list-all-conversations.component.scss +2 -0
  150. package/src/app/component/list-conversations/list-conversations.component.html +22 -22
  151. package/src/app/component/menu-options/menu-options.component.html +30 -20
  152. package/src/app/component/menu-options/menu-options.component.spec.ts +125 -9
  153. package/src/app/component/message/audio/audio.component.html +13 -15
  154. package/src/app/component/message/audio/audio.component.spec.ts +140 -5
  155. package/src/app/component/message/audio/audio.component.ts +1 -0
  156. package/src/app/component/message/audio-sync/audio-sync.component.scss +1 -0
  157. package/src/app/component/message/audio-sync/audio-sync.component.spec.ts +81 -1
  158. package/src/app/component/message/audio-sync/audio-sync.component.ts +133 -86
  159. package/src/app/component/message/avatar/avatar.component.html +2 -2
  160. package/src/app/component/message/avatar/avatar.component.spec.ts +99 -7
  161. package/src/app/component/message/bubble-message/bubble-message.component.html +39 -52
  162. package/src/app/component/message/bubble-message/bubble-message.component.scss +59 -1
  163. package/src/app/component/message/bubble-message/bubble-message.component.spec.ts +154 -57
  164. package/src/app/component/message/bubble-message/bubble-message.component.ts +152 -110
  165. package/src/app/component/message/buttons/action-button/action-button.component.html +3 -4
  166. package/src/app/component/message/buttons/action-button/action-button.component.spec.ts +49 -5
  167. package/src/app/component/message/buttons/link-button/link-button.component.scss +5 -8
  168. package/src/app/component/message/buttons/link-button/link-button.component.spec.ts +50 -5
  169. package/src/app/component/message/buttons/text-button/text-button.component.spec.ts +44 -5
  170. package/src/app/component/message/carousel/carousel.component.html +29 -16
  171. package/src/app/component/message/carousel/carousel.component.scss +20 -8
  172. package/src/app/component/message/carousel/carousel.component.spec.ts +80 -3
  173. package/src/app/component/message/carousel/carousel.component.ts +16 -0
  174. package/src/app/component/message/frame/frame.component.html +9 -4
  175. package/src/app/component/message/frame/frame.component.spec.ts +34 -15
  176. package/src/app/component/message/frame/frame.component.ts +7 -2
  177. package/src/app/component/message/html/html.component.html +1 -1
  178. package/src/app/component/message/html/html.component.scss +1 -1
  179. package/src/app/component/message/html/html.component.spec.ts +24 -7
  180. package/src/app/component/message/image/image.component.html +12 -10
  181. package/src/app/component/message/image/image.component.scss +16 -0
  182. package/src/app/component/message/image/image.component.spec.ts +101 -15
  183. package/src/app/component/message/image/image.component.ts +90 -51
  184. package/src/app/component/message/info-message/info-message.component.spec.ts +26 -14
  185. package/src/app/component/message/json-sources/json-sources.component.html +38 -0
  186. package/src/app/component/message/json-sources/json-sources.component.scss +201 -0
  187. package/src/app/component/message/json-sources/json-sources.component.ts +89 -0
  188. package/src/app/component/message/like-unlike/like-unlike.component.html +7 -9
  189. package/src/app/component/message/like-unlike/like-unlike.component.spec.ts +31 -3
  190. package/src/app/component/message/return-receipt/return-receipt.component.spec.ts +38 -17
  191. package/src/app/component/message/text/text.component.html +3 -3
  192. package/src/app/component/message/text/text.component.scss +80 -86
  193. package/src/app/component/message/text/text.component.spec.ts +106 -13
  194. package/src/app/component/message-attachment/message-attachment.component.spec.ts +134 -13
  195. package/src/app/component/selection-department/selection-department.component.html +21 -23
  196. package/src/app/component/selection-department/selection-department.component.spec.ts +159 -14
  197. package/src/app/component/selection-department/selection-department.component.ts +8 -1
  198. package/src/app/component/send-button/send-button.component.html +5 -13
  199. package/src/app/component/send-button/send-button.component.spec.ts +2 -2
  200. package/src/app/component/star-rating-widget/star-rating-widget.component.html +51 -81
  201. package/src/app/directives/tooltip.directive.spec.ts +8 -4
  202. package/src/app/modals/confirm-close/confirm-close.component.html +20 -8
  203. package/src/app/modals/confirm-close/confirm-close.component.scss +3 -0
  204. package/src/app/modals/confirm-close/confirm-close.component.spec.ts +13 -4
  205. package/src/app/modals/confirm-close/confirm-close.component.ts +8 -1
  206. package/src/app/pipe/html-entites-encode.pipe.spec.ts +35 -2
  207. package/src/app/pipe/marked.pipe.spec.ts +38 -2
  208. package/src/app/pipe/marked.pipe.ts +51 -41
  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 +59 -2
  214. package/src/app/providers/json-sources-parser.service.ts +175 -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 +39 -16
  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 +702 -0
  222. package/src/app/providers/voice/voice-streaming.types.ts +112 -0
  223. package/src/app/providers/voice/voice.service.spec.ts +170 -3
  224. package/src/app/providers/voice/voice.service.ts +691 -17
  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 +14 -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 +106 -100
  232. package/src/assets/i18n/es.json +107 -101
  233. package/src/assets/i18n/fr.json +107 -101
  234. package/src/assets/i18n/it.json +107 -99
  235. package/src/assets/sounds/keyboard.mp3 +0 -0
  236. package/src/assets/twp/index-dev.html +18 -0
  237. package/src/assets/twp/tiledesk_widget_files/widget-css-override-example.css +14 -0
  238. package/src/chat21-core/providers/chat-manager.spec.ts +72 -0
  239. package/src/chat21-core/providers/scripts/script.service.spec.ts +12 -2
  240. package/src/chat21-core/utils/constants.ts +4 -0
  241. package/src/chat21-core/utils/utils-message.ts +23 -1
  242. package/src/widget-config-template.json +3 -1
  243. package/src/widget-config.json +28 -27
  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,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: [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);
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 track = jasmine.createSpyObj<MediaStreamTrack>('MediaStreamTrack', ['stop']);
52
- const stream = new MediaStream([track]);
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
  });