@chat21/chat21-web-widget 5.1.34-rc1 → 5.1.34

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 (193) hide show
  1. package/.github/workflows/docker-community-push-latest.yml +13 -23
  2. package/.github/workflows/docker-image-tag-community-tag-push.yml +12 -22
  3. package/CHANGELOG.md +8 -129
  4. package/Dockerfile +5 -4
  5. package/angular.json +3 -21
  6. package/docs/changelog/this-branch.md +0 -36
  7. package/env.sample +2 -3
  8. package/nginx.conf +2 -22
  9. package/package.json +3 -10
  10. package/src/app/app.component.html +2 -2
  11. package/src/app/app.component.scss +14 -25
  12. package/src/app/app.component.spec.ts +6 -21
  13. package/src/app/app.component.ts +9 -10
  14. package/src/app/app.module.ts +0 -13
  15. package/src/app/component/conversation-detail/conversation/conversation.component.html +11 -25
  16. package/src/app/component/conversation-detail/conversation/conversation.component.scss +2 -40
  17. package/src/app/component/conversation-detail/conversation/conversation.component.spec.ts +75 -644
  18. package/src/app/component/conversation-detail/conversation/conversation.component.ts +14 -100
  19. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.html +13 -25
  20. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.spec.ts +5 -123
  21. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.ts +0 -1
  22. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.html +10 -23
  23. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.scss +1 -19
  24. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.spec.ts +149 -242
  25. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.ts +5 -8
  26. package/src/app/component/conversation-detail/conversation-emojii/conversation-emojii.component.spec.ts +3 -53
  27. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.html +96 -200
  28. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss +6 -211
  29. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.spec.ts +78 -452
  30. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts +76 -291
  31. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.html +53 -113
  32. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.scss +4 -12
  33. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.spec.ts +29 -274
  34. package/src/app/component/conversation-detail/conversation-internal-frame/conversation-internal-frame.component.html +9 -23
  35. package/src/app/component/conversation-detail/conversation-internal-frame/conversation-internal-frame.component.spec.ts +8 -80
  36. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.html +23 -29
  37. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.spec.ts +16 -185
  38. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.ts +14 -34
  39. package/src/app/component/error-alert/error-alert.component.spec.ts +5 -65
  40. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.html +7 -16
  41. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.scss +0 -21
  42. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.spec.ts +7 -89
  43. package/src/app/component/form/form-builder/form-builder.component.html +1 -1
  44. package/src/app/component/form/form-builder/form-builder.component.spec.ts +21 -163
  45. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.html +4 -8
  46. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.scss +5 -10
  47. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.spec.ts +16 -90
  48. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.ts +0 -26
  49. package/src/app/component/form/inputs/form-label/form-label.component.spec.ts +11 -45
  50. package/src/app/component/form/inputs/form-radio-button/form-radio-button.component.spec.ts +6 -24
  51. package/src/app/component/form/inputs/form-select/form-select.component.spec.ts +5 -14
  52. package/src/app/component/form/inputs/form-text/form-text.component.html +12 -14
  53. package/src/app/component/form/inputs/form-text/form-text.component.scss +1 -11
  54. package/src/app/component/form/inputs/form-text/form-text.component.spec.ts +17 -113
  55. package/src/app/component/form/inputs/form-text/form-text.component.ts +3 -35
  56. package/src/app/component/form/inputs/form-textarea/form-textarea.component.html +11 -13
  57. package/src/app/component/form/inputs/form-textarea/form-textarea.component.scss +5 -6
  58. package/src/app/component/form/inputs/form-textarea/form-textarea.component.spec.ts +13 -149
  59. package/src/app/component/form/inputs/form-textarea/form-textarea.component.ts +0 -26
  60. package/src/app/component/form/prechat-form/prechat-form.component.html +11 -14
  61. package/src/app/component/form/prechat-form/prechat-form.component.spec.ts +10 -102
  62. package/src/app/component/form/prechat-form/prechat-form.component.ts +1 -8
  63. package/src/app/component/home/home.component.html +31 -38
  64. package/src/app/component/home/home.component.scss +2 -4
  65. package/src/app/component/home/home.component.spec.ts +11 -226
  66. package/src/app/component/home-conversations/home-conversations.component.html +26 -30
  67. package/src/app/component/home-conversations/home-conversations.component.scss +0 -3
  68. package/src/app/component/home-conversations/home-conversations.component.spec.ts +36 -212
  69. package/src/app/component/last-message/last-message.component.html +9 -15
  70. package/src/app/component/last-message/last-message.component.scss +2 -16
  71. package/src/app/component/last-message/last-message.component.spec.ts +23 -204
  72. package/src/app/component/last-message/last-message.component.ts +1 -4
  73. package/src/app/component/launcher-button/launcher-button.component.html +13 -8
  74. package/src/app/component/launcher-button/launcher-button.component.spec.ts +8 -104
  75. package/src/app/component/list-all-conversations/list-all-conversations.component.html +17 -12
  76. package/src/app/component/list-all-conversations/list-all-conversations.component.scss +0 -2
  77. package/src/app/component/list-conversations/list-conversations.component.html +22 -22
  78. package/src/app/component/menu-options/menu-options.component.html +20 -30
  79. package/src/app/component/menu-options/menu-options.component.spec.ts +9 -125
  80. package/src/app/component/message/audio/audio.component.html +15 -13
  81. package/src/app/component/message/audio/audio.component.spec.ts +5 -140
  82. package/src/app/component/message/audio/audio.component.ts +5 -1
  83. package/src/app/component/message/avatar/avatar.component.html +2 -2
  84. package/src/app/component/message/avatar/avatar.component.spec.ts +7 -99
  85. package/src/app/component/message/bubble-message/bubble-message.component.html +51 -38
  86. package/src/app/component/message/bubble-message/bubble-message.component.scss +1 -54
  87. package/src/app/component/message/bubble-message/bubble-message.component.spec.ts +57 -154
  88. package/src/app/component/message/bubble-message/bubble-message.component.ts +11 -89
  89. package/src/app/component/message/buttons/action-button/action-button.component.html +4 -3
  90. package/src/app/component/message/buttons/action-button/action-button.component.spec.ts +5 -49
  91. package/src/app/component/message/buttons/link-button/link-button.component.scss +8 -5
  92. package/src/app/component/message/buttons/link-button/link-button.component.spec.ts +5 -50
  93. package/src/app/component/message/buttons/text-button/text-button.component.spec.ts +5 -44
  94. package/src/app/component/message/carousel/carousel.component.html +16 -29
  95. package/src/app/component/message/carousel/carousel.component.scss +8 -20
  96. package/src/app/component/message/carousel/carousel.component.spec.ts +3 -80
  97. package/src/app/component/message/carousel/carousel.component.ts +0 -16
  98. package/src/app/component/message/frame/frame.component.html +4 -9
  99. package/src/app/component/message/frame/frame.component.spec.ts +15 -34
  100. package/src/app/component/message/frame/frame.component.ts +2 -7
  101. package/src/app/component/message/html/html.component.html +1 -1
  102. package/src/app/component/message/html/html.component.scss +1 -1
  103. package/src/app/component/message/html/html.component.spec.ts +7 -24
  104. package/src/app/component/message/image/image.component.html +10 -12
  105. package/src/app/component/message/image/image.component.scss +0 -16
  106. package/src/app/component/message/image/image.component.spec.ts +15 -101
  107. package/src/app/component/message/image/image.component.ts +51 -90
  108. package/src/app/component/message/info-message/info-message.component.spec.ts +14 -26
  109. package/src/app/component/message/like-unlike/like-unlike.component.html +9 -7
  110. package/src/app/component/message/like-unlike/like-unlike.component.spec.ts +3 -31
  111. package/src/app/component/message/return-receipt/return-receipt.component.spec.ts +17 -38
  112. package/src/app/component/message/text/text.component.html +3 -3
  113. package/src/app/component/message/text/text.component.scss +86 -80
  114. package/src/app/component/message/text/text.component.spec.ts +13 -106
  115. package/src/app/component/message-attachment/message-attachment.component.spec.ts +13 -134
  116. package/src/app/component/selection-department/selection-department.component.html +23 -21
  117. package/src/app/component/selection-department/selection-department.component.spec.ts +14 -159
  118. package/src/app/component/selection-department/selection-department.component.ts +1 -8
  119. package/src/app/component/send-button/send-button.component.html +13 -5
  120. package/src/app/component/send-button/send-button.component.spec.ts +2 -2
  121. package/src/app/component/star-rating-widget/star-rating-widget.component.html +81 -51
  122. package/src/app/directives/tooltip.directive.spec.ts +4 -8
  123. package/src/app/modals/confirm-close/confirm-close.component.html +8 -20
  124. package/src/app/modals/confirm-close/confirm-close.component.scss +0 -3
  125. package/src/app/modals/confirm-close/confirm-close.component.spec.ts +4 -13
  126. package/src/app/modals/confirm-close/confirm-close.component.ts +1 -8
  127. package/src/app/pipe/html-entites-encode.pipe.spec.ts +2 -35
  128. package/src/app/pipe/marked.pipe.spec.ts +2 -38
  129. package/src/app/pipe/marked.pipe.ts +41 -51
  130. package/src/app/providers/app-config.service.ts +2 -4
  131. package/src/app/providers/brand.service.spec.ts +2 -23
  132. package/src/app/providers/brand.service.ts +1 -1
  133. package/src/app/providers/global-settings.service.spec.ts +14 -1009
  134. package/src/app/providers/global-settings.service.ts +2 -82
  135. package/src/app/providers/translator.service.ts +6 -26
  136. package/src/app/sass/_variables.scss +0 -3
  137. package/src/app/sass/animations.scss +1 -19
  138. package/src/app/utils/globals.ts +1 -21
  139. package/src/app/utils/utils-resources.ts +1 -1
  140. package/src/assets/i18n/en.json +99 -106
  141. package/src/assets/i18n/es.json +100 -107
  142. package/src/assets/i18n/fr.json +100 -107
  143. package/src/assets/i18n/it.json +98 -107
  144. package/src/assets/twp/index-dev.html +0 -18
  145. package/src/chat21-core/models/message.ts +1 -2
  146. package/src/chat21-core/providers/firebase/firebase-conversation-handler.ts +2 -3
  147. package/src/chat21-core/providers/mqtt/mqtt-conversation-handler.ts +0 -12
  148. package/src/chat21-core/providers/scripts/script.service.spec.ts +2 -12
  149. package/src/chat21-core/providers/tiledesk/tiledesk-requests.service.ts +1 -1
  150. package/src/chat21-core/utils/utils-message.ts +0 -7
  151. package/src/chat21-core/utils/utils.ts +2 -5
  152. package/src/widget-config-template.json +1 -4
  153. package/src/widget-config.json +1 -4
  154. package/tsconfig.json +0 -5
  155. package/.angular-mcp-cache/package.json +0 -1
  156. package/.cursor/angular18-accessibility-auditor-skill.md +0 -442
  157. package/.cursor/mcp.json +0 -15
  158. package/.github/workflows/build.yml +0 -22
  159. package/.github/workflows/playwright.yml +0 -27
  160. package/mocks/voice-websocket-mock/server.cjs +0 -245
  161. package/playwright.config.ts +0 -41
  162. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.html +0 -46
  163. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.scss +0 -83
  164. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.ts +0 -192
  165. package/src/app/component/form/prechat-form-test-mock.ts +0 -35
  166. package/src/app/component/message/audio-sync/audio-sync.component.html +0 -18
  167. package/src/app/component/message/audio-sync/audio-sync.component.scss +0 -65
  168. package/src/app/component/message/audio-sync/audio-sync.component.spec.ts +0 -103
  169. package/src/app/component/message/audio-sync/audio-sync.component.ts +0 -643
  170. package/src/app/providers/tts-audio-playback-coordinator.service.spec.ts +0 -117
  171. package/src/app/providers/tts-audio-playback-coordinator.service.ts +0 -109
  172. package/src/app/providers/voice/STT&TTS/openai-voice.config.ts +0 -12
  173. package/src/app/providers/voice/STT&TTS/openai-voice.provider.ts +0 -171
  174. package/src/app/providers/voice/STT&TTS/speech-provider.abstract.ts +0 -39
  175. package/src/app/providers/voice/audio.types.ts +0 -40
  176. package/src/app/providers/voice/vad.service.spec.ts +0 -28
  177. package/src/app/providers/voice/vad.service.ts +0 -70
  178. package/src/app/providers/voice/voice-streaming.service.spec.ts +0 -23
  179. package/src/app/providers/voice/voice-streaming.service.ts +0 -702
  180. package/src/app/providers/voice/voice-streaming.types.ts +0 -112
  181. package/src/app/providers/voice/voice.service.spec.ts +0 -227
  182. package/src/app/providers/voice/voice.service.ts +0 -973
  183. package/src/app/shims/onnxruntime-web-wasm.ts +0 -4
  184. package/src/assets/onnx/ort-wasm-simd-threaded.mjs +0 -59
  185. package/src/assets/onnx/ort-wasm-simd-threaded.wasm +0 -0
  186. package/src/assets/sounds/keyboard.mp3 +0 -0
  187. package/src/assets/twp/tiledesk_widget_files/widget-css-override-example.css +0 -14
  188. package/src/assets/vad/silero_vad_legacy.onnx +0 -0
  189. package/src/assets/vad/vad.worklet.bundle.min.js +0 -1
  190. package/src/chat21-core/providers/chat-manager.spec.ts +0 -72
  191. package/tests/widget-form-rich.spec.ts +0 -67
  192. package/tests/widget-index-dev-settings.spec.ts +0 -52
  193. package/tests/widget-twp-iframe.spec.ts +0 -39
@@ -1,643 +0,0 @@
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 { VoiceService } from 'src/app/providers/voice/voice.service';
16
- import { Globals } from 'src/app/utils/globals';
17
-
18
- /** HAVE_METADATA: metadati già disponibili (tipico audio servito da cache). */
19
- const HAVE_METADATA = 1;
20
- const BROWSER_TTS_OUTPUT_FORMAT = 'mp3_44100_128';
21
-
22
- @Component({
23
- selector: 'chat-audio-sync',
24
- templateUrl: './audio-sync.component.html',
25
- styleUrl: './audio-sync.component.scss',
26
- })
27
- export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
28
- @Input() message: MessageModel | null = null;
29
- @Input() color?: string;
30
-
31
- @ViewChild('audioPlayer') audioRef!: ElementRef<HTMLAudioElement>;
32
- @ViewChild('transcriptBox') transcriptBox!: ElementRef<HTMLElement>;
33
-
34
- words: {
35
- text: string;
36
- start: number;
37
- end: number;
38
- state: 'future' | 'active' | 'past';
39
- }[] = [];
40
-
41
- currentTime = 0;
42
- duration = 1;
43
- activeIndex = -1;
44
-
45
- private timingReady = false;
46
- private onMetadataLoaded: () => void;
47
- private onPlaybackEnded: () => void;
48
-
49
- /** Id univoco per il coordinatore (di solito `message.uid`). */
50
- private playbackOwnerId = '';
51
- private destroyed = false;
52
- private playbackRequested = false;
53
- private playbackStarted = false;
54
- private streamAbort?: AbortController;
55
- private mediaSourceObjectUrl?: string;
56
- private stopAllSub?: Subscription;
57
- private preemptSub?: Subscription;
58
-
59
- constructor(
60
- private readonly cdr: ChangeDetectorRef,
61
- private readonly ttsPlayback: TtsAudioPlaybackCoordinator,
62
- private readonly globals: Globals,
63
- private readonly voiceService: VoiceService,
64
- ) {}
65
-
66
- /** `false` = messaggio già in storico: niente autoplay / karaoke. Da `message.isJustRecived`. */
67
- private get skipSyncAnimation(): boolean {
68
- return this.message?.isJustRecived === false;
69
- }
70
-
71
- ngOnChanges(changes: SimpleChanges): void {
72
- if (!changes['message']) {
73
- return;
74
- }
75
- if (this.audioRef?.nativeElement && this.timingReady) {
76
- const d = this.audioRef.nativeElement.duration;
77
- if (Number.isFinite(d) && d > 0) {
78
- this.duration = d;
79
- }
80
- this.buildFakeTiming();
81
- if (this.skipSyncAnimation) {
82
- this.markAllWordsPast();
83
- } else if (this.playbackStarted) {
84
- this.syncStatesFromCurrentTime();
85
- }
86
- }
87
- }
88
-
89
- ngAfterViewInit(): void {
90
- const audio = this.audioRef.nativeElement;
91
-
92
- this.playbackOwnerId =
93
- (this.message?.uid && String(this.message.uid).trim()) ||
94
- `tts-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
95
-
96
- this.onPlaybackEnded = () => {
97
- this.playbackStarted = false;
98
- this.cleanupStreaming();
99
- this.ttsPlayback.releaseIfCurrent(this.playbackOwnerId);
100
- if (this.skipSyncAnimation) {
101
- return;
102
- }
103
- this.markAllWordsPast();
104
- if (this.message) {
105
- this.message.isJustRecived = false;
106
- }
107
- this.cdr.detectChanges();
108
- };
109
-
110
- this.onMetadataLoaded = () => {
111
- // La durata potrebbe arrivare tardi (specie con streaming).
112
- const d = audio.duration;
113
- if (Number.isFinite(d) && d > 0) {
114
- this.duration = d;
115
- } else if (!this.timingReady) {
116
- this.duration = this.estimateDurationSecondsFromText();
117
- }
118
-
119
- this.timingReady = true;
120
- this.buildFakeTiming();
121
- if (this.skipSyncAnimation) {
122
- this.markAllWordsPast();
123
- this.cdr.detectChanges();
124
- return;
125
- }
126
- if (this.playbackStarted) {
127
- this.syncStatesFromCurrentTime();
128
- }
129
- this.cdr.detectChanges();
130
- };
131
-
132
- audio.addEventListener('loadedmetadata', this.onMetadataLoaded);
133
- audio.addEventListener('ended', this.onPlaybackEnded);
134
-
135
- // Prepara subito le parole (durata stimata) e poi aggiorna quando arriva la metadata reale.
136
- this.duration = this.estimateDurationSecondsFromText();
137
- this.timingReady = true;
138
- this.buildFakeTiming();
139
- if (this.skipSyncAnimation) {
140
- this.markAllWordsPast();
141
- this.cdr.detectChanges();
142
- return;
143
- }
144
- this.cdr.detectChanges();
145
-
146
- setTimeout(() => {
147
- if (this.playbackRequested || this.destroyed) {
148
- return;
149
- }
150
- this.playbackRequested = true;
151
- this.ttsPlayback.requestStart(this.playbackOwnerId, () => {
152
- if (this.destroyed) {
153
- this.ttsPlayback.releaseIfCurrent(this.playbackOwnerId);
154
- return;
155
- }
156
- this.playbackStarted = true;
157
- this.syncStatesFromCurrentTime();
158
- this.cdr.detectChanges();
159
- this.startPlayback(audio);
160
- });
161
- }, 200);
162
-
163
- // Stop signal: user pressed X while this TTS was playing or queued.
164
- this.stopAllSub = this.ttsPlayback.stopAllPlayback$.subscribe(() => {
165
- if (!this.playbackRequested && !this.playbackStarted) {
166
- return;
167
- }
168
- this.destroyed = true;
169
- this.playbackStarted = false;
170
- this.cleanupStreaming();
171
- try {
172
- audio.pause();
173
- audio.currentTime = 0;
174
- } catch {
175
- /* ignore */
176
- }
177
- this.markAllWordsPast();
178
- if (this.message) {
179
- this.message.isJustRecived = false;
180
- }
181
- this.cdr.detectChanges();
182
- });
183
-
184
- // Preempt signal: a newer message requested start while this one was playing.
185
- // Only react when the emitted id matches this component's own ownerId.
186
- this.preemptSub = this.ttsPlayback.preemptPlayback$.subscribe((stoppedId) => {
187
- if (stoppedId !== this.playbackOwnerId) {
188
- return;
189
- }
190
- this.playbackStarted = false;
191
- this.cleanupStreaming();
192
- try {
193
- audio.pause();
194
- audio.currentTime = 0;
195
- } catch {
196
- /* ignore */
197
- }
198
- this.markAllWordsPast();
199
- if (this.message) {
200
- this.message.isJustRecived = false;
201
- }
202
- this.cdr.detectChanges();
203
- // No releaseIfCurrent call — the coordinator already cleared currentOwnerId before emitting.
204
- });
205
- }
206
-
207
- ngOnDestroy(): void {
208
- this.destroyed = true;
209
- this.playbackStarted = false;
210
- this.cleanupStreaming();
211
- this.stopAllSub?.unsubscribe();
212
- this.stopAllSub = undefined;
213
- this.preemptSub?.unsubscribe();
214
- this.preemptSub = undefined;
215
-
216
- const audio = this.audioRef?.nativeElement;
217
- if (audio) {
218
- try {
219
- audio.pause();
220
- audio.currentTime = 0;
221
- } catch {
222
- /* ignore */
223
- }
224
- }
225
- this.ttsPlayback.release(this.playbackOwnerId);
226
-
227
- if (!audio) {
228
- return;
229
- }
230
- if (this.onMetadataLoaded) {
231
- audio.removeEventListener('loadedmetadata', this.onMetadataLoaded);
232
- }
233
- if (this.onPlaybackEnded) {
234
- audio.removeEventListener('ended', this.onPlaybackEnded);
235
- }
236
- }
237
-
238
- private startPlayback(audio: HTMLAudioElement): void {
239
- const messageSrc = (this.message as any)?.metadata?.src as string | undefined;
240
-
241
- if (this.message?.type === 'tts') {
242
- const streamEndpoint = this.voiceService.proxyTtsStreamUrl;
243
- const fullFileEndpoint = this.voiceService.proxyTtsUrl;
244
- if (streamEndpoint) {
245
- this.startStreamingFromEndpoint(audio, streamEndpoint, fullFileEndpoint, messageSrc);
246
- return;
247
- }
248
- if (fullFileEndpoint) {
249
- this.fetchFullFileFromEndpoint(audio, fullFileEndpoint);
250
- return;
251
- }
252
- if (messageSrc) {
253
- this.playDirectUrl(audio, messageSrc);
254
- return;
255
- }
256
- this.handlePlaybackError();
257
- return;
258
- }
259
-
260
- if (!messageSrc) {
261
- this.playbackStarted = false;
262
- this.ttsPlayback.releaseIfCurrent(this.playbackOwnerId);
263
- this.markAllWordsPast();
264
- if (this.message) {
265
- this.message.isJustRecived = false;
266
- }
267
- this.cdr.detectChanges();
268
- return;
269
- }
270
-
271
- this.playDirectUrl(audio, messageSrc);
272
- }
273
-
274
- private playDirectUrl(audio: HTMLAudioElement, src: string): void {
275
- audio.src = src;
276
- try {
277
- audio.currentTime = 0;
278
- } catch {
279
- /* ignore */
280
- }
281
- audio.play().catch(() => this.handlePlaybackError());
282
- }
283
-
284
- private startStreamingFromEndpoint(
285
- audio: HTMLAudioElement,
286
- endpoint: string,
287
- fullFileEndpoint?: string | null,
288
- directFallbackSrc?: string,
289
- ): void {
290
- this.cleanupStreaming();
291
-
292
- const jwt = this.getJwtToken();
293
- const voiceSettings = this.getVoiceSettingsBody();
294
- const requestBody = this.buildTtsRequestBody(voiceSettings);
295
- let fallbackUsed = false;
296
- const fallback = () => {
297
- if (fallbackUsed) {
298
- this.handlePlaybackError();
299
- return;
300
- }
301
- fallbackUsed = true;
302
- this.cleanupStreaming();
303
- if (fullFileEndpoint) {
304
- this.fetchFullFileFromEndpoint(audio, fullFileEndpoint);
305
- return;
306
- }
307
- if (directFallbackSrc) {
308
- this.playDirectUrl(audio, directFallbackSrc);
309
- return;
310
- }
311
- this.handlePlaybackError();
312
- };
313
-
314
- // <audio src="..."> non può inviare header/body: serve fetch().
315
- const hasMse = typeof (window as any).MediaSource !== 'undefined';
316
- if (!hasMse) {
317
- fallback();
318
- return;
319
- }
320
-
321
- const MediaSourceCtor = (window as any).MediaSource as typeof MediaSource;
322
- const mediaSource = new MediaSourceCtor();
323
- const objectUrl = URL.createObjectURL(mediaSource);
324
- this.mediaSourceObjectUrl = objectUrl;
325
- audio.src = objectUrl;
326
-
327
- const abort = new AbortController();
328
- this.streamAbort = abort;
329
-
330
- const onSourceOpen = async () => {
331
- mediaSource.removeEventListener('sourceopen', onSourceOpen);
332
- try {
333
- const headers: Record<string, string> = {
334
- 'Content-Type': 'application/json',
335
- 'Authorization': `${jwt}`
336
- };
337
-
338
- const response = await fetch(endpoint, {
339
- method: 'POST',
340
- headers,
341
- body: JSON.stringify(requestBody),
342
- signal: abort.signal,
343
- });
344
- if (!response.ok || !response.body) {
345
- throw new Error(`TTS stream request failed (${response.status})`);
346
- }
347
-
348
- const headerType = (response.headers.get('content-type') || '').split(';')[0].trim();
349
- if (headerType && !MediaSourceCtor.isTypeSupported(headerType)) {
350
- // Fallback: fetch completo e play via blob (no streaming).
351
- fallback();
352
- return;
353
- }
354
-
355
- const mime = headerType || 'audio/mpeg';
356
- if (!MediaSourceCtor.isTypeSupported(mime)) {
357
- fallback();
358
- return;
359
- }
360
-
361
- const sourceBuffer = mediaSource.addSourceBuffer(mime);
362
- sourceBuffer.mode = 'sequence';
363
-
364
- const reader = response.body.getReader();
365
- const queue: Uint8Array[] = [];
366
- let doneReading = false;
367
- let started = false;
368
-
369
- const tryEndOfStream = () => {
370
- if (doneReading && queue.length === 0 && !sourceBuffer.updating) {
371
- try {
372
- mediaSource.endOfStream();
373
- } catch {
374
- /* ignore */
375
- }
376
- }
377
- };
378
-
379
- const pump = () => {
380
- if (abort.signal.aborted) {
381
- return;
382
- }
383
- if (sourceBuffer.updating) {
384
- return;
385
- }
386
- const chunk = queue.shift();
387
- if (!chunk) {
388
- tryEndOfStream();
389
- return;
390
- }
391
- try {
392
- const ab = chunk.buffer.slice(
393
- chunk.byteOffset,
394
- chunk.byteOffset + chunk.byteLength,
395
- ) as ArrayBuffer;
396
- sourceBuffer.appendBuffer(ab);
397
- } catch {
398
- fallback();
399
- }
400
- };
401
-
402
- sourceBuffer.addEventListener('updateend', () => {
403
- if (!started && this.playbackStarted && !this.destroyed) {
404
- started = true;
405
- audio.play().catch(() => fallback());
406
- }
407
- pump();
408
- });
409
-
410
- // Primo pump (se arrivano subito chunk)
411
- pump();
412
-
413
- while (!abort.signal.aborted) {
414
- const { value, done } = await reader.read();
415
- if (done) {
416
- doneReading = true;
417
- break;
418
- }
419
- if (value && value.byteLength > 0) {
420
- queue.push(value);
421
- pump();
422
- }
423
- }
424
-
425
- doneReading = true;
426
- tryEndOfStream();
427
- } catch {
428
- if (!abort.signal.aborted) {
429
- fallback();
430
- }
431
- }
432
- };
433
-
434
- mediaSource.addEventListener('sourceopen', onSourceOpen);
435
- }
436
-
437
- private handlePlaybackError(): void {
438
- this.playbackStarted = false;
439
- this.cleanupStreaming();
440
- this.ttsPlayback.releaseIfCurrent(this.playbackOwnerId);
441
- this.markAllWordsPast();
442
- if (this.message) {
443
- this.message.isJustRecived = false;
444
- }
445
- this.cdr.detectChanges();
446
- }
447
-
448
- private cleanupStreaming(): void {
449
- try {
450
- this.streamAbort?.abort();
451
- } catch {
452
- /* ignore */
453
- }
454
- this.streamAbort = undefined;
455
-
456
- if (this.mediaSourceObjectUrl) {
457
- try {
458
- URL.revokeObjectURL(this.mediaSourceObjectUrl);
459
- } catch {
460
- /* ignore */
461
- }
462
- this.mediaSourceObjectUrl = undefined;
463
- }
464
- }
465
-
466
- private getJwtToken(): string | null {
467
- const token = (this.globals?.tiledeskToken || this.globals?.jwt || '').trim();
468
- return token.length > 0 ? token : null;
469
- }
470
-
471
- private getVoiceSettingsBody(): unknown {
472
- const raw = (this.message as any)?.metadata?.voiceSettings;
473
- if (raw === null || raw === undefined) {
474
- return {};
475
- }
476
- if (typeof raw === 'string') {
477
- const s = raw.trim();
478
- if (!s) return {};
479
- try {
480
- return JSON.parse(s);
481
- } catch {
482
- // se non è JSON valido, invialo come stringa (il backend può gestirlo)
483
- return { voiceSettings: raw };
484
- }
485
- }
486
- return raw;
487
- }
488
-
489
- private async fetchAsBlobAndPlay(
490
- audio: HTMLAudioElement,
491
- endpoint: string,
492
- jwt: string | null,
493
- requestBody: unknown,
494
- ): Promise<void> {
495
- try {
496
- const headers: Record<string, string> = {
497
- 'Content-Type': 'application/json',
498
- 'Authorization': `${jwt}`
499
- };
500
-
501
- const response = await fetch(endpoint, {
502
- method: 'POST',
503
- headers,
504
- body: JSON.stringify(requestBody ?? {}),
505
- signal: this.streamAbort?.signal,
506
- });
507
-
508
- if (!response.ok) {
509
- throw new Error(`TTS request failed (${response.status})`);
510
- }
511
-
512
- const blob = await response.blob();
513
- if (this.destroyed) {
514
- return;
515
- }
516
-
517
- const objectUrl = URL.createObjectURL(blob);
518
- this.mediaSourceObjectUrl = objectUrl;
519
- audio.src = objectUrl;
520
- audio.play().catch(() => this.handlePlaybackError());
521
- } catch {
522
- this.handlePlaybackError();
523
- }
524
- }
525
-
526
- private fetchFullFileFromEndpoint(audio: HTMLAudioElement, endpoint: string): void {
527
- const jwt = this.getJwtToken();
528
- const voiceSettings = this.getVoiceSettingsBody();
529
- const requestBody = this.buildTtsRequestBody(voiceSettings, false);
530
- void this.fetchAsBlobAndPlay(audio, endpoint, jwt, requestBody);
531
- }
532
-
533
- private buildTtsRequestBody(voiceSettings: unknown, streaming = true): unknown {
534
- const text = this.message?.text ?? '';
535
- if (
536
- voiceSettings &&
537
- typeof voiceSettings === 'object' &&
538
- !Array.isArray(voiceSettings)
539
- ) {
540
- return {
541
- outputFormat: BROWSER_TTS_OUTPUT_FORMAT,
542
- ...(voiceSettings as Record<string, unknown>),
543
- text,
544
- streaming,
545
- };
546
- }
547
- return {
548
- voiceSettings,
549
- text,
550
- streaming,
551
- outputFormat: BROWSER_TTS_OUTPUT_FORMAT,
552
- };
553
- }
554
-
555
- private markAllWordsPast(): void {
556
- this.words.forEach((w) => {
557
- w.state = 'past';
558
- });
559
- this.activeIndex = -1;
560
- }
561
-
562
- private estimateDurationSecondsFromText(): number {
563
- const rawWords = (this.message?.text || '')
564
- .trim()
565
- .split(/\s+/)
566
- .filter((w) => w.length > 0);
567
- if (rawWords.length === 0) {
568
- return 1;
569
- }
570
- // ~140 WPM → ~0.43s/word
571
- return Math.max(1, rawWords.length * 0.43);
572
- }
573
-
574
- buildFakeTiming(): void {
575
- const rawWords = (this.message?.text || '')
576
- .trim()
577
- .split(/\s+/)
578
- .filter((w) => w.length > 0);
579
- if (rawWords.length === 0) {
580
- this.words = [];
581
- return;
582
- }
583
- const step = this.duration / rawWords.length;
584
-
585
- this.words = rawWords.map((w, i) => ({
586
- text: w,
587
- start: i * step,
588
- end: (i + 1) * step,
589
- state: 'future' as const,
590
- }));
591
- }
592
-
593
- syncStatesFromCurrentTime(): void {
594
- if (this.skipSyncAnimation) {
595
- return;
596
- }
597
- const audio = this.audioRef?.nativeElement;
598
- if (!audio || this.words.length === 0) {
599
- return;
600
- }
601
- this.currentTime = audio.currentTime;
602
- let newActiveIndex = -1;
603
-
604
- this.words.forEach((w, i) => {
605
- if (this.currentTime >= w.end) {
606
- w.state = 'past';
607
- } else if (this.currentTime >= w.start && this.currentTime < w.end) {
608
- w.state = 'active';
609
- newActiveIndex = i;
610
- } else {
611
- w.state = 'future';
612
- }
613
- });
614
-
615
- if (newActiveIndex !== this.activeIndex) {
616
- this.activeIndex = newActiveIndex;
617
- this.scrollToActive();
618
- }
619
- }
620
-
621
- onTimeUpdate(): void {
622
- if (!this.playbackStarted) {
623
- return;
624
- }
625
- this.syncStatesFromCurrentTime();
626
- }
627
-
628
- scrollToActive(): void {
629
- const container = this.transcriptBox?.nativeElement;
630
- const active = container?.querySelector('.active') as HTMLElement;
631
-
632
- if (active) {
633
- active.scrollIntoView({
634
- behavior: 'smooth',
635
- block: 'center',
636
- });
637
- }
638
- }
639
-
640
- trackByIndex(index: number): number {
641
- return index;
642
- }
643
- }