@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
@@ -1,23 +1,158 @@
1
1
  import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+ import { LoggerInstance } from 'src/chat21-core/providers/logger/loggerInstance';
3
+ import { CustomLogger } from 'src/chat21-core/providers/logger/customLogger';
4
+ import { NGXLogger } from 'ngx-logger';
2
5
 
3
6
  import { AudioComponent } from './audio.component';
4
7
 
5
- describe('AudioTrackComponent', () => {
8
+ describe('AudioComponent', () => {
6
9
  let component: AudioComponent;
7
10
  let fixture: ComponentFixture<AudioComponent>;
11
+ const ngxlogger = jasmine.createSpyObj('NGXLogger', ['log', 'trace', 'debug', 'warn', 'error', 'info']);
12
+ const customLogger = new CustomLogger(ngxlogger);
13
+ const arrayBuf = new ArrayBuffer(64);
14
+
15
+ const fakeBuffer = {
16
+ duration: 90,
17
+ getChannelData: () => new Float32Array(4000),
18
+ } as unknown as AudioBuffer;
8
19
 
9
20
  beforeEach(async () => {
21
+ LoggerInstance.setInstance(customLogger);
22
+ spyOn(window, 'fetch').and.returnValue(
23
+ Promise.resolve({
24
+ arrayBuffer: () => Promise.resolve(arrayBuf),
25
+ } as Response),
26
+ );
27
+ spyOn(AudioContext.prototype, 'decodeAudioData').and.returnValue(Promise.resolve(fakeBuffer));
28
+
10
29
  await TestBed.configureTestingModule({
11
- declarations: [ AudioComponent ]
30
+ declarations: [AudioComponent],
12
31
  })
13
- .compileComponents();
32
+ .overrideComponent(AudioComponent, {
33
+ set: {
34
+ template: `
35
+ <div class="audio-container">
36
+ <div class="audio-track"></div>
37
+ <div class="audio-player-custom">
38
+ <audio #audioElement></audio>
39
+ <canvas #canvasElement width="120" height="32"></canvas>
40
+ </div>
41
+ </div>`,
42
+ },
43
+ })
44
+ .compileComponents();
14
45
 
15
46
  fixture = TestBed.createComponent(AudioComponent);
16
47
  component = fixture.componentInstance;
17
- fixture.detectChanges();
48
+ component.stylesMap = new Map<string, string>([
49
+ ['bubbleSentBackground', 'rgba(10, 20, 30, 1)'],
50
+ ['bubbleSentTextColor', '#112233'],
51
+ ]);
52
+ component.color = '#000000';
18
53
  });
19
54
 
20
- it('should create', () => {
55
+ it('should create', async () => {
56
+ const blob = new Blob([new Uint8Array(arrayBuf.byteLength)], { type: 'audio/wav' });
57
+ component.audioBlob = blob;
58
+ fixture.detectChanges();
59
+ await fixture.whenStable();
60
+ fixture.detectChanges();
21
61
  expect(component).toBeTruthy();
22
62
  });
63
+
64
+ it('formatTime should pad seconds under 10', () => {
65
+ expect(component.formatTime(0)).toBe('0:00');
66
+ expect(component.formatTime(9)).toBe('0:09');
67
+ expect(component.formatTime(70)).toBe('1:10');
68
+ });
69
+
70
+ it('extractFirstColor should parse first rgba from gradient string', () => {
71
+ expect(component.extractFirstColor('linear-gradient(rgba(1, 2, 3, 0.5), red)')).toBe('rgba(1, 2, 3, 0.5)');
72
+ expect(component.extractFirstColor('no-color')).toBeNull();
73
+ });
74
+
75
+ it('drawWaveform should return early when canvas context missing', () => {
76
+ const canvas = document.createElement('canvas');
77
+ spyOn(canvas, 'getContext').and.returnValue(null);
78
+ (component as any).waveformCanvas = { nativeElement: canvas };
79
+ (component as any).audioBuffer = fakeBuffer;
80
+ (component as any).audioDuration = 10;
81
+ (component as any).audioElement = {
82
+ nativeElement: { currentTime: 0, paused: true },
83
+ };
84
+ expect(() => component.drawWaveform(fakeBuffer)).not.toThrow();
85
+ });
86
+
87
+ it('drawWaveform should render bars when context exists', () => {
88
+ const fillRect = jasmine.createSpy('fillRect');
89
+ const clearRect = jasmine.createSpy('clearRect');
90
+ const canvas = document.createElement('canvas');
91
+ canvas.width = 200;
92
+ canvas.height = 40;
93
+ spyOn(canvas, 'getContext').and.returnValue({ fillRect, clearRect } as any);
94
+ (component as any).waveformCanvas = { nativeElement: canvas };
95
+ (component as any).audioElement = {
96
+ nativeElement: { currentTime: 0, paused: true },
97
+ };
98
+ (component as any).audioDuration = 10;
99
+ component.drawWaveform(fakeBuffer);
100
+ expect(clearRect).toHaveBeenCalled();
101
+ expect(fillRect).toHaveBeenCalled();
102
+ });
103
+
104
+ it('ngAfterViewInit with blob should wire object URL and CSS vars', async () => {
105
+ const blob = new Blob([new Uint8Array(128)], { type: 'audio/wav' });
106
+ component.audioBlob = blob;
107
+ spyOn(URL, 'createObjectURL').and.returnValue('blob:mock-audio');
108
+ fixture.detectChanges();
109
+ await fixture.whenStable();
110
+ fixture.detectChanges();
111
+ expect(component.rawAudioUrl).toBe('blob:mock-audio');
112
+ expect(URL.createObjectURL).toHaveBeenCalledWith(blob);
113
+ });
114
+
115
+ it('ngAfterViewInit with metadata.src should fetch and decode', async () => {
116
+ component.audioBlob = null;
117
+ component.metadata = { src: 'blob:from-meta' };
118
+ fixture.detectChanges();
119
+ await fixture.whenStable();
120
+ fixture.detectChanges();
121
+ expect(window.fetch).toHaveBeenCalled();
122
+ expect(component.audioDuration).toBe(90);
123
+ });
124
+
125
+ it('playPauseAudio should toggle play state when buffer ready', () => {
126
+ spyOn(window, 'requestAnimationFrame').and.stub();
127
+ (component as any).audioBuffer = fakeBuffer;
128
+ (component as any).audioDuration = 10;
129
+ const play = jasmine.createSpy('play').and.returnValue(Promise.resolve());
130
+ const pause = jasmine.createSpy('pause');
131
+ const canvas = document.createElement('canvas');
132
+ canvas.width = 120;
133
+ canvas.height = 32;
134
+ spyOn(canvas, 'getContext').and.returnValue({
135
+ fillRect: jasmine.createSpy(),
136
+ clearRect: jasmine.createSpy(),
137
+ } as any);
138
+ (component as any).waveformCanvas = { nativeElement: canvas };
139
+ (component as any).audioElement = {
140
+ nativeElement: { paused: true, currentTime: 0, play, pause, ontimeupdate: null as any, onended: null as any },
141
+ };
142
+ (component as any).audioContext = { resume: jasmine.createSpy().and.returnValue(Promise.resolve()) };
143
+
144
+ component.playPauseAudio();
145
+ expect(play).toHaveBeenCalled();
146
+ expect(component.isPlaying).toBe(true);
147
+
148
+ (component as any).audioElement.nativeElement.paused = false;
149
+ component.playPauseAudio();
150
+ expect(pause).toHaveBeenCalled();
151
+ });
152
+
153
+ it('getAudioDuration should set audioDuration from decoded buffer', async () => {
154
+ component.metadata = { src: 'blob:x' };
155
+ await component.getAudioDuration();
156
+ expect(component.audioDuration).toBe(90);
157
+ });
23
158
  });
@@ -18,6 +18,7 @@ export class AudioComponent implements AfterViewInit {
18
18
  @Input() metadata: any | null = null;
19
19
  @Input() color: string;
20
20
  @Input() stylesMap: Map<string, string>;
21
+ @Input() translationMap: Map<string, string>;
21
22
 
22
23
  audioUrl: SafeUrl | null = null;
23
24
  rawAudioUrl: string | null = null;
@@ -17,7 +17,7 @@
17
17
  .lyrics {
18
18
  font-size: inherit;
19
19
  margin: 0;
20
- line-height: 1.4em;
20
+ // line-height: 1.4em;
21
21
  font-style: normal;
22
22
  letter-spacing: normal;
23
23
  font-stretch: normal;
@@ -1,14 +1,39 @@
1
1
  import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+ import { CommonModule } from '@angular/common';
2
3
 
3
4
  import { AudioSyncComponent } from './audio-sync.component';
5
+ import { TtsAudioPlaybackCoordinator } from 'src/app/providers/tts-audio-playback-coordinator.service';
6
+ import { VoiceService } from 'src/app/providers/voice/voice.service';
7
+ import { Globals } from 'src/app/utils/globals';
4
8
 
5
9
  describe('AudioSyncComponent', () => {
6
10
  let component: AudioSyncComponent;
7
11
  let fixture: ComponentFixture<AudioSyncComponent>;
12
+ let voiceService: { proxyTtsStreamUrl: string | null; proxyTtsUrl: string | null };
8
13
 
9
14
  beforeEach(async () => {
15
+ voiceService = {
16
+ proxyTtsStreamUrl: 'https://speech.example.com/api/tts/stream',
17
+ proxyTtsUrl: 'https://speech.example.com/api/tts',
18
+ };
19
+
10
20
  await TestBed.configureTestingModule({
11
- imports: [AudioSyncComponent]
21
+ declarations: [AudioSyncComponent],
22
+ imports: [CommonModule],
23
+ providers: [
24
+ {
25
+ provide: TtsAudioPlaybackCoordinator,
26
+ useValue: {
27
+ requestStart: (_ownerId: string, start: () => void) => start(),
28
+ releaseIfCurrent: jasmine.createSpy('releaseIfCurrent'),
29
+ release: jasmine.createSpy('release'),
30
+ stopAllPlayback$: { subscribe: () => ({ unsubscribe: () => undefined }) },
31
+ preemptPlayback$: { subscribe: () => ({ unsubscribe: () => undefined }) },
32
+ },
33
+ },
34
+ { provide: Globals, useValue: { tiledeskToken: 'JWT test-token', jwt: '' } },
35
+ { provide: VoiceService, useValue: voiceService },
36
+ ],
12
37
  })
13
38
  .compileComponents();
14
39
 
@@ -20,4 +45,59 @@ describe('AudioSyncComponent', () => {
20
45
  it('should create', () => {
21
46
  expect(component).toBeTruthy();
22
47
  });
48
+
49
+ it('starts TTS playback from the proxy streaming endpoint first', () => {
50
+ component.message = {
51
+ uid: 'm1',
52
+ type: 'tts',
53
+ text: 'hello',
54
+ metadata: {},
55
+ isJustRecived: true,
56
+ } as any;
57
+ const audio = document.createElement('audio');
58
+ const startStreaming = spyOn(component as any, 'startStreamingFromEndpoint').and.stub();
59
+
60
+ (component as any).startPlayback(audio);
61
+
62
+ expect(startStreaming).toHaveBeenCalledWith(
63
+ audio,
64
+ 'https://speech.example.com/api/tts/stream',
65
+ 'https://speech.example.com/api/tts',
66
+ undefined,
67
+ );
68
+ });
69
+
70
+ it('requests browser-compatible MP3 for proxy REST TTS by default', () => {
71
+ component.message = {
72
+ uid: 'm1',
73
+ type: 'tts',
74
+ text: 'hello',
75
+ metadata: {},
76
+ } as any;
77
+
78
+ const body = (component as any).buildTtsRequestBody({});
79
+
80
+ expect(body).toEqual({
81
+ text: 'hello',
82
+ streaming: true,
83
+ outputFormat: 'mp3_44100_128',
84
+ });
85
+ });
86
+
87
+ it('does not override an explicit TTS outputFormat from message voice settings', () => {
88
+ component.message = {
89
+ uid: 'm1',
90
+ type: 'tts',
91
+ text: 'hello',
92
+ metadata: {},
93
+ } as any;
94
+
95
+ const body = (component as any).buildTtsRequestBody({ outputFormat: 'pcm_16000' });
96
+
97
+ expect(body).toEqual({
98
+ text: 'hello',
99
+ streaming: true,
100
+ outputFormat: 'pcm_16000',
101
+ });
102
+ });
23
103
  });
@@ -9,12 +9,15 @@ import {
9
9
  SimpleChanges,
10
10
  ViewChild,
11
11
  } from '@angular/core';
12
+ import { Subscription } from 'rxjs';
12
13
  import { MessageModel } from 'src/chat21-core/models/message';
13
14
  import { TtsAudioPlaybackCoordinator } from 'src/app/providers/tts-audio-playback-coordinator.service';
15
+ import { VoiceService } from 'src/app/providers/voice/voice.service';
14
16
  import { Globals } from 'src/app/utils/globals';
15
17
 
16
18
  /** HAVE_METADATA: metadati già disponibili (tipico audio servito da cache). */
17
19
  const HAVE_METADATA = 1;
20
+ const BROWSER_TTS_OUTPUT_FORMAT = 'mp3_44100_128';
18
21
 
19
22
  @Component({
20
23
  selector: 'chat-audio-sync',
@@ -50,11 +53,14 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
50
53
  private playbackStarted = false;
51
54
  private streamAbort?: AbortController;
52
55
  private mediaSourceObjectUrl?: string;
56
+ private stopAllSub?: Subscription;
57
+ private preemptSub?: Subscription;
53
58
 
54
59
  constructor(
55
60
  private readonly cdr: ChangeDetectorRef,
56
61
  private readonly ttsPlayback: TtsAudioPlaybackCoordinator,
57
62
  private readonly globals: Globals,
63
+ private readonly voiceService: VoiceService,
58
64
  ) {}
59
65
 
60
66
  /** `false` = messaggio già in storico: niente autoplay / karaoke. Da `message.isJustRecived`. */
@@ -153,12 +159,59 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
153
159
  this.startPlayback(audio);
154
160
  });
155
161
  }, 200);
162
+
163
+ // Stop signal: user pressed X while this TTS was playing or queued.
164
+ this.stopAllSub = this.ttsPlayback.stopAllPlayback$.subscribe(() => {
165
+ if (!this.playbackRequested && !this.playbackStarted) {
166
+ return;
167
+ }
168
+ this.destroyed = true;
169
+ this.playbackStarted = false;
170
+ this.cleanupStreaming();
171
+ try {
172
+ audio.pause();
173
+ audio.currentTime = 0;
174
+ } catch {
175
+ /* ignore */
176
+ }
177
+ this.markAllWordsPast();
178
+ if (this.message) {
179
+ this.message.isJustRecived = false;
180
+ }
181
+ this.cdr.detectChanges();
182
+ });
183
+
184
+ // Preempt signal: a newer message requested start while this one was playing.
185
+ // Only react when the emitted id matches this component's own ownerId.
186
+ this.preemptSub = this.ttsPlayback.preemptPlayback$.subscribe((stoppedId) => {
187
+ if (stoppedId !== this.playbackOwnerId) {
188
+ return;
189
+ }
190
+ this.playbackStarted = false;
191
+ this.cleanupStreaming();
192
+ try {
193
+ audio.pause();
194
+ audio.currentTime = 0;
195
+ } catch {
196
+ /* ignore */
197
+ }
198
+ this.markAllWordsPast();
199
+ if (this.message) {
200
+ this.message.isJustRecived = false;
201
+ }
202
+ this.cdr.detectChanges();
203
+ // No releaseIfCurrent call — the coordinator already cleared currentOwnerId before emitting.
204
+ });
156
205
  }
157
206
 
158
207
  ngOnDestroy(): void {
159
208
  this.destroyed = true;
160
209
  this.playbackStarted = false;
161
210
  this.cleanupStreaming();
211
+ this.stopAllSub?.unsubscribe();
212
+ this.stopAllSub = undefined;
213
+ this.preemptSub?.unsubscribe();
214
+ this.preemptSub = undefined;
162
215
 
163
216
  const audio = this.audioRef?.nativeElement;
164
217
  if (audio) {
@@ -183,8 +236,28 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
183
236
  }
184
237
 
185
238
  private startPlayback(audio: HTMLAudioElement): void {
186
- const src = (this.message as any)?.metadata?.src as string | undefined;
187
- if (!src) {
239
+ const messageSrc = (this.message as any)?.metadata?.src as string | undefined;
240
+
241
+ if (this.message?.type === 'tts') {
242
+ const streamEndpoint = this.voiceService.proxyTtsStreamUrl;
243
+ const fullFileEndpoint = this.voiceService.proxyTtsUrl;
244
+ if (streamEndpoint) {
245
+ this.startStreamingFromEndpoint(audio, streamEndpoint, fullFileEndpoint, messageSrc);
246
+ return;
247
+ }
248
+ if (fullFileEndpoint) {
249
+ this.fetchFullFileFromEndpoint(audio, fullFileEndpoint);
250
+ return;
251
+ }
252
+ if (messageSrc) {
253
+ this.playDirectUrl(audio, messageSrc);
254
+ return;
255
+ }
256
+ this.handlePlaybackError();
257
+ return;
258
+ }
259
+
260
+ if (!messageSrc) {
188
261
  this.playbackStarted = false;
189
262
  this.ttsPlayback.releaseIfCurrent(this.playbackOwnerId);
190
263
  this.markAllWordsPast();
@@ -195,11 +268,10 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
195
268
  return;
196
269
  }
197
270
 
198
- if (this.message?.type === 'tts') {
199
- this.startStreamingFromEndpoint(audio, src);
200
- return;
201
- }
271
+ this.playDirectUrl(audio, messageSrc);
272
+ }
202
273
 
274
+ private playDirectUrl(audio: HTMLAudioElement, src: string): void {
203
275
  audio.src = src;
204
276
  try {
205
277
  audio.currentTime = 0;
@@ -209,16 +281,40 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
209
281
  audio.play().catch(() => this.handlePlaybackError());
210
282
  }
211
283
 
212
- private startStreamingFromEndpoint(audio: HTMLAudioElement, endpoint: string): void {
284
+ private startStreamingFromEndpoint(
285
+ audio: HTMLAudioElement,
286
+ endpoint: string,
287
+ fullFileEndpoint?: string | null,
288
+ directFallbackSrc?: string,
289
+ ): void {
213
290
  this.cleanupStreaming();
214
291
 
215
292
  const jwt = this.getJwtToken();
216
293
  const voiceSettings = this.getVoiceSettingsBody();
217
294
  const requestBody = this.buildTtsRequestBody(voiceSettings);
295
+ let fallbackUsed = false;
296
+ const fallback = () => {
297
+ if (fallbackUsed) {
298
+ this.handlePlaybackError();
299
+ return;
300
+ }
301
+ fallbackUsed = true;
302
+ this.cleanupStreaming();
303
+ if (fullFileEndpoint) {
304
+ this.fetchFullFileFromEndpoint(audio, fullFileEndpoint);
305
+ return;
306
+ }
307
+ if (directFallbackSrc) {
308
+ this.playDirectUrl(audio, directFallbackSrc);
309
+ return;
310
+ }
311
+ this.handlePlaybackError();
312
+ };
313
+
218
314
  // <audio src="..."> non può inviare header/body: serve fetch().
219
315
  const hasMse = typeof (window as any).MediaSource !== 'undefined';
220
316
  if (!hasMse) {
221
- this.fetchAsBlobAndPlay(audio, endpoint, jwt, requestBody);
317
+ fallback();
222
318
  return;
223
319
  }
224
320
 
@@ -250,14 +346,15 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
250
346
  }
251
347
 
252
348
  const headerType = (response.headers.get('content-type') || '').split(';')[0].trim();
253
- const mime = (headerType && MediaSourceCtor.isTypeSupported(headerType))
254
- ? headerType
255
- : 'audio/mpeg';
349
+ if (headerType && !MediaSourceCtor.isTypeSupported(headerType)) {
350
+ // Fallback: fetch completo e play via blob (no streaming).
351
+ fallback();
352
+ return;
353
+ }
256
354
 
355
+ const mime = headerType || 'audio/mpeg';
257
356
  if (!MediaSourceCtor.isTypeSupported(mime)) {
258
- this.cleanupStreaming();
259
- // Fallback: fetch completo e play via blob (no streaming).
260
- this.fetchAsBlobAndPlay(audio, endpoint, jwt, requestBody);
357
+ fallback();
261
358
  return;
262
359
  }
263
360
 
@@ -298,15 +395,14 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
298
395
  ) as ArrayBuffer;
299
396
  sourceBuffer.appendBuffer(ab);
300
397
  } catch {
301
- this.cleanupStreaming();
302
- this.fetchAsBlobAndPlay(audio, endpoint, jwt, requestBody);
398
+ fallback();
303
399
  }
304
400
  };
305
401
 
306
402
  sourceBuffer.addEventListener('updateend', () => {
307
403
  if (!started && this.playbackStarted && !this.destroyed) {
308
404
  started = true;
309
- audio.play().catch(() => this.handlePlaybackError());
405
+ audio.play().catch(() => fallback());
310
406
  }
311
407
  pump();
312
408
  });
@@ -330,7 +426,7 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
330
426
  tryEndOfStream();
331
427
  } catch {
332
428
  if (!abort.signal.aborted) {
333
- this.handlePlaybackError();
429
+ fallback();
334
430
  }
335
431
  }
336
432
  };
@@ -402,9 +498,6 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
402
498
  'Authorization': `${jwt}`
403
499
  };
404
500
 
405
- console.log('headers', headers);
406
- console.log('requestBody', requestBody);
407
-
408
501
  const response = await fetch(endpoint, {
409
502
  method: 'POST',
410
503
  headers,
@@ -430,16 +523,33 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
430
523
  }
431
524
  }
432
525
 
433
- private buildTtsRequestBody(voiceSettings: unknown): unknown {
526
+ private fetchFullFileFromEndpoint(audio: HTMLAudioElement, endpoint: string): void {
527
+ const jwt = this.getJwtToken();
528
+ const voiceSettings = this.getVoiceSettingsBody();
529
+ const requestBody = this.buildTtsRequestBody(voiceSettings, false);
530
+ void this.fetchAsBlobAndPlay(audio, endpoint, jwt, requestBody);
531
+ }
532
+
533
+ private buildTtsRequestBody(voiceSettings: unknown, streaming = true): unknown {
434
534
  const text = this.message?.text ?? '';
435
535
  if (
436
536
  voiceSettings &&
437
537
  typeof voiceSettings === 'object' &&
438
538
  !Array.isArray(voiceSettings)
439
539
  ) {
440
- return { ...(voiceSettings as Record<string, unknown>), text, streaming: true };
540
+ return {
541
+ outputFormat: BROWSER_TTS_OUTPUT_FORMAT,
542
+ ...(voiceSettings as Record<string, unknown>),
543
+ text,
544
+ streaming,
545
+ };
441
546
  }
442
- return { voiceSettings, text, streaming: true };
547
+ return {
548
+ voiceSettings,
549
+ text,
550
+ streaming,
551
+ outputFormat: BROWSER_TTS_OUTPUT_FORMAT,
552
+ };
443
553
  }
444
554
 
445
555
  private markAllWordsPast(): void {
@@ -1,9 +1,9 @@
1
1
  <div class="c21-icon-avatar">
2
2
  <div class="c21-avatar-image profile_image">
3
3
  <!-- is a BOT -->
4
- <img *ngIf="senderID?.indexOf('bot_') !== -1 || senderFullname.toLowerCase().includes('bot')" [src]="url" (error)="onBotImgError($event)" (load)="onLoadedBot($event)"/>
4
+ <img *ngIf="senderID?.indexOf('bot_') !== -1 || senderFullname.toLowerCase().includes('bot')" [src]="url" [attr.alt]="senderFullname || 'Bot'" role="img" (error)="onBotImgError($event)" (load)="onLoadedBot($event)"/>
5
5
  <!-- is a HUMAN -->
6
- <img *ngIf="senderID?.indexOf('bot_') === -1 && !senderFullname.toLowerCase().includes('bot')" [src]="url" (error)="onHumanImgError($event)" (load)="onLoadedHuman($event)"/>
6
+ <img *ngIf="senderID?.indexOf('bot_') === -1 && !senderFullname.toLowerCase().includes('bot')" [src]="url" [attr.alt]="senderFullname || 'User'" role="img" (error)="onHumanImgError($event)" (load)="onLoadedHuman($event)"/>
7
7
  </div>
8
8
  </div>
9
9