@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
@@ -1,148 +1,191 @@
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, 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
- convertColorToRGBA = convertColorToRGBA
30
-
31
- // ========== begin:: check message type functions ======= //
32
- messageType = messageType;
33
-
34
- MESSAGE_TYPE_MINE = MESSAGE_TYPE_MINE;
35
- MESSAGE_TYPE_OTHERS = MESSAGE_TYPE_OTHERS;
36
- // ========== end:: check message type functions ======= //
37
- sizeImage : { width: number, height: number}
38
- fullnameColor: string;
39
- private logger: LoggerService = LoggerInstance.getInstance()
40
- constructor(public sanitizer: DomSanitizer) { }
41
-
42
- ngOnInit() {
43
- // console.log("---- > MSG:", this.message);
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
+ @HostBinding('class.hidden-bubble') get hostHiddenBubble() { return !this.hasRenderableContent(); }
44
+
45
+ hasRenderableContent(): boolean {
46
+ const msg = this.message;
47
+ if (!msg) return false;
48
+ if (isImage(msg) || isFile(msg) || isFrame(msg) || isAudio(msg)) return true;
49
+ if (this.jsonSources && this.jsonSources.length > 0) return true;
50
+ // For url_preview messages, `text` may carry the raw JSON payload (not display text):
51
+ // if sources parsing yielded nothing, the bubble must stay hidden.
52
+ if (this.isUrlPreviewMessage) return false;
53
+ return !!(msg.text && String(msg.text).trim().length > 0);
44
54
  }
45
55
 
46
- ngOnChanges() {
47
- if (this.message && this.message.metadata && typeof this.message.metadata === 'object' ) {
48
- this.sizeImage = this.getMetadataSize(this.message.metadata)
49
- }
56
+ readonly isImage = isImage;
57
+ readonly isFile = isFile;
58
+ readonly isFrame = isFrame;
59
+ readonly isAudio = isAudio;
60
+ readonly isJsonSources = isJsonSources;
61
+ readonly isAudioTTS = isAudioTTS;
62
+ readonly messageType = messageType;
63
+ readonly convertColorToRGBA = convertColorToRGBA;
64
+ readonly MESSAGE_TYPE_MINE = MESSAGE_TYPE_MINE;
65
+ readonly MESSAGE_TYPE_OTHERS = MESSAGE_TYPE_OTHERS;
66
+
67
+ sizeImage: { width: number; height: number } = { width: 0, height: 0 };
68
+ fullnameColor: string = '';
69
+ jsonSources: JsonSourceItem[] | null = null;
70
+ isUrlPreviewMessage = false;
71
+
72
+ private urlPreviewReqId = 0;
73
+
74
+ constructor(
75
+ public sanitizer: DomSanitizer,
76
+ public voiceService: VoiceService,
77
+ private jsonSourcesParser: JsonSourcesParserService
78
+ ) { }
50
79
 
51
- if(this.fontColor){
52
- this.fullnameColor = convertColorToRGBA(this.fontColor, 65)
80
+ ngOnInit() {
81
+ // If this TTS message arrived while the voice proxy was active, mark it so
82
+ // audio-sync never replays it after the session ends.
83
+ if (isAudioTTS(this.message) && this.voiceService.isWssVoiceActive && this.message?.uid) {
84
+ this.voiceService.markProxyHandled(this.message.uid);
53
85
  }
54
- if(this.message && this.message.sender_fullname && this.message.sender_fullname.trim() !== ''){
55
- this.fullnameColor = getColorBck(this.message.sender_fullname)
86
+
87
+ // Set up karaoke observable for TTS messages during WSS sessions.
88
+ if (isAudioTTS(this.message) && this.message?.text) {
89
+ const text = this.message.text;
90
+ const rawWords = text.trim().split(/\s+/).filter((w) => w.length > 0);
91
+ // Always start as 'past' (fully visible). The karaoke RAF loop will drive
92
+ // words through future→active→past for the current speaking turn; using
93
+ // 'future' here would dimm old/history messages the moment voice opens.
94
+ const initialWords: VoiceTtsKaraokeWord[] = rawWords.map((w) => ({ text: w, state: 'past' as const }));
95
+
96
+ this._wssKaraokeWords$ = this.voiceService.voiceTtsKaraoke$.pipe(
97
+ startWith({ text, words: initialWords, activeIndex: -1 }),
98
+ map((frame) =>
99
+ frame.text === text
100
+ ? (frame.words as VoiceTtsKaraokeWord[])
101
+ : initialWords,
102
+ ),
103
+ );
56
104
  }
105
+ }
57
106
 
107
+ ngOnDestroy(): void {
108
+ this._kSub?.unsubscribe();
109
+ this._kSub = undefined;
58
110
  }
59
111
 
60
- /**
61
- *
62
- * @param message
63
- */
64
- // getMetadataSize(metadata): any {
65
- // if(metadata.width === undefined){
66
- // metadata.width= MAX_WIDTH_IMAGES
67
- // }
68
- // if(metadata.height === undefined){
69
- // metadata.height = MAX_WIDTH_IMAGES
70
- // }
71
- // // const MAX_WIDTH_IMAGES = 300;
72
- // const sizeImage = {
73
- // width: metadata.width,
74
- // height: metadata.height
75
- // };
76
- // // that.g.wdLog(['message::: ', metadata);
77
- // if (metadata.width && metadata.width > MAX_WIDTH_IMAGES) {
78
- // const rapporto = (metadata['width'] / metadata['height']);
79
- // sizeImage.width = MAX_WIDTH_IMAGES;
80
- // sizeImage.height = MAX_WIDTH_IMAGES / rapporto;
81
- // }
82
- // return sizeImage; // h.toString();
83
- // }
84
-
85
- /**
86
- *
87
- * @param message
88
- */
89
- getMetadataSize(metadata): {width, height} {
90
- // if (metadata.width === undefined) {
91
- // metadata.width = MAX_WIDTH_IMAGES
92
- // }
93
- // if (metadata.height === undefined) {
94
- // metadata.height = MAX_WIDTH_IMAGES
95
- // }
96
-
97
- const sizeImage = {
98
- width: metadata.width,
99
- height: metadata.height
100
- };
101
-
102
-
103
- if (metadata.width && metadata.width < MAX_WIDTH_IMAGES) {
104
- if (metadata.width <= 55) {
105
- const ratio = (metadata['width'] / metadata['height']);
106
- sizeImage.width = MIN_WIDTH_IMAGES;
107
- sizeImage.height = MIN_WIDTH_IMAGES / ratio;
108
- } else if (metadata.width > 55) {
109
- sizeImage.width = metadata.width;
110
- sizeImage.height = metadata.height
111
- }
112
- } else if (metadata.width && metadata.width > MAX_WIDTH_IMAGES) {
113
- const ratio = (metadata['width'] / metadata['height']);
114
- sizeImage.width = MAX_WIDTH_IMAGES;
115
- sizeImage.height = MAX_WIDTH_IMAGES / ratio;
112
+ ngOnChanges(): void {
113
+ if (this.message?.metadata && typeof this.message.metadata === 'object') {
114
+ this.sizeImage = calcImageSize(this.message.metadata);
116
115
  }
117
- return sizeImage
118
- }
119
116
 
120
- // ========= begin:: event emitter function ============//
117
+ this.fullnameColor = this.fontColor
118
+ ? convertColorToRGBA(this.fontColor, 65)
119
+ : this.fullnameColor;
120
+
121
+ if (this.message?.sender_fullname?.trim()) {
122
+ this.fullnameColor = getColorBck(this.message.sender_fullname);
123
+ }
124
+
125
+ // One-shot: activate word streaming for newly-arrived bot text messages during a voice session.
126
+ // Reset isJustRecived so the animation never replays on subsequent change detection cycles.
127
+ if (
128
+ !this._isStreaming &&
129
+ this.streamOnArrival &&
130
+ this.message?.isJustRecived === true &&
131
+ this.messageType(this.MESSAGE_TYPE_OTHERS, this.message) &&
132
+ !this.isAudio(this.message) &&
133
+ !this.isAudioTTS(this.message) &&
134
+ this.message?.type !== 'html'
135
+ ) {
136
+ this._isStreaming = true;
137
+ this._streamingWords = (this.message.text ?? '')
138
+ .trim()
139
+ .split(/\s+/)
140
+ .filter(w => w.length > 0)
141
+ .map((word, index) => ({ word, index }));
142
+ this.message.isJustRecived = false;
143
+ }
121
144
 
122
- // returnOpenAttachment(event: String) {
123
- // this.onOpenAttachment.emit(event)
124
- // }
145
+ if (this.message?.type !== TYPE_MSG_URL_PREVIEW) {
146
+ this.jsonSources = null;
147
+ return;
148
+ }
125
149
 
126
- // /** */
127
- // returnClickOnAttachmentButton(event: any) {
128
- // this.onClickAttachmentButton.emit(event)
129
- // }
150
+ // url_preview payload can live on message root OR inside metadata/attributes depending on the integration.
151
+ const urlPreviewLike =
152
+ this.message?.type === TYPE_MSG_URL_PREVIEW
153
+ || this.message?.metadata?.type === TYPE_MSG_URL_PREVIEW
154
+ || this.message?.attributes?.type === TYPE_MSG_URL_PREVIEW;
155
+ this.isUrlPreviewMessage = !!urlPreviewLike;
156
+ if (urlPreviewLike) this.loadJsonSourcesFromUrlPreviewMessage();
157
+ }
130
158
 
131
- onBeforeMessageRenderFN(event){
132
- const messageOBJ = { message: this.message, sanitizer: this.sanitizer, messageEl: event.messageEl, component: event.component}
133
- this.onBeforeMessageRender.emit(messageOBJ)
159
+ private async loadJsonSourcesFromUrlPreviewMessage(): Promise<void> {
160
+ // Protect the UI from out-of-order async responses when the input `message` changes quickly.
161
+ const reqId = ++this.urlPreviewReqId;
162
+ // 1) Parse-only, so the UI can render immediately (no url-preview calls).
163
+ const baseSources = this.jsonSourcesParser.parseBaseFromMessage(this.message);
164
+ this.jsonSources = baseSources;
165
+
166
+ // 2) Enrich in background via url-preview, then merge missing fields.
167
+ const enriched = await this.jsonSourcesParser.enrichSources(baseSources);
168
+ if (reqId !== this.urlPreviewReqId) return;
169
+ this.jsonSources = enriched;
134
170
  }
135
171
 
136
- onAfterMessageRenderFN(event){
137
- const messageOBJ = { message: this.message, sanitizer: this.sanitizer, messageEl: event.messageEl, component: event.component}
138
- this.onAfterMessageRender.emit(messageOBJ)
172
+ trackWord(_index: number, item: { word: string; index: number }): number {
173
+ return item.index;
139
174
  }
140
175
 
141
- onElementRenderedFN(event){
142
- this.onElementRendered.emit({element: event.element, status: event.status})
176
+ trackKaraokeWord(index: number): number {
177
+ return index;
143
178
  }
144
179
 
145
- // ========= END:: event emitter function ============//
180
+ onBeforeMessageRenderFN(event: any): void {
181
+ this.onBeforeMessageRender.emit({ message: this.message, sanitizer: this.sanitizer, messageEl: event.messageEl, component: event.component });
182
+ }
146
183
 
184
+ onAfterMessageRenderFN(event: any): void {
185
+ this.onAfterMessageRender.emit({ message: this.message, sanitizer: this.sanitizer, messageEl: event.messageEl, component: event.component });
186
+ }
147
187
 
188
+ onElementRenderedFN(event: any): void {
189
+ this.onElementRendered.emit({ element: event.element, status: event.status });
190
+ }
148
191
  }
@@ -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}}" -->
@@ -1,4 +1,6 @@
1
- import { async, ComponentFixture, TestBed } from '@angular/core/testing';
1
+ import { SimpleChange } from '@angular/core';
2
+ import { ComponentFixture, TestBed, fakeAsync, tick, waitForAsync } from '@angular/core/testing';
3
+ import { By } from '@angular/platform-browser';
2
4
 
3
5
  import { ActionButtonComponent } from './action-button.component';
4
6
 
@@ -6,20 +8,62 @@ describe('ActionButtonComponent', () => {
6
8
  let component: ActionButtonComponent;
7
9
  let fixture: ComponentFixture<ActionButtonComponent>;
8
10
 
9
- beforeEach(async(() => {
11
+ beforeEach(waitForAsync(() => {
10
12
  TestBed.configureTestingModule({
11
- declarations: [ ActionButtonComponent ]
12
- })
13
- .compileComponents();
13
+ declarations: [ActionButtonComponent],
14
+ }).compileComponents();
14
15
  }));
15
16
 
16
17
  beforeEach(() => {
17
18
  fixture = TestBed.createComponent(ActionButtonComponent);
18
19
  component = fixture.componentInstance;
20
+ component.button = { value: 'OK', action: 'go' };
19
21
  fixture.detectChanges();
20
22
  });
21
23
 
22
24
  it('should create', () => {
23
25
  expect(component).toBeTruthy();
24
26
  });
27
+
28
+ it('ngOnChanges should map theme CSS variables onto .action', () => {
29
+ component.fontSize = '16px';
30
+ component.backgroundColor = '#111';
31
+ component.textColor = '#222';
32
+ component.hoverBackgroundColor = '#333';
33
+ component.hoverTextColor = '#444';
34
+ component.ngOnChanges({
35
+ fontSize: new SimpleChange(null, '16px', true),
36
+ });
37
+ const el = fixture.nativeElement.querySelector('.action') as HTMLElement;
38
+ expect(el.style.getPropertyValue('--buttonFontSize').trim()).toBe('16px');
39
+ expect(el.style.getPropertyValue('--buttonBackgroundColor').trim()).toBeTruthy();
40
+ });
41
+
42
+ it('actionButtonAction should emit when action present', fakeAsync(() => {
43
+ spyOn(component.onButtonClicked, 'emit');
44
+ const de = fixture.debugElement.query(By.css('.action'));
45
+ de.triggerEventHandler('click', {});
46
+ tick(500);
47
+ expect(component.onButtonClicked.emit).toHaveBeenCalled();
48
+ }));
49
+
50
+ it('actionButtonAction should no-op when button has no action', () => {
51
+ component.button = { value: 'X', action: '' };
52
+ fixture.detectChanges();
53
+ spyOn(component.onButtonClicked, 'emit');
54
+ component.actionButtonAction();
55
+ expect(component.onButtonClicked.emit).not.toHaveBeenCalled();
56
+ });
57
+
58
+ it('mouseover and mouseout should not throw', () => {
59
+ expect(() => component.onMouseOver({} as any)).not.toThrow();
60
+ expect(() => component.onMouseOut({} as any)).not.toThrow();
61
+ });
62
+
63
+ it('template should mark archived conversations as disabled', () => {
64
+ component.isConversationArchived = true;
65
+ fixture.detectChanges();
66
+ const el = fixture.nativeElement.querySelector('.action');
67
+ expect(el.classList.contains('disabled')).toBe(true);
68
+ });
25
69
  });
@@ -42,20 +42,17 @@
42
42
  transition: background-color .6s ease;
43
43
  .icon-button-action {
44
44
  position: absolute;
45
- top: 50%;
46
- right: 8px;
47
- transform: translateY(-50%);
45
+ top: -1px;
46
+ right: 1px;
48
47
  svg {
49
- fill: var(--textColor);
48
+ fill: var(--buttonTextColor);
50
49
  }
51
50
  }
52
51
  .icon-button-action-self{
53
52
  position: absolute;
54
- top: 50%;
55
- right: 8px;
56
- transform: translateY(-50%);
53
+ right: 1px;
57
54
  svg {
58
- fill: var(--textColor);
55
+ fill: var(--buttonTextColor);
59
56
  }
60
57
  }
61
58
  &:focus,
@@ -1,4 +1,6 @@
1
- import { async, ComponentFixture, TestBed } from '@angular/core/testing';
1
+ import { SimpleChange } from '@angular/core';
2
+ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
3
+ import { By } from '@angular/platform-browser';
2
4
 
3
5
  import { LinkButtonComponent } from './link-button.component';
4
6
 
@@ -6,20 +8,63 @@ describe('LinkButtonComponent', () => {
6
8
  let component: LinkButtonComponent;
7
9
  let fixture: ComponentFixture<LinkButtonComponent>;
8
10
 
9
- beforeEach(async(() => {
11
+ beforeEach(waitForAsync(() => {
10
12
  TestBed.configureTestingModule({
11
- declarations: [ LinkButtonComponent ]
12
- })
13
- .compileComponents();
13
+ declarations: [LinkButtonComponent],
14
+ }).compileComponents();
14
15
  }));
15
16
 
16
17
  beforeEach(() => {
17
18
  fixture = TestBed.createComponent(LinkButtonComponent);
18
19
  component = fixture.componentInstance;
20
+ component.button = { value: 'Open', link: 'https://x.test', target: 'blank' };
19
21
  fixture.detectChanges();
20
22
  });
21
23
 
22
24
  it('should create', () => {
23
25
  expect(component).toBeTruthy();
24
26
  });
27
+
28
+ it('ngOnChanges should set CSS variables on .url', () => {
29
+ component.fontSize = '12px';
30
+ component.backgroundColor = '#aaa';
31
+ component.textColor = '#bbb';
32
+ component.hoverBackgroundColor = '#ccc';
33
+ component.hoverTextColor = '#ddd';
34
+ component.ngOnChanges({
35
+ fontSize: new SimpleChange(null, '12px', true),
36
+ });
37
+ const el = fixture.nativeElement.querySelector('.url') as HTMLElement;
38
+ expect(el.style.getPropertyValue('--buttonFontSize').trim()).toBe('12px');
39
+ });
40
+
41
+ it('actionButtonUrl should emit when link set', () => {
42
+ spyOn(component.onButtonClicked, 'emit');
43
+ component.actionButtonUrl();
44
+ expect(component.onButtonClicked.emit).toHaveBeenCalled();
45
+ });
46
+
47
+ it('actionButtonUrl should not emit when link empty', () => {
48
+ component.button = { value: 'x', link: '' };
49
+ spyOn(component.onButtonClicked, 'emit');
50
+ component.actionButtonUrl();
51
+ expect(component.onButtonClicked.emit).not.toHaveBeenCalled();
52
+ });
53
+
54
+ it('should render external icon when target is not self', () => {
55
+ component.button = { value: 'L', link: 'u', target: 'blank' };
56
+ fixture.detectChanges();
57
+ expect(fixture.debugElement.query(By.css('.icon-button-action'))).toBeTruthy();
58
+ });
59
+
60
+ it('should render self icon when target is self', () => {
61
+ component.button = { value: 'L', link: 'u', target: 'self' };
62
+ fixture.detectChanges();
63
+ expect(fixture.debugElement.query(By.css('.icon-button-action-self'))).toBeTruthy();
64
+ });
65
+
66
+ it('mouseover and mouseout should not throw', () => {
67
+ expect(() => component.onMouseOver({} as any)).not.toThrow();
68
+ expect(() => component.onMouseOut({} as any)).not.toThrow();
69
+ });
25
70
  });
@@ -1,4 +1,6 @@
1
- import { async, ComponentFixture, TestBed } from '@angular/core/testing';
1
+ import { SimpleChange } from '@angular/core';
2
+ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
3
+ import { By } from '@angular/platform-browser';
2
4
 
3
5
  import { TextButtonComponent } from './text-button.component';
4
6
 
@@ -6,20 +8,57 @@ describe('TextButtonComponent', () => {
6
8
  let component: TextButtonComponent;
7
9
  let fixture: ComponentFixture<TextButtonComponent>;
8
10
 
9
- beforeEach(async(() => {
11
+ beforeEach(waitForAsync(() => {
10
12
  TestBed.configureTestingModule({
11
- declarations: [ TextButtonComponent ]
12
- })
13
- .compileComponents();
13
+ declarations: [TextButtonComponent],
14
+ }).compileComponents();
14
15
  }));
15
16
 
16
17
  beforeEach(() => {
17
18
  fixture = TestBed.createComponent(TextButtonComponent);
18
19
  component = fixture.componentInstance;
20
+ component.button = { value: 'Reply' };
21
+ component.isConversationArchived = false;
19
22
  fixture.detectChanges();
20
23
  });
21
24
 
22
25
  it('should create', () => {
23
26
  expect(component).toBeTruthy();
24
27
  });
28
+
29
+ it('ngOnChanges should set CSS variables on .text', () => {
30
+ component.fontSize = '13px';
31
+ component.backgroundColor = '#111';
32
+ component.textColor = '#222';
33
+ component.hoverBackgroundColor = '#333';
34
+ component.hoverTextColor = '#444';
35
+ component.ngOnChanges({
36
+ fontSize: new SimpleChange(null, '13px', true),
37
+ });
38
+ const el = fixture.nativeElement.querySelector('.text') as HTMLElement;
39
+ expect(el.style.getPropertyValue('--buttonFontSize').trim()).toBe('13px');
40
+ });
41
+
42
+ it('actionButtonText should emit click payload', () => {
43
+ spyOn(component.onButtonClicked, 'emit');
44
+ component.actionButtonText();
45
+ expect(component.onButtonClicked.emit).toHaveBeenCalled();
46
+ });
47
+
48
+ it('click on template should invoke actionButtonText', () => {
49
+ spyOn(component, 'actionButtonText');
50
+ fixture.debugElement.query(By.css('.text')).triggerEventHandler('click', {});
51
+ expect(component.actionButtonText).toHaveBeenCalled();
52
+ });
53
+
54
+ it('should add disabled class when conversation archived', () => {
55
+ component.isConversationArchived = true;
56
+ fixture.detectChanges();
57
+ expect(fixture.nativeElement.querySelector('.text').classList.contains('disabled')).toBe(true);
58
+ });
59
+
60
+ it('mouseover and mouseout should not throw', () => {
61
+ expect(() => component.onMouseOver({} as any)).not.toThrow();
62
+ expect(() => component.onMouseOut({} as any)).not.toThrow();
63
+ });
25
64
  });
@@ -1,19 +1,28 @@
1
- <div class="wrapper">
2
- <div id="left" class="arrow left" (click)="goTo('previous')" *ngIf="activeElement > 1">
3
- <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000">
1
+ <div class="wrapper"
2
+ role="region"
3
+ aria-roledescription="carousel"
4
+ [attr.aria-label]="translationMap?.get('CAROUSEL_LABEL') || 'Cards carousel'">
5
+ <button type="button"
6
+ id="left"
7
+ class="arrow left c21-button-clean"
8
+ [attr.aria-label]="translationMap?.get('CAROUSEL_PREVIOUS') || 'Previous slide'"
9
+ (click)="goTo('previous')" *ngIf="activeElement > 1">
10
+ <svg aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000">
4
11
  <path d="M0 0h24v24H0V0z" fill="none"/><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12l4.58-4.59z"/>
5
12
  </svg>
6
- </div>
13
+ </button>
7
14
  <div class="carousel">
8
- <!-- <div class="card" style="width: 17px;"></div> -->
9
- <div class="card" *ngFor="let card of gallery; let i = index">
10
- <!-- <div [style.opacity]="i+1 === activeElement? 1: 0.5"> -->
15
+ <div class="card"
16
+ *ngFor="let card of gallery; let i = index"
17
+ role="group"
18
+ aria-roledescription="slide"
19
+ [attr.aria-label]="getSlideLabel(i + 1, gallery?.length)">
11
20
  <div>
12
21
  <div class="card-image" *ngIf="card?.preview?.src !== ''">
13
- <img [src]="card?.preview?.src" alt="img" draggable="false">
22
+ <img [src]="card?.preview?.src" [attr.alt]="card?.title || 'Carousel image'" draggable="false">
14
23
  </div>
15
24
  <div class="card-image card-image-placeholder" *ngIf="card?.preview?.src == ''">
16
- <img src="assets/images/icons/no-image.svg" alt="img" draggable="false">
25
+ <img src="assets/images/icons/no-image.svg" alt="" draggable="false">
17
26
  <span>Image not available</span>
18
27
  </div>
19
28
  <div class="card-content">
@@ -21,20 +30,24 @@
21
30
  <div class="card-description">{{card?.description}}</div>
22
31
  </div>
23
32
  <div class="buttons" *ngIf="card?.buttons && card?.buttons.length > 0">
24
- <div *ngFor="let button of card?.buttons"
25
- class="single-button action"
33
+ <button type="button" *ngFor="let button of card?.buttons"
34
+ class="single-button action c21-button-clean"
26
35
  [ngClass]="{'disabled': (isConversationArchived || (!isLastMessage && button.type !== TYPE_BUTTON.URL)), 'active': button?.active}"
36
+ [attr.aria-label]="button?.value"
27
37
  (click)="actionButtonClick($event, button, i)" >
28
38
  {{button.value}}
29
- </div>
39
+ </button>
30
40
  </div>
31
41
  </div>
32
42
  </div>
33
- <!-- <div class="card" style="width: 17px;"></div> -->
34
43
  </div>
35
- <div id="right" class="arrow right" (click)="goTo('next')" *ngIf="activeElement !== gallery.length">
36
- <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000">
44
+ <button type="button"
45
+ id="right"
46
+ class="arrow right c21-button-clean"
47
+ [attr.aria-label]="translationMap?.get('CAROUSEL_NEXT') || 'Next slide'"
48
+ (click)="goTo('next')" *ngIf="activeElement !== gallery.length">
49
+ <svg aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000">
37
50
  <path d="M0 0h24v24H0V0z" fill="none"/><path d="M10.02 6L8.61 7.41 13.19 12l-4.58 4.59L10.02 18l6-6-6-6z"/>
38
51
  </svg>
39
- </div>
52
+ </button>
40
53
  </div>