@chat21/chat21-web-widget 5.1.30 → 5.1.32-rc13

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 (64) hide show
  1. package/.github/workflows/docker-community-push-latest.yml +23 -13
  2. package/.github/workflows/docker-image-tag-community-tag-push.yml +22 -12
  3. package/CHANGELOG.md +89 -2
  4. package/Dockerfile +4 -5
  5. package/angular.json +5 -2
  6. package/deploy_amazon_beta.sh +17 -7
  7. package/docs/changelog/this-branch.md +36 -0
  8. package/nginx.conf +22 -2
  9. package/package.json +4 -1
  10. package/src/app/app.component.ts +10 -9
  11. package/src/app/app.module.ts +11 -0
  12. package/src/app/component/conversation-detail/conversation/conversation.component.html +9 -2
  13. package/src/app/component/conversation-detail/conversation/conversation.component.scss +12 -2
  14. package/src/app/component/conversation-detail/conversation/conversation.component.ts +46 -5
  15. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.html +9 -5
  16. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.scss +19 -1
  17. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.ts +2 -0
  18. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.html +128 -80
  19. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss +117 -13
  20. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts +120 -8
  21. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.html +43 -0
  22. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.scss +79 -0
  23. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.ts +192 -0
  24. package/src/app/component/last-message/last-message.component.ts +4 -1
  25. package/src/app/component/message/audio/audio.component.ts +0 -5
  26. package/src/app/component/message/audio-sync/audio-sync.component.html +18 -0
  27. package/src/app/component/message/audio-sync/audio-sync.component.scss +64 -0
  28. package/src/app/component/message/audio-sync/audio-sync.component.spec.ts +23 -0
  29. package/src/app/component/message/audio-sync/audio-sync.component.ts +558 -0
  30. package/src/app/component/message/bubble-message/bubble-message.component.html +6 -1
  31. package/src/app/component/message/bubble-message/bubble-message.component.ts +2 -1
  32. package/src/app/providers/global-settings.service.ts +21 -0
  33. package/src/app/providers/translator.service.ts +2 -0
  34. package/src/app/providers/tts-audio-playback-coordinator.service.ts +93 -0
  35. package/src/app/providers/voice/STT&TTS/openai-voice.config.ts +12 -0
  36. package/src/app/providers/voice/STT&TTS/openai-voice.provider.ts +171 -0
  37. package/src/app/providers/voice/STT&TTS/speech-provider.abstract.ts +39 -0
  38. package/src/app/providers/voice/audio.types.ts +34 -0
  39. package/src/app/providers/voice/vad.service.spec.ts +28 -0
  40. package/src/app/providers/voice/vad.service.ts +70 -0
  41. package/src/app/providers/voice/voice.service.spec.ts +60 -0
  42. package/src/app/providers/voice/voice.service.ts +376 -0
  43. package/src/app/sass/_variables.scss +3 -0
  44. package/src/app/shims/onnxruntime-web-wasm.ts +4 -0
  45. package/src/app/utils/conversation-sender-classifier.ts +21 -0
  46. package/src/app/utils/globals.ts +7 -1
  47. package/src/assets/i18n/en.json +1 -0
  48. package/src/assets/i18n/es.json +1 -0
  49. package/src/assets/i18n/fr.json +1 -0
  50. package/src/assets/i18n/it.json +1 -0
  51. package/src/assets/onnx/ort-wasm-simd-threaded.mjs +59 -0
  52. package/src/assets/onnx/ort-wasm-simd-threaded.wasm +0 -0
  53. package/src/assets/vad/silero_vad_legacy.onnx +0 -0
  54. package/src/assets/vad/vad.worklet.bundle.min.js +1 -0
  55. package/src/chat21-core/models/message.ts +2 -1
  56. package/src/chat21-core/providers/firebase/firebase-conversation-handler.ts +3 -2
  57. package/src/chat21-core/providers/mqtt/mqtt-conversation-handler.ts +12 -0
  58. package/src/chat21-core/providers/tiledesk/tiledesk-requests.service.ts +1 -1
  59. package/src/chat21-core/utils/utils-message.ts +7 -0
  60. package/src/chat21-core/utils/utils.ts +5 -2
  61. package/src/launch.js +41 -32
  62. package/src/launch_template.js +41 -32
  63. package/tsconfig.json +5 -0
  64. package/deploy_amazon_prod.sh +0 -41
@@ -0,0 +1,558 @@
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 { Subscription } from 'rxjs';
13
+ import { MessageModel } from 'src/chat21-core/models/message';
14
+ import { TtsAudioPlaybackCoordinator } from 'src/app/providers/tts-audio-playback-coordinator.service';
15
+ import { Globals } from 'src/app/utils/globals';
16
+
17
+ /** HAVE_METADATA: metadati già disponibili (tipico audio servito da cache). */
18
+ const HAVE_METADATA = 1;
19
+
20
+ @Component({
21
+ selector: 'chat-audio-sync',
22
+ templateUrl: './audio-sync.component.html',
23
+ styleUrl: './audio-sync.component.scss',
24
+ })
25
+ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
26
+ @Input() message: MessageModel | null = null;
27
+ @Input() color?: string;
28
+
29
+ @ViewChild('audioPlayer') audioRef!: ElementRef<HTMLAudioElement>;
30
+ @ViewChild('transcriptBox') transcriptBox!: ElementRef<HTMLElement>;
31
+
32
+ words: {
33
+ text: string;
34
+ start: number;
35
+ end: number;
36
+ state: 'future' | 'active' | 'past';
37
+ }[] = [];
38
+
39
+ currentTime = 0;
40
+ duration = 1;
41
+ activeIndex = -1;
42
+
43
+ private timingReady = false;
44
+ private onMetadataLoaded: () => void;
45
+ private onPlaybackEnded: () => void;
46
+
47
+ /** Id univoco per il coordinatore (di solito `message.uid`). */
48
+ private playbackOwnerId = '';
49
+ private destroyed = false;
50
+ private playbackRequested = false;
51
+ private playbackStarted = false;
52
+ private streamAbort?: AbortController;
53
+ private mediaSourceObjectUrl?: string;
54
+ private stopAllSub?: Subscription;
55
+
56
+ constructor(
57
+ private readonly cdr: ChangeDetectorRef,
58
+ private readonly ttsPlayback: TtsAudioPlaybackCoordinator,
59
+ private readonly globals: Globals,
60
+ ) {}
61
+
62
+ /** `false` = messaggio già in storico: niente autoplay / karaoke. Da `message.isJustRecived`. */
63
+ private get skipSyncAnimation(): boolean {
64
+ return this.message?.isJustRecived === false;
65
+ }
66
+
67
+ ngOnChanges(changes: SimpleChanges): void {
68
+ if (!changes['message']) {
69
+ return;
70
+ }
71
+ if (this.audioRef?.nativeElement && this.timingReady) {
72
+ const d = this.audioRef.nativeElement.duration;
73
+ if (Number.isFinite(d) && d > 0) {
74
+ this.duration = d;
75
+ }
76
+ this.buildFakeTiming();
77
+ if (this.skipSyncAnimation) {
78
+ this.markAllWordsPast();
79
+ } else if (this.playbackStarted) {
80
+ this.syncStatesFromCurrentTime();
81
+ }
82
+ }
83
+ }
84
+
85
+ ngAfterViewInit(): void {
86
+ const audio = this.audioRef.nativeElement;
87
+
88
+ this.playbackOwnerId =
89
+ (this.message?.uid && String(this.message.uid).trim()) ||
90
+ `tts-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
91
+
92
+ this.onPlaybackEnded = () => {
93
+ this.playbackStarted = false;
94
+ this.cleanupStreaming();
95
+ this.ttsPlayback.releaseIfCurrent(this.playbackOwnerId);
96
+ if (this.skipSyncAnimation) {
97
+ return;
98
+ }
99
+ this.markAllWordsPast();
100
+ if (this.message) {
101
+ this.message.isJustRecived = false;
102
+ }
103
+ this.cdr.detectChanges();
104
+ };
105
+
106
+ this.onMetadataLoaded = () => {
107
+ // La durata potrebbe arrivare tardi (specie con streaming).
108
+ const d = audio.duration;
109
+ if (Number.isFinite(d) && d > 0) {
110
+ this.duration = d;
111
+ } else if (!this.timingReady) {
112
+ this.duration = this.estimateDurationSecondsFromText();
113
+ }
114
+
115
+ this.timingReady = true;
116
+ this.buildFakeTiming();
117
+ if (this.skipSyncAnimation) {
118
+ this.markAllWordsPast();
119
+ this.cdr.detectChanges();
120
+ return;
121
+ }
122
+ if (this.playbackStarted) {
123
+ this.syncStatesFromCurrentTime();
124
+ }
125
+ this.cdr.detectChanges();
126
+ };
127
+
128
+ audio.addEventListener('loadedmetadata', this.onMetadataLoaded);
129
+ audio.addEventListener('ended', this.onPlaybackEnded);
130
+
131
+ // Prepara subito le parole (durata stimata) e poi aggiorna quando arriva la metadata reale.
132
+ this.duration = this.estimateDurationSecondsFromText();
133
+ this.timingReady = true;
134
+ this.buildFakeTiming();
135
+ if (this.skipSyncAnimation) {
136
+ this.markAllWordsPast();
137
+ this.cdr.detectChanges();
138
+ return;
139
+ }
140
+ this.cdr.detectChanges();
141
+
142
+ setTimeout(() => {
143
+ if (this.playbackRequested || this.destroyed) {
144
+ return;
145
+ }
146
+ this.playbackRequested = true;
147
+ this.ttsPlayback.requestStart(this.playbackOwnerId, () => {
148
+ if (this.destroyed) {
149
+ this.ttsPlayback.releaseIfCurrent(this.playbackOwnerId);
150
+ return;
151
+ }
152
+ this.playbackStarted = true;
153
+ this.syncStatesFromCurrentTime();
154
+ this.cdr.detectChanges();
155
+ this.startPlayback(audio);
156
+ });
157
+ }, 200);
158
+
159
+ // Stop signal: user pressed X while this TTS was playing or queued.
160
+ this.stopAllSub = this.ttsPlayback.stopAllPlayback$.subscribe(() => {
161
+ if (!this.playbackRequested && !this.playbackStarted) {
162
+ return;
163
+ }
164
+ this.destroyed = true;
165
+ this.playbackStarted = false;
166
+ this.cleanupStreaming();
167
+ try {
168
+ audio.pause();
169
+ audio.currentTime = 0;
170
+ } catch {
171
+ /* ignore */
172
+ }
173
+ this.markAllWordsPast();
174
+ if (this.message) {
175
+ this.message.isJustRecived = false;
176
+ }
177
+ this.cdr.detectChanges();
178
+ });
179
+ }
180
+
181
+ ngOnDestroy(): void {
182
+ this.destroyed = true;
183
+ this.playbackStarted = false;
184
+ this.cleanupStreaming();
185
+ this.stopAllSub?.unsubscribe();
186
+ this.stopAllSub = undefined;
187
+
188
+ const audio = this.audioRef?.nativeElement;
189
+ if (audio) {
190
+ try {
191
+ audio.pause();
192
+ audio.currentTime = 0;
193
+ } catch {
194
+ /* ignore */
195
+ }
196
+ }
197
+ this.ttsPlayback.release(this.playbackOwnerId);
198
+
199
+ if (!audio) {
200
+ return;
201
+ }
202
+ if (this.onMetadataLoaded) {
203
+ audio.removeEventListener('loadedmetadata', this.onMetadataLoaded);
204
+ }
205
+ if (this.onPlaybackEnded) {
206
+ audio.removeEventListener('ended', this.onPlaybackEnded);
207
+ }
208
+ }
209
+
210
+ private startPlayback(audio: HTMLAudioElement): void {
211
+ const src = (this.message as any)?.metadata?.src as string | undefined;
212
+ if (!src) {
213
+ this.playbackStarted = false;
214
+ this.ttsPlayback.releaseIfCurrent(this.playbackOwnerId);
215
+ this.markAllWordsPast();
216
+ if (this.message) {
217
+ this.message.isJustRecived = false;
218
+ }
219
+ this.cdr.detectChanges();
220
+ return;
221
+ }
222
+
223
+ if (this.message?.type === 'tts') {
224
+ this.startStreamingFromEndpoint(audio, src);
225
+ return;
226
+ }
227
+
228
+ audio.src = src;
229
+ try {
230
+ audio.currentTime = 0;
231
+ } catch {
232
+ /* ignore */
233
+ }
234
+ audio.play().catch(() => this.handlePlaybackError());
235
+ }
236
+
237
+ private startStreamingFromEndpoint(audio: HTMLAudioElement, endpoint: string): void {
238
+ this.cleanupStreaming();
239
+
240
+ const jwt = this.getJwtToken();
241
+ const voiceSettings = this.getVoiceSettingsBody();
242
+ const requestBody = this.buildTtsRequestBody(voiceSettings);
243
+ // <audio src="..."> non può inviare header/body: serve fetch().
244
+ const hasMse = typeof (window as any).MediaSource !== 'undefined';
245
+ if (!hasMse) {
246
+ this.fetchAsBlobAndPlay(audio, endpoint, jwt, requestBody);
247
+ return;
248
+ }
249
+
250
+ const MediaSourceCtor = (window as any).MediaSource as typeof MediaSource;
251
+ const mediaSource = new MediaSourceCtor();
252
+ const objectUrl = URL.createObjectURL(mediaSource);
253
+ this.mediaSourceObjectUrl = objectUrl;
254
+ audio.src = objectUrl;
255
+
256
+ const abort = new AbortController();
257
+ this.streamAbort = abort;
258
+
259
+ const onSourceOpen = async () => {
260
+ mediaSource.removeEventListener('sourceopen', onSourceOpen);
261
+ try {
262
+ const headers: Record<string, string> = {
263
+ 'Content-Type': 'application/json',
264
+ 'Authorization': `${jwt}`
265
+ };
266
+
267
+ const response = await fetch(endpoint, {
268
+ method: 'POST',
269
+ headers,
270
+ body: JSON.stringify(requestBody),
271
+ signal: abort.signal,
272
+ });
273
+ if (!response.ok || !response.body) {
274
+ throw new Error(`TTS stream request failed (${response.status})`);
275
+ }
276
+
277
+ const headerType = (response.headers.get('content-type') || '').split(';')[0].trim();
278
+ const mime = (headerType && MediaSourceCtor.isTypeSupported(headerType))
279
+ ? headerType
280
+ : 'audio/mpeg';
281
+
282
+ if (!MediaSourceCtor.isTypeSupported(mime)) {
283
+ this.cleanupStreaming();
284
+ // Fallback: fetch completo e play via blob (no streaming).
285
+ this.fetchAsBlobAndPlay(audio, endpoint, jwt, requestBody);
286
+ return;
287
+ }
288
+
289
+ const sourceBuffer = mediaSource.addSourceBuffer(mime);
290
+ sourceBuffer.mode = 'sequence';
291
+
292
+ const reader = response.body.getReader();
293
+ const queue: Uint8Array[] = [];
294
+ let doneReading = false;
295
+ let started = false;
296
+
297
+ const tryEndOfStream = () => {
298
+ if (doneReading && queue.length === 0 && !sourceBuffer.updating) {
299
+ try {
300
+ mediaSource.endOfStream();
301
+ } catch {
302
+ /* ignore */
303
+ }
304
+ }
305
+ };
306
+
307
+ const pump = () => {
308
+ if (abort.signal.aborted) {
309
+ return;
310
+ }
311
+ if (sourceBuffer.updating) {
312
+ return;
313
+ }
314
+ const chunk = queue.shift();
315
+ if (!chunk) {
316
+ tryEndOfStream();
317
+ return;
318
+ }
319
+ try {
320
+ const ab = chunk.buffer.slice(
321
+ chunk.byteOffset,
322
+ chunk.byteOffset + chunk.byteLength,
323
+ ) as ArrayBuffer;
324
+ sourceBuffer.appendBuffer(ab);
325
+ } catch {
326
+ this.cleanupStreaming();
327
+ this.fetchAsBlobAndPlay(audio, endpoint, jwt, requestBody);
328
+ }
329
+ };
330
+
331
+ sourceBuffer.addEventListener('updateend', () => {
332
+ if (!started && this.playbackStarted && !this.destroyed) {
333
+ started = true;
334
+ audio.play().catch(() => this.handlePlaybackError());
335
+ }
336
+ pump();
337
+ });
338
+
339
+ // Primo pump (se arrivano subito chunk)
340
+ pump();
341
+
342
+ while (!abort.signal.aborted) {
343
+ const { value, done } = await reader.read();
344
+ if (done) {
345
+ doneReading = true;
346
+ break;
347
+ }
348
+ if (value && value.byteLength > 0) {
349
+ queue.push(value);
350
+ pump();
351
+ }
352
+ }
353
+
354
+ doneReading = true;
355
+ tryEndOfStream();
356
+ } catch {
357
+ if (!abort.signal.aborted) {
358
+ this.handlePlaybackError();
359
+ }
360
+ }
361
+ };
362
+
363
+ mediaSource.addEventListener('sourceopen', onSourceOpen);
364
+ }
365
+
366
+ private handlePlaybackError(): void {
367
+ this.playbackStarted = false;
368
+ this.cleanupStreaming();
369
+ this.ttsPlayback.releaseIfCurrent(this.playbackOwnerId);
370
+ this.markAllWordsPast();
371
+ if (this.message) {
372
+ this.message.isJustRecived = false;
373
+ }
374
+ this.cdr.detectChanges();
375
+ }
376
+
377
+ private cleanupStreaming(): void {
378
+ try {
379
+ this.streamAbort?.abort();
380
+ } catch {
381
+ /* ignore */
382
+ }
383
+ this.streamAbort = undefined;
384
+
385
+ if (this.mediaSourceObjectUrl) {
386
+ try {
387
+ URL.revokeObjectURL(this.mediaSourceObjectUrl);
388
+ } catch {
389
+ /* ignore */
390
+ }
391
+ this.mediaSourceObjectUrl = undefined;
392
+ }
393
+ }
394
+
395
+ private getJwtToken(): string | null {
396
+ const token = (this.globals?.tiledeskToken || this.globals?.jwt || '').trim();
397
+ return token.length > 0 ? token : null;
398
+ }
399
+
400
+ private getVoiceSettingsBody(): unknown {
401
+ const raw = (this.message as any)?.metadata?.voiceSettings;
402
+ if (raw === null || raw === undefined) {
403
+ return {};
404
+ }
405
+ if (typeof raw === 'string') {
406
+ const s = raw.trim();
407
+ if (!s) return {};
408
+ try {
409
+ return JSON.parse(s);
410
+ } catch {
411
+ // se non è JSON valido, invialo come stringa (il backend può gestirlo)
412
+ return { voiceSettings: raw };
413
+ }
414
+ }
415
+ return raw;
416
+ }
417
+
418
+ private async fetchAsBlobAndPlay(
419
+ audio: HTMLAudioElement,
420
+ endpoint: string,
421
+ jwt: string | null,
422
+ requestBody: unknown,
423
+ ): Promise<void> {
424
+ try {
425
+ const headers: Record<string, string> = {
426
+ 'Content-Type': 'application/json',
427
+ 'Authorization': `${jwt}`
428
+ };
429
+
430
+ console.log('headers', headers);
431
+ console.log('requestBody', requestBody);
432
+
433
+ const response = await fetch(endpoint, {
434
+ method: 'POST',
435
+ headers,
436
+ body: JSON.stringify(requestBody ?? {}),
437
+ signal: this.streamAbort?.signal,
438
+ });
439
+
440
+ if (!response.ok) {
441
+ throw new Error(`TTS request failed (${response.status})`);
442
+ }
443
+
444
+ const blob = await response.blob();
445
+ if (this.destroyed) {
446
+ return;
447
+ }
448
+
449
+ const objectUrl = URL.createObjectURL(blob);
450
+ this.mediaSourceObjectUrl = objectUrl;
451
+ audio.src = objectUrl;
452
+ audio.play().catch(() => this.handlePlaybackError());
453
+ } catch {
454
+ this.handlePlaybackError();
455
+ }
456
+ }
457
+
458
+ private buildTtsRequestBody(voiceSettings: unknown): unknown {
459
+ const text = this.message?.text ?? '';
460
+ if (
461
+ voiceSettings &&
462
+ typeof voiceSettings === 'object' &&
463
+ !Array.isArray(voiceSettings)
464
+ ) {
465
+ return { ...(voiceSettings as Record<string, unknown>), text, streaming: true };
466
+ }
467
+ return { voiceSettings, text, streaming: true };
468
+ }
469
+
470
+ private markAllWordsPast(): void {
471
+ this.words.forEach((w) => {
472
+ w.state = 'past';
473
+ });
474
+ this.activeIndex = -1;
475
+ }
476
+
477
+ private estimateDurationSecondsFromText(): number {
478
+ const rawWords = (this.message?.text || '')
479
+ .trim()
480
+ .split(/\s+/)
481
+ .filter((w) => w.length > 0);
482
+ if (rawWords.length === 0) {
483
+ return 1;
484
+ }
485
+ // ~140 WPM → ~0.43s/word
486
+ return Math.max(1, rawWords.length * 0.43);
487
+ }
488
+
489
+ buildFakeTiming(): void {
490
+ const rawWords = (this.message?.text || '')
491
+ .trim()
492
+ .split(/\s+/)
493
+ .filter((w) => w.length > 0);
494
+ if (rawWords.length === 0) {
495
+ this.words = [];
496
+ return;
497
+ }
498
+ const step = this.duration / rawWords.length;
499
+
500
+ this.words = rawWords.map((w, i) => ({
501
+ text: w,
502
+ start: i * step,
503
+ end: (i + 1) * step,
504
+ state: 'future' as const,
505
+ }));
506
+ }
507
+
508
+ syncStatesFromCurrentTime(): void {
509
+ if (this.skipSyncAnimation) {
510
+ return;
511
+ }
512
+ const audio = this.audioRef?.nativeElement;
513
+ if (!audio || this.words.length === 0) {
514
+ return;
515
+ }
516
+ this.currentTime = audio.currentTime;
517
+ let newActiveIndex = -1;
518
+
519
+ this.words.forEach((w, i) => {
520
+ if (this.currentTime >= w.end) {
521
+ w.state = 'past';
522
+ } else if (this.currentTime >= w.start && this.currentTime < w.end) {
523
+ w.state = 'active';
524
+ newActiveIndex = i;
525
+ } else {
526
+ w.state = 'future';
527
+ }
528
+ });
529
+
530
+ if (newActiveIndex !== this.activeIndex) {
531
+ this.activeIndex = newActiveIndex;
532
+ this.scrollToActive();
533
+ }
534
+ }
535
+
536
+ onTimeUpdate(): void {
537
+ if (!this.playbackStarted) {
538
+ return;
539
+ }
540
+ this.syncStatesFromCurrentTime();
541
+ }
542
+
543
+ scrollToActive(): void {
544
+ const container = this.transcriptBox?.nativeElement;
545
+ const active = container?.querySelector('.active') as HTMLElement;
546
+
547
+ if (active) {
548
+ active.scrollIntoView({
549
+ behavior: 'smooth',
550
+ block: 'center',
551
+ });
552
+ }
553
+ }
554
+
555
+ trackByIndex(index: number): number {
556
+ return index;
557
+ }
558
+ }
@@ -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,11 +1125,22 @@ 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) {
1131
1136
  globals.size = TEMP;
1132
1137
  }
1138
+
1139
+ TEMP = tiledeskSettings['closeChatInConversation'];
1140
+ // this.logger.debug('[GLOBAL-SET] setVariablesFromSettings > closeChatInConversation:: ', TEMP]);
1141
+ if (TEMP !== undefined) {
1142
+ globals.closeChatInConversation = (TEMP === true) ? true : false;
1143
+ }
1133
1144
  }
1134
1145
 
1135
1146
  /**
@@ -1867,6 +1878,11 @@ export class GlobalSettingsService {
1867
1878
  globals.showAttachmentFooterButton = stringToBoolean(TEMP);
1868
1879
  }
1869
1880
 
1881
+ TEMP = getParameterByName(windowContext, 'tiledesk_showAudioStreamFooterButton');
1882
+ if (TEMP) {
1883
+ globals.showAudioStreamFooterButton = stringToBoolean(TEMP);
1884
+ }
1885
+
1870
1886
  TEMP = getParameterByName(windowContext, 'tiledesk_showEmojiFooterButton');
1871
1887
  if (TEMP) {
1872
1888
  globals.showEmojiFooterButton = stringToBoolean(TEMP);
@@ -1876,6 +1892,11 @@ export class GlobalSettingsService {
1876
1892
  if (TEMP) {
1877
1893
  globals.size = TEMP;
1878
1894
  }
1895
+
1896
+ TEMP = getParameterByName(windowContext, 'tiledesk_closeChatInConversation');
1897
+ if (TEMP) {
1898
+ globals.closeChatInConversation = stringToBoolean(TEMP);
1899
+ }
1879
1900
 
1880
1901
  }
1881
1902
 
@@ -302,6 +302,7 @@ export class TranslatorService {
302
302
  'CLOSED',
303
303
  'LABEL_PREVIEW',
304
304
  'MAX_ATTACHMENT',
305
+ 'MAX_ATTACHMENT_ERROR',
305
306
  'EMOJI'
306
307
  ];
307
308
 
@@ -358,6 +359,7 @@ export class TranslatorService {
358
359
  globals.LABEL_PREVIEW = res['LABEL_PREVIEW']
359
360
  globals.LABEL_ERROR_FIELD_REQUIRED= res['LABEL_ERROR_FIELD_REQUIRED']
360
361
  globals.MAX_ATTACHMENT = res['MAX_ATTACHMENT']
362
+ globals.MAX_ATTACHMENT_ERROR = res['MAX_ATTACHMENT_ERROR']
361
363
  globals.EMOJI = res['EMOJI']
362
364
 
363
365