@chat21/chat21-web-widget 5.1.34 → 5.2.1

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 (191) 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/CHANGELOG.md +25 -0
  6. package/Dockerfile +4 -5
  7. package/README.md +1 -1
  8. package/angular.json +21 -3
  9. package/docs/ACCESSIBILITY-STATEMENT.md +388 -0
  10. package/docs/TILEDESK_WIDGET_ACCESSIBILITY_ALIGNMENT.md +60 -0
  11. package/docs/TILEDESK_WIDGET_ACCESSIBILITY_STATEMENT_COMPLETE.md +386 -0
  12. package/env.sample +3 -2
  13. package/mocks/voice-websocket-mock/server.cjs +245 -0
  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.module.ts +13 -0
  20. package/src/app/component/conversation-detail/conversation/conversation.component.html +25 -11
  21. package/src/app/component/conversation-detail/conversation/conversation.component.scss +38 -0
  22. package/src/app/component/conversation-detail/conversation/conversation.component.spec.ts +644 -75
  23. package/src/app/component/conversation-detail/conversation/conversation.component.ts +70 -2
  24. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.html +25 -13
  25. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.spec.ts +123 -5
  26. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.ts +1 -0
  27. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.html +23 -10
  28. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.scss +18 -0
  29. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.spec.ts +241 -149
  30. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.ts +8 -5
  31. package/src/app/component/conversation-detail/conversation-emojii/conversation-emojii.component.spec.ts +53 -3
  32. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.html +203 -110
  33. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss +212 -1
  34. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.spec.ts +458 -78
  35. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts +288 -76
  36. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.html +113 -53
  37. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.scss +12 -4
  38. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.spec.ts +274 -29
  39. package/src/app/component/conversation-detail/conversation-internal-frame/conversation-internal-frame.component.html +23 -9
  40. package/src/app/component/conversation-detail/conversation-internal-frame/conversation-internal-frame.component.spec.ts +80 -8
  41. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.html +29 -23
  42. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.spec.ts +185 -16
  43. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.ts +34 -14
  44. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.html +46 -0
  45. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.scss +83 -0
  46. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.ts +192 -0
  47. package/src/app/component/error-alert/error-alert.component.spec.ts +65 -5
  48. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.html +16 -7
  49. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.scss +21 -0
  50. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.spec.ts +89 -7
  51. package/src/app/component/form/form-builder/form-builder.component.html +1 -1
  52. package/src/app/component/form/form-builder/form-builder.component.spec.ts +163 -21
  53. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.html +8 -4
  54. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.scss +10 -5
  55. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.spec.ts +90 -16
  56. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.ts +26 -0
  57. package/src/app/component/form/inputs/form-label/form-label.component.spec.ts +45 -11
  58. package/src/app/component/form/inputs/form-radio-button/form-radio-button.component.spec.ts +24 -6
  59. package/src/app/component/form/inputs/form-select/form-select.component.spec.ts +14 -5
  60. package/src/app/component/form/inputs/form-text/form-text.component.html +14 -12
  61. package/src/app/component/form/inputs/form-text/form-text.component.scss +11 -1
  62. package/src/app/component/form/inputs/form-text/form-text.component.spec.ts +113 -17
  63. package/src/app/component/form/inputs/form-text/form-text.component.ts +35 -3
  64. package/src/app/component/form/inputs/form-textarea/form-textarea.component.html +13 -11
  65. package/src/app/component/form/inputs/form-textarea/form-textarea.component.scss +6 -5
  66. package/src/app/component/form/inputs/form-textarea/form-textarea.component.spec.ts +149 -13
  67. package/src/app/component/form/inputs/form-textarea/form-textarea.component.ts +26 -0
  68. package/src/app/component/form/prechat-form/prechat-form.component.html +14 -11
  69. package/src/app/component/form/prechat-form/prechat-form.component.spec.ts +102 -10
  70. package/src/app/component/form/prechat-form/prechat-form.component.ts +8 -1
  71. package/src/app/component/form/prechat-form-test-mock.ts +35 -0
  72. package/src/app/component/home/home.component.html +38 -31
  73. package/src/app/component/home/home.component.scss +4 -2
  74. package/src/app/component/home/home.component.spec.ts +226 -11
  75. package/src/app/component/home-conversations/home-conversations.component.html +30 -26
  76. package/src/app/component/home-conversations/home-conversations.component.scss +3 -0
  77. package/src/app/component/home-conversations/home-conversations.component.spec.ts +212 -36
  78. package/src/app/component/last-message/last-message.component.html +15 -9
  79. package/src/app/component/last-message/last-message.component.scss +16 -2
  80. package/src/app/component/last-message/last-message.component.spec.ts +204 -23
  81. package/src/app/component/launcher-button/launcher-button.component.html +8 -13
  82. package/src/app/component/launcher-button/launcher-button.component.spec.ts +104 -8
  83. package/src/app/component/list-all-conversations/list-all-conversations.component.html +12 -17
  84. package/src/app/component/list-all-conversations/list-all-conversations.component.scss +2 -0
  85. package/src/app/component/list-conversations/list-conversations.component.html +22 -22
  86. package/src/app/component/menu-options/menu-options.component.html +30 -20
  87. package/src/app/component/menu-options/menu-options.component.spec.ts +125 -9
  88. package/src/app/component/message/audio/audio.component.html +13 -15
  89. package/src/app/component/message/audio/audio.component.spec.ts +140 -5
  90. package/src/app/component/message/audio/audio.component.ts +1 -5
  91. package/src/app/component/message/audio-sync/audio-sync.component.html +18 -0
  92. package/src/app/component/message/audio-sync/audio-sync.component.scss +65 -0
  93. package/src/app/component/message/audio-sync/audio-sync.component.spec.ts +112 -0
  94. package/src/app/component/message/audio-sync/audio-sync.component.ts +714 -0
  95. package/src/app/component/message/avatar/avatar.component.html +2 -2
  96. package/src/app/component/message/avatar/avatar.component.spec.ts +99 -7
  97. package/src/app/component/message/bubble-message/bubble-message.component.html +41 -51
  98. package/src/app/component/message/bubble-message/bubble-message.component.scss +54 -1
  99. package/src/app/component/message/bubble-message/bubble-message.component.spec.ts +147 -57
  100. package/src/app/component/message/bubble-message/bubble-message.component.ts +95 -13
  101. package/src/app/component/message/buttons/action-button/action-button.component.html +3 -4
  102. package/src/app/component/message/buttons/action-button/action-button.component.spec.ts +49 -5
  103. package/src/app/component/message/buttons/link-button/link-button.component.scss +5 -8
  104. package/src/app/component/message/buttons/link-button/link-button.component.spec.ts +50 -5
  105. package/src/app/component/message/buttons/text-button/text-button.component.spec.ts +44 -5
  106. package/src/app/component/message/carousel/carousel.component.html +29 -16
  107. package/src/app/component/message/carousel/carousel.component.scss +20 -8
  108. package/src/app/component/message/carousel/carousel.component.spec.ts +80 -3
  109. package/src/app/component/message/carousel/carousel.component.ts +16 -0
  110. package/src/app/component/message/frame/frame.component.html +9 -4
  111. package/src/app/component/message/frame/frame.component.spec.ts +34 -15
  112. package/src/app/component/message/frame/frame.component.ts +7 -2
  113. package/src/app/component/message/html/html.component.html +1 -1
  114. package/src/app/component/message/html/html.component.scss +1 -1
  115. package/src/app/component/message/html/html.component.spec.ts +24 -7
  116. package/src/app/component/message/image/image.component.html +12 -10
  117. package/src/app/component/message/image/image.component.scss +16 -0
  118. package/src/app/component/message/image/image.component.spec.ts +101 -15
  119. package/src/app/component/message/image/image.component.ts +90 -51
  120. package/src/app/component/message/info-message/info-message.component.spec.ts +26 -14
  121. package/src/app/component/message/json-sources/json-sources.component.html +6 -5
  122. package/src/app/component/message/json-sources/json-sources.component.scss +26 -18
  123. package/src/app/component/message/json-sources/json-sources.component.ts +41 -0
  124. package/src/app/component/message/like-unlike/like-unlike.component.html +7 -9
  125. package/src/app/component/message/like-unlike/like-unlike.component.spec.ts +31 -3
  126. package/src/app/component/message/return-receipt/return-receipt.component.spec.ts +38 -17
  127. package/src/app/component/message/text/text.component.html +3 -3
  128. package/src/app/component/message/text/text.component.scss +80 -86
  129. package/src/app/component/message/text/text.component.spec.ts +106 -13
  130. package/src/app/component/message-attachment/message-attachment.component.spec.ts +134 -13
  131. package/src/app/component/selection-department/selection-department.component.html +21 -23
  132. package/src/app/component/selection-department/selection-department.component.spec.ts +159 -14
  133. package/src/app/component/selection-department/selection-department.component.ts +8 -1
  134. package/src/app/component/send-button/send-button.component.html +5 -13
  135. package/src/app/component/send-button/send-button.component.spec.ts +2 -2
  136. package/src/app/component/star-rating-widget/star-rating-widget.component.html +51 -81
  137. package/src/app/directives/tooltip.directive.spec.ts +8 -4
  138. package/src/app/modals/confirm-close/confirm-close.component.html +20 -8
  139. package/src/app/modals/confirm-close/confirm-close.component.scss +3 -0
  140. package/src/app/modals/confirm-close/confirm-close.component.spec.ts +13 -4
  141. package/src/app/modals/confirm-close/confirm-close.component.ts +8 -1
  142. package/src/app/pipe/html-entites-encode.pipe.spec.ts +35 -2
  143. package/src/app/pipe/marked.pipe.spec.ts +38 -2
  144. package/src/app/pipe/marked.pipe.ts +51 -41
  145. package/src/app/providers/app-config.service.ts +4 -2
  146. package/src/app/providers/brand.service.spec.ts +23 -2
  147. package/src/app/providers/brand.service.ts +1 -1
  148. package/src/app/providers/global-settings.service.spec.ts +1009 -14
  149. package/src/app/providers/global-settings.service.ts +40 -2
  150. package/src/app/providers/json-sources-parser.service.ts +13 -1
  151. package/src/app/providers/translator.service.ts +24 -7
  152. package/src/app/providers/tts-audio-playback-coordinator.service.spec.ts +116 -0
  153. package/src/app/providers/tts-audio-playback-coordinator.service.ts +122 -0
  154. package/src/app/providers/voice/STT&TTS/openai-voice.config.ts +12 -0
  155. package/src/app/providers/voice/STT&TTS/openai-voice.provider.ts +156 -0
  156. package/src/app/providers/voice/STT&TTS/speech-provider.abstract.ts +39 -0
  157. package/src/app/providers/voice/audio.types.ts +40 -0
  158. package/src/app/providers/voice/vad.service.spec.ts +28 -0
  159. package/src/app/providers/voice/vad.service.ts +70 -0
  160. package/src/app/providers/voice/voice-streaming.service.spec.ts +23 -0
  161. package/src/app/providers/voice/voice-streaming.service.ts +702 -0
  162. package/src/app/providers/voice/voice-streaming.types.ts +112 -0
  163. package/src/app/providers/voice/voice.service.spec.ts +227 -0
  164. package/src/app/providers/voice/voice.service.ts +969 -0
  165. package/src/app/sass/_variables.scss +2 -0
  166. package/src/app/sass/animations.scss +19 -1
  167. package/src/app/shims/onnxruntime-web-wasm.ts +4 -0
  168. package/src/app/utils/globals.ts +14 -0
  169. package/src/app/utils/utils-resources.ts +1 -1
  170. package/src/assets/i18n/en.json +128 -100
  171. package/src/assets/i18n/es.json +128 -100
  172. package/src/assets/i18n/fr.json +128 -100
  173. package/src/assets/i18n/it.json +128 -98
  174. package/src/assets/onnx/ort-wasm-simd-threaded.mjs +59 -0
  175. package/src/assets/onnx/ort-wasm-simd-threaded.wasm +0 -0
  176. package/src/assets/sounds/keyboard.mp3 +0 -0
  177. package/src/assets/vad/silero_vad_legacy.onnx +0 -0
  178. package/src/assets/vad/vad.worklet.bundle.min.js +1 -0
  179. package/src/chat21-core/models/message.ts +2 -1
  180. package/src/chat21-core/providers/chat-manager.spec.ts +72 -0
  181. package/src/chat21-core/providers/firebase/firebase-conversation-handler.ts +3 -2
  182. package/src/chat21-core/providers/firebase/firebase-init-service.ts +5 -5
  183. package/src/chat21-core/providers/mqtt/mqtt-conversation-handler.ts +12 -0
  184. package/src/chat21-core/providers/scripts/script.service.spec.ts +12 -2
  185. package/src/chat21-core/utils/utils-message.ts +7 -0
  186. package/src/widget-config-template.json +3 -1
  187. package/src/widget-config.json +28 -27
  188. package/tests/widget-form-rich.spec.ts +67 -0
  189. package/tests/widget-index-dev-settings.spec.ts +52 -0
  190. package/tests/widget-twp-iframe.spec.ts +39 -0
  191. package/tsconfig.json +5 -0
@@ -1,4 +1,4 @@
1
- import { Component, ComponentFactoryResolver, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild, ViewContainerRef } from '@angular/core';
1
+ import { Component, ComponentFactoryResolver, ElementRef, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges, ViewChild, ViewContainerRef } from '@angular/core';
2
2
  import { error } from 'console';
3
3
  import { FILE_SIZE_LIMIT } from 'src/app/utils/constants';
4
4
  import { Globals } from 'src/app/utils/globals';
@@ -15,13 +15,18 @@ import { TYPE_MSG_FILE, TYPE_MSG_IMAGE, TYPE_MSG_TEXT } from 'src/chat21-core/ut
15
15
  import { convertColorToRGBA, isAllowedUrlInText, isEmoji } from 'src/chat21-core/utils/utils';
16
16
  import { findAndRemoveEmoji, isImage } from 'src/chat21-core/utils/utils-message';
17
17
  import { ProjectModel } from 'src/models/project';
18
+ import { Subscription } from 'rxjs';
19
+ import { VoiceService } from 'src/app/providers/voice/voice.service';
20
+ import { VoiceStreamingSessionConfig } from 'src/app/providers/voice/voice-streaming.types';
21
+ import { TtsAudioPlaybackCoordinator } from 'src/app/providers/tts-audio-playback-coordinator.service';
22
+ import { TiledeskAuthService } from 'src/chat21-core/providers/tiledesk/tiledesk-auth.service';
18
23
 
19
24
  @Component({
20
25
  selector: 'chat-conversation-footer',
21
26
  templateUrl: './conversation-footer.component.html',
22
27
  styleUrls: ['./conversation-footer.component.scss']
23
28
  })
24
- export class ConversationFooterComponent implements OnInit, OnChanges {
29
+ export class ConversationFooterComponent implements OnInit, OnChanges, OnDestroy {
25
30
 
26
31
  @Input() conversationWith: string;
27
32
  @Input() attributes: string;
@@ -32,9 +37,11 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
32
37
  @Input() userFullname: string;
33
38
  @Input() userEmail: string;
34
39
  @Input() showAttachmentFooterButton: boolean;
35
- @Input() showEmojiFooterButton: boolean
36
- @Input() showAudioRecorderFooterButton: boolean
40
+ @Input() showEmojiFooterButton: boolean;
41
+ @Input() showAudioRecorderFooterButton: boolean;
42
+ @Input() showAudioStreamFooterButton: boolean;
37
43
  // @Input() showContinueConversationButton: boolean;
44
+ @Input() closeChatInConversation: boolean;
38
45
  @Input() isConversationArchived: boolean;
39
46
  @Input() hideTextAreaContent: boolean;
40
47
  @Input() hideTextReply: boolean;
@@ -52,6 +59,9 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
52
59
  @Output() onChangeTextArea = new EventEmitter<any>();
53
60
  @Output() onAttachmentFileButtonClicked = new EventEmitter<any>();
54
61
  @Output() onNewConversationButtonClicked = new EventEmitter();
62
+ @Output() onStreamAudioActiveChange = new EventEmitter<boolean>();
63
+ @Output() onStreamAudioConnectingChange = new EventEmitter<boolean>();
64
+ @Output() onCloseChatButtonClicked = new EventEmitter();
55
65
 
56
66
  @ViewChild('chat21_file') public chat21_file: ElementRef;
57
67
  // @ViewChild('emojii_container', {read: ViewContainerRef}) selector;
@@ -85,24 +95,61 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
85
95
 
86
96
  showAlertEmoji: boolean = false
87
97
 
98
+ /** Stream audio UI: icona equalizer → X; alert con onde animate sopra il footer */
99
+ isStreamAudioActive = false;
100
+ /** True while the WebSocket session is being established (between click and session_started). */
101
+ isStreamAudioConnecting = false;
102
+ /** True while the bot's TTS audio is playing — mic segments are suppressed, spectrum turns grey. */
103
+ isBotSpeaking = false;
104
+ /** Sottoscrizione ai segmenti audio (VAD → WebM) dal {@link VoiceService}. */
105
+ private voiceAudioSubscription?: Subscription;
106
+ /** Sottoscrizione a `transcript` finale dalla WSS. */
107
+ private voiceTranscriptSubscription?: Subscription;
108
+ /** Sottoscrizione al volume audio (real-time) dal {@link VoiceService}. */
109
+ private voiceVolumeSubscription?: Subscription;
110
+ /** Sottoscrizione allo stato TTS (bot sta parlando). */
111
+ private botSpeakingSub?: Subscription;
112
+ /** Passato a {@link StreamAudioSpectrumComponent} per disegnare la linea spettro. */
113
+ currentVolume = 0;
114
+ /** Last user utterance transcribed — persists during bot processing to show in voice panel. */
115
+ lastVoiceTranscript = '';
116
+
117
+ get voiceStatusLabel(): string {
118
+ if (this.isStreamAudioConnecting && !this.isStreamAudioActive) {
119
+ return this.translationMap?.get('VOICE_CONNECTING') || 'Connecting...';
120
+ }
121
+ if (this.isStreamAudioActive && this.isBotSpeaking) {
122
+ return this.translationMap?.get('VOICE_PROCESSING') || 'Processing...';
123
+ }
124
+ return this.translationMap?.get('VOICE_LISTENING') || 'Listening...';
125
+ }
126
+
88
127
  file_size_limit = FILE_SIZE_LIMIT;
89
128
  attachmentTooltip: string = '';
90
129
 
91
130
 
92
131
  convertColorToRGBA = convertColorToRGBA;
93
132
  private logger: LoggerService = LoggerInstance.getInstance()
94
- constructor(private chatManager: ChatManager,
95
- private typingService: TypingService,
96
- private uploadService: UploadService) { }
133
+ constructor(
134
+ private chatManager: ChatManager,
135
+ private typingService: TypingService,
136
+ private uploadService: UploadService,
137
+ private voiceService: VoiceService,
138
+ private ttsPlayback: TtsAudioPlaybackCoordinator,
139
+ private tiledeskAuthService: TiledeskAuthService,
140
+ public g: Globals,
141
+ ) {}
97
142
 
98
143
  ngOnInit() {
99
144
  // this.updateAttachmentTooltip();
100
145
  }
101
146
 
102
-
103
147
  ngOnChanges(changes: SimpleChanges){
104
148
  if(changes['conversationWith'] && changes['conversationWith'].currentValue !== undefined){
105
149
  this.conversationHandlerService = this.chatManager.getConversationHandlerByConversationId(this.conversationWith);
150
+ this.isStreamAudioActive = false;
151
+ this.ttsPlayback.cancelAll();
152
+ void this.stopVoice();
106
153
  }
107
154
  if(changes['hideTextReply'] && changes['hideTextReply'].currentValue !== undefined){
108
155
  this.restoreTextArea();
@@ -142,6 +189,159 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
142
189
  // }, 500);
143
190
  // }
144
191
 
192
+ /**
193
+ * Stream voce: con `voiceIngress` solo WSS (no VAD) — transcript + TTS dal server.
194
+ * Senza ingresso WSS: VAD + upload per segmento.
195
+ */
196
+ async initVoice() {
197
+ this.voiceAudioSubscription?.unsubscribe();
198
+ this.voiceVolumeSubscription?.unsubscribe();
199
+ this.botSpeakingSub?.unsubscribe();
200
+ this.voiceTranscriptSubscription?.unsubscribe();
201
+
202
+ const voiceIngress = this.buildVoiceIngressStreamConfig();
203
+ this.voiceAudioSubscription = undefined;
204
+ this.voiceTranscriptSubscription = this.voiceService.voiceTranscript$.subscribe(({ text }) => {
205
+ // Guard: stop accepting transcript text once the proxy is processing (thinking/speaking)
206
+ if (text && !this.isBotSpeaking) {
207
+ this.textInputTextArea = text;
208
+ this.lastVoiceTranscript = text;
209
+ // The proxy publishes the user utterance to Chat21 via AMQP on utterance-end;
210
+ // no sendMessage call is needed here — doing so would produce duplicate messages.
211
+ }
212
+ });
213
+
214
+ this.voiceVolumeSubscription = this.voiceService.volume$.subscribe((volume) => {
215
+ this.currentVolume = volume;
216
+ });
217
+ this.botSpeakingSub = this.voiceService.isAcquisitionBlocked$.subscribe((blocked) => {
218
+ this.isBotSpeaking = blocked;
219
+ if (blocked) {
220
+ // Proxy has started thinking/speaking — clear the textarea preview
221
+ this.textInputTextArea = '';
222
+ }
223
+ });
224
+ await this.voiceService.startSession(voiceIngress ? { voiceIngressStream: voiceIngress } : {});
225
+ }
226
+
227
+ private buildVoiceIngressStreamConfig(): VoiceStreamingSessionConfig | null {
228
+ const token = this.tiledeskAuthService.getTiledeskToken() ?? '';
229
+ const sender = this.tiledeskAuthService.getCurrentUser()?.uid ?? '';
230
+ const recipient = this.conversationWith ?? '';
231
+ if (!token || !sender || !recipient) {
232
+ this.logger.warn('[CONV-FOOTER] buildVoiceIngressStreamConfig: missing required fields', {
233
+ hasToken: !!token,
234
+ hasSender: !!sender,
235
+ hasRecipient: !!recipient,
236
+ });
237
+ return null;
238
+ }
239
+ const { recipientFullname, attributes, channelType } = this.buildSendMessageContext();
240
+ this.logger.log('[CONV-FOOTER] buildVoiceIngressStreamConfig', { sender, recipient, channelType });
241
+ return {
242
+ token,
243
+ sender,
244
+ recipient,
245
+ // Use Deepgram multilingual code-switching so the model detects the spoken
246
+ // language from the audio stream regardless of browser locale.
247
+ // Source: https://developers.deepgram.com/docs/multilingual-code-switching
248
+ lang: 'multi',
249
+ text: '',
250
+ type: 'text',
251
+ recipient_fullname: recipientFullname ?? '',
252
+ sender_fullname: recipientFullname ?? '',
253
+ attributes: attributes ?? {},
254
+ metadata: '',
255
+ channel_type: channelType ?? '',
256
+ };
257
+ }
258
+
259
+ /**
260
+ * Merge `attributes` di componente con `additional_attributes` e risolve `recipientFullname` come in sendMessage.
261
+ */
262
+ private buildSendMessageContext(additional_attributes?: any) {
263
+ let recipientFullname = this.translationMap.get('GUEST_LABEL');
264
+ const g_attributes = this.attributes;
265
+ const attributes = <any>{};
266
+ if (g_attributes) {
267
+ for (const [key, value] of Object.entries(g_attributes)) {
268
+ attributes[key] = value;
269
+ }
270
+ }
271
+ if (additional_attributes) {
272
+ for (const [key, value] of Object.entries(additional_attributes)) {
273
+ attributes[key] = value;
274
+ }
275
+ }
276
+ const senderId = this.senderId;
277
+ const projectid = this.project.id;
278
+ const channelType = this.channelType;
279
+ const userFullname = this.userFullname;
280
+ const userEmail = this.userEmail;
281
+ const conversationWith = this.conversationWith;
282
+
283
+ if (userFullname) {
284
+ recipientFullname = userFullname;
285
+ } else if (userEmail) {
286
+ recipientFullname = userEmail;
287
+ } else if (attributes && attributes['userFullname']) {
288
+ recipientFullname = attributes['userFullname'];
289
+ } else {
290
+ recipientFullname = this.translationMap.get('GUEST_LABEL');
291
+ }
292
+
293
+ return {
294
+ recipientFullname,
295
+ attributes,
296
+ senderId,
297
+ projectid,
298
+ channelType,
299
+ conversationWith,
300
+ };
301
+ }
302
+ async stopVoice(options?: { discardInProgressSegment?: boolean }) {
303
+ // Stop all active TTS audio immediately and reveal all text.
304
+ this.ttsPlayback.stopAll();
305
+
306
+ this.voiceAudioSubscription?.unsubscribe();
307
+ this.voiceAudioSubscription = undefined;
308
+
309
+ this.voiceTranscriptSubscription?.unsubscribe();
310
+ this.voiceTranscriptSubscription = undefined;
311
+
312
+ this.voiceVolumeSubscription?.unsubscribe();
313
+ this.voiceVolumeSubscription = undefined;
314
+
315
+ this.botSpeakingSub?.unsubscribe();
316
+ this.botSpeakingSub = undefined;
317
+ this.isBotSpeaking = false;
318
+
319
+ await this.voiceService.stopSession(options);
320
+ this.currentVolume = 0;
321
+ this.textInputTextArea = '';
322
+ this.lastVoiceTranscript = '';
323
+ }
324
+
325
+ /**
326
+ * Messaggio in arrivo da un altro mittente mentre lo stream è attivo: con VAD legacy scarta il segmento in corso.
327
+ * Con sola sessione WSS non ha effetto sul mic (nessun recorder a segmenti locale).
328
+ */
329
+ interruptStreamDueToPeerMessage(): void {
330
+ if (!this.isStreamAudioActive) {
331
+ return;
332
+ }
333
+ this.logger.log('[CONV-FOOTER] discard recording segment: incoming message from peer (stream stays on)');
334
+ try {
335
+ this.voiceService.discardCurrentRecordingSegment();
336
+ } catch (e) {
337
+ this.logger.error('[CONV-FOOTER] interruptStreamDueToPeerMessage', e);
338
+ }
339
+ }
340
+
341
+ ngOnDestroy() {
342
+ void this.stopVoice();
343
+ }
344
+
145
345
  // ========= begin:: functions send image ======= //
146
346
  // START LOAD IMAGE //
147
347
  /** load the selected image locally and open the pop up preview */
@@ -379,40 +579,14 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
379
579
  // msg = replaceEndOfLine(msg);
380
580
  // msg = msg.trim();
381
581
 
382
- let recipientFullname = this.translationMap.get('GUEST_LABEL');
383
- // sponziello: adds ADDITIONAL ATTRIBUTES TO THE MESSAGE
384
- const g_attributes = this.attributes;
385
- // added <any> to resolve the Error occurred during the npm installation: Property 'userFullname' does not exist on type '{}'
386
- const attributes = <any>{};
387
- if (g_attributes) {
388
- for (const [key, value] of Object.entries(g_attributes)) {
389
- attributes[key] = value;
390
- }
391
- }
392
- if (additional_attributes) {
393
- for (const [key, value] of Object.entries(additional_attributes)) {
394
- attributes[key] = value;
395
- }
396
- }
397
- // fine-sponziello
398
- // this.conversationHandlerService = this.chatManager.getConversationHandlerByConversationId(this.conversationWith)
399
- const senderId = this.senderId;
400
- const projectid = this.project.id;
401
- const channelType = this.channelType;
402
- const userFullname = this.userFullname;
403
- const userEmail = this.userEmail;
404
- const conversationWith = this.conversationWith;
405
-
406
-
407
- if (userFullname) {
408
- recipientFullname = userFullname;
409
- } else if (userEmail) {
410
- recipientFullname = userEmail;
411
- } else if (attributes && attributes['userFullname']) {
412
- recipientFullname = attributes['userFullname'];
413
- } else {
414
- recipientFullname = this.translationMap.get('GUEST_LABEL');
415
- }
582
+ const {
583
+ recipientFullname,
584
+ attributes,
585
+ senderId,
586
+ projectid,
587
+ channelType,
588
+ conversationWith,
589
+ } = this.buildSendMessageContext(additional_attributes);
416
590
 
417
591
  this.onBeforeMessageSent.emit({
418
592
  senderFullname: recipientFullname,
@@ -521,7 +695,7 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
521
695
  }
522
696
  }
523
697
 
524
- prepareAndUpload(audioBlob: Blob) {
698
+ prepareAndUpload(audioBlob: Blob, text: string = '') {
525
699
 
526
700
  this.isFilePendingToUpload = true;
527
701
 
@@ -551,7 +725,7 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
551
725
  this.logger.log('[UPLOAD] metadata:', metadata);
552
726
 
553
727
  // stesso metodo che già usi
554
- this.uploadSingle(metadata, file, '');
728
+ this.uploadSingle(metadata, file, text);
555
729
  }
556
730
 
557
731
  // Funzione per convertire Blob in Base64 usando FileReader
@@ -658,6 +832,42 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
658
832
  }
659
833
  }
660
834
 
835
+ async onStreamPressed(event: Event) {
836
+ this.logger.log('[CONV-FOOTER] onStreamPressed:event', event);
837
+ event.preventDefault();
838
+ if (this.showAlertEmoji) {
839
+ return;
840
+ }
841
+ // Treat a click during connecting as a cancel request (same as turning off).
842
+ const turningOn = !this.isStreamAudioActive && !this.isStreamAudioConnecting;
843
+ this.logger.log('[CONV-FOOTER] onStreamPressed', { turningOn });
844
+ if (turningOn) {
845
+ this.isStreamAudioConnecting = true;
846
+ this.onStreamAudioConnectingChange.emit(true);
847
+ try {
848
+ this.currentVolume = 0;
849
+ await this.initVoice();
850
+ this.isStreamAudioActive = true;
851
+ } catch (e) {
852
+ this.logger.error('[CONV-FOOTER] onStreamPressed: initVoice failed', e);
853
+ this.isStreamAudioActive = false;
854
+ this.ttsPlayback.cancelAll();
855
+ } finally {
856
+ this.isStreamAudioConnecting = false;
857
+ this.onStreamAudioConnectingChange.emit(false);
858
+ }
859
+ } else {
860
+ await this.stopVoice();
861
+ this.isStreamAudioActive = false;
862
+ // Close-stream-button clicked: stop any playing/queued TTS audio.
863
+ this.ttsPlayback.cancelAll();
864
+ this.isStreamAudioConnecting = false;
865
+ this.onStreamAudioConnectingChange.emit(false);
866
+ }
867
+ this.onStreamAudioActiveChange.emit(this.isStreamAudioActive);
868
+ this.logger.log('[CONV-FOOTER] isStreamAudioActive', this.isStreamAudioActive);
869
+ }
870
+
661
871
  async onEmojiiPickerClicked(){
662
872
  // if(this.loadPickerModule){
663
873
  // this.loadPickerModule = false;
@@ -709,6 +919,10 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
709
919
  this.onNewConversationButtonClicked.emit();
710
920
  }
711
921
 
922
+ onCloseChat(event){
923
+ this.onCloseChatButtonClicked.emit();
924
+ }
925
+
712
926
  // onContinueConversation(){
713
927
  // this.hideTextAreaContent = false;
714
928
  // this.onBackButton.emit(false)
@@ -745,48 +959,46 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
745
959
  }
746
960
 
747
961
  /**
748
- * when I press a key I call this method which:
749
- * check if 'enter' has been pressed
750
- * if you clear text
751
- * set field height as min by default
752
- * takes out the focus and resets it after a few moments
753
- * (this is a patch to keep the focus and eliminate the br of the send !!!)
754
- * send message
962
+ * Single keyboard handler for the message textarea.
963
+ *
964
+ * - Enter (no modifier) -> send message
965
+ * - Shift / Alt / Ctrl / Meta + Enter -> insert a newline (default browser behavior)
966
+ * - Tab -> prevented, to keep focus inside the chat
967
+ *
968
+ * Modifier check is intentionally on `keydown` because `keypress` is deprecated
969
+ * and does not consistently fire for modifier combos across browsers.
755
970
  * @param event
756
971
  */
757
- onkeypress(event) {
972
+ onkeydown(event: KeyboardEvent) {
758
973
  const keyCode = event.which || event.keyCode;
759
- this.textInputTextArea = ((document.getElementById('chat21-main-message-context') as HTMLInputElement).value);
760
- if (keyCode === 13) { // ENTER pressed
761
- if(this.showAlertEmoji){
762
- return;
974
+
975
+ if (keyCode === 13) { // ENTER
976
+ const hasModifier = event.metaKey || event.shiftKey || event.altKey || event.ctrlKey;
977
+ if (hasModifier) {
978
+ // Let the textarea insert a newline on its own (do not preventDefault).
979
+ return;
980
+ }
981
+
982
+ // Plain Enter -> send the message
983
+ event.preventDefault();
984
+
985
+ if (this.showAlertEmoji) {
986
+ return;
763
987
  }
988
+
989
+ const target = document.getElementById('chat21-main-message-context') as HTMLInputElement;
990
+ if (target) {
991
+ this.textInputTextArea = target.value;
992
+ }
993
+
764
994
  if (this.textInputTextArea && this.textInputTextArea.trim() !== '') {
765
- // that.logger.log('[CONV-FOOTER] sendMessage -> ', this.textInputTextArea);
766
- // this.resizeInputField();
767
- // this.messagingService.sendMessage(msg, TYPE_MSG_TEXT);
768
- // this.setDepartment();
769
- // this.textInputTextArea = replaceBr(this.textInputTextArea);
770
995
  this.sendMessage(this.textInputTextArea, TYPE_MSG_TEXT);
771
- // this.restoreTextArea();
772
996
  }
773
- } else if (keyCode === 9) { // TAB pressed
774
- event.preventDefault();
997
+ return;
775
998
  }
776
- }
777
999
 
778
-
779
- /**
780
- * HANDLE: cmd+enter, shiftKey+enter, alt+enter, ctrl+enter
781
- * @param event
782
- */
783
- onkeydown(event){
784
- const keyCode = event.which || event.keyCode;
785
- // metaKey -> COMMAND , shiftKey -> SHIFT, altKey -> ALT, ctrlKey -> CONTROL
786
- if( (event.metaKey || event.shiftKey || event.altKey || event.ctrlKey) && keyCode===13){
1000
+ if (keyCode === 9) { // TAB
787
1001
  event.preventDefault();
788
- this.textInputTextArea += '\r\n';
789
- this.resizeInputField();
790
1002
  }
791
1003
  }
792
1004