@chat21/chat21-web-widget 5.1.33 → 5.1.34-rc1

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 (201) 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/docker-community-push-latest.yml +23 -13
  5. package/.github/workflows/docker-image-tag-community-tag-push.yml +22 -12
  6. package/.github/workflows/playwright.yml +27 -0
  7. package/CHANGELOG.md +130 -6
  8. package/Dockerfile +4 -5
  9. package/angular.json +24 -4
  10. package/docs/changelog/this-branch.md +36 -0
  11. package/env.sample +3 -2
  12. package/mocks/voice-websocket-mock/server.cjs +245 -0
  13. package/nginx.conf +22 -2
  14. package/package.json +10 -3
  15. package/playwright.config.ts +41 -0
  16. package/src/app/app.component.html +2 -2
  17. package/src/app/app.component.scss +25 -14
  18. package/src/app/app.component.spec.ts +21 -6
  19. package/src/app/app.component.ts +10 -9
  20. package/src/app/app.module.ts +15 -0
  21. package/src/app/component/conversation-detail/conversation/conversation.component.html +25 -11
  22. package/src/app/component/conversation-detail/conversation/conversation.component.scss +40 -2
  23. package/src/app/component/conversation-detail/conversation/conversation.component.spec.ts +644 -75
  24. package/src/app/component/conversation-detail/conversation/conversation.component.ts +100 -14
  25. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.html +25 -13
  26. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.spec.ts +123 -5
  27. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.ts +1 -0
  28. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.html +23 -10
  29. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.scss +33 -2
  30. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.spec.ts +242 -149
  31. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.ts +8 -6
  32. package/src/app/component/conversation-detail/conversation-emojii/conversation-emojii.component.spec.ts +53 -3
  33. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.html +200 -96
  34. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss +211 -6
  35. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.spec.ts +452 -78
  36. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts +291 -76
  37. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.html +113 -53
  38. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.scss +12 -4
  39. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.spec.ts +274 -29
  40. package/src/app/component/conversation-detail/conversation-internal-frame/conversation-internal-frame.component.html +23 -9
  41. package/src/app/component/conversation-detail/conversation-internal-frame/conversation-internal-frame.component.spec.ts +80 -8
  42. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.html +29 -23
  43. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.spec.ts +185 -16
  44. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.ts +34 -14
  45. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.html +46 -0
  46. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.scss +83 -0
  47. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.ts +192 -0
  48. package/src/app/component/error-alert/error-alert.component.spec.ts +65 -5
  49. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.html +16 -7
  50. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.scss +21 -0
  51. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.spec.ts +89 -7
  52. package/src/app/component/form/form-builder/form-builder.component.html +1 -1
  53. package/src/app/component/form/form-builder/form-builder.component.spec.ts +163 -21
  54. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.html +8 -4
  55. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.scss +10 -5
  56. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.spec.ts +90 -16
  57. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.ts +26 -0
  58. package/src/app/component/form/inputs/form-label/form-label.component.spec.ts +45 -11
  59. package/src/app/component/form/inputs/form-radio-button/form-radio-button.component.spec.ts +24 -6
  60. package/src/app/component/form/inputs/form-select/form-select.component.spec.ts +14 -5
  61. package/src/app/component/form/inputs/form-text/form-text.component.html +14 -12
  62. package/src/app/component/form/inputs/form-text/form-text.component.scss +11 -1
  63. package/src/app/component/form/inputs/form-text/form-text.component.spec.ts +113 -17
  64. package/src/app/component/form/inputs/form-text/form-text.component.ts +35 -3
  65. package/src/app/component/form/inputs/form-textarea/form-textarea.component.html +13 -11
  66. package/src/app/component/form/inputs/form-textarea/form-textarea.component.scss +6 -5
  67. package/src/app/component/form/inputs/form-textarea/form-textarea.component.spec.ts +149 -13
  68. package/src/app/component/form/inputs/form-textarea/form-textarea.component.ts +26 -0
  69. package/src/app/component/form/prechat-form/prechat-form.component.html +14 -11
  70. package/src/app/component/form/prechat-form/prechat-form.component.spec.ts +102 -10
  71. package/src/app/component/form/prechat-form/prechat-form.component.ts +8 -1
  72. package/src/app/component/form/prechat-form-test-mock.ts +35 -0
  73. package/src/app/component/home/home.component.html +38 -31
  74. package/src/app/component/home/home.component.scss +4 -2
  75. package/src/app/component/home/home.component.spec.ts +226 -11
  76. package/src/app/component/home-conversations/home-conversations.component.html +30 -26
  77. package/src/app/component/home-conversations/home-conversations.component.scss +3 -0
  78. package/src/app/component/home-conversations/home-conversations.component.spec.ts +212 -36
  79. package/src/app/component/last-message/last-message.component.html +15 -9
  80. package/src/app/component/last-message/last-message.component.scss +16 -2
  81. package/src/app/component/last-message/last-message.component.spec.ts +204 -23
  82. package/src/app/component/last-message/last-message.component.ts +4 -1
  83. package/src/app/component/launcher-button/launcher-button.component.html +8 -13
  84. package/src/app/component/launcher-button/launcher-button.component.spec.ts +104 -8
  85. package/src/app/component/list-all-conversations/list-all-conversations.component.html +12 -17
  86. package/src/app/component/list-all-conversations/list-all-conversations.component.scss +2 -0
  87. package/src/app/component/list-conversations/list-conversations.component.html +22 -22
  88. package/src/app/component/menu-options/menu-options.component.html +30 -20
  89. package/src/app/component/menu-options/menu-options.component.spec.ts +125 -9
  90. package/src/app/component/message/audio/audio.component.html +13 -15
  91. package/src/app/component/message/audio/audio.component.spec.ts +140 -5
  92. package/src/app/component/message/audio/audio.component.ts +1 -5
  93. package/src/app/component/message/audio-sync/audio-sync.component.html +18 -0
  94. package/src/app/component/message/audio-sync/audio-sync.component.scss +65 -0
  95. package/src/app/component/message/audio-sync/audio-sync.component.spec.ts +103 -0
  96. package/src/app/component/message/audio-sync/audio-sync.component.ts +643 -0
  97. package/src/app/component/message/avatar/avatar.component.html +2 -2
  98. package/src/app/component/message/avatar/avatar.component.spec.ts +99 -7
  99. package/src/app/component/message/bubble-message/bubble-message.component.html +43 -51
  100. package/src/app/component/message/bubble-message/bubble-message.component.scss +59 -1
  101. package/src/app/component/message/bubble-message/bubble-message.component.spec.ts +154 -57
  102. package/src/app/component/message/bubble-message/bubble-message.component.ts +152 -109
  103. package/src/app/component/message/buttons/action-button/action-button.component.html +3 -4
  104. package/src/app/component/message/buttons/action-button/action-button.component.spec.ts +49 -5
  105. package/src/app/component/message/buttons/link-button/link-button.component.scss +5 -8
  106. package/src/app/component/message/buttons/link-button/link-button.component.spec.ts +50 -5
  107. package/src/app/component/message/buttons/text-button/text-button.component.spec.ts +44 -5
  108. package/src/app/component/message/carousel/carousel.component.html +29 -16
  109. package/src/app/component/message/carousel/carousel.component.scss +20 -8
  110. package/src/app/component/message/carousel/carousel.component.spec.ts +80 -3
  111. package/src/app/component/message/carousel/carousel.component.ts +16 -0
  112. package/src/app/component/message/frame/frame.component.html +9 -4
  113. package/src/app/component/message/frame/frame.component.spec.ts +34 -15
  114. package/src/app/component/message/frame/frame.component.ts +7 -2
  115. package/src/app/component/message/html/html.component.html +1 -1
  116. package/src/app/component/message/html/html.component.scss +1 -1
  117. package/src/app/component/message/html/html.component.spec.ts +24 -7
  118. package/src/app/component/message/image/image.component.html +12 -10
  119. package/src/app/component/message/image/image.component.scss +16 -0
  120. package/src/app/component/message/image/image.component.spec.ts +101 -15
  121. package/src/app/component/message/image/image.component.ts +90 -51
  122. package/src/app/component/message/info-message/info-message.component.spec.ts +26 -14
  123. package/src/app/component/message/json-sources/json-sources.component.html +38 -0
  124. package/src/app/component/message/json-sources/json-sources.component.scss +201 -0
  125. package/src/app/component/message/json-sources/json-sources.component.ts +89 -0
  126. package/src/app/component/message/like-unlike/like-unlike.component.html +7 -9
  127. package/src/app/component/message/like-unlike/like-unlike.component.spec.ts +31 -3
  128. package/src/app/component/message/return-receipt/return-receipt.component.spec.ts +38 -17
  129. package/src/app/component/message/text/text.component.html +3 -3
  130. package/src/app/component/message/text/text.component.scss +80 -86
  131. package/src/app/component/message/text/text.component.spec.ts +106 -13
  132. package/src/app/component/message-attachment/message-attachment.component.spec.ts +134 -13
  133. package/src/app/component/selection-department/selection-department.component.html +21 -23
  134. package/src/app/component/selection-department/selection-department.component.spec.ts +159 -14
  135. package/src/app/component/selection-department/selection-department.component.ts +8 -1
  136. package/src/app/component/send-button/send-button.component.html +5 -13
  137. package/src/app/component/send-button/send-button.component.spec.ts +2 -2
  138. package/src/app/component/star-rating-widget/star-rating-widget.component.html +51 -81
  139. package/src/app/directives/tooltip.directive.spec.ts +8 -4
  140. package/src/app/modals/confirm-close/confirm-close.component.html +20 -8
  141. package/src/app/modals/confirm-close/confirm-close.component.scss +3 -0
  142. package/src/app/modals/confirm-close/confirm-close.component.spec.ts +13 -4
  143. package/src/app/modals/confirm-close/confirm-close.component.ts +8 -1
  144. package/src/app/pipe/html-entites-encode.pipe.spec.ts +35 -2
  145. package/src/app/pipe/marked.pipe.spec.ts +38 -2
  146. package/src/app/pipe/marked.pipe.ts +51 -41
  147. package/src/app/providers/app-config.service.ts +4 -2
  148. package/src/app/providers/brand.service.spec.ts +23 -2
  149. package/src/app/providers/brand.service.ts +1 -1
  150. package/src/app/providers/global-settings.service.spec.ts +1009 -14
  151. package/src/app/providers/global-settings.service.ts +82 -2
  152. package/src/app/providers/json-sources-parser.service.ts +175 -0
  153. package/src/app/providers/translator.service.ts +26 -6
  154. package/src/app/providers/tts-audio-playback-coordinator.service.spec.ts +117 -0
  155. package/src/app/providers/tts-audio-playback-coordinator.service.ts +109 -0
  156. package/src/app/providers/url-preview.service.ts +82 -0
  157. package/src/app/providers/voice/STT&TTS/openai-voice.config.ts +12 -0
  158. package/src/app/providers/voice/STT&TTS/openai-voice.provider.ts +171 -0
  159. package/src/app/providers/voice/STT&TTS/speech-provider.abstract.ts +39 -0
  160. package/src/app/providers/voice/audio.types.ts +40 -0
  161. package/src/app/providers/voice/vad.service.spec.ts +28 -0
  162. package/src/app/providers/voice/vad.service.ts +70 -0
  163. package/src/app/providers/voice/voice-streaming.service.spec.ts +23 -0
  164. package/src/app/providers/voice/voice-streaming.service.ts +702 -0
  165. package/src/app/providers/voice/voice-streaming.types.ts +112 -0
  166. package/src/app/providers/voice/voice.service.spec.ts +227 -0
  167. package/src/app/providers/voice/voice.service.ts +973 -0
  168. package/src/app/sass/_variables.scss +3 -0
  169. package/src/app/sass/animations.scss +19 -1
  170. package/src/app/shims/onnxruntime-web-wasm.ts +4 -0
  171. package/src/app/utils/globals.ts +21 -1
  172. package/src/app/utils/json-sources-utils.ts +27 -0
  173. package/src/app/utils/url-utils.ts +98 -0
  174. package/src/app/utils/utils-resources.ts +1 -1
  175. package/src/assets/i18n/en.json +106 -99
  176. package/src/assets/i18n/es.json +107 -100
  177. package/src/assets/i18n/fr.json +107 -100
  178. package/src/assets/i18n/it.json +107 -98
  179. package/src/assets/onnx/ort-wasm-simd-threaded.mjs +59 -0
  180. package/src/assets/onnx/ort-wasm-simd-threaded.wasm +0 -0
  181. package/src/assets/sounds/keyboard.mp3 +0 -0
  182. package/src/assets/twp/chatbot-panel.html +3 -1
  183. package/src/assets/twp/index-dev.html +18 -0
  184. package/src/assets/twp/tiledesk_widget_files/widget-css-override-example.css +14 -0
  185. package/src/assets/vad/silero_vad_legacy.onnx +0 -0
  186. package/src/assets/vad/vad.worklet.bundle.min.js +1 -0
  187. package/src/chat21-core/models/message.ts +2 -1
  188. package/src/chat21-core/providers/chat-manager.spec.ts +72 -0
  189. package/src/chat21-core/providers/firebase/firebase-conversation-handler.ts +3 -2
  190. package/src/chat21-core/providers/mqtt/mqtt-conversation-handler.ts +12 -0
  191. package/src/chat21-core/providers/scripts/script.service.spec.ts +12 -2
  192. package/src/chat21-core/providers/tiledesk/tiledesk-requests.service.ts +1 -1
  193. package/src/chat21-core/utils/constants.ts +4 -0
  194. package/src/chat21-core/utils/utils-message.ts +45 -6
  195. package/src/chat21-core/utils/utils.ts +5 -2
  196. package/src/widget-config-template.json +4 -1
  197. package/src/widget-config.json +4 -1
  198. package/tests/widget-form-rich.spec.ts +67 -0
  199. package/tests/widget-index-dev-settings.spec.ts +52 -0
  200. package/tests/widget-twp-iframe.spec.ts +39 -0
  201. package/tsconfig.json +5 -0
@@ -6,7 +6,6 @@ import { BehaviorSubject, Observable } from 'rxjs';
6
6
  import { Globals } from '../utils/globals';
7
7
  import { convertColorToRGBA, detectIfIsMobile, getImageUrlThumb, getParameterByName, stringToBoolean, stringToNumber } from '../utils/utils';
8
8
 
9
- import { TemplateBindingParseResult } from '@angular/compiler';
10
9
  import { AppStorageService } from '../../chat21-core/providers/abstract/app-storage.service';
11
10
  import { LoggerService } from '../../chat21-core/providers/abstract/logger.service';
12
11
  import { LoggerInstance } from '../../chat21-core/providers/logger/loggerInstance';
@@ -67,6 +66,8 @@ export class GlobalSettingsService {
67
66
  this.globals.logLevel = this.appConfigService.getConfig().logLevel
68
67
  /**SET PERSISTENCE parameter */
69
68
  this.globals.persistence = this.appConfigService.getConfig().authPersistence
69
+ /**SET CLOSE CHAT IN CONVERSATION parameter */
70
+ this.globals.closeChatInConversation = this.appConfigService.getConfig().closeChatInConversation;
70
71
 
71
72
  // ------------------------------- //
72
73
  /** LOAD PARAMETERS FROM SERVER
@@ -339,6 +340,8 @@ export class GlobalSettingsService {
339
340
  this.setCssIframe();
340
341
  /** set main style */
341
342
  this.setStyle();
343
+ /** external CSS override: last stylesheet in document head (max cascade priority vs bundle) */
344
+ this.applyCustomCssOverrideFromGlobals();
342
345
  this.obsSettingsService.next(true);
343
346
  }
344
347
 
@@ -419,6 +422,28 @@ export class GlobalSettingsService {
419
422
 
420
423
  document.documentElement.style.setProperty('--font-family', family);
421
424
  }
425
+
426
+ /**
427
+ * Loads `globals.cssSource` (set only from tiledeskSettings) as the last stylesheet in head
428
+ * so rules with the same specificity override local / bundled CSS.
429
+ */
430
+ private applyCustomCssOverrideFromGlobals(): void {
431
+ const id = 'tiledesk-widget-css-override';
432
+ document.getElementById(id)?.remove();
433
+
434
+ const href = (this.globals.cssSource || '').trim();
435
+ console.log('href', href);
436
+ if (!href) {
437
+ return;
438
+ }
439
+
440
+ const link = document.createElement('link');
441
+ link.id = id;
442
+ link.rel = 'stylesheet';
443
+ link.href = href;
444
+ link.setAttribute('data-tiledesk-css-override', 'true');
445
+ document.head.appendChild(link);
446
+ }
422
447
  /**
423
448
  * A: setVariablesFromService
424
449
  */
@@ -573,6 +598,9 @@ export class GlobalSettingsService {
573
598
  if (variables.hasOwnProperty('allowedUploadExtentions')) {
574
599
  globals['fileUploadAccept'] = variables['allowedUploadExtentions'];
575
600
  }
601
+ if(variables.hasOwnProperty('showAudioStreamFooterButton')) {
602
+ globals['showAudioStreamFooterButton'] = variables['showAudioStreamFooterButton'];
603
+ }
576
604
 
577
605
  }
578
606
  }
@@ -639,6 +667,11 @@ export class GlobalSettingsService {
639
667
  let TEMP: any;
640
668
  const tiledeskSettings = windowContext['tiledeskSettings'];
641
669
  // this.logger.debug('[GLOBAL-SET] setVariablesFromSettings > tiledeskSettings: ', tiledeskSettings);
670
+ /** css override URL: solo tiledeskSettings, mai da URL / query params */
671
+ TEMP = tiledeskSettings['cssSource'];
672
+ if (TEMP !== undefined) {
673
+ globals.cssSource = TEMP;
674
+ }
642
675
  TEMP = tiledeskSettings['tenant'];
643
676
  // this.logger.debug('[GLOBAL-SET] setVariablesFromSettings > tenant:: ', TEMP);
644
677
  if (TEMP !== undefined) {
@@ -702,7 +735,7 @@ export class GlobalSettingsService {
702
735
  }
703
736
  TEMP = tiledeskSettings['lang'];
704
737
  // this.logger.debug('[GLOBAL-SET] setVariablesFromSettings > lang:: ', TEMP);
705
- if (TemplateBindingParseResult) {
738
+ if (TEMP !== undefined) {
706
739
  globals.lang = TEMP;
707
740
  // globals.setParameter('lang', TEMP);
708
741
  }
@@ -919,6 +952,14 @@ export class GlobalSettingsService {
919
952
  if (TEMP !== undefined) {
920
953
  globals.soundEnabled = TEMP;
921
954
  }
955
+ TEMP = tiledeskSettings['keyboardSoundVolume'];
956
+ if (TEMP !== undefined) {
957
+ globals.keyboardSoundVolume = +TEMP;
958
+ }
959
+ TEMP = tiledeskSettings['keyboardSoundFile'];
960
+ if (TEMP !== undefined) {
961
+ globals.keyboardSoundFile = TEMP;
962
+ }
922
963
  TEMP = tiledeskSettings['openExternalLinkButton'];
923
964
  // this.logger.debug('[GLOBAL-SET] setVariablesFromSettings > openExternalLinkButton:: ', TEMP]);
924
965
  if (TEMP !== undefined) {
@@ -1125,11 +1166,22 @@ export class GlobalSettingsService {
1125
1166
  if (TEMP !== undefined) {
1126
1167
  globals.showAudioRecorderFooterButton = (TEMP === true) ? true : false;
1127
1168
  }
1169
+ TEMP = tiledeskSettings['showAudioStreamFooterButton'];
1170
+ // this.logger.debug('[GLOBAL-SET] setVariablesFromSettings > showAudioStreamFooterButton:: ', TEMP]);
1171
+ if (TEMP !== undefined) {
1172
+ globals.showAudioStreamFooterButton = (TEMP === true) ? true : false;
1173
+ }
1128
1174
  TEMP = tiledeskSettings['size'];
1129
1175
  // this.logger.debug('[GLOBAL-SET] setVariablesFromSettings > size:: ', TEMP]);
1130
1176
  if (TEMP !== undefined) {
1131
1177
  globals.size = TEMP;
1132
1178
  }
1179
+
1180
+ TEMP = tiledeskSettings['closeChatInConversation'];
1181
+ // this.logger.debug('[GLOBAL-SET] setVariablesFromSettings > closeChatInConversation:: ', TEMP]);
1182
+ if (TEMP !== undefined) {
1183
+ globals.closeChatInConversation = (TEMP === true) ? true : false;
1184
+ }
1133
1185
  }
1134
1186
 
1135
1187
  /**
@@ -1297,6 +1349,14 @@ export class GlobalSettingsService {
1297
1349
  if (TEMP !== null) {
1298
1350
  this.globals.soundEnabled = TEMP;
1299
1351
  }
1352
+ TEMP = el.nativeElement.getAttribute('keyboardSoundVolume');
1353
+ if (TEMP !== null) {
1354
+ this.globals.keyboardSoundVolume = +TEMP;
1355
+ }
1356
+ TEMP = el.nativeElement.getAttribute('keyboardSoundFile');
1357
+ if (TEMP !== null) {
1358
+ this.globals.keyboardSoundFile = TEMP;
1359
+ }
1300
1360
  TEMP = el.nativeElement.getAttribute('openExternalLinkButton');
1301
1361
  if (TEMP !== null) {
1302
1362
  this.globals.openExternalLinkButton = TEMP;
@@ -1696,6 +1756,16 @@ export class GlobalSettingsService {
1696
1756
  globals.soundEnabled = stringToBoolean(TEMP);
1697
1757
  }
1698
1758
 
1759
+ TEMP = getParameterByName(windowContext, 'tiledesk_keyboardSoundVolume');
1760
+ if (TEMP) {
1761
+ globals.keyboardSoundVolume = +TEMP;
1762
+ }
1763
+
1764
+ TEMP = getParameterByName(windowContext, 'tiledesk_keyboardSoundFile');
1765
+ if (TEMP) {
1766
+ globals.keyboardSoundFile = TEMP;
1767
+ }
1768
+
1699
1769
  TEMP = getParameterByName(windowContext, 'tiledesk_openExternalLinkButton');
1700
1770
  if (TEMP) {
1701
1771
  globals.openExternalLinkButton = stringToBoolean(TEMP);
@@ -1867,6 +1937,11 @@ export class GlobalSettingsService {
1867
1937
  globals.showAttachmentFooterButton = stringToBoolean(TEMP);
1868
1938
  }
1869
1939
 
1940
+ TEMP = getParameterByName(windowContext, 'tiledesk_showAudioStreamFooterButton');
1941
+ if (TEMP) {
1942
+ globals.showAudioStreamFooterButton = stringToBoolean(TEMP);
1943
+ }
1944
+
1870
1945
  TEMP = getParameterByName(windowContext, 'tiledesk_showEmojiFooterButton');
1871
1946
  if (TEMP) {
1872
1947
  globals.showEmojiFooterButton = stringToBoolean(TEMP);
@@ -1876,6 +1951,11 @@ export class GlobalSettingsService {
1876
1951
  if (TEMP) {
1877
1952
  globals.size = TEMP;
1878
1953
  }
1954
+
1955
+ TEMP = getParameterByName(windowContext, 'tiledesk_closeChatInConversation');
1956
+ if (TEMP) {
1957
+ globals.closeChatInConversation = stringToBoolean(TEMP);
1958
+ }
1879
1959
 
1880
1960
  }
1881
1961
 
@@ -0,0 +1,175 @@
1
+ import { Injectable } from '@angular/core';
2
+ import { JSON_SOURCE_FIELD_TITLE, JSON_SOURCE_FIELD_URL } from 'src/chat21-core/utils/constants';
3
+ import { UrlPreviewService } from 'src/app/providers/url-preview.service';
4
+ import { extractUrlsFromText } from 'src/app/utils/url-utils';
5
+ import { JsonSourceItem } from 'src/app/component/message/json-sources/json-sources.component';
6
+ import { mergeJsonSourcesMissingFields } from 'src/app/utils/json-sources-utils';
7
+
8
+ export type UrlPreviewMessage = {
9
+ type?: string; // "url_preview"
10
+ text?: string;
11
+ };
12
+
13
+ /**
14
+ * Parse and enrich "url_preview" messages into `JsonSourceItem[]`.
15
+ *
16
+ * Rules:
17
+ * - The payload is always read from `msg.text`, regardless of `activeMode`.
18
+ * - `msg.text` may be either:
19
+ * - a JSON array of source objects (`{source_name, source_file_name, ...}`), or
20
+ * - a plain string from which URLs are extracted (split by whitespace/punctuation).
21
+ * - After building the initial array, `url-preview` is called only for items that miss
22
+ * title or description, and missing fields are merged in (never overwriting).
23
+ */
24
+ @Injectable({ providedIn: 'root' })
25
+ export class JsonSourcesParserService {
26
+ constructor(private urlPreviewService: UrlPreviewService) {}
27
+
28
+ /**
29
+ * Parse-only: returns sources immediately (no url-preview calls).
30
+ * Use this to render the list instantly, then call `enrichSources()` in background.
31
+ */
32
+ parseBaseFromMessage(messageLike?: any): JsonSourceItem[] | null {
33
+ const payload = this.getUrlPreviewPayload(messageLike);
34
+ return this.parseBaseJsonSources(payload);
35
+ }
36
+
37
+ /**
38
+ * Parse + enrich: kept for backward compatibility with older callers.
39
+ * If you need instant rendering, prefer `parseBaseFromMessage()` + `enrichSources()`.
40
+ */
41
+ /**
42
+ * Best-practice entrypoint for UI components:
43
+ * accepts a full `MessageModel`/message-like object, and supports url_preview payload
44
+ * living either on the root message OR inside `metadata` OR inside `attributes`.
45
+ */
46
+ async parseFromMessage(messageLike?: any): Promise<JsonSourceItem[] | null> {
47
+ const base = this.parseBaseFromMessage(messageLike);
48
+ return this.enrichSources(base);
49
+ }
50
+
51
+ async enrichSources(baseSources?: JsonSourceItem[] | null): Promise<JsonSourceItem[] | null> {
52
+ const sources = (baseSources || []).filter((s) => !!s?.link);
53
+ if (sources.length === 0) return baseSources || null;
54
+
55
+ // Only call url-preview for items missing the most relevant fields.
56
+ const incompleteUrls = sources
57
+ .filter(s => !!s.link && (!s.title || !s.description))
58
+ .map(s => s.link!)
59
+ .slice(0, 10);
60
+
61
+ if (incompleteUrls.length === 0) return sources;
62
+
63
+ const previews = await this.urlPreviewService.previewUrls(incompleteUrls);
64
+ const previewItems: JsonSourceItem[] = (previews || []).map(p => ({
65
+ link: p.url,
66
+ title: p.title || p.siteName || p.url,
67
+ description: p.description,
68
+ image: p.image,
69
+ favicon: p.favicon,
70
+ favicon_hd: p.favicon_hd
71
+ }));
72
+
73
+ if (previewItems.length === 0) return sources;
74
+ return mergeJsonSourcesMissingFields(sources, previewItems);
75
+ }
76
+
77
+ async parseJsonSources(msg?: UrlPreviewMessage | null): Promise<JsonSourceItem[] | null> {
78
+ const base = this.parseBaseJsonSources(msg);
79
+ return this.enrichSources(base);
80
+ }
81
+
82
+ private getUrlPreviewPayload(messageLike?: any): UrlPreviewMessage | null {
83
+ if (!messageLike) return null;
84
+ const candidates: any[] = [
85
+ messageLike,
86
+ (messageLike?.metadata && typeof messageLike.metadata === 'object') ? messageLike.metadata : null,
87
+ (messageLike?.attributes && typeof messageLike.attributes === 'object') ? messageLike.attributes : null
88
+ ].filter(Boolean);
89
+ return (candidates.find((c) => c?.type === 'url_preview') || null) as UrlPreviewMessage | null;
90
+ }
91
+
92
+ private parseBaseJsonSources(msg?: UrlPreviewMessage | null): JsonSourceItem[] | null {
93
+ if (!msg || msg.type !== 'url_preview') return null;
94
+
95
+ // Regardless of `activeMode`, the payload is always read from `msg.text`.
96
+ // It can be either a JSON array of source objects, or a plain string with URLs.
97
+ return this.isJsonArrayOfObjects(msg.text)
98
+ ? this.mapTextToSources(msg.text)
99
+ : this.mapListToSources(msg.text);
100
+ }
101
+
102
+ private mapListToSources(listValue?: string): JsonSourceItem[] | null {
103
+ const urls = extractUrlsFromText((listValue || '').toString(), 10);
104
+ return urls.length ? urls.map(u => ({ link: u, title: u })) : null;
105
+ }
106
+
107
+ private isJsonArrayOfObjects(text?: string): boolean {
108
+ if (!text) return false;
109
+ try {
110
+ const parsed = this.parseJsonLenient(text);
111
+ return Array.isArray(parsed) && parsed.some(it => it && typeof it === 'object' && !Array.isArray(it));
112
+ } catch {
113
+ return false;
114
+ }
115
+ }
116
+
117
+ private mapTextToSources(text?: string): JsonSourceItem[] | null {
118
+ if (!text) return null;
119
+ try {
120
+ const parsed = this.parseJsonLenient(text);
121
+ return this.mapSourcesArray(parsed);
122
+ } catch {
123
+ return null;
124
+ }
125
+ }
126
+
127
+ private mapSourcesArray(input: any): JsonSourceItem[] | null {
128
+ const arr = Array.isArray(input) ? input : null;
129
+ if (!arr || arr.length === 0) return null;
130
+ const mapped = arr
131
+ .filter((s: any) => s && typeof s === 'object' && typeof s[JSON_SOURCE_FIELD_URL] === 'string')
132
+ .map((s: any): JsonSourceItem | null => {
133
+ const rawUrl = (s[JSON_SOURCE_FIELD_URL] || '').toString().trim();
134
+ const normalized = extractUrlsFromText(rawUrl, 1)[0];
135
+ if (!normalized) return null;
136
+ return {
137
+ link: normalized,
138
+ title: (s[JSON_SOURCE_FIELD_TITLE] || rawUrl).toString(),
139
+ description: typeof s.source_description === 'string' ? s.source_description : undefined,
140
+ image: typeof s.source_image === 'string' ? s.source_image : undefined
141
+ };
142
+ })
143
+ .filter((x: JsonSourceItem | null): x is JsonSourceItem => !!x && !!x.link);
144
+ return mapped.length ? mapped : null;
145
+ }
146
+
147
+ private parseJsonLenient(input: string): any {
148
+ const trimmed = (input || '').trim();
149
+ try {
150
+ const parsed = JSON.parse(trimmed);
151
+ if (typeof parsed === 'string') {
152
+ const inner = parsed.trim();
153
+ if ((inner.startsWith('[') && inner.endsWith(']')) || (inner.startsWith('{') && inner.endsWith('}'))) {
154
+ return this.parseJsonLenient(inner);
155
+ }
156
+ }
157
+ return parsed;
158
+ } catch {
159
+ const cleaned = trimmed
160
+ .replace(/^```(?:json)?\s*/i, '')
161
+ .replace(/```$/i, '')
162
+ .trim()
163
+ .replace(/,\s*([}\]])/g, '$1');
164
+ const parsed = JSON.parse(cleaned);
165
+ if (typeof parsed === 'string') {
166
+ const inner = parsed.trim();
167
+ if ((inner.startsWith('[') && inner.endsWith(']')) || (inner.startsWith('{') && inner.endsWith('}'))) {
168
+ return this.parseJsonLenient(inner);
169
+ }
170
+ }
171
+ return parsed;
172
+ }
173
+ }
174
+ }
175
+
@@ -234,12 +234,27 @@ export class TranslatorService {
234
234
  this._translate.use(lang);
235
235
  this.logger.debug(`[TRANSLATOR-SERV] »»»» initI18n - »»» loadRemoteTranslations ?`, environment.loadRemoteTranslations);
236
236
  this._translate.setTranslation(lang, data, true);
237
- // if (environment.loadRemoteTranslations) {
238
- // // console.log(`»»»» initI18n - »»» remote translation `, data);
239
- // this._translate.setTranslation(lang, data, true);
240
- // } else {
241
- // this._translate.setTranslation(lang, data, true);
242
- // }
237
+ this.syncDocumentLang(lang);
238
+ }
239
+
240
+ /**
241
+ * Synchronize the document `<html lang="...">` attribute with the active i18n language
242
+ * (WCAG 3.1.1 Language of Page, EN 301 549 § 9.3.1.1).
243
+ *
244
+ * The widget runs inside its own iframe, so we update the document of that iframe.
245
+ * If the widget is also embedded in a parent page (Tiledesk launcher), the parent
246
+ * page already declares its own `lang`, which we intentionally leave untouched.
247
+ */
248
+ private syncDocumentLang(lang: string) {
249
+ if (!lang) { return; }
250
+ const normalized = lang.toLowerCase().substring(0, 2);
251
+ try {
252
+ if (typeof document !== 'undefined' && document.documentElement) {
253
+ document.documentElement.setAttribute('lang', normalized);
254
+ }
255
+ } catch (e) {
256
+ this.logger.warn('[TRANSLATOR-SERV] syncDocumentLang error', e);
257
+ }
243
258
  }
244
259
 
245
260
  /** */
@@ -302,6 +317,9 @@ export class TranslatorService {
302
317
  'CLOSED',
303
318
  'LABEL_PREVIEW',
304
319
  'MAX_ATTACHMENT',
320
+ 'EMOJI',
321
+ 'BUTTON_OPEN_CHAT',
322
+ 'MAX_ATTACHMENT_ERROR',
305
323
  'EMOJI'
306
324
  ];
307
325
 
@@ -358,7 +376,9 @@ export class TranslatorService {
358
376
  globals.LABEL_PREVIEW = res['LABEL_PREVIEW']
359
377
  globals.LABEL_ERROR_FIELD_REQUIRED= res['LABEL_ERROR_FIELD_REQUIRED']
360
378
  globals.MAX_ATTACHMENT = res['MAX_ATTACHMENT']
379
+ globals.MAX_ATTACHMENT_ERROR = res['MAX_ATTACHMENT_ERROR']
361
380
  globals.EMOJI = res['EMOJI']
381
+ globals.BUTTON_OPEN_CHAT = res['BUTTON_OPEN_CHAT']
362
382
 
363
383
 
364
384
  if(globals.WELCOME_TITLE === 'WELLCOME_TITLE') globals.WELCOME_TITLE = res['WELCOME_TITLE']
@@ -0,0 +1,117 @@
1
+ import { TestBed } from '@angular/core/testing';
2
+ import { TtsAudioPlaybackCoordinator } from './tts-audio-playback-coordinator.service';
3
+
4
+ describe('TtsAudioPlaybackCoordinator', () => {
5
+ let coordinator: TtsAudioPlaybackCoordinator;
6
+
7
+ beforeEach(() => {
8
+ TestBed.configureTestingModule({ providers: [TtsAudioPlaybackCoordinator] });
9
+ coordinator = TestBed.inject(TtsAudioPlaybackCoordinator);
10
+ });
11
+
12
+ // ── Basic lifecycle ───────────────────────────────────────────────────────
13
+
14
+ it('should start playing immediately when nothing is active', () => {
15
+ const start = jasmine.createSpy('start');
16
+ coordinator.requestStart('msg-1', start);
17
+ expect(start).toHaveBeenCalledTimes(1);
18
+ });
19
+
20
+ it('isTTSPlaying$ should be true while playing and false after release', () => {
21
+ const states: boolean[] = [];
22
+ coordinator.isTTSPlaying$.subscribe((v) => states.push(v));
23
+
24
+ coordinator.requestStart('msg-1', () => {});
25
+ coordinator.releaseIfCurrent('msg-1');
26
+
27
+ expect(states).toEqual([false, true, false]);
28
+ });
29
+
30
+ it('stopAll clears the queue, sets playing=false, and emits stopAllPlayback$', () => {
31
+ const stopNextSpy = spyOn((coordinator as any)._stopAll$, 'next').and.callThrough();
32
+
33
+ coordinator.requestStart('msg-1', () => {});
34
+ coordinator.stopAll();
35
+
36
+ expect(stopNextSpy).toHaveBeenCalledTimes(1);
37
+ const states: boolean[] = [];
38
+ coordinator.isTTSPlaying$.subscribe((v) => states.push(v));
39
+ expect(states).toEqual([false]);
40
+ });
41
+
42
+ // ── Preemption tests (SPEC-002) ───────────────────────────────────────────
43
+
44
+ it('requestStart while playing preempts old owner: new start() is called immediately', () => {
45
+ const start1 = jasmine.createSpy('start1');
46
+ const start2 = jasmine.createSpy('start2');
47
+
48
+ coordinator.requestStart('msg-1', start1);
49
+ coordinator.requestStart('msg-2', start2);
50
+
51
+ expect(start1).toHaveBeenCalledTimes(1);
52
+ expect(start2).toHaveBeenCalledTimes(1); // started immediately, not queued
53
+ });
54
+
55
+ it('preemptPlayback$ emits evicted ownerId only (not the new owner)', () => {
56
+ const preempted: string[] = [];
57
+ coordinator.preemptPlayback$.subscribe((id) => preempted.push(id));
58
+
59
+ coordinator.requestStart('msg-1', () => {});
60
+ coordinator.requestStart('msg-2', () => {}); // preempts msg-1
61
+
62
+ expect(preempted).toEqual(['msg-1']);
63
+ });
64
+
65
+ it('preemptPlayback$ does NOT emit the new owner id', () => {
66
+ const preempted: string[] = [];
67
+ coordinator.preemptPlayback$.subscribe((id) => preempted.push(id));
68
+
69
+ coordinator.requestStart('msg-1', () => {});
70
+ coordinator.requestStart('msg-2', () => {});
71
+
72
+ expect(preempted).not.toContain('msg-2');
73
+ });
74
+
75
+ it('isTTSPlaying$ stays true after preemption until new owner releases', () => {
76
+ const states: boolean[] = [];
77
+ coordinator.isTTSPlaying$.subscribe((v) => states.push(v));
78
+
79
+ coordinator.requestStart('msg-1', () => {}); // true
80
+ coordinator.requestStart('msg-2', () => {}); // still true (preemption, new owner active)
81
+ coordinator.releaseIfCurrent('msg-2'); // false
82
+
83
+ expect(states).toEqual([false, true, false]);
84
+ });
85
+
86
+ it('releaseIfCurrent for an evicted owner is a no-op', () => {
87
+ const states: boolean[] = [];
88
+ coordinator.isTTSPlaying$.subscribe((v) => states.push(v));
89
+
90
+ coordinator.requestStart('msg-1', () => {});
91
+ coordinator.requestStart('msg-2', () => {}); // msg-1 evicted
92
+
93
+ // Old owner calls release after being preempted — should not affect playing state
94
+ coordinator.releaseIfCurrent('msg-1');
95
+
96
+ expect(states).toEqual([false, true]); // no extra false emission
97
+ });
98
+
99
+ it('chain of preemptions: each new requestStart immediately evicts the current owner', () => {
100
+ const preempted: string[] = [];
101
+ coordinator.preemptPlayback$.subscribe((id) => preempted.push(id));
102
+
103
+ coordinator.requestStart('msg-1', () => {});
104
+ coordinator.requestStart('msg-2', () => {});
105
+ coordinator.requestStart('msg-3', () => {});
106
+
107
+ expect(preempted).toEqual(['msg-1', 'msg-2']);
108
+ });
109
+
110
+ it('requestStart is idempotent for the current owner', () => {
111
+ const start = jasmine.createSpy('start');
112
+ coordinator.requestStart('msg-1', start);
113
+ coordinator.requestStart('msg-1', start); // same owner — should be ignored
114
+
115
+ expect(start).toHaveBeenCalledTimes(1);
116
+ });
117
+ });
@@ -0,0 +1,109 @@
1
+ import { Injectable } from '@angular/core';
2
+ import { BehaviorSubject, Observable, Subject } from 'rxjs';
3
+
4
+ /**
5
+ * Garantisce un solo messaggio TTS in riproduzione alla volta.
6
+ * Quando arriva un nuovo messaggio TTS mentre un altro è in corso, quello vecchio viene
7
+ * interrotto immediatamente (preemption) e il nuovo parte subito.
8
+ */
9
+ @Injectable({ providedIn: 'root' })
10
+ export class TtsAudioPlaybackCoordinator {
11
+ private currentOwnerId: string | null = null;
12
+ private readonly queue: Array<{ ownerId: string; start: () => void }> = [];
13
+
14
+ /** Emits true while any TTS is playing or queued; false when the queue is fully drained. */
15
+ private readonly _isTTSPlaying$ = new BehaviorSubject<boolean>(false);
16
+ readonly isTTSPlaying$ = this._isTTSPlaying$.asObservable();
17
+
18
+ /** Emits once when stopAll() is called — signals every AudioSyncComponent to abort immediately. */
19
+ private readonly _stopAll$ = new Subject<void>();
20
+ readonly stopAllPlayback$: Observable<void> = this._stopAll$.asObservable();
21
+
22
+ /**
23
+ * Emits the ownerId of the component being preempted (stopped mid-playback by a newer message).
24
+ * Only the component whose ownerId matches should react — unlike stopAll$ which targets everyone.
25
+ */
26
+ private readonly _preemptCurrent$ = new Subject<string>();
27
+ readonly preemptPlayback$: Observable<string> = this._preemptCurrent$.asObservable();
28
+
29
+ /**
30
+ * Richiede l'avvio della riproduzione TTS per `ownerId`.
31
+ * Se un altro TTS è già in corso, viene interrotto immediatamente (preemption) e
32
+ * `ownerId` parte subito. Qualsiasi coda pendente viene svuotata.
33
+ */
34
+ requestStart(ownerId: string, start: () => void): void {
35
+ const id = (ownerId || '').trim();
36
+ if (!id) {
37
+ return;
38
+ }
39
+ if (this.currentOwnerId === id) {
40
+ return;
41
+ }
42
+
43
+ if (this.currentOwnerId) {
44
+ // Preempt: signal only the evicted owner to stop (not a broadcast stopAll).
45
+ // This avoids stopping the component that is about to start playing.
46
+ const evicted = this.currentOwnerId;
47
+ this.queue.length = 0;
48
+ this.currentOwnerId = null;
49
+ this._preemptCurrent$.next(evicted);
50
+ } else {
51
+ this.queue.length = 0;
52
+ }
53
+
54
+ this.currentOwnerId = id;
55
+ if (!this._isTTSPlaying$.getValue()) {
56
+ this._isTTSPlaying$.next(true);
57
+ }
58
+ try {
59
+ start();
60
+ } catch {
61
+ this.releaseIfCurrent(id);
62
+ }
63
+ }
64
+
65
+ /** Chiamare a fine riproduzione naturale (`ended`) se questo messaggio era ancora “attivo”. */
66
+ releaseIfCurrent(ownerId: string): void {
67
+ const id = (ownerId || '').trim();
68
+ if (!id) {
69
+ return;
70
+ }
71
+ if (this.currentOwnerId !== id) {
72
+ // Se era in coda, rimuovilo.
73
+ const idx = this.queue.findIndex((j) => j.ownerId === id);
74
+ if (idx !== -1) {
75
+ this.queue.splice(idx, 1);
76
+ }
77
+ return;
78
+ }
79
+
80
+ this.currentOwnerId = null;
81
+ const next = this.queue.shift();
82
+ if (!next) {
83
+ this._isTTSPlaying$.next(false);
84
+ return;
85
+ }
86
+ this.currentOwnerId = next.ownerId;
87
+ try {
88
+ next.start();
89
+ } catch {
90
+ this.releaseIfCurrent(next.ownerId);
91
+ }
92
+ }
93
+
94
+ /** Distruzione componente o stop esplicito. */
95
+ release(ownerId: string): void {
96
+ this.releaseIfCurrent(ownerId);
97
+ }
98
+
99
+ /**
100
+ * Stops all TTS playback immediately and clears the queue.
101
+ * Broadcasts on stopAllPlayback$ so every AudioSyncComponent can abort its stream and reveal all text.
102
+ */
103
+ stopAll(): void {
104
+ this.queue.length = 0;
105
+ this.currentOwnerId = null;
106
+ this._isTTSPlaying$.next(false);
107
+ this._stopAll$.next();
108
+ }
109
+ }