@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,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,8 +37,9 @@ 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;
38
44
  @Input() isConversationArchived: boolean;
39
45
  @Input() hideTextAreaContent: boolean;
@@ -42,6 +48,7 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
42
48
  @Input() isEmojiiPickerShow: boolean;
43
49
  @Input() footerMessagePlaceholder: string;
44
50
  @Input() fileUploadAccept: string;
51
+ @Input() closeChatInConversation: boolean;
45
52
  @Input() dropEvent: Event;
46
53
  @Input() poweredBy: string;
47
54
  @Input() stylesMap: Map<string, string>
@@ -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,67 @@ 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
+
127
+ get maxAttachmentLabel(): string {
128
+ const template = this.translationMap?.get('MAX_ATTACHMENT')
129
+ || `Max allowed size {{FILE_SIZE_LIMIT}}Mb`;
130
+ return template.replace(/\{\{FILE_SIZE_LIMIT\}\}/g, String(this.file_size_limit));
131
+ }
132
+
88
133
  file_size_limit = FILE_SIZE_LIMIT;
89
134
  attachmentTooltip: string = '';
135
+ isErrorNetwork: boolean = false;
90
136
 
91
137
 
92
138
  convertColorToRGBA = convertColorToRGBA;
93
139
  private logger: LoggerService = LoggerInstance.getInstance()
94
- constructor(private chatManager: ChatManager,
95
- private typingService: TypingService,
96
- private uploadService: UploadService) { }
140
+ constructor(
141
+ private chatManager: ChatManager,
142
+ private typingService: TypingService,
143
+ private uploadService: UploadService,
144
+ private voiceService: VoiceService,
145
+ private ttsPlayback: TtsAudioPlaybackCoordinator,
146
+ private tiledeskAuthService: TiledeskAuthService,
147
+ public g: Globals,
148
+ ) {}
97
149
 
98
150
  ngOnInit() {
99
151
  // this.updateAttachmentTooltip();
100
152
  }
101
153
 
102
-
103
154
  ngOnChanges(changes: SimpleChanges){
104
155
  if(changes['conversationWith'] && changes['conversationWith'].currentValue !== undefined){
105
156
  this.conversationHandlerService = this.chatManager.getConversationHandlerByConversationId(this.conversationWith);
157
+ this.isStreamAudioActive = false;
158
+ void this.stopVoice();
106
159
  }
107
160
  if(changes['hideTextReply'] && changes['hideTextReply'].currentValue !== undefined){
108
161
  this.restoreTextArea();
@@ -142,6 +195,159 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
142
195
  // }, 500);
143
196
  // }
144
197
 
198
+ /**
199
+ * Stream voce: con `voiceIngress` solo WSS (no VAD) — transcript + TTS dal server.
200
+ * Senza ingresso WSS: VAD + upload per segmento.
201
+ */
202
+ async initVoice() {
203
+ this.voiceAudioSubscription?.unsubscribe();
204
+ this.voiceVolumeSubscription?.unsubscribe();
205
+ this.botSpeakingSub?.unsubscribe();
206
+ this.voiceTranscriptSubscription?.unsubscribe();
207
+
208
+ const voiceIngress = this.buildVoiceIngressStreamConfig();
209
+ this.voiceAudioSubscription = undefined;
210
+ this.voiceTranscriptSubscription = this.voiceService.voiceTranscript$.subscribe(({ text }) => {
211
+ // Guard: stop accepting transcript text once the proxy is processing (thinking/speaking)
212
+ if (text && !this.isBotSpeaking) {
213
+ this.textInputTextArea = text;
214
+ this.lastVoiceTranscript = text;
215
+ // The proxy publishes the user utterance to Chat21 via AMQP on utterance-end;
216
+ // no sendMessage call is needed here — doing so would produce duplicate messages.
217
+ }
218
+ });
219
+
220
+ this.voiceVolumeSubscription = this.voiceService.volume$.subscribe((volume) => {
221
+ this.currentVolume = volume;
222
+ });
223
+ this.botSpeakingSub = this.voiceService.isAcquisitionBlocked$.subscribe((blocked) => {
224
+ this.isBotSpeaking = blocked;
225
+ if (blocked) {
226
+ // Proxy has started thinking/speaking — clear the textarea preview
227
+ this.textInputTextArea = '';
228
+ }
229
+ });
230
+ await this.voiceService.startSession(voiceIngress ? { voiceIngressStream: voiceIngress } : {});
231
+ }
232
+
233
+ private buildVoiceIngressStreamConfig(): VoiceStreamingSessionConfig | null {
234
+ const token = this.tiledeskAuthService.getTiledeskToken() ?? '';
235
+ const sender = this.tiledeskAuthService.getCurrentUser()?.uid ?? '';
236
+ const recipient = this.conversationWith ?? '';
237
+ if (!token || !sender || !recipient) {
238
+ this.logger.warn('[CONV-FOOTER] buildVoiceIngressStreamConfig: missing required fields', {
239
+ hasToken: !!token,
240
+ hasSender: !!sender,
241
+ hasRecipient: !!recipient,
242
+ });
243
+ return null;
244
+ }
245
+ const { recipientFullname, attributes, channelType } = this.buildSendMessageContext();
246
+ this.logger.log('[CONV-FOOTER] buildVoiceIngressStreamConfig', { sender, recipient, channelType });
247
+ return {
248
+ token,
249
+ sender,
250
+ recipient,
251
+ // Use Deepgram multilingual code-switching so the model detects the spoken
252
+ // language from the audio stream regardless of browser locale.
253
+ // Source: https://developers.deepgram.com/docs/multilingual-code-switching
254
+ lang: 'multi',
255
+ text: '',
256
+ type: 'text',
257
+ recipient_fullname: recipientFullname ?? '',
258
+ sender_fullname: recipientFullname ?? '',
259
+ attributes: attributes ?? {},
260
+ metadata: '',
261
+ channel_type: channelType ?? '',
262
+ };
263
+ }
264
+
265
+ /**
266
+ * Merge `attributes` di componente con `additional_attributes` e risolve `recipientFullname` come in sendMessage.
267
+ */
268
+ private buildSendMessageContext(additional_attributes?: any) {
269
+ let recipientFullname = this.translationMap.get('GUEST_LABEL');
270
+ const g_attributes = this.attributes;
271
+ const attributes = <any>{};
272
+ if (g_attributes) {
273
+ for (const [key, value] of Object.entries(g_attributes)) {
274
+ attributes[key] = value;
275
+ }
276
+ }
277
+ if (additional_attributes) {
278
+ for (const [key, value] of Object.entries(additional_attributes)) {
279
+ attributes[key] = value;
280
+ }
281
+ }
282
+ const senderId = this.senderId;
283
+ const projectid = this.project.id;
284
+ const channelType = this.channelType;
285
+ const userFullname = this.userFullname;
286
+ const userEmail = this.userEmail;
287
+ const conversationWith = this.conversationWith;
288
+
289
+ if (userFullname) {
290
+ recipientFullname = userFullname;
291
+ } else if (userEmail) {
292
+ recipientFullname = userEmail;
293
+ } else if (attributes && attributes['userFullname']) {
294
+ recipientFullname = attributes['userFullname'];
295
+ } else {
296
+ recipientFullname = this.translationMap.get('GUEST_LABEL');
297
+ }
298
+
299
+ return {
300
+ recipientFullname,
301
+ attributes,
302
+ senderId,
303
+ projectid,
304
+ channelType,
305
+ conversationWith,
306
+ };
307
+ }
308
+ async stopVoice(options?: { discardInProgressSegment?: boolean }) {
309
+ // Stop all active TTS audio immediately and reveal all text.
310
+ this.ttsPlayback.stopAll();
311
+
312
+ this.voiceAudioSubscription?.unsubscribe();
313
+ this.voiceAudioSubscription = undefined;
314
+
315
+ this.voiceTranscriptSubscription?.unsubscribe();
316
+ this.voiceTranscriptSubscription = undefined;
317
+
318
+ this.voiceVolumeSubscription?.unsubscribe();
319
+ this.voiceVolumeSubscription = undefined;
320
+
321
+ this.botSpeakingSub?.unsubscribe();
322
+ this.botSpeakingSub = undefined;
323
+ this.isBotSpeaking = false;
324
+
325
+ await this.voiceService.stopSession(options);
326
+ this.currentVolume = 0;
327
+ this.textInputTextArea = '';
328
+ this.lastVoiceTranscript = '';
329
+ }
330
+
331
+ /**
332
+ * Messaggio in arrivo da un altro mittente mentre lo stream è attivo: con VAD legacy scarta il segmento in corso.
333
+ * Con sola sessione WSS non ha effetto sul mic (nessun recorder a segmenti locale).
334
+ */
335
+ interruptStreamDueToPeerMessage(): void {
336
+ if (!this.isStreamAudioActive) {
337
+ return;
338
+ }
339
+ this.logger.log('[CONV-FOOTER] discard recording segment: incoming message from peer (stream stays on)');
340
+ try {
341
+ this.voiceService.discardCurrentRecordingSegment();
342
+ } catch (e) {
343
+ this.logger.error('[CONV-FOOTER] interruptStreamDueToPeerMessage', e);
344
+ }
345
+ }
346
+
347
+ ngOnDestroy() {
348
+ void this.stopVoice();
349
+ }
350
+
145
351
  // ========= begin:: functions send image ======= //
146
352
  // START LOAD IMAGE //
147
353
  /** load the selected image locally and open the pop up preview */
@@ -379,40 +585,14 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
379
585
  // msg = replaceEndOfLine(msg);
380
586
  // msg = msg.trim();
381
587
 
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
- }
588
+ const {
589
+ recipientFullname,
590
+ attributes,
591
+ senderId,
592
+ projectid,
593
+ channelType,
594
+ conversationWith,
595
+ } = this.buildSendMessageContext(additional_attributes);
416
596
 
417
597
  this.onBeforeMessageSent.emit({
418
598
  senderFullname: recipientFullname,
@@ -521,7 +701,7 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
521
701
  }
522
702
  }
523
703
 
524
- prepareAndUpload(audioBlob: Blob) {
704
+ prepareAndUpload(audioBlob: Blob, text: string = '') {
525
705
 
526
706
  this.isFilePendingToUpload = true;
527
707
 
@@ -551,7 +731,7 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
551
731
  this.logger.log('[UPLOAD] metadata:', metadata);
552
732
 
553
733
  // stesso metodo che già usi
554
- this.uploadSingle(metadata, file, '');
734
+ this.uploadSingle(metadata, file, text);
555
735
  }
556
736
 
557
737
  // Funzione per convertire Blob in Base64 usando FileReader
@@ -658,6 +838,39 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
658
838
  }
659
839
  }
660
840
 
841
+ async onStreamPressed(event: Event) {
842
+ this.logger.log('[CONV-FOOTER] onStreamPressed:event', event);
843
+ event.preventDefault();
844
+ if (this.showAlertEmoji) {
845
+ return;
846
+ }
847
+ // Treat a click during connecting as a cancel request (same as turning off).
848
+ const turningOn = !this.isStreamAudioActive && !this.isStreamAudioConnecting;
849
+ this.logger.log('[CONV-FOOTER] onStreamPressed', { turningOn });
850
+ if (turningOn) {
851
+ this.isStreamAudioConnecting = true;
852
+ this.onStreamAudioConnectingChange.emit(true);
853
+ try {
854
+ this.currentVolume = 0;
855
+ await this.initVoice();
856
+ this.isStreamAudioActive = true;
857
+ } catch (e) {
858
+ this.logger.error('[CONV-FOOTER] onStreamPressed: initVoice failed', e);
859
+ this.isStreamAudioActive = false;
860
+ } finally {
861
+ this.isStreamAudioConnecting = false;
862
+ this.onStreamAudioConnectingChange.emit(false);
863
+ }
864
+ } else {
865
+ await this.stopVoice();
866
+ this.isStreamAudioActive = false;
867
+ this.isStreamAudioConnecting = false;
868
+ this.onStreamAudioConnectingChange.emit(false);
869
+ }
870
+ this.onStreamAudioActiveChange.emit(this.isStreamAudioActive);
871
+ this.logger.log('[CONV-FOOTER] isStreamAudioActive', this.isStreamAudioActive);
872
+ }
873
+
661
874
  async onEmojiiPickerClicked(){
662
875
  // if(this.loadPickerModule){
663
876
  // this.loadPickerModule = false;
@@ -709,6 +922,10 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
709
922
  this.onNewConversationButtonClicked.emit();
710
923
  }
711
924
 
925
+ onCloseChat(event){
926
+ this.onCloseChatButtonClicked.emit();
927
+ }
928
+
712
929
  // onContinueConversation(){
713
930
  // this.hideTextAreaContent = false;
714
931
  // this.onBackButton.emit(false)
@@ -745,48 +962,46 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
745
962
  }
746
963
 
747
964
  /**
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
965
+ * Single keyboard handler for the message textarea.
966
+ *
967
+ * - Enter (no modifier) -> send message
968
+ * - Shift / Alt / Ctrl / Meta + Enter -> insert a newline (default browser behavior)
969
+ * - Tab -> prevented, to keep focus inside the chat
970
+ *
971
+ * Modifier check is intentionally on `keydown` because `keypress` is deprecated
972
+ * and does not consistently fire for modifier combos across browsers.
755
973
  * @param event
756
974
  */
757
- onkeypress(event) {
975
+ onkeydown(event: KeyboardEvent) {
758
976
  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;
977
+
978
+ if (keyCode === 13) { // ENTER
979
+ const hasModifier = event.metaKey || event.shiftKey || event.altKey || event.ctrlKey;
980
+ if (hasModifier) {
981
+ // Let the textarea insert a newline on its own (do not preventDefault).
982
+ return;
983
+ }
984
+
985
+ // Plain Enter -> send the message
986
+ event.preventDefault();
987
+
988
+ if (this.showAlertEmoji) {
989
+ return;
763
990
  }
991
+
992
+ const target = document.getElementById('chat21-main-message-context') as HTMLInputElement;
993
+ if (target) {
994
+ this.textInputTextArea = target.value;
995
+ }
996
+
764
997
  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
998
  this.sendMessage(this.textInputTextArea, TYPE_MSG_TEXT);
771
- // this.restoreTextArea();
772
999
  }
773
- } else if (keyCode === 9) { // TAB pressed
774
- event.preventDefault();
1000
+ return;
775
1001
  }
776
- }
777
1002
 
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){
1003
+ if (keyCode === 9) { // TAB
787
1004
  event.preventDefault();
788
- this.textInputTextArea += '\r\n';
789
- this.resizeInputField();
790
1005
  }
791
1006
  }
792
1007