@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.
- 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/docker-community-push-latest.yml +23 -13
- package/.github/workflows/docker-image-tag-community-tag-push.yml +22 -12
- package/.github/workflows/playwright.yml +27 -0
- package/CHANGELOG.md +130 -6
- package/Dockerfile +4 -5
- package/angular.json +24 -4
- package/docs/changelog/this-branch.md +36 -0
- package/env.sample +3 -2
- package/mocks/voice-websocket-mock/server.cjs +245 -0
- package/nginx.conf +22 -2
- 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.component.ts +10 -9
- package/src/app/app.module.ts +15 -0
- package/src/app/component/conversation-detail/conversation/conversation.component.html +25 -11
- package/src/app/component/conversation-detail/conversation/conversation.component.scss +40 -2
- package/src/app/component/conversation-detail/conversation/conversation.component.spec.ts +644 -75
- package/src/app/component/conversation-detail/conversation/conversation.component.ts +100 -14
- 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 +33 -2
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.spec.ts +242 -149
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.ts +8 -6
- 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 +200 -96
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss +211 -6
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.spec.ts +452 -78
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts +291 -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/last-message/last-message.component.ts +4 -1
- 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 +103 -0
- package/src/app/component/message/audio-sync/audio-sync.component.ts +643 -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 +43 -51
- package/src/app/component/message/bubble-message/bubble-message.component.scss +59 -1
- package/src/app/component/message/bubble-message/bubble-message.component.spec.ts +154 -57
- package/src/app/component/message/bubble-message/bubble-message.component.ts +152 -109
- 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 +38 -0
- package/src/app/component/message/json-sources/json-sources.component.scss +201 -0
- package/src/app/component/message/json-sources/json-sources.component.ts +89 -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 +82 -2
- package/src/app/providers/json-sources-parser.service.ts +175 -0
- package/src/app/providers/translator.service.ts +26 -6
- package/src/app/providers/tts-audio-playback-coordinator.service.spec.ts +117 -0
- package/src/app/providers/tts-audio-playback-coordinator.service.ts +109 -0
- package/src/app/providers/url-preview.service.ts +82 -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 +171 -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 +973 -0
- package/src/app/sass/_variables.scss +3 -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 +21 -1
- package/src/app/utils/json-sources-utils.ts +27 -0
- package/src/app/utils/url-utils.ts +98 -0
- package/src/app/utils/utils-resources.ts +1 -1
- package/src/assets/i18n/en.json +106 -99
- package/src/assets/i18n/es.json +107 -100
- package/src/assets/i18n/fr.json +107 -100
- package/src/assets/i18n/it.json +107 -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/twp/chatbot-panel.html +3 -1
- package/src/assets/twp/index-dev.html +18 -0
- package/src/assets/twp/tiledesk_widget_files/widget-css-override-example.css +14 -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/mqtt/mqtt-conversation-handler.ts +12 -0
- package/src/chat21-core/providers/scripts/script.service.spec.ts +12 -2
- package/src/chat21-core/providers/tiledesk/tiledesk-requests.service.ts +1 -1
- package/src/chat21-core/utils/constants.ts +4 -0
- package/src/chat21-core/utils/utils-message.ts +45 -6
- package/src/chat21-core/utils/utils.ts +5 -2
- package/src/widget-config-template.json +4 -1
- package/src/widget-config.json +4 -1
- 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,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(
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
*
|
|
749
|
-
*
|
|
750
|
-
*
|
|
751
|
-
*
|
|
752
|
-
*
|
|
753
|
-
*
|
|
754
|
-
*
|
|
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
|
-
|
|
975
|
+
onkeydown(event: KeyboardEvent) {
|
|
758
976
|
const keyCode = event.which || event.keyCode;
|
|
759
|
-
|
|
760
|
-
if (keyCode === 13) { // ENTER
|
|
761
|
-
|
|
762
|
-
|
|
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
|
-
|
|
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
|
|