@chat21/chat21-web-widget 5.1.32-rc9 → 5.1.33-rc8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +54 -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-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 +17 -7
  93. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.scss +15 -3
  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 +192 -84
  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 -18
  111. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.scss +56 -2
  112. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.ts +135 -5
  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 -0
  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 +133 -86
  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 +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 +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 +517 -13
  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
@@ -32,4 +32,57 @@
32
32
  chat-audio {
33
33
  display: flex;
34
34
  }
35
- }
35
+ }
36
+ .streaming-text {
37
+ padding: 0;
38
+ margin: 0;
39
+ line-height: 1.5;
40
+
41
+ .stream-word {
42
+ display: inline;
43
+ opacity: 0;
44
+ animation: stream-word-in 200ms ease-out forwards;
45
+ }
46
+ }
47
+
48
+ @keyframes stream-word-in {
49
+ from {
50
+ opacity: 0;
51
+ transform: translateY(3px);
52
+ }
53
+ to {
54
+ opacity: 1;
55
+ transform: translateY(0);
56
+ }
57
+ }
58
+
59
+ // -- WSS TTS Karaoke ----------------------------------------------------------
60
+ .wss-karaoke {
61
+ // Match chat-text (ShadowDom) visual layout so there's no jump when voice opens.
62
+ // font-size must be set explicitly: the parent has font-size:10px but chat-text
63
+ // overrides it via :host { font-size: var(--font-size-bubble-message, 14px) }.
64
+ display: block;
65
+ margin: 0;
66
+ padding: 12px 16px;
67
+ font-size: var(--font-size-bubble-message, 14px);
68
+ line-height: 1.4em;
69
+ font-weight: 300;
70
+ overflow: hidden;
71
+ }
72
+
73
+ .wss-word {
74
+ display: inline;
75
+ transition: opacity 120ms ease;
76
+
77
+ &.future {
78
+ opacity: 0.35;
79
+ }
80
+
81
+ &.active {
82
+ opacity: 1;
83
+ }
84
+
85
+ &.past {
86
+ opacity: 1;
87
+ }
88
+ }
@@ -1,26 +1,62 @@
1
- import { async, ComponentFixture, TestBed } from '@angular/core/testing';
1
+ import { NO_ERRORS_SCHEMA } from '@angular/core';
2
+ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
2
3
  import { By } from '@angular/platform-browser';
4
+ import { of } from 'rxjs';
5
+ import { MAX_WIDTH_IMAGES, MIN_WIDTH_IMAGES } from 'src/chat21-core/utils/constants';
6
+ import { VoiceService } from 'src/app/providers/voice/voice.service';
7
+ import { JsonSourcesParserService } from 'src/app/providers/json-sources-parser.service';
3
8
 
4
9
  import { BubbleMessageComponent } from './bubble-message.component';
5
- import { NO_ERRORS_SCHEMA } from '@angular/core';
6
10
 
7
11
  describe('BubbleMessageComponent', () => {
8
12
  let component: BubbleMessageComponent;
9
13
  let fixture: ComponentFixture<BubbleMessageComponent>;
10
14
 
11
- beforeEach(async(() => {
15
+ const voiceServiceMock = {
16
+ isWssVoiceActive: false,
17
+ markProxyHandled: jasmine.createSpy('markProxyHandled'),
18
+ voiceTtsKaraoke$: of({ text: '', words: [], activeIndex: -1 }),
19
+ };
20
+
21
+ const jsonSourcesParserMock = {
22
+ parseBaseFromMessage: jasmine.createSpy('parseBaseFromMessage').and.returnValue(null),
23
+ enrichSources: jasmine.createSpy('enrichSources').and.resolveTo(null),
24
+ };
25
+
26
+ const textMessage: any = {
27
+ attributes: { projectId: 'p1' },
28
+ channel_type: 'group',
29
+ recipient: 'support-group-x',
30
+ recipient_fullname: 'Guest ',
31
+ sender: 'bot_1',
32
+ sender_fullname: 'BOT2',
33
+ status: 150,
34
+ text: 'Hello',
35
+ timestamp: 1629273999970,
36
+ type: 'text',
37
+ uid: 'm1',
38
+ isSender: false,
39
+ };
40
+
41
+ beforeEach(waitForAsync(() => {
12
42
  TestBed.configureTestingModule({
13
- declarations: [ BubbleMessageComponent ],
43
+ declarations: [BubbleMessageComponent],
14
44
  schemas: [NO_ERRORS_SCHEMA],
15
- imports: [
45
+ providers: [
46
+ { provide: VoiceService, useValue: voiceServiceMock },
47
+ { provide: JsonSourcesParserService, useValue: jsonSourcesParserMock },
16
48
  ],
17
- })
18
- .compileComponents();
49
+ }).compileComponents();
19
50
  }));
20
51
 
21
52
  beforeEach(() => {
22
53
  fixture = TestBed.createComponent(BubbleMessageComponent);
23
54
  component = fixture.componentInstance;
55
+ component.stylesMap = new Map([
56
+ ['buttonFontSize', '14px'],
57
+ ['themeColor', '#000'],
58
+ ['foregroundColor', '#fff'],
59
+ ]);
24
60
  fixture.detectChanges();
25
61
  });
26
62
 
@@ -28,54 +64,115 @@ describe('BubbleMessageComponent', () => {
28
64
  expect(component).toBeTruthy();
29
65
  });
30
66
 
31
- it('should have a "chat-text" child element', () => {
32
- const messages: any = {
33
- attributes: {
34
- projectId: "6013ec749b32000045be650e",
35
- tiledesk_message_id: "611cbf8ffb379b00346660e7"
36
- },
37
- channel_type: "group",
38
- recipient: "support-group-6013ec749b32000045be650e-4904aee91f8b487aad117bcda860549d",
39
- recipient_fullname: "Guest ",
40
- sender: "bot_602256f6c001b800342cb76f",
41
- sender_fullname: "BOT2",
42
- status: 150,
43
- text: "Hello 👋. I'm a bot 🤖.\n\nChoose one of the options below or write a message to reach our staff.",
44
- timestamp: 1629273999970,
45
- type: "text",
46
- uid: "-MhNI3eaIoLTOLoX3TAu",
47
- isSender: false
48
- }
49
- component.message = messages
50
- // component.textColor = 'black'
51
- fixture.detectChanges()
52
- const textChild = fixture.debugElement.query(By.css('chat-text'))
53
- textChild.properties.text
54
- expect(textChild).toBeTruthy();
55
- })
56
-
57
- it('should have a text inside "chat-text" child element', () => {
58
- const messages: any = {
59
- attributes: {
60
- projectId: "6013ec749b32000045be650e",
61
- tiledesk_message_id: "611cbf8ffb379b00346660e7"
62
- },
63
- channel_type: "group",
64
- recipient: "support-group-6013ec749b32000045be650e-4904aee91f8b487aad117bcda860549d",
65
- recipient_fullname: "Guest ",
66
- sender: "bot_602256f6c001b800342cb76f",
67
- sender_fullname: "BOT2",
68
- status: 150,
69
- text: "Hello 👋. I'm a bot 🤖.\n\nChoose one of the options below or write a message to reach our staff.",
70
- timestamp: 1629273999970,
71
- type: "text",
72
- uid: "-MhNI3eaIoLTOLoX3TAu",
73
- isSender: false
74
- }
75
- component.message = messages
76
- // component.textColor = 'black'
77
- fixture.detectChanges()
78
- const textChild = fixture.debugElement.query(By.css('chat-text'))
79
- expect(textChild.properties.text).toEqual(messages.text)
80
- })
67
+ it('should have a chat-text child for plain text messages', () => {
68
+ component.message = textMessage;
69
+ fixture.detectChanges();
70
+ expect(fixture.debugElement.query(By.css('chat-text'))).toBeTruthy();
71
+ });
72
+
73
+ it('should bind chat-text inputs from message', () => {
74
+ component.message = textMessage;
75
+ fixture.detectChanges();
76
+ const textChild = fixture.debugElement.query(By.css('chat-text'));
77
+ expect(textChild.properties.text).toEqual(textMessage.text);
78
+ });
79
+
80
+ describe('ngOnChanges', () => {
81
+ it('should compute sizeImage from message metadata object', () => {
82
+ component.message = {
83
+ ...textMessage,
84
+ metadata: { width: 100, height: 50 },
85
+ };
86
+ component.ngOnChanges();
87
+ expect(component.sizeImage.width).toBe(100);
88
+ });
89
+
90
+ it('should cap width when metadata exceeds MAX_WIDTH_IMAGES (calcImageSize)', () => {
91
+ component.message = {
92
+ ...textMessage,
93
+ metadata: { width: MAX_WIDTH_IMAGES * 2, height: 100 },
94
+ };
95
+ component.ngOnChanges();
96
+ expect(component.sizeImage.width).toBe(MAX_WIDTH_IMAGES);
97
+ });
98
+
99
+ it('should scale up narrow thumbnails when width <= 55 (calcImageSize)', () => {
100
+ component.message = {
101
+ ...textMessage,
102
+ metadata: { width: 40, height: 80 },
103
+ };
104
+ component.ngOnChanges();
105
+ expect(component.sizeImage.width).toBe(MIN_WIDTH_IMAGES);
106
+ expect(component.sizeImage.height).toBe(MIN_WIDTH_IMAGES / (40 / 80));
107
+ });
108
+
109
+ it('should keep metadata dimensions for mid-sized images', () => {
110
+ component.message = {
111
+ ...textMessage,
112
+ metadata: { width: 120, height: 60 },
113
+ };
114
+ component.ngOnChanges();
115
+ expect(component.sizeImage.width).toBe(120);
116
+ expect(component.sizeImage.height).toBe(60);
117
+ });
118
+
119
+ it('should leave width undefined when metadata has no width (calcImageSize)', () => {
120
+ component.message = {
121
+ ...textMessage,
122
+ metadata: { width: undefined, height: 10 },
123
+ };
124
+ component.ngOnChanges();
125
+ expect(component.sizeImage.width).toBeUndefined();
126
+ expect(component.sizeImage.height).toBe(10);
127
+ });
128
+
129
+ it('should ignore non-object metadata', () => {
130
+ component.message = { ...textMessage, metadata: 'x' as any };
131
+ component.ngOnChanges();
132
+ expect(component.sizeImage).toEqual({ width: 0, height: 0 });
133
+ });
134
+
135
+ it('should derive fullnameColor from fontColor', () => {
136
+ component.message = textMessage;
137
+ component.fontColor = '#ff0000';
138
+ component.ngOnChanges();
139
+ expect(component.fullnameColor).toBeTruthy();
140
+ });
141
+
142
+ it('should prefer sender fullname color when name present', () => {
143
+ component.message = { ...textMessage, sender_fullname: 'Anna' };
144
+ component.fontColor = '#00ff00';
145
+ component.ngOnChanges();
146
+ expect(component.fullnameColor).toBeTruthy();
147
+ });
148
+ });
149
+
150
+ describe('emitters', () => {
151
+ beforeEach(() => {
152
+ component.message = textMessage;
153
+ });
154
+
155
+ it('onBeforeMessageRenderFN should emit with sanitizer and message', () => {
156
+ spyOn(component.onBeforeMessageRender, 'emit');
157
+ const ev = { messageEl: {}, component: {} };
158
+ component.onBeforeMessageRenderFN(ev);
159
+ expect(component.onBeforeMessageRender.emit).toHaveBeenCalled();
160
+ const arg = (component.onBeforeMessageRender.emit as jasmine.Spy).calls.mostRecent().args[0];
161
+ expect(arg.message).toBe(component.message);
162
+ expect(arg.sanitizer).toBe(component.sanitizer);
163
+ });
164
+
165
+ it('onAfterMessageRenderFN should emit', () => {
166
+ spyOn(component.onAfterMessageRender, 'emit');
167
+ const ev = { messageEl: {}, component: {} };
168
+ component.onAfterMessageRenderFN(ev);
169
+ expect(component.onAfterMessageRender.emit).toHaveBeenCalled();
170
+ });
171
+
172
+ it('onElementRenderedFN should forward element and status', () => {
173
+ spyOn(component.onElementRendered, 'emit');
174
+ component.onElementRenderedFN({ element: 'image', status: true });
175
+ expect(component.onElementRendered.emit).toHaveBeenCalledWith({ element: 'image', status: true });
176
+ });
177
+ });
81
178
  });
@@ -1,149 +1,177 @@
1
- import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
1
+ import { Component, EventEmitter, HostBinding, Input, OnDestroy, OnInit, Output } from '@angular/core';
2
+ import { Observable, Subscription } from 'rxjs';
3
+ import { map, startWith } from 'rxjs/operators';
2
4
  import { DomSanitizer } from '@angular/platform-browser';
3
5
  import { MessageModel } from 'src/chat21-core/models/message';
4
- import { LoggerService } from 'src/chat21-core/providers/abstract/logger.service';
5
- import { LoggerInstance } from 'src/chat21-core/providers/logger/loggerInstance';
6
- import { MAX_WIDTH_IMAGES, MESSAGE_TYPE_MINE, MESSAGE_TYPE_OTHERS, MIN_WIDTH_IMAGES } from 'src/chat21-core/utils/constants';
6
+ import { MESSAGE_TYPE_MINE, MESSAGE_TYPE_OTHERS, TYPE_MSG_URL_PREVIEW } from 'src/chat21-core/utils/constants';
7
7
  import { convertColorToRGBA } from 'src/chat21-core/utils/utils';
8
- import { isAudio, isAudioTTS, isFile, isFrame, isImage, messageType } from 'src/chat21-core/utils/utils-message';
8
+ import { JsonSourcesParserService } from 'src/app/providers/json-sources-parser.service';
9
+ import { calcImageSize, isAudio, isAudioTTS, isFile, isFrame, isImage, isJsonSources, messageType } from 'src/chat21-core/utils/utils-message';
9
10
  import { getColorBck } from 'src/chat21-core/utils/utils-user';
11
+ import { VoiceService } from 'src/app/providers/voice/voice.service';
12
+ import { JsonSourceItem } from '../json-sources/json-sources.component';
13
+ import { VoiceTtsKaraokeWord } from 'src/app/providers/voice/voice-streaming.types';
10
14
 
11
15
  @Component({
12
16
  selector: 'chat-bubble-message',
13
17
  templateUrl: './bubble-message.component.html',
14
18
  styleUrls: ['./bubble-message.component.scss']
15
19
  })
16
- export class BubbleMessageComponent implements OnInit {
20
+ export class BubbleMessageComponent implements OnInit, OnDestroy {
17
21
 
18
22
  @Input() message: MessageModel;
19
23
  @Input() isSameSender: boolean;
20
24
  @Input() fontColor: string;
21
25
  @Input() stylesMap: Map<string, string>;
26
+ @Input() translationMap: Map<string, string>;
27
+ /** When true, a newly-arrived bot text message reveals its words one by one. */
28
+ @Input() streamOnArrival = false;
29
+ /** One-shot flag: set once in ngOnChanges, never reverts so animation isn't replayed. */
30
+ _isStreaming = false;
31
+ /** Precomputed word list; rebuilt only when the message text changes. */
32
+ _streamingWords: Array<{ word: string; index: number }> = [];
33
+ /** Live karaoke word states driven by voiceTtsKaraoke$ during an active WSS session. */
34
+ _wssKaraokeWords$?: Observable<VoiceTtsKaraokeWord[]>;
35
+
36
+ private _kSub?: Subscription;
22
37
  @Output() onBeforeMessageRender = new EventEmitter();
23
38
  @Output() onAfterMessageRender = new EventEmitter();
24
- @Output() onElementRendered = new EventEmitter<{element: string, status: boolean}>();
25
- isImage = isImage;
26
- isFile = isFile;
27
- isFrame = isFrame;
28
- isAudio = isAudio;
29
- isAudioTTS=isAudioTTS;
30
- convertColorToRGBA = convertColorToRGBA
31
-
32
- // ========== begin:: check message type functions ======= //
33
- messageType = messageType;
34
-
35
- MESSAGE_TYPE_MINE = MESSAGE_TYPE_MINE;
36
- MESSAGE_TYPE_OTHERS = MESSAGE_TYPE_OTHERS;
37
- // ========== end:: check message type functions ======= //
38
- sizeImage : { width: number, height: number}
39
- fullnameColor: string;
40
- private logger: LoggerService = LoggerInstance.getInstance()
41
- constructor(public sanitizer: DomSanitizer) { }
39
+ @Output() onElementRendered = new EventEmitter<{ element: string; status: boolean }>();
40
+
41
+ @HostBinding('class.no-background') get hostNoBackground() { return this.jsonSources !== null && this.jsonSources.length > 0; }
42
+ @HostBinding('class.json-resources') get hostIsJsonResources() { return this.jsonSources !== null && this.jsonSources.length > 0; }
43
+
44
+ readonly isImage = isImage;
45
+ readonly isFile = isFile;
46
+ readonly isFrame = isFrame;
47
+ readonly isAudio = isAudio;
48
+ readonly isJsonSources = isJsonSources;
49
+ readonly isAudioTTS = isAudioTTS;
50
+ readonly messageType = messageType;
51
+ readonly convertColorToRGBA = convertColorToRGBA;
52
+ readonly MESSAGE_TYPE_MINE = MESSAGE_TYPE_MINE;
53
+ readonly MESSAGE_TYPE_OTHERS = MESSAGE_TYPE_OTHERS;
54
+
55
+ sizeImage: { width: number; height: number } = { width: 0, height: 0 };
56
+ fullnameColor: string = '';
57
+ jsonSources: JsonSourceItem[] | null = null;
58
+
59
+ private urlPreviewReqId = 0;
60
+
61
+ constructor(
62
+ public sanitizer: DomSanitizer,
63
+ public voiceService: VoiceService,
64
+ private jsonSourcesParser: JsonSourcesParserService
65
+ ) { }
42
66
 
43
67
  ngOnInit() {
44
- // console.log("---- > MSG:", this.message);
45
- }
46
-
47
- ngOnChanges() {
48
- if (this.message && this.message.metadata && typeof this.message.metadata === 'object' ) {
49
- this.sizeImage = this.getMetadataSize(this.message.metadata)
68
+ // If this TTS message arrived while the voice proxy was active, mark it so
69
+ // audio-sync never replays it after the session ends.
70
+ if (isAudioTTS(this.message) && this.voiceService.isWssVoiceActive && this.message?.uid) {
71
+ this.voiceService.markProxyHandled(this.message.uid);
50
72
  }
51
73
 
52
- if(this.fontColor){
53
- this.fullnameColor = convertColorToRGBA(this.fontColor, 65)
54
- }
55
- if(this.message && this.message.sender_fullname && this.message.sender_fullname.trim() !== ''){
56
- this.fullnameColor = getColorBck(this.message.sender_fullname)
74
+ // Set up karaoke observable for TTS messages during WSS sessions.
75
+ if (isAudioTTS(this.message) && this.message?.text) {
76
+ const text = this.message.text;
77
+ const rawWords = text.trim().split(/\s+/).filter((w) => w.length > 0);
78
+ // Always start as 'past' (fully visible). The karaoke RAF loop will drive
79
+ // words through future→active→past for the current speaking turn; using
80
+ // 'future' here would dimm old/history messages the moment voice opens.
81
+ const initialWords: VoiceTtsKaraokeWord[] = rawWords.map((w) => ({ text: w, state: 'past' as const }));
82
+
83
+ this._wssKaraokeWords$ = this.voiceService.voiceTtsKaraoke$.pipe(
84
+ startWith({ text, words: initialWords, activeIndex: -1 }),
85
+ map((frame) =>
86
+ frame.text === text
87
+ ? (frame.words as VoiceTtsKaraokeWord[])
88
+ : initialWords,
89
+ ),
90
+ );
57
91
  }
92
+ }
58
93
 
94
+ ngOnDestroy(): void {
95
+ this._kSub?.unsubscribe();
96
+ this._kSub = undefined;
59
97
  }
60
98
 
61
- /**
62
- *
63
- * @param message
64
- */
65
- // getMetadataSize(metadata): any {
66
- // if(metadata.width === undefined){
67
- // metadata.width= MAX_WIDTH_IMAGES
68
- // }
69
- // if(metadata.height === undefined){
70
- // metadata.height = MAX_WIDTH_IMAGES
71
- // }
72
- // // const MAX_WIDTH_IMAGES = 300;
73
- // const sizeImage = {
74
- // width: metadata.width,
75
- // height: metadata.height
76
- // };
77
- // // that.g.wdLog(['message::: ', metadata);
78
- // if (metadata.width && metadata.width > MAX_WIDTH_IMAGES) {
79
- // const rapporto = (metadata['width'] / metadata['height']);
80
- // sizeImage.width = MAX_WIDTH_IMAGES;
81
- // sizeImage.height = MAX_WIDTH_IMAGES / rapporto;
82
- // }
83
- // return sizeImage; // h.toString();
84
- // }
85
-
86
- /**
87
- *
88
- * @param message
89
- */
90
- getMetadataSize(metadata): {width, height} {
91
- // if (metadata.width === undefined) {
92
- // metadata.width = MAX_WIDTH_IMAGES
93
- // }
94
- // if (metadata.height === undefined) {
95
- // metadata.height = MAX_WIDTH_IMAGES
96
- // }
97
-
98
- const sizeImage = {
99
- width: metadata.width,
100
- height: metadata.height
101
- };
102
-
103
-
104
- if (metadata.width && metadata.width < MAX_WIDTH_IMAGES) {
105
- if (metadata.width <= 55) {
106
- const ratio = (metadata['width'] / metadata['height']);
107
- sizeImage.width = MIN_WIDTH_IMAGES;
108
- sizeImage.height = MIN_WIDTH_IMAGES / ratio;
109
- } else if (metadata.width > 55) {
110
- sizeImage.width = metadata.width;
111
- sizeImage.height = metadata.height
112
- }
113
- } else if (metadata.width && metadata.width > MAX_WIDTH_IMAGES) {
114
- const ratio = (metadata['width'] / metadata['height']);
115
- sizeImage.width = MAX_WIDTH_IMAGES;
116
- sizeImage.height = MAX_WIDTH_IMAGES / ratio;
99
+ ngOnChanges(): void {
100
+ if (this.message?.metadata && typeof this.message.metadata === 'object') {
101
+ this.sizeImage = calcImageSize(this.message.metadata);
102
+ }
103
+
104
+ this.fullnameColor = this.fontColor
105
+ ? convertColorToRGBA(this.fontColor, 65)
106
+ : this.fullnameColor;
107
+
108
+ if (this.message?.sender_fullname?.trim()) {
109
+ this.fullnameColor = getColorBck(this.message.sender_fullname);
117
110
  }
118
- return sizeImage
119
- }
120
111
 
121
- // ========= begin:: event emitter function ============//
112
+ // One-shot: activate word streaming for newly-arrived bot text messages during a voice session.
113
+ // Reset isJustRecived so the animation never replays on subsequent change detection cycles.
114
+ if (
115
+ !this._isStreaming &&
116
+ this.streamOnArrival &&
117
+ this.message?.isJustRecived === true &&
118
+ this.messageType(this.MESSAGE_TYPE_OTHERS, this.message) &&
119
+ !this.isAudio(this.message) &&
120
+ !this.isAudioTTS(this.message) &&
121
+ this.message?.type !== 'html'
122
+ ) {
123
+ this._isStreaming = true;
124
+ this._streamingWords = (this.message.text ?? '')
125
+ .trim()
126
+ .split(/\s+/)
127
+ .filter(w => w.length > 0)
128
+ .map((word, index) => ({ word, index }));
129
+ this.message.isJustRecived = false;
130
+ }
122
131
 
123
- // returnOpenAttachment(event: String) {
124
- // this.onOpenAttachment.emit(event)
125
- // }
132
+ if (this.message?.type !== TYPE_MSG_URL_PREVIEW) {
133
+ this.jsonSources = null;
134
+ return;
135
+ }
126
136
 
127
- // /** */
128
- // returnClickOnAttachmentButton(event: any) {
129
- // this.onClickAttachmentButton.emit(event)
130
- // }
137
+ // url_preview payload can live on message root OR inside metadata/attributes depending on the integration.
138
+ const urlPreviewLike =
139
+ this.message?.type === TYPE_MSG_URL_PREVIEW
140
+ || this.message?.metadata?.type === TYPE_MSG_URL_PREVIEW
141
+ || this.message?.attributes?.type === TYPE_MSG_URL_PREVIEW;
142
+ if (urlPreviewLike) this.loadJsonSourcesFromUrlPreviewMessage();
143
+ }
131
144
 
132
- onBeforeMessageRenderFN(event){
133
- const messageOBJ = { message: this.message, sanitizer: this.sanitizer, messageEl: event.messageEl, component: event.component}
134
- this.onBeforeMessageRender.emit(messageOBJ)
145
+ private async loadJsonSourcesFromUrlPreviewMessage(): Promise<void> {
146
+ // Protect the UI from out-of-order async responses when the input `message` changes quickly.
147
+ const reqId = ++this.urlPreviewReqId;
148
+ // 1) Parse-only, so the UI can render immediately (no url-preview calls).
149
+ const baseSources = this.jsonSourcesParser.parseBaseFromMessage(this.message);
150
+ this.jsonSources = baseSources;
151
+
152
+ // 2) Enrich in background via url-preview, then merge missing fields.
153
+ const enriched = await this.jsonSourcesParser.enrichSources(baseSources);
154
+ if (reqId !== this.urlPreviewReqId) return;
155
+ this.jsonSources = enriched;
135
156
  }
136
157
 
137
- onAfterMessageRenderFN(event){
138
- const messageOBJ = { message: this.message, sanitizer: this.sanitizer, messageEl: event.messageEl, component: event.component}
139
- this.onAfterMessageRender.emit(messageOBJ)
158
+ trackWord(_index: number, item: { word: string; index: number }): number {
159
+ return item.index;
140
160
  }
141
161
 
142
- onElementRenderedFN(event){
143
- this.onElementRendered.emit({element: event.element, status: event.status})
162
+ trackKaraokeWord(index: number): number {
163
+ return index;
144
164
  }
145
165
 
146
- // ========= END:: event emitter function ============//
166
+ onBeforeMessageRenderFN(event: any): void {
167
+ this.onBeforeMessageRender.emit({ message: this.message, sanitizer: this.sanitizer, messageEl: event.messageEl, component: event.component });
168
+ }
147
169
 
170
+ onAfterMessageRenderFN(event: any): void {
171
+ this.onAfterMessageRender.emit({ message: this.message, sanitizer: this.sanitizer, messageEl: event.messageEl, component: event.component });
172
+ }
148
173
 
174
+ onElementRenderedFN(event: any): void {
175
+ this.onElementRendered.emit({ element: event.element, status: event.status });
176
+ }
149
177
  }
@@ -1,8 +1,7 @@
1
- <div #actionButton id="actionButton" class="button-in-msg action"
1
+ <div #actionButton class="button-in-msg action action-button"
2
2
  [ngClass]="{'disabled': isConversationArchived}"
3
- (click)="actionButtonAction()"
4
- (mouseover)="onMouseOver($event)"
3
+ (click)="actionButtonAction()"
4
+ (mouseover)="onMouseOver($event)"
5
5
  (mouseout)="onMouseOut($event)">
6
6
  {{button?.value}}
7
7
  </div>
8
- <!-- title="{{button?.value}}" -->