@chat21/chat21-web-widget 5.1.32-rc1 → 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 (25) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/deploy_amazon_beta.sh +17 -7
  3. package/nginx.conf +22 -2
  4. package/package.json +1 -1
  5. package/src/app/app.module.ts +2 -0
  6. package/src/app/component/conversation-detail/conversation/conversation.component.html +2 -1
  7. package/src/app/component/conversation-detail/conversation/conversation.component.scss +10 -0
  8. package/src/app/component/conversation-detail/conversation/conversation.component.ts +13 -1
  9. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.html +7 -3
  10. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.scss +18 -0
  11. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.html +24 -41
  12. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss +37 -51
  13. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts +37 -26
  14. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.html +43 -0
  15. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.scss +79 -0
  16. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.ts +192 -0
  17. package/src/app/component/message/audio-sync/audio-sync.component.html +0 -1
  18. package/src/app/component/message/audio-sync/audio-sync.component.scss +0 -1
  19. package/src/app/component/message/audio-sync/audio-sync.component.ts +378 -17
  20. package/src/app/providers/tts-audio-playback-coordinator.service.ts +93 -0
  21. package/src/app/providers/voice/voice.service.ts +117 -5
  22. package/src/app/sass/_variables.scss +2 -0
  23. package/src/launch.js +41 -32
  24. package/src/launch_template.js +41 -32
  25. package/deploy_amazon_prod.sh +0 -41
@@ -1,7 +1,7 @@
1
1
  import { Inject, Injectable, Optional } from '@angular/core';
2
2
  import type { MicVAD } from '@ricky0123/vad-web';
3
3
  import { getDefaultRealTimeVADOptions } from '@ricky0123/vad-web';
4
- import { BehaviorSubject, Observable, Subject } from 'rxjs';
4
+ import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs';
5
5
  import { LoggerInstance } from 'src/chat21-core/providers/logger/loggerInstance';
6
6
  import { LoggerService } from 'src/chat21-core/providers/abstract/logger.service';
7
7
 
@@ -12,6 +12,7 @@ import {
12
12
  } from './audio.types';
13
13
  import { SpeechToTextProvider } from './STT&TTS/speech-provider.abstract';
14
14
  import { VadService } from './vad.service';
15
+ import { TtsAudioPlaybackCoordinator } from '../tts-audio-playback-coordinator.service';
15
16
 
16
17
  const VOICE_RECORDING_MIME = 'audio/webm';
17
18
 
@@ -33,10 +34,29 @@ export class VoiceService {
33
34
  /** Emesso a ogni fine segmento parlato: audio WebM + opzionalmente `transcript` / `transcriptionError`. */
34
35
  readonly audioSegment$: Observable<VoiceSegmentPayload> = this.audioSegmentSubject.asObservable();
35
36
 
37
+ private readonly speechStartSubject = new Subject<void>();
38
+ /** Emesso quando il microfono intercetta parlato (VAD speech start). */
39
+ readonly speechStart$: Observable<void> = this.speechStartSubject.asObservable();
40
+
41
+ private readonly speechEndSubject = new Subject<void>();
42
+ /** Emesso quando il parlato termina (VAD speech end). */
43
+ readonly speechEnd$: Observable<void> = this.speechEndSubject.asObservable();
44
+
36
45
  // 🔊 REALTIME VOLUME STREAM
37
46
  private readonly volumeSubject = new BehaviorSubject<number>(0);
38
47
  readonly volume$: Observable<number> = this.volumeSubject.asObservable();
39
48
 
49
+ // 🎙️ TTS GATE — suppresses segment emission while TTS is playing
50
+ private isTTSActive = false;
51
+ private ttsGateSub?: Subscription;
52
+
53
+ // 🚫 ACQUISITION GATE — pauses VAD from speech-end until TTS response cycle completes
54
+ private isWaitingForResponse = false;
55
+ private responseTimeoutId?: ReturnType<typeof setTimeout>;
56
+ private readonly _isAcquisitionBlocked$ = new BehaviorSubject<boolean>(false);
57
+ /** Emits `true` from user speech-end until VAD resumes after TTS finishes; drives the grey orb. */
58
+ readonly isAcquisitionBlocked$: Observable<boolean> = this._isAcquisitionBlocked$.asObservable();
59
+
40
60
  // 🎧 AUDIO ANALYSER
41
61
  private audioContext?: AudioContext;
42
62
  private analyser?: AnalyserNode;
@@ -47,6 +67,7 @@ export class VoiceService {
47
67
 
48
68
  constructor(
49
69
  private readonly vadService: VadService,
70
+ private readonly ttsPlayback: TtsAudioPlaybackCoordinator,
50
71
  @Optional() @Inject(SpeechToTextProvider) private readonly speechToText: SpeechToTextProvider | null,
51
72
  ) {}
52
73
 
@@ -83,11 +104,18 @@ export class VoiceService {
83
104
  },
84
105
  onSpeechStart: () => {
85
106
  this.logger.log('[VoiceService] speech start');
107
+ this.speechStartSubject.next();
86
108
  this.startMediaRecorderSegment();
87
109
  },
88
110
  onSpeechEnd: () => {
89
111
  this.logger.log('[VoiceService] speech end');
112
+ this.speechEndSubject.next();
90
113
  this.stopMediaRecorderSegment();
114
+ // Pause VAD immediately — new recordings are blocked until the TTS response cycle completes.
115
+ this.isWaitingForResponse = true;
116
+ this._isAcquisitionBlocked$.next(true);
117
+ this.setResponseSafetyTimeout();
118
+ void this.vad?.pause();
91
119
  },
92
120
  minSpeechMs: 480,
93
121
  redemptionMs: 1920,
@@ -98,11 +126,31 @@ export class VoiceService {
98
126
 
99
127
  // 🔁 start volume loop
100
128
  this.startVolumeLoop();
129
+
130
+ // 🎙️ gate segments while TTS is playing; resume VAD when TTS cycle completes
131
+ this.ttsGateSub = this.ttsPlayback.isTTSPlaying$.subscribe((playing) => {
132
+ this.isTTSActive = playing;
133
+ this.logger.log('[VoiceService] TTS gate', playing ? 'closed (bot speaking)' : 'open (listening)');
134
+ if (!playing && this.isWaitingForResponse) {
135
+ this.resumeVadAfterResponse();
136
+ }
137
+ });
101
138
  }
102
139
 
103
- async stopSession(): Promise<void> {
104
- if (this.mediaRecorder?.state === 'recording') {
105
- this.mediaRecorder.stop();
140
+ /**
141
+ * @param options.discardInProgressSegment — non inviare STT/upload per il segmento WebM corrente (es. interruzione da messaggio in arrivo).
142
+ */
143
+ async stopSession(options?: { discardInProgressSegment?: boolean }): Promise<void> {
144
+ const discard = options?.discardInProgressSegment === true;
145
+
146
+ if (this.mediaRecorder) {
147
+ if (discard) {
148
+ this.mediaRecorder.onstop = null;
149
+ this.mediaRecorder.ondataavailable = null;
150
+ }
151
+ if (this.mediaRecorder.state === 'recording') {
152
+ this.mediaRecorder.stop();
153
+ }
106
154
  }
107
155
 
108
156
  this.mediaRecorder = undefined;
@@ -132,6 +180,63 @@ export class VoiceService {
132
180
  this.volumeSubject.next(0);
133
181
 
134
182
  this.onRecordingComplete = undefined;
183
+
184
+ // 🎙️ release TTS gate subscription
185
+ this.ttsGateSub?.unsubscribe();
186
+ this.ttsGateSub = undefined;
187
+ this.isTTSActive = false;
188
+
189
+ // 🚫 clear acquisition gate
190
+ clearTimeout(this.responseTimeoutId);
191
+ this.responseTimeoutId = undefined;
192
+ this.isWaitingForResponse = false;
193
+ this._isAcquisitionBlocked$.next(false);
194
+ }
195
+
196
+ /**
197
+ * Scarta il segmento WebM in corso (nessun upload/STT) senza chiudere VAD, mic o sessione.
198
+ * Lo stream resta in ascolto per il prossimo `onSpeechStart`.
199
+ */
200
+ discardCurrentRecordingSegment(): void {
201
+ if (this.mediaRecorder) {
202
+ this.mediaRecorder.onstop = null;
203
+ this.mediaRecorder.ondataavailable = null;
204
+ if (this.mediaRecorder.state === 'recording') {
205
+ this.mediaRecorder.stop();
206
+ }
207
+ }
208
+ this.mediaRecorder = undefined;
209
+ this.audioChunks = [];
210
+ this.logger.log('[VoiceService] discarded in-progress segment; VAD session unchanged');
211
+ }
212
+
213
+ /**
214
+ * 🔄 RESUME VAD AFTER RESPONSE
215
+ * Called when isTTSPlaying$ goes false while isWaitingForResponse is true,
216
+ * or by the safety timeout if no TTS response arrives within 30 s.
217
+ */
218
+ private resumeVadAfterResponse(): void {
219
+ this.isWaitingForResponse = false;
220
+ clearTimeout(this.responseTimeoutId);
221
+ this.responseTimeoutId = undefined;
222
+ this._isAcquisitionBlocked$.next(false);
223
+ if (this.vad) {
224
+ this.vad.start().catch((e) =>
225
+ this.logger.log('[VoiceService] VAD resume error', e),
226
+ );
227
+ }
228
+ }
229
+
230
+ /**
231
+ * ⏱️ SAFETY TIMEOUT
232
+ * Forces VAD re-enable after 30 s in case no TTS response ever arrives.
233
+ */
234
+ private setResponseSafetyTimeout(): void {
235
+ clearTimeout(this.responseTimeoutId);
236
+ this.responseTimeoutId = setTimeout(() => {
237
+ this.logger.log('[VoiceService] safety timeout: re-enabling VAD acquisition');
238
+ this.resumeVadAfterResponse();
239
+ }, 30_000);
135
240
  }
136
241
 
137
242
  /**
@@ -161,7 +266,9 @@ export class VoiceService {
161
266
  return;
162
267
  }
163
268
 
164
- this.analyser.getByteFrequencyData(this.dataArray);
269
+ this.analyser.getByteFrequencyData(
270
+ this.dataArray as Parameters<AnalyserNode['getByteFrequencyData']>[0],
271
+ );
165
272
 
166
273
  let sum = 0;
167
274
  for (let i = 0; i < this.dataArray.length; i++) {
@@ -255,6 +362,11 @@ export class VoiceService {
255
362
  * 📡 EMIT RESULT
256
363
  */
257
364
  private emitSegmentPayload(payload: VoiceSegmentPayload): void {
365
+ if (this.isTTSActive) {
366
+ this.logger.log('[VoiceService] segment suppressed — TTS is playing');
367
+ return;
368
+ }
369
+
258
370
  this.logger.log( '[VoiceService] segment ready', payload.transcript ?? payload.transcriptionError ?? payload.blob.size);
259
371
 
260
372
  this.audioSegmentSubject.next(payload);
@@ -38,6 +38,8 @@
38
38
  --chat-footer-logo-height: 30px;
39
39
  --chat-footer-close-button-height: 30px;
40
40
  --chat-footer-border-radius: 16px;
41
+ --chat-footer-stream-button-height: 0px; //96px;
42
+ --chat-footer-stream-button-padding: 10px 0;
41
43
  --chat-footer-background-color: #f6f7fb;
42
44
  --chat-footer-color: #1a1a1a;
43
45
  --chat-footer-border-color-error: #aa0404;
package/src/launch.js CHANGED
@@ -218,67 +218,76 @@ function loadIframe(tiledeskScriptBaseLocation) {
218
218
  iDiv.appendChild(ifrm);
219
219
 
220
220
  // Funzione helper per caricare iframe con fallback per compatibilità CSP (Wix, etc.)
221
- // Usa Blob URL come metodo principale (più compatibile con CSP) con fallback a srcdoc e document.write
222
- function loadIframeContent(iframe, htmlContent, baseLocation) {
223
- var isLocalhost = baseLocation.includes('localhost');
221
+ // Priorità: document.write / srcdoc prima della Blob URL. Le Blob URL spesso danno origine opaca
222
+ // (blob:null): l'iframe non può leggere window.parent.tiledeskSettings → projectid mancante.
223
+ function loadIframeContent(iframe, htmlContent) {
224
224
  var blobUrl = null;
225
-
226
- // Metodo 1: Blob URL (più compatibile con CSP di Wix e altre piattaforme)
227
- // Usa Blob URL come metodo principale perché è meno spesso bloccato da CSP rispetto a srcdoc
225
+
226
+ // 1) document.write: iframe stessa origine della pagina host tiledeskSettings sul parent accessibile
227
+ try {
228
+ var cw = iframe.contentWindow;
229
+ if (cw && cw.document) {
230
+ cw.document.open();
231
+ cw.document.write(htmlContent);
232
+ cw.document.close();
233
+ return;
234
+ }
235
+ } catch (e) {
236
+ console.warn('[Tiledesk] iframe document.write failed, trying srcdoc/blob:', e);
237
+ }
238
+
239
+ // 2) srcdoc: stessa origine del parent (HTML5); utile se document.write è bloccato
240
+ if ('srcdoc' in iframe) {
241
+ try {
242
+ iframe.srcdoc = htmlContent;
243
+ return;
244
+ } catch (e) {
245
+ console.warn('[Tiledesk] iframe srcdoc failed, trying blob:', e);
246
+ }
247
+ }
248
+
249
+ // 3) Blob URL (spesso permesso da CSP dove srcdoc/write no; può rompere lettura parent.tiledeskSettings)
228
250
  if (typeof Blob !== 'undefined' && typeof URL !== 'undefined' && URL.createObjectURL) {
229
251
  try {
230
252
  var blob = new Blob([htmlContent], { type: 'text/html;charset=utf-8' });
231
253
  blobUrl = URL.createObjectURL(blob);
232
254
  iframe.src = blobUrl;
233
-
234
- // Cleanup del blob URL dopo il caricamento per liberare memoria
255
+
235
256
  var originalOnload = iframe.onload;
236
257
  iframe.onload = function() {
237
- // Revoca il blob URL dopo un delay per assicurarsi che tutto sia caricato
238
258
  setTimeout(function() {
239
259
  if (blobUrl) {
240
260
  try {
241
261
  URL.revokeObjectURL(blobUrl);
242
262
  blobUrl = null;
243
- } catch(e) {
244
- console.warn('Error revoking blob URL:', e);
263
+ } catch (err) {
264
+ console.warn('Error revoking blob URL:', err);
245
265
  }
246
266
  }
247
267
  }, 1000);
248
268
  if (originalOnload) originalOnload.call(this);
249
269
  };
250
- return; // Blob URL impostato con successo
251
- } catch(e) {
252
- console.warn('Blob URL not available, trying srcdoc:', e);
270
+ return;
271
+ } catch (e) {
272
+ console.warn('Blob URL not available:', e);
253
273
  }
254
274
  }
255
-
256
- // Metodo 2: srcdoc (fallback se Blob URL non disponibile)
257
- // Skip per localhost (usa document.write per compatibilità sviluppo)
258
- if (!isLocalhost && 'srcdoc' in iframe) {
259
- try {
260
- iframe.srcdoc = htmlContent;
261
- return; // srcdoc impostato
262
- } catch(e) {
263
- console.warn('srcdoc not allowed, trying document.write:', e);
264
- }
265
- }
266
-
267
- // Metodo 3: document.write (fallback finale, funziona su localhost e browser vecchi)
268
- if (isLocalhost || (iframe.contentWindow && iframe.contentWindow.document)) {
275
+
276
+ // 4) Ultimo tentativo document.write (iframe magari non pronto al primo passo)
277
+ if (iframe.contentWindow && iframe.contentWindow.document) {
269
278
  try {
270
279
  iframe.contentWindow.document.open();
271
280
  iframe.contentWindow.document.write(htmlContent);
272
281
  iframe.contentWindow.document.close();
273
- return; // document.write completato
274
- } catch(e) {
275
- console.error('All iframe loading methods failed:', e);
282
+ return;
283
+ } catch (e) {
284
+ console.error('[Tiledesk] All iframe loading methods failed:', e);
276
285
  }
277
286
  }
278
287
  }
279
288
 
280
289
  // Carica il contenuto dell'iframe con fallback automatico
281
- loadIframeContent(ifrm, srcTileDesk, tiledeskScriptBaseLocation);
290
+ loadIframeContent(ifrm, srcTileDesk);
282
291
 
283
292
 
284
293
  }
@@ -219,67 +219,76 @@ function loadIframe(tiledeskScriptBaseLocation) {
219
219
  iDiv.appendChild(ifrm);
220
220
 
221
221
  // Funzione helper per caricare iframe con fallback per compatibilità CSP (Wix, etc.)
222
- // Usa Blob URL come metodo principale (più compatibile con CSP) con fallback a srcdoc e document.write
223
- function loadIframeContent(iframe, htmlContent, baseLocation) {
224
- var isLocalhost = baseLocation.includes('localhost');
222
+ // Priorità: document.write / srcdoc prima della Blob URL. Le Blob URL spesso danno origine opaca
223
+ // (blob:null): l'iframe non può leggere window.parent.tiledeskSettings → projectid mancante.
224
+ function loadIframeContent(iframe, htmlContent) {
225
225
  var blobUrl = null;
226
-
227
- // Metodo 1: Blob URL (più compatibile con CSP di Wix e altre piattaforme)
228
- // Usa Blob URL come metodo principale perché è meno spesso bloccato da CSP rispetto a srcdoc
226
+
227
+ // 1) document.write: iframe stessa origine della pagina host tiledeskSettings sul parent accessibile
228
+ try {
229
+ var cw = iframe.contentWindow;
230
+ if (cw && cw.document) {
231
+ cw.document.open();
232
+ cw.document.write(htmlContent);
233
+ cw.document.close();
234
+ return;
235
+ }
236
+ } catch (e) {
237
+ console.warn('[Tiledesk] iframe document.write failed, trying srcdoc/blob:', e);
238
+ }
239
+
240
+ // 2) srcdoc: stessa origine del parent (HTML5); utile se document.write è bloccato
241
+ if ('srcdoc' in iframe) {
242
+ try {
243
+ iframe.srcdoc = htmlContent;
244
+ return;
245
+ } catch (e) {
246
+ console.warn('[Tiledesk] iframe srcdoc failed, trying blob:', e);
247
+ }
248
+ }
249
+
250
+ // 3) Blob URL (spesso permesso da CSP dove srcdoc/write no; può rompere lettura parent.tiledeskSettings)
229
251
  if (typeof Blob !== 'undefined' && typeof URL !== 'undefined' && URL.createObjectURL) {
230
252
  try {
231
253
  var blob = new Blob([htmlContent], { type: 'text/html;charset=utf-8' });
232
254
  blobUrl = URL.createObjectURL(blob);
233
255
  iframe.src = blobUrl;
234
-
235
- // Cleanup del blob URL dopo il caricamento per liberare memoria
256
+
236
257
  var originalOnload = iframe.onload;
237
258
  iframe.onload = function() {
238
- // Revoca il blob URL dopo un delay per assicurarsi che tutto sia caricato
239
259
  setTimeout(function() {
240
260
  if (blobUrl) {
241
261
  try {
242
262
  URL.revokeObjectURL(blobUrl);
243
263
  blobUrl = null;
244
- } catch(e) {
245
- console.warn('Error revoking blob URL:', e);
264
+ } catch (err) {
265
+ console.warn('Error revoking blob URL:', err);
246
266
  }
247
267
  }
248
268
  }, 1000);
249
269
  if (originalOnload) originalOnload.call(this);
250
270
  };
251
- return; // Blob URL impostato con successo
252
- } catch(e) {
253
- console.warn('Blob URL not available, trying srcdoc:', e);
254
- }
255
- }
256
-
257
- // Metodo 2: srcdoc (fallback se Blob URL non disponibile)
258
- // Skip per localhost (usa document.write per compatibilità sviluppo)
259
- if (!isLocalhost && 'srcdoc' in iframe) {
260
- try {
261
- iframe.srcdoc = htmlContent;
262
- return; // srcdoc impostato
263
- } catch(e) {
264
- console.warn('srcdoc not allowed, trying document.write:', e);
271
+ return;
272
+ } catch (e) {
273
+ console.warn('Blob URL not available:', e);
265
274
  }
266
275
  }
267
-
268
- // Metodo 3: document.write (fallback finale, funziona su localhost e browser vecchi)
269
- if (isLocalhost || (iframe.contentWindow && iframe.contentWindow.document)) {
276
+
277
+ // 4) Ultimo tentativo document.write (iframe magari non pronto al primo passo)
278
+ if (iframe.contentWindow && iframe.contentWindow.document) {
270
279
  try {
271
280
  iframe.contentWindow.document.open();
272
281
  iframe.contentWindow.document.write(htmlContent);
273
282
  iframe.contentWindow.document.close();
274
- return; // document.write completato
275
- } catch(e) {
276
- console.error('All iframe loading methods failed:', e);
283
+ return;
284
+ } catch (e) {
285
+ console.error('[Tiledesk] All iframe loading methods failed:', e);
277
286
  }
278
287
  }
279
288
  }
280
289
 
281
290
  // Carica il contenuto dell'iframe con fallback automatico
282
- loadIframeContent(ifrm, srcTileDesk, tiledeskScriptBaseLocation);
291
+ loadIframeContent(ifrm, srcTileDesk);
283
292
 
284
293
 
285
294
  }
@@ -1,41 +0,0 @@
1
- # npm version patch
2
- version=`node -e 'console.log(require("./package.json").version)'`
3
- echo "version $version"
4
-
5
- npm i
6
-
7
- cp src/environments/real_data/environment.prod.ts src/environments/environment.prod.ts
8
-
9
- # --build-optimizer=false if localstorage is disabled (webview) appears https://github.com/firebase/angularfire/issues/970
10
- ng build --configuration="prod" --aot=true
11
- ##--base-href='./v5/' --output-hashing none
12
-
13
- ### SET HASHING : START ###
14
- cp ./src/launch_template.js ./dist/browser/launch.js
15
- node ./src/build_launch.js
16
- ### SET HASHING : END ###
17
-
18
- #### FIREBASE #####
19
- # cd dist
20
- # # aws s3 sync . s3://tiledesk-widget/v5/latest/
21
- # aws s3 sync . s3://tiledesk-widget/v5/$version/ --cache-control max-age=300
22
- # aws s3 sync . s3://tiledesk-widget/v5/ --cache-control max-age=300
23
- # cd ..
24
-
25
- # #### MQTT #####
26
- cd dist/browser
27
- # aws s3 sync . s3://tiledesk-widget/v5/latest/
28
- aws s3 sync . s3://tiledesk-widget/v6/$version/ --cache-control max-age=86400 --exclude='launch.js' #8days
29
- aws s3 sync . s3://tiledesk-widget/v6/$version/ --cache-control "no-store,no-cache,private" --exclude='*' --include='launch.js'
30
- aws s3 sync . s3://tiledesk-widget/v6/ --cache-control max-age=86400 --exclude='launch.js' #8days
31
- aws s3 sync . s3://tiledesk-widget/v6/ --cache-control "no-store,no-cache,private" --exclude='*' --include='launch.js'
32
- cd ../..
33
-
34
- aws cloudfront create-invalidation --distribution-id E3EJDWEHY08CZZ --paths "/*"
35
-
36
- git restore src/environments/environment.prod.ts
37
-
38
- echo new version deployed $version on s3://tiledesk-widget/v6
39
- echo available on https://s3.eu-west-1.amazonaws.com/tiledesk-widget/v6/index.html
40
- echo https://widget.tiledesk.com/v6/index.html
41
- echo https://widget.tiledesk.com/v6/$version/index.html