@chat21/chat21-web-widget 5.1.30-rc1 → 5.1.32-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 (43) hide show
  1. package/CHANGELOG.md +44 -71
  2. package/angular.json +3 -1
  3. package/deploy_amazon_beta.sh +7 -17
  4. package/deploy_amazon_prod.sh +41 -0
  5. package/docs/changelog/this-branch.md +47 -0
  6. package/package.json +4 -1
  7. package/src/app/app.component.ts +1 -2
  8. package/src/app/app.module.ts +9 -0
  9. package/src/app/component/conversation-detail/conversation/conversation.component.html +4 -0
  10. package/src/app/component/conversation-detail/conversation/conversation.component.ts +8 -0
  11. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.html +2 -2
  12. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.ts +2 -0
  13. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.html +42 -0
  14. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss +91 -0
  15. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts +101 -7
  16. package/src/app/component/message/audio/audio.component.ts +0 -5
  17. package/src/app/component/message/audio-sync/audio-sync.component.html +19 -0
  18. package/src/app/component/message/audio-sync/audio-sync.component.scss +65 -0
  19. package/src/app/component/message/audio-sync/audio-sync.component.spec.ts +23 -0
  20. package/src/app/component/message/audio-sync/audio-sync.component.ts +197 -0
  21. package/src/app/component/message/bubble-message/bubble-message.component.html +6 -1
  22. package/src/app/component/message/bubble-message/bubble-message.component.ts +2 -1
  23. package/src/app/providers/global-settings.service.ts +10 -0
  24. package/src/app/providers/voice/STT&TTS/openai-voice.config.ts +12 -0
  25. package/src/app/providers/voice/STT&TTS/openai-voice.provider.ts +171 -0
  26. package/src/app/providers/voice/STT&TTS/speech-provider.abstract.ts +39 -0
  27. package/src/app/providers/voice/audio.types.ts +34 -0
  28. package/src/app/providers/voice/vad.service.spec.ts +28 -0
  29. package/src/app/providers/voice/vad.service.ts +70 -0
  30. package/src/app/providers/voice/voice.service.spec.ts +60 -0
  31. package/src/app/providers/voice/voice.service.ts +264 -0
  32. package/src/app/shims/onnxruntime-web-wasm.ts +4 -0
  33. package/src/app/utils/conversation-sender-classifier.ts +21 -0
  34. package/src/app/utils/globals.ts +3 -0
  35. package/src/assets/onnx/ort-wasm-simd-threaded.mjs +59 -0
  36. package/src/assets/onnx/ort-wasm-simd-threaded.wasm +0 -0
  37. package/src/assets/vad/silero_vad_legacy.onnx +0 -0
  38. package/src/assets/vad/vad.worklet.bundle.min.js +1 -0
  39. package/src/chat21-core/models/message.ts +2 -1
  40. package/src/chat21-core/providers/firebase/firebase-conversation-handler.ts +3 -2
  41. package/src/chat21-core/providers/mqtt/mqtt-conversation-handler.ts +12 -0
  42. package/src/chat21-core/utils/utils-message.ts +7 -0
  43. 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,15 @@ 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';
18
20
 
19
21
  @Component({
20
22
  selector: 'chat-conversation-footer',
21
23
  templateUrl: './conversation-footer.component.html',
22
24
  styleUrls: ['./conversation-footer.component.scss']
23
25
  })
24
- export class ConversationFooterComponent implements OnInit, OnChanges {
26
+ export class ConversationFooterComponent implements OnInit, OnChanges, OnDestroy {
25
27
 
26
28
  @Input() conversationWith: string;
27
29
  @Input() attributes: string;
@@ -32,8 +34,9 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
32
34
  @Input() userFullname: string;
33
35
  @Input() userEmail: string;
34
36
  @Input() showAttachmentFooterButton: boolean;
35
- @Input() showEmojiFooterButton: boolean
36
- @Input() showAudioRecorderFooterButton: boolean
37
+ @Input() showEmojiFooterButton: boolean;
38
+ @Input() showAudioRecorderFooterButton: boolean;
39
+ @Input() showAudioStreamFooterButton: boolean;
37
40
  // @Input() showContinueConversationButton: boolean;
38
41
  @Input() isConversationArchived: boolean;
39
42
  @Input() hideTextAreaContent: boolean;
@@ -53,6 +56,7 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
53
56
  @Output() onChangeTextArea = new EventEmitter<any>();
54
57
  @Output() onAttachmentFileButtonClicked = new EventEmitter<any>();
55
58
  @Output() onNewConversationButtonClicked = new EventEmitter();
59
+ @Output() onStreamAudioActiveChange = new EventEmitter<boolean>();
56
60
  @Output() onCloseChatButtonClicked = new EventEmitter();
57
61
 
58
62
  @ViewChild('chat21_file') public chat21_file: ElementRef;
@@ -87,6 +91,17 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
87
91
 
88
92
  showAlertEmoji: boolean = false
89
93
 
94
+ /** Stream audio UI: icona equalizer → X; alert con onde animate sopra il footer */
95
+ isStreamAudioActive = false;
96
+ /** Sottoscrizione ai segmenti audio (VAD → WebM) dal {@link VoiceService}. */
97
+ private voiceAudioSubscription?: Subscription;
98
+ /** Sottoscrizione al volume audio (real-time) dal {@link VoiceService}. */
99
+ private voiceVolumeSubscription?: Subscription;
100
+ currentVolume = 0;
101
+ wavePath1 = '';
102
+ wavePath2 = '';
103
+ wavePath3 = '';
104
+
90
105
  file_size_limit = FILE_SIZE_LIMIT;
91
106
  attachmentTooltip: string = '';
92
107
  isErrorNetwork: boolean = false;
@@ -96,7 +111,8 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
96
111
  private logger: LoggerService = LoggerInstance.getInstance()
97
112
  constructor(private chatManager: ChatManager,
98
113
  private typingService: TypingService,
99
- private uploadService: UploadService) { }
114
+ private uploadService: UploadService,
115
+ private voiceService: VoiceService) { }
100
116
 
101
117
  ngOnInit() {
102
118
  // this.updateAttachmentTooltip();
@@ -106,6 +122,8 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
106
122
  ngOnChanges(changes: SimpleChanges){
107
123
  if(changes['conversationWith'] && changes['conversationWith'].currentValue !== undefined){
108
124
  this.conversationHandlerService = this.chatManager.getConversationHandlerByConversationId(this.conversationWith);
125
+ this.isStreamAudioActive = false;
126
+ void this.stopVoice();
109
127
  }
110
128
  if(changes['hideTextReply'] && changes['hideTextReply'].currentValue !== undefined){
111
129
  this.restoreTextArea();
@@ -145,6 +163,59 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
145
163
  // }, 500);
146
164
  // }
147
165
 
166
+ /**
167
+ * Microfono + VAD: ogni fine parlato il servizio emette su `audioSegment$` → upload.
168
+ */
169
+ async initVoice() {
170
+ this.voiceAudioSubscription?.unsubscribe();
171
+ this.voiceVolumeSubscription?.unsubscribe();
172
+
173
+ this.voiceAudioSubscription = this.voiceService.audioSegment$.subscribe((rec) => {
174
+ console.log('[CONV-FOOTER] audioSegment$', rec);
175
+ this.prepareAndUpload(rec.blob);
176
+ });
177
+ this.voiceVolumeSubscription = this.voiceService.volume$.subscribe((volume) => {
178
+ this.currentVolume = volume;
179
+ this.updateWave(volume);
180
+ });
181
+ await this.voiceService.startSession();
182
+ }
183
+
184
+ async stopVoice() {
185
+ this.voiceAudioSubscription?.unsubscribe();
186
+ this.voiceAudioSubscription = undefined;
187
+
188
+ this.voiceVolumeSubscription?.unsubscribe();
189
+ this.voiceVolumeSubscription = undefined;
190
+
191
+ await this.voiceService.stopSession();
192
+ }
193
+
194
+ updateWave(volume: number) {
195
+ const intensity = Math.min(volume / 80, 1); // più sensibile
196
+
197
+ const amp1 = 4 + intensity * 22;
198
+ const amp2 = 2 + intensity * 16;
199
+ const amp3 = 1 + intensity * 12;
200
+
201
+ this.wavePath1 = this.buildWave(42, amp1);
202
+ this.wavePath2 = this.buildWave(50, amp2);
203
+ this.wavePath3 = this.buildWave(58, amp3);
204
+ }
205
+
206
+ buildWave(y: number, amp: number): string {
207
+ return `
208
+ M6 ${y}
209
+ Q24 ${y - amp} 42 ${y}
210
+ T78 ${y}
211
+ T98 ${y}
212
+ `;
213
+ }
214
+
215
+ ngOnDestroy() {
216
+ void this.stopVoice();
217
+ }
218
+
148
219
  // ========= begin:: functions send image ======= //
149
220
  // START LOAD IMAGE //
150
221
  /** load the selected image locally and open the pop up preview */
@@ -524,7 +595,7 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
524
595
  }
525
596
  }
526
597
 
527
- prepareAndUpload(audioBlob: Blob) {
598
+ prepareAndUpload(audioBlob: Blob, text: string = '') {
528
599
 
529
600
  this.isFilePendingToUpload = true;
530
601
 
@@ -554,7 +625,7 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
554
625
  this.logger.log('[UPLOAD] metadata:', metadata);
555
626
 
556
627
  // stesso metodo che già usi
557
- this.uploadSingle(metadata, file, '');
628
+ this.uploadSingle(metadata, file, text);
558
629
  }
559
630
 
560
631
  // Funzione per convertire Blob in Base64 usando FileReader
@@ -661,6 +732,29 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
661
732
  }
662
733
  }
663
734
 
735
+ async onStreamPressed(event: Event) {
736
+ this.logger.log('[CONV-FOOTER] onStreamPressed:event', event);
737
+ event.preventDefault();
738
+ if (this.showAlertEmoji) {
739
+ return;
740
+ }
741
+ const turningOn = !this.isStreamAudioActive;
742
+ if (turningOn) {
743
+ try {
744
+ await this.initVoice();
745
+ this.isStreamAudioActive = true;
746
+ } catch (e) {
747
+ this.logger.error('[CONV-FOOTER] onStreamPressed: initVoice failed', e);
748
+ this.isStreamAudioActive = false;
749
+ }
750
+ } else {
751
+ await this.stopVoice();
752
+ this.isStreamAudioActive = false;
753
+ }
754
+ this.onStreamAudioActiveChange.emit(this.isStreamAudioActive);
755
+ this.logger.log('[CONV-FOOTER] isStreamAudioActive', this.isStreamAudioActive);
756
+ }
757
+
664
758
  async onEmojiiPickerClicked(){
665
759
  // if(this.loadPickerModule){
666
760
  // this.loadPickerModule = false;
@@ -154,15 +154,10 @@ export class AudioComponent implements AfterViewInit {
154
154
  // });
155
155
 
156
156
  const response = await fetch(this.rawAudioUrl!);
157
- this.logger.debug('getAudioDuration: response ---> ', response)
158
157
  const arrayBuffer = await response.arrayBuffer();
159
- this.logger.debug('getAudioDuration: arrayBuffer ---> ', arrayBuffer)
160
158
  const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
161
- this.logger.debug('getAudioDuration: audioContext ---> ', audioContext)
162
159
  const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
163
- this.logger.debug('getAudioDuration: audioBuffer ---> ', audioBuffer)
164
160
  this.audioDuration = audioBuffer.duration;
165
- this.logger.debug('getAudioDuration: audioDuration ---> ', this.audioDuration)
166
161
 
167
162
  }
168
163
 
@@ -0,0 +1,19 @@
1
+ <div class="lyrics-container">
2
+
3
+ <audio
4
+ #audioPlayer
5
+ [src]="message?.metadata?.src"
6
+ (timeupdate)="onTimeUpdate()"
7
+ style="display:none">
8
+ </audio>
9
+
10
+ <p class="lyrics message_innerhtml marked" #transcriptBox [style.color]="color">
11
+ <span
12
+ *ngFor="let w of words; let i = index; trackBy: trackByIndex"
13
+ class="word"
14
+ [ngClass]="w.state">
15
+ {{ w.text }}
16
+ </span>
17
+ </p>
18
+
19
+ </div>
@@ -0,0 +1,65 @@
1
+ :host {
2
+ display: block;
3
+ font-size: var(--font-size-bubble-message, 14px);
4
+ }
5
+
6
+ /* Allineato a text.component.scss (.message_innerhtml, p) */
7
+ .message_innerhtml {
8
+ margin: 0;
9
+
10
+ &.marked {
11
+ padding: 12px 16px;
12
+ margin-block-start: 0em !important;
13
+ margin-block-end: 0em !important;
14
+ }
15
+ }
16
+
17
+ .lyrics {
18
+ font-size: inherit;
19
+ margin: 0;
20
+ line-height: 1.4em;
21
+ font-style: normal;
22
+ letter-spacing: normal;
23
+ font-stretch: normal;
24
+ font-variant: normal;
25
+ font-weight: 300;
26
+ overflow: hidden;
27
+
28
+ display: flex;
29
+ flex-wrap: wrap;
30
+ gap: 6px;
31
+ /* Colore bubble: da [style.color] / @Input() color — ereditato dalle .word */
32
+ }
33
+
34
+ /* base word */
35
+ .word {
36
+ transition:
37
+ transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1),
38
+ color 0.3s ease,
39
+ opacity 0.3s ease,
40
+ filter 0.3s ease;
41
+
42
+ will-change: transform;
43
+ }
44
+
45
+ /* FUTURE */
46
+ .word.future {
47
+ opacity: 0;
48
+ transform: scale(0.98);
49
+ }
50
+
51
+ /* PAST: stesso colore del testo bubble (@Input color sul <p>) */
52
+ .word.past {
53
+ opacity: 1;
54
+ color: inherit;
55
+ transform: scale(1);
56
+ }
57
+
58
+ /* ACTIVE (solo momentaneo, tipo “karaoke flash”) */
59
+ .word.active {
60
+ opacity: 1;
61
+ color: #00c3ff;
62
+ font-weight: 700;
63
+ transform: scale(1.18);
64
+ text-shadow: 0 0 10px rgba(0, 195, 255, 0.35);
65
+ }
@@ -0,0 +1,23 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { AudioSyncComponent } from './audio-sync.component';
4
+
5
+ describe('AudioSyncComponent', () => {
6
+ let component: AudioSyncComponent;
7
+ let fixture: ComponentFixture<AudioSyncComponent>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ imports: [AudioSyncComponent]
12
+ })
13
+ .compileComponents();
14
+
15
+ fixture = TestBed.createComponent(AudioSyncComponent);
16
+ component = fixture.componentInstance;
17
+ fixture.detectChanges();
18
+ });
19
+
20
+ it('should create', () => {
21
+ expect(component).toBeTruthy();
22
+ });
23
+ });
@@ -0,0 +1,197 @@
1
+ import {
2
+ AfterViewInit,
3
+ ChangeDetectorRef,
4
+ Component,
5
+ ElementRef,
6
+ Input,
7
+ OnChanges,
8
+ OnDestroy,
9
+ SimpleChanges,
10
+ ViewChild,
11
+ } from '@angular/core';
12
+ import { MessageModel } from 'src/chat21-core/models/message';
13
+
14
+ /** HAVE_METADATA: metadati già disponibili (tipico audio servito da cache). */
15
+ const HAVE_METADATA = 1;
16
+
17
+ @Component({
18
+ selector: 'chat-audio-sync',
19
+ templateUrl: './audio-sync.component.html',
20
+ styleUrl: './audio-sync.component.scss',
21
+ })
22
+ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
23
+ @Input() message: MessageModel | null = null;
24
+ @Input() color?: string;
25
+
26
+ @ViewChild('audioPlayer') audioRef!: ElementRef<HTMLAudioElement>;
27
+ @ViewChild('transcriptBox') transcriptBox!: ElementRef<HTMLElement>;
28
+
29
+ words: {
30
+ text: string;
31
+ start: number;
32
+ end: number;
33
+ state: 'future' | 'active' | 'past';
34
+ }[] = [];
35
+
36
+ currentTime = 0;
37
+ duration = 1;
38
+ activeIndex = -1;
39
+
40
+ private timingReady = false;
41
+ private onMetadataLoaded: () => void;
42
+ private onPlaybackEnded: () => void;
43
+
44
+ constructor(private readonly cdr: ChangeDetectorRef) {}
45
+
46
+ /** `false` = messaggio già in storico: niente autoplay / karaoke. Da `message.isJustRecived`. */
47
+ private get skipSyncAnimation(): boolean {
48
+ return this.message?.isJustRecived === false;
49
+ }
50
+
51
+ ngOnChanges(changes: SimpleChanges): void {
52
+ if (!changes['message']) {
53
+ return;
54
+ }
55
+ if (this.audioRef?.nativeElement && this.timingReady) {
56
+ this.duration = this.audioRef.nativeElement.duration || 1;
57
+ this.buildFakeTiming();
58
+ if (this.skipSyncAnimation) {
59
+ this.markAllWordsPast();
60
+ } else {
61
+ this.syncStatesFromCurrentTime();
62
+ }
63
+ }
64
+ }
65
+
66
+ ngAfterViewInit(): void {
67
+ const audio = this.audioRef.nativeElement;
68
+
69
+ this.onPlaybackEnded = () => {
70
+ if (this.skipSyncAnimation) {
71
+ return;
72
+ }
73
+ this.markAllWordsPast();
74
+ if (this.message) {
75
+ this.message.isJustRecived = false;
76
+ }
77
+ this.cdr.detectChanges();
78
+ };
79
+
80
+ this.onMetadataLoaded = () => {
81
+ if (this.timingReady) {
82
+ return;
83
+ }
84
+ this.timingReady = true;
85
+ this.duration = audio.duration || 1;
86
+ this.buildFakeTiming();
87
+ if (this.skipSyncAnimation) {
88
+ this.markAllWordsPast();
89
+ this.cdr.detectChanges();
90
+ return;
91
+ }
92
+ this.syncStatesFromCurrentTime();
93
+ this.cdr.detectChanges();
94
+
95
+ setTimeout(() => {
96
+ audio.play().catch(() => {
97
+ this.syncStatesFromCurrentTime();
98
+ this.cdr.detectChanges();
99
+ });
100
+ }, 200);
101
+ };
102
+
103
+ audio.addEventListener('loadedmetadata', this.onMetadataLoaded);
104
+ audio.addEventListener('ended', this.onPlaybackEnded);
105
+
106
+ if (audio.readyState >= HAVE_METADATA) {
107
+ this.onMetadataLoaded();
108
+ }
109
+ }
110
+
111
+ ngOnDestroy(): void {
112
+ const audio = this.audioRef?.nativeElement;
113
+ if (!audio) {
114
+ return;
115
+ }
116
+ if (this.onMetadataLoaded) {
117
+ audio.removeEventListener('loadedmetadata', this.onMetadataLoaded);
118
+ }
119
+ if (this.onPlaybackEnded) {
120
+ audio.removeEventListener('ended', this.onPlaybackEnded);
121
+ }
122
+ }
123
+
124
+ private markAllWordsPast(): void {
125
+ this.words.forEach((w) => {
126
+ w.state = 'past';
127
+ });
128
+ this.activeIndex = -1;
129
+ }
130
+
131
+ buildFakeTiming(): void {
132
+ const rawWords = (this.message?.text || '')
133
+ .trim()
134
+ .split(/\s+/)
135
+ .filter((w) => w.length > 0);
136
+ if (rawWords.length === 0) {
137
+ this.words = [];
138
+ return;
139
+ }
140
+ const step = this.duration / rawWords.length;
141
+
142
+ this.words = rawWords.map((w, i) => ({
143
+ text: w,
144
+ start: i * step,
145
+ end: (i + 1) * step,
146
+ state: 'future' as const,
147
+ }));
148
+ }
149
+
150
+ syncStatesFromCurrentTime(): void {
151
+ if (this.skipSyncAnimation) {
152
+ return;
153
+ }
154
+ const audio = this.audioRef?.nativeElement;
155
+ if (!audio || this.words.length === 0) {
156
+ return;
157
+ }
158
+ this.currentTime = audio.currentTime;
159
+ let newActiveIndex = -1;
160
+
161
+ this.words.forEach((w, i) => {
162
+ if (this.currentTime >= w.end) {
163
+ w.state = 'past';
164
+ } else if (this.currentTime >= w.start && this.currentTime < w.end) {
165
+ w.state = 'active';
166
+ newActiveIndex = i;
167
+ } else {
168
+ w.state = 'future';
169
+ }
170
+ });
171
+
172
+ if (newActiveIndex !== this.activeIndex) {
173
+ this.activeIndex = newActiveIndex;
174
+ this.scrollToActive();
175
+ }
176
+ }
177
+
178
+ onTimeUpdate(): void {
179
+ this.syncStatesFromCurrentTime();
180
+ }
181
+
182
+ scrollToActive(): void {
183
+ const container = this.transcriptBox?.nativeElement;
184
+ const active = container?.querySelector('.active') as HTMLElement;
185
+
186
+ if (active) {
187
+ active.scrollIntoView({
188
+ behavior: 'smooth',
189
+ block: 'center',
190
+ });
191
+ }
192
+ }
193
+
194
+ trackByIndex(index: number): number {
195
+ return index;
196
+ }
197
+ }
@@ -64,6 +64,11 @@
64
64
  [stylesMap]="stylesMap">
65
65
  </chat-audio>
66
66
 
67
+ <chat-audio-sync *ngIf="isAudioTTS(message)"
68
+ [message]="message"
69
+ [color]="fontColor">
70
+ </chat-audio-sync>
71
+
67
72
 
68
73
  <!-- <chat-frame *ngIf="message.metadata && message.metadata.type && message.metadata.type.includes('video')"
69
74
  [metadata]="message.metadata"
@@ -75,7 +80,7 @@
75
80
  <!-- <div *ngIf="message.type == 'text'"> -->
76
81
 
77
82
  <!-- tooltip="{{message.timestamp | dateAgo}} ({{message.timestamp | date:'shortDate'}} {{message.timestamp | date:'HH:mm:ss'}})" placement="bottom" -->
78
- <div *ngIf="message?.text && !isAudio(message)" >
83
+ <div *ngIf="message?.text && (!isAudio(message) && !isAudioTTS(message))" >
79
84
 
80
85
  <!-- [htmlEnabled]="(message?.type==='html')? true : false" -->
81
86
  <chat-text *ngIf="message?.type !=='html'"
@@ -5,7 +5,7 @@ import { LoggerService } from 'src/chat21-core/providers/abstract/logger.service
5
5
  import { LoggerInstance } from 'src/chat21-core/providers/logger/loggerInstance';
6
6
  import { MAX_WIDTH_IMAGES, MESSAGE_TYPE_MINE, MESSAGE_TYPE_OTHERS, MIN_WIDTH_IMAGES } from 'src/chat21-core/utils/constants';
7
7
  import { convertColorToRGBA } from 'src/chat21-core/utils/utils';
8
- import { isAudio, isFile, isFrame, isImage, messageType } from 'src/chat21-core/utils/utils-message';
8
+ import { isAudio, isAudioTTS, isFile, isFrame, isImage, messageType } from 'src/chat21-core/utils/utils-message';
9
9
  import { getColorBck } from 'src/chat21-core/utils/utils-user';
10
10
 
11
11
  @Component({
@@ -26,6 +26,7 @@ export class BubbleMessageComponent implements OnInit {
26
26
  isFile = isFile;
27
27
  isFrame = isFrame;
28
28
  isAudio = isAudio;
29
+ isAudioTTS=isAudioTTS;
29
30
  convertColorToRGBA = convertColorToRGBA
30
31
 
31
32
  // ========== begin:: check message type functions ======= //
@@ -1125,6 +1125,11 @@ export class GlobalSettingsService {
1125
1125
  if (TEMP !== undefined) {
1126
1126
  globals.showAudioRecorderFooterButton = (TEMP === true) ? true : false;
1127
1127
  }
1128
+ TEMP = tiledeskSettings['showAudioStreamFooterButton'];
1129
+ // this.logger.debug('[GLOBAL-SET] setVariablesFromSettings > showAudioStreamFooterButton:: ', TEMP]);
1130
+ if (TEMP !== undefined) {
1131
+ globals.showAudioStreamFooterButton = (TEMP === true) ? true : false;
1132
+ }
1128
1133
  TEMP = tiledeskSettings['size'];
1129
1134
  // this.logger.debug('[GLOBAL-SET] setVariablesFromSettings > size:: ', TEMP]);
1130
1135
  if (TEMP !== undefined) {
@@ -1873,6 +1878,11 @@ export class GlobalSettingsService {
1873
1878
  globals.showAttachmentFooterButton = stringToBoolean(TEMP);
1874
1879
  }
1875
1880
 
1881
+ TEMP = getParameterByName(windowContext, 'tiledesk_showAudioStreamFooterButton');
1882
+ if (TEMP) {
1883
+ globals.showAudioStreamFooterButton = stringToBoolean(TEMP);
1884
+ }
1885
+
1876
1886
  TEMP = getParameterByName(windowContext, 'tiledesk_showEmojiFooterButton');
1877
1887
  if (TEMP) {
1878
1888
  globals.showEmojiFooterButton = stringToBoolean(TEMP);
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Configurazione opzionale per i servizi voce OpenAI (da `environment` o runtime).
3
+ */
4
+ export interface OpenAiVoiceEnvironmentConfig {
5
+ /** Obbligatoria per chiamate API reali; se assente, STT/TTS non inviano richieste. */
6
+ apiKey?: string;
7
+ baseUrl?: string;
8
+ transcriptionModel?: string;
9
+ ttsModel?: string;
10
+ /** Voce predefinita TTS (es. `alloy`). */
11
+ ttsVoice?: string;
12
+ }