@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.
- package/.angular-mcp-cache/package.json +1 -0
- package/.cursor/angular18-accessibility-auditor-skill.md +442 -0
- package/.cursor/mcp.json +15 -0
- package/.github/workflows/playwright.yml +27 -0
- package/CHANGELOG.md +25 -0
- package/Dockerfile +4 -5
- package/README.md +1 -1
- package/angular.json +21 -3
- package/docs/ACCESSIBILITY-STATEMENT.md +388 -0
- package/docs/TILEDESK_WIDGET_ACCESSIBILITY_ALIGNMENT.md +60 -0
- package/docs/TILEDESK_WIDGET_ACCESSIBILITY_STATEMENT_COMPLETE.md +386 -0
- package/env.sample +3 -2
- package/mocks/voice-websocket-mock/server.cjs +245 -0
- package/package.json +10 -3
- package/playwright.config.ts +41 -0
- package/src/app/app.component.html +2 -2
- package/src/app/app.component.scss +25 -14
- package/src/app/app.component.spec.ts +21 -6
- package/src/app/app.module.ts +13 -0
- package/src/app/component/conversation-detail/conversation/conversation.component.html +25 -11
- package/src/app/component/conversation-detail/conversation/conversation.component.scss +38 -0
- package/src/app/component/conversation-detail/conversation/conversation.component.spec.ts +644 -75
- package/src/app/component/conversation-detail/conversation/conversation.component.ts +70 -2
- package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.html +25 -13
- package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.spec.ts +123 -5
- package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.ts +1 -0
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.html +23 -10
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.scss +18 -0
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.spec.ts +241 -149
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.ts +8 -5
- package/src/app/component/conversation-detail/conversation-emojii/conversation-emojii.component.spec.ts +53 -3
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.html +203 -110
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss +212 -1
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.spec.ts +458 -78
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts +288 -76
- package/src/app/component/conversation-detail/conversation-header/conversation-header.component.html +113 -53
- package/src/app/component/conversation-detail/conversation-header/conversation-header.component.scss +12 -4
- package/src/app/component/conversation-detail/conversation-header/conversation-header.component.spec.ts +274 -29
- package/src/app/component/conversation-detail/conversation-internal-frame/conversation-internal-frame.component.html +23 -9
- package/src/app/component/conversation-detail/conversation-internal-frame/conversation-internal-frame.component.spec.ts +80 -8
- package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.html +29 -23
- package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.spec.ts +185 -16
- package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.ts +34 -14
- package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.html +46 -0
- package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.scss +83 -0
- package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.ts +192 -0
- package/src/app/component/error-alert/error-alert.component.spec.ts +65 -5
- package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.html +16 -7
- package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.scss +21 -0
- package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.spec.ts +89 -7
- package/src/app/component/form/form-builder/form-builder.component.html +1 -1
- package/src/app/component/form/form-builder/form-builder.component.spec.ts +163 -21
- package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.html +8 -4
- package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.scss +10 -5
- package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.spec.ts +90 -16
- package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.ts +26 -0
- package/src/app/component/form/inputs/form-label/form-label.component.spec.ts +45 -11
- package/src/app/component/form/inputs/form-radio-button/form-radio-button.component.spec.ts +24 -6
- package/src/app/component/form/inputs/form-select/form-select.component.spec.ts +14 -5
- package/src/app/component/form/inputs/form-text/form-text.component.html +14 -12
- package/src/app/component/form/inputs/form-text/form-text.component.scss +11 -1
- package/src/app/component/form/inputs/form-text/form-text.component.spec.ts +113 -17
- package/src/app/component/form/inputs/form-text/form-text.component.ts +35 -3
- package/src/app/component/form/inputs/form-textarea/form-textarea.component.html +13 -11
- package/src/app/component/form/inputs/form-textarea/form-textarea.component.scss +6 -5
- package/src/app/component/form/inputs/form-textarea/form-textarea.component.spec.ts +149 -13
- package/src/app/component/form/inputs/form-textarea/form-textarea.component.ts +26 -0
- package/src/app/component/form/prechat-form/prechat-form.component.html +14 -11
- package/src/app/component/form/prechat-form/prechat-form.component.spec.ts +102 -10
- package/src/app/component/form/prechat-form/prechat-form.component.ts +8 -1
- package/src/app/component/form/prechat-form-test-mock.ts +35 -0
- package/src/app/component/home/home.component.html +38 -31
- package/src/app/component/home/home.component.scss +4 -2
- package/src/app/component/home/home.component.spec.ts +226 -11
- package/src/app/component/home-conversations/home-conversations.component.html +30 -26
- package/src/app/component/home-conversations/home-conversations.component.scss +3 -0
- package/src/app/component/home-conversations/home-conversations.component.spec.ts +212 -36
- package/src/app/component/last-message/last-message.component.html +15 -9
- package/src/app/component/last-message/last-message.component.scss +16 -2
- package/src/app/component/last-message/last-message.component.spec.ts +204 -23
- package/src/app/component/launcher-button/launcher-button.component.html +8 -13
- package/src/app/component/launcher-button/launcher-button.component.spec.ts +104 -8
- package/src/app/component/list-all-conversations/list-all-conversations.component.html +12 -17
- package/src/app/component/list-all-conversations/list-all-conversations.component.scss +2 -0
- package/src/app/component/list-conversations/list-conversations.component.html +22 -22
- package/src/app/component/menu-options/menu-options.component.html +30 -20
- package/src/app/component/menu-options/menu-options.component.spec.ts +125 -9
- package/src/app/component/message/audio/audio.component.html +13 -15
- package/src/app/component/message/audio/audio.component.spec.ts +140 -5
- package/src/app/component/message/audio/audio.component.ts +1 -5
- package/src/app/component/message/audio-sync/audio-sync.component.html +18 -0
- package/src/app/component/message/audio-sync/audio-sync.component.scss +65 -0
- package/src/app/component/message/audio-sync/audio-sync.component.spec.ts +112 -0
- package/src/app/component/message/audio-sync/audio-sync.component.ts +714 -0
- package/src/app/component/message/avatar/avatar.component.html +2 -2
- package/src/app/component/message/avatar/avatar.component.spec.ts +99 -7
- package/src/app/component/message/bubble-message/bubble-message.component.html +41 -51
- package/src/app/component/message/bubble-message/bubble-message.component.scss +54 -1
- package/src/app/component/message/bubble-message/bubble-message.component.spec.ts +147 -57
- package/src/app/component/message/bubble-message/bubble-message.component.ts +95 -13
- package/src/app/component/message/buttons/action-button/action-button.component.html +3 -4
- package/src/app/component/message/buttons/action-button/action-button.component.spec.ts +49 -5
- package/src/app/component/message/buttons/link-button/link-button.component.scss +5 -8
- package/src/app/component/message/buttons/link-button/link-button.component.spec.ts +50 -5
- package/src/app/component/message/buttons/text-button/text-button.component.spec.ts +44 -5
- package/src/app/component/message/carousel/carousel.component.html +29 -16
- package/src/app/component/message/carousel/carousel.component.scss +20 -8
- package/src/app/component/message/carousel/carousel.component.spec.ts +80 -3
- package/src/app/component/message/carousel/carousel.component.ts +16 -0
- package/src/app/component/message/frame/frame.component.html +9 -4
- package/src/app/component/message/frame/frame.component.spec.ts +34 -15
- package/src/app/component/message/frame/frame.component.ts +7 -2
- package/src/app/component/message/html/html.component.html +1 -1
- package/src/app/component/message/html/html.component.scss +1 -1
- package/src/app/component/message/html/html.component.spec.ts +24 -7
- package/src/app/component/message/image/image.component.html +12 -10
- package/src/app/component/message/image/image.component.scss +16 -0
- package/src/app/component/message/image/image.component.spec.ts +101 -15
- package/src/app/component/message/image/image.component.ts +90 -51
- package/src/app/component/message/info-message/info-message.component.spec.ts +26 -14
- package/src/app/component/message/json-sources/json-sources.component.html +6 -5
- package/src/app/component/message/json-sources/json-sources.component.scss +26 -18
- package/src/app/component/message/json-sources/json-sources.component.ts +41 -0
- package/src/app/component/message/like-unlike/like-unlike.component.html +7 -9
- package/src/app/component/message/like-unlike/like-unlike.component.spec.ts +31 -3
- package/src/app/component/message/return-receipt/return-receipt.component.spec.ts +38 -17
- package/src/app/component/message/text/text.component.html +3 -3
- package/src/app/component/message/text/text.component.scss +80 -86
- package/src/app/component/message/text/text.component.spec.ts +106 -13
- package/src/app/component/message-attachment/message-attachment.component.spec.ts +134 -13
- package/src/app/component/selection-department/selection-department.component.html +21 -23
- package/src/app/component/selection-department/selection-department.component.spec.ts +159 -14
- package/src/app/component/selection-department/selection-department.component.ts +8 -1
- package/src/app/component/send-button/send-button.component.html +5 -13
- package/src/app/component/send-button/send-button.component.spec.ts +2 -2
- package/src/app/component/star-rating-widget/star-rating-widget.component.html +51 -81
- package/src/app/directives/tooltip.directive.spec.ts +8 -4
- package/src/app/modals/confirm-close/confirm-close.component.html +20 -8
- package/src/app/modals/confirm-close/confirm-close.component.scss +3 -0
- package/src/app/modals/confirm-close/confirm-close.component.spec.ts +13 -4
- package/src/app/modals/confirm-close/confirm-close.component.ts +8 -1
- package/src/app/pipe/html-entites-encode.pipe.spec.ts +35 -2
- package/src/app/pipe/marked.pipe.spec.ts +38 -2
- package/src/app/pipe/marked.pipe.ts +51 -41
- package/src/app/providers/app-config.service.ts +4 -2
- package/src/app/providers/brand.service.spec.ts +23 -2
- package/src/app/providers/brand.service.ts +1 -1
- package/src/app/providers/global-settings.service.spec.ts +1009 -14
- package/src/app/providers/global-settings.service.ts +40 -2
- package/src/app/providers/json-sources-parser.service.ts +13 -1
- package/src/app/providers/translator.service.ts +24 -7
- package/src/app/providers/tts-audio-playback-coordinator.service.spec.ts +116 -0
- package/src/app/providers/tts-audio-playback-coordinator.service.ts +122 -0
- package/src/app/providers/voice/STT&TTS/openai-voice.config.ts +12 -0
- package/src/app/providers/voice/STT&TTS/openai-voice.provider.ts +156 -0
- package/src/app/providers/voice/STT&TTS/speech-provider.abstract.ts +39 -0
- package/src/app/providers/voice/audio.types.ts +40 -0
- package/src/app/providers/voice/vad.service.spec.ts +28 -0
- package/src/app/providers/voice/vad.service.ts +70 -0
- package/src/app/providers/voice/voice-streaming.service.spec.ts +23 -0
- package/src/app/providers/voice/voice-streaming.service.ts +702 -0
- package/src/app/providers/voice/voice-streaming.types.ts +112 -0
- package/src/app/providers/voice/voice.service.spec.ts +227 -0
- package/src/app/providers/voice/voice.service.ts +969 -0
- package/src/app/sass/_variables.scss +2 -0
- package/src/app/sass/animations.scss +19 -1
- package/src/app/shims/onnxruntime-web-wasm.ts +4 -0
- package/src/app/utils/globals.ts +14 -0
- package/src/app/utils/utils-resources.ts +1 -1
- package/src/assets/i18n/en.json +128 -100
- package/src/assets/i18n/es.json +128 -100
- package/src/assets/i18n/fr.json +128 -100
- package/src/assets/i18n/it.json +128 -98
- package/src/assets/onnx/ort-wasm-simd-threaded.mjs +59 -0
- package/src/assets/onnx/ort-wasm-simd-threaded.wasm +0 -0
- package/src/assets/sounds/keyboard.mp3 +0 -0
- package/src/assets/vad/silero_vad_legacy.onnx +0 -0
- package/src/assets/vad/vad.worklet.bundle.min.js +1 -0
- package/src/chat21-core/models/message.ts +2 -1
- package/src/chat21-core/providers/chat-manager.spec.ts +72 -0
- package/src/chat21-core/providers/firebase/firebase-conversation-handler.ts +3 -2
- package/src/chat21-core/providers/firebase/firebase-init-service.ts +5 -5
- package/src/chat21-core/providers/mqtt/mqtt-conversation-handler.ts +12 -0
- package/src/chat21-core/providers/scripts/script.service.spec.ts +12 -2
- package/src/chat21-core/utils/utils-message.ts +7 -0
- package/src/widget-config-template.json +3 -1
- package/src/widget-config.json +28 -27
- package/tests/widget-form-rich.spec.ts +67 -0
- package/tests/widget-index-dev-settings.spec.ts +52 -0
- package/tests/widget-twp-iframe.spec.ts +39 -0
- package/tsconfig.json +5 -0
package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts
CHANGED
|
@@ -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(
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
*
|
|
749
|
-
*
|
|
750
|
-
*
|
|
751
|
-
*
|
|
752
|
-
*
|
|
753
|
-
*
|
|
754
|
-
*
|
|
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
|
-
|
|
972
|
+
onkeydown(event: KeyboardEvent) {
|
|
758
973
|
const keyCode = event.which || event.keyCode;
|
|
759
|
-
|
|
760
|
-
if (keyCode === 13) { // ENTER
|
|
761
|
-
|
|
762
|
-
|
|
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
|
-
|
|
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
|
|