@chat21/chat21-web-widget 5.1.32-rc9 → 5.1.33-rc8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.angular-mcp-cache/package.json +1 -0
- package/.cursor/angular18-accessibility-auditor-skill.md +442 -0
- package/.cursor/mcp.json +15 -0
- package/.github/workflows/playwright.yml +27 -0
- package/.playwright-mcp/console-2026-05-08T15-31-09-000Z.log +17 -0
- package/.playwright-mcp/console-2026-05-08T15-32-19-412Z.log +89 -0
- package/.playwright-mcp/console-2026-05-08T16-18-48-424Z.log +133 -0
- package/.playwright-mcp/console-2026-05-11T12-54-06-869Z.log +13 -0
- package/.playwright-mcp/console-2026-05-11T12-54-56-229Z.log +147 -0
- package/.playwright-mcp/console-2026-05-11T12-55-47-174Z.log +183 -0
- package/.playwright-mcp/console-2026-05-11T15-34-03-590Z.log +210 -0
- package/.playwright-mcp/console-2026-05-12T15-07-31-880Z.log +118 -0
- package/.playwright-mcp/page-2026-05-08T15-32-19-900Z.yml +851 -0
- package/.playwright-mcp/page-2026-05-08T15-32-47-264Z.yml +857 -0
- package/.playwright-mcp/page-2026-05-08T15-33-17-089Z.yml +1110 -0
- package/.playwright-mcp/page-2026-05-08T15-33-23-486Z.yml +1069 -0
- package/.playwright-mcp/page-2026-05-08T15-33-45-390Z.yml +1076 -0
- package/.playwright-mcp/page-2026-05-08T15-33-52-666Z.yml +1072 -0
- package/.playwright-mcp/page-2026-05-08T15-34-01-338Z.yml +1085 -0
- package/.playwright-mcp/page-2026-05-08T15-34-07-227Z.yml +1072 -0
- package/.playwright-mcp/page-2026-05-08T15-34-13-875Z.yml +1072 -0
- package/.playwright-mcp/page-2026-05-08T15-34-21-885Z.yml +1109 -0
- package/.playwright-mcp/page-2026-05-08T15-34-32-755Z.yml +1109 -0
- package/.playwright-mcp/page-2026-05-08T15-35-09-607Z.yml +1119 -0
- package/.playwright-mcp/page-2026-05-08T15-35-14-242Z.yml +1109 -0
- package/.playwright-mcp/page-2026-05-08T16-18-48-671Z.yml +44 -0
- package/.playwright-mcp/page-2026-05-08T16-18-52-753Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-19-13-919Z.yml +68 -0
- package/.playwright-mcp/page-2026-05-08T16-19-17-977Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-19-25-733Z.yml +120 -0
- package/.playwright-mcp/page-2026-05-08T16-19-29-252Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-19-39-269Z.yml +80 -0
- package/.playwright-mcp/page-2026-05-08T16-19-43-915Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-20-04-407Z.yml +81 -0
- package/.playwright-mcp/page-2026-05-08T16-20-08-984Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-20-32-397Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-20-58-658Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-21-12-320Z.yml +86 -0
- package/.playwright-mcp/page-2026-05-08T16-21-39-154Z.yml +91 -0
- package/.playwright-mcp/page-2026-05-08T16-21-45-420Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-22-21-062Z.yml +0 -0
- package/.playwright-mcp/page-2026-05-08T16-22-58-232Z.yml +91 -0
- package/.playwright-mcp/page-2026-05-08T16-23-36-520Z.yml +0 -0
- package/.playwright-mcp/page-2026-05-08T16-23-46-805Z.yml +100 -0
- package/.playwright-mcp/page-2026-05-08T16-23-55-169Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-24-26-574Z.yml +91 -0
- package/.playwright-mcp/page-2026-05-08T16-25-34-414Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-25-59-831Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-26-21-809Z.yml +91 -0
- package/.playwright-mcp/page-2026-05-08T16-26-47-443Z.yml +105 -0
- package/.playwright-mcp/page-2026-05-08T16-26-56-136Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-27-59-610Z.yml +48 -0
- package/.playwright-mcp/page-2026-05-11T12-54-07-180Z.yml +44 -0
- package/.playwright-mcp/page-2026-05-11T12-54-56-946Z.yml +4 -0
- package/.playwright-mcp/page-2026-05-11T12-55-47-503Z.yml +24 -0
- package/.playwright-mcp/page-2026-05-11T12-56-00-766Z.yml +28 -0
- package/.playwright-mcp/page-2026-05-11T12-56-06-438Z.yml +90 -0
- package/.playwright-mcp/page-2026-05-11T12-57-56-838Z.yml +106 -0
- package/.playwright-mcp/page-2026-05-11T12-58-00-124Z.yml +106 -0
- package/.playwright-mcp/page-2026-05-11T12-59-08-836Z.yml +61 -0
- package/.playwright-mcp/page-2026-05-11T12-59-12-088Z.yml +61 -0
- package/.playwright-mcp/page-2026-05-11T12-59-26-215Z.yml +69 -0
- package/.playwright-mcp/page-2026-05-11T12-59-29-519Z.yml +69 -0
- package/.playwright-mcp/page-2026-05-11T12-59-37-309Z.yml +0 -0
- package/.playwright-mcp/page-2026-05-11T12-59-39-968Z.yml +79 -0
- package/.playwright-mcp/page-2026-05-11T12-59-45-983Z.yml +78 -0
- package/.playwright-mcp/page-2026-05-11T12-59-49-951Z.yml +78 -0
- package/.playwright-mcp/page-2026-05-11T15-34-04-515Z.yml +0 -0
- package/.playwright-mcp/page-2026-05-12T15-07-32-171Z.yml +44 -0
- package/.playwright-mcp/page-2026-05-12T15-08-09-820Z.yml +119 -0
- package/CHANGELOG.md +54 -4
- package/angular.json +20 -3
- package/deploy_amazon_beta.sh +7 -17
- package/deploy_amazon_prod.sh +41 -0
- package/docs/TILEDESK_WIDGET_ACCESSIBILITY_STATEMENT_COMPLETE.md +379 -0
- package/env.sample +3 -2
- package/mocks/voice-websocket-mock/server.cjs +245 -0
- package/package.json +7 -3
- package/playwright-report/index.html +90 -0
- package/playwright.config.ts +41 -0
- package/src/app/app.component.html +2 -2
- package/src/app/app.component.scss +25 -14
- package/src/app/app.component.spec.ts +21 -6
- package/src/app/app.module.ts +4 -0
- package/src/app/component/conversation-detail/conversation/conversation.component.html +19 -11
- package/src/app/component/conversation-detail/conversation/conversation.component.scss +28 -0
- package/src/app/component/conversation-detail/conversation/conversation.component.spec.ts +644 -75
- package/src/app/component/conversation-detail/conversation/conversation.component.ts +61 -17
- package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.html +25 -13
- package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.spec.ts +123 -5
- package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.ts +1 -0
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.html +17 -7
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.scss +15 -3
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.spec.ts +249 -149
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.ts +0 -1
- package/src/app/component/conversation-detail/conversation-emojii/conversation-emojii.component.spec.ts +53 -3
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component copy.html +172 -0
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.html +112 -62
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss +133 -7
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.spec.ts +452 -78
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts +192 -84
- package/src/app/component/conversation-detail/conversation-header/conversation-header.component.html +113 -53
- package/src/app/component/conversation-detail/conversation-header/conversation-header.component.scss +12 -4
- package/src/app/component/conversation-detail/conversation-header/conversation-header.component.spec.ts +274 -29
- package/src/app/component/conversation-detail/conversation-internal-frame/conversation-internal-frame.component.html +23 -9
- package/src/app/component/conversation-detail/conversation-internal-frame/conversation-internal-frame.component.spec.ts +80 -8
- package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.html +29 -23
- package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.spec.ts +185 -16
- package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.ts +34 -14
- package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.html +43 -18
- package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.scss +56 -2
- package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.ts +135 -5
- package/src/app/component/error-alert/error-alert.component.spec.ts +65 -5
- package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.html +16 -7
- package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.scss +21 -0
- package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.spec.ts +89 -7
- package/src/app/component/form/form-builder/form-builder.component.html +1 -1
- package/src/app/component/form/form-builder/form-builder.component.spec.ts +163 -21
- package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.html +8 -4
- package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.scss +10 -5
- package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.spec.ts +90 -16
- package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.ts +26 -0
- package/src/app/component/form/inputs/form-label/form-label.component.spec.ts +45 -11
- package/src/app/component/form/inputs/form-radio-button/form-radio-button.component.spec.ts +24 -6
- package/src/app/component/form/inputs/form-select/form-select.component.spec.ts +14 -5
- package/src/app/component/form/inputs/form-text/form-text.component.html +14 -12
- package/src/app/component/form/inputs/form-text/form-text.component.scss +11 -1
- package/src/app/component/form/inputs/form-text/form-text.component.spec.ts +113 -17
- package/src/app/component/form/inputs/form-text/form-text.component.ts +26 -0
- package/src/app/component/form/inputs/form-textarea/form-textarea.component.html +13 -11
- package/src/app/component/form/inputs/form-textarea/form-textarea.component.scss +6 -5
- package/src/app/component/form/inputs/form-textarea/form-textarea.component.spec.ts +149 -13
- package/src/app/component/form/inputs/form-textarea/form-textarea.component.ts +26 -0
- package/src/app/component/form/prechat-form/prechat-form.component.html +14 -11
- package/src/app/component/form/prechat-form/prechat-form.component.spec.ts +102 -10
- package/src/app/component/form/prechat-form/prechat-form.component.ts +8 -1
- package/src/app/component/form/prechat-form-test-mock.ts +35 -0
- package/src/app/component/home/home.component.html +38 -31
- package/src/app/component/home/home.component.scss +4 -2
- package/src/app/component/home/home.component.spec.ts +226 -11
- package/src/app/component/home-conversations/home-conversations.component.html +30 -26
- package/src/app/component/home-conversations/home-conversations.component.scss +3 -0
- package/src/app/component/home-conversations/home-conversations.component.spec.ts +212 -36
- package/src/app/component/last-message/last-message.component.html +15 -9
- package/src/app/component/last-message/last-message.component.scss +16 -2
- package/src/app/component/last-message/last-message.component.spec.ts +204 -23
- package/src/app/component/launcher-button/launcher-button.component.html +8 -13
- package/src/app/component/launcher-button/launcher-button.component.spec.ts +104 -8
- package/src/app/component/list-all-conversations/list-all-conversations.component.html +12 -17
- package/src/app/component/list-all-conversations/list-all-conversations.component.scss +2 -0
- package/src/app/component/list-conversations/list-conversations.component.html +22 -22
- package/src/app/component/menu-options/menu-options.component.html +30 -20
- package/src/app/component/menu-options/menu-options.component.spec.ts +125 -9
- package/src/app/component/message/audio/audio.component.html +13 -15
- package/src/app/component/message/audio/audio.component.spec.ts +140 -5
- package/src/app/component/message/audio/audio.component.ts +1 -0
- package/src/app/component/message/audio-sync/audio-sync.component.scss +1 -0
- package/src/app/component/message/audio-sync/audio-sync.component.spec.ts +81 -1
- package/src/app/component/message/audio-sync/audio-sync.component.ts +133 -86
- package/src/app/component/message/avatar/avatar.component.html +2 -2
- package/src/app/component/message/avatar/avatar.component.spec.ts +99 -7
- package/src/app/component/message/bubble-message/bubble-message.component.html +39 -52
- package/src/app/component/message/bubble-message/bubble-message.component.scss +54 -1
- package/src/app/component/message/bubble-message/bubble-message.component.spec.ts +154 -57
- package/src/app/component/message/bubble-message/bubble-message.component.ts +138 -110
- package/src/app/component/message/buttons/action-button/action-button.component.html +3 -4
- package/src/app/component/message/buttons/action-button/action-button.component.spec.ts +49 -5
- package/src/app/component/message/buttons/link-button/link-button.component.scss +5 -8
- package/src/app/component/message/buttons/link-button/link-button.component.spec.ts +50 -5
- package/src/app/component/message/buttons/text-button/text-button.component.spec.ts +44 -5
- package/src/app/component/message/carousel/carousel.component.html +29 -16
- package/src/app/component/message/carousel/carousel.component.scss +20 -8
- package/src/app/component/message/carousel/carousel.component.spec.ts +80 -3
- package/src/app/component/message/carousel/carousel.component.ts +16 -0
- package/src/app/component/message/frame/frame.component.html +9 -4
- package/src/app/component/message/frame/frame.component.spec.ts +34 -15
- package/src/app/component/message/frame/frame.component.ts +7 -2
- package/src/app/component/message/html/html.component.html +1 -1
- package/src/app/component/message/html/html.component.scss +1 -1
- package/src/app/component/message/html/html.component.spec.ts +24 -7
- package/src/app/component/message/image/image.component.html +12 -10
- package/src/app/component/message/image/image.component.scss +16 -0
- package/src/app/component/message/image/image.component.spec.ts +101 -15
- package/src/app/component/message/image/image.component.ts +90 -51
- package/src/app/component/message/info-message/info-message.component.spec.ts +26 -14
- package/src/app/component/message/json-sources/json-sources.component.html +38 -0
- package/src/app/component/message/json-sources/json-sources.component.scss +197 -0
- package/src/app/component/message/json-sources/json-sources.component.ts +89 -0
- package/src/app/component/message/like-unlike/like-unlike.component.html +7 -9
- package/src/app/component/message/like-unlike/like-unlike.component.spec.ts +31 -3
- package/src/app/component/message/return-receipt/return-receipt.component.spec.ts +38 -17
- package/src/app/component/message/text/text.component.html +3 -3
- package/src/app/component/message/text/text.component.scss +80 -86
- package/src/app/component/message/text/text.component.spec.ts +106 -13
- package/src/app/component/message-attachment/message-attachment.component.spec.ts +134 -13
- package/src/app/component/selection-department/selection-department.component.html +21 -23
- package/src/app/component/selection-department/selection-department.component.spec.ts +159 -14
- package/src/app/component/selection-department/selection-department.component.ts +8 -1
- package/src/app/component/send-button/send-button.component.html +5 -13
- package/src/app/component/send-button/send-button.component.spec.ts +2 -2
- package/src/app/component/star-rating-widget/star-rating-widget.component.html +51 -81
- package/src/app/directives/tooltip.directive.spec.ts +8 -4
- package/src/app/modals/confirm-close/confirm-close.component.html +20 -8
- package/src/app/modals/confirm-close/confirm-close.component.scss +3 -0
- package/src/app/modals/confirm-close/confirm-close.component.spec.ts +13 -4
- package/src/app/modals/confirm-close/confirm-close.component.ts +8 -1
- package/src/app/pipe/html-entites-encode.pipe.spec.ts +35 -2
- package/src/app/pipe/marked.pipe.spec.ts +38 -2
- package/src/app/providers/app-config.service.ts +4 -2
- package/src/app/providers/brand.service.spec.ts +23 -2
- package/src/app/providers/brand.service.ts +1 -1
- package/src/app/providers/global-settings.service.spec.ts +1009 -14
- package/src/app/providers/global-settings.service.ts +30 -2
- package/src/app/providers/json-sources-parser.service.ts +182 -0
- package/src/app/providers/translator.service.ts +24 -6
- package/src/app/providers/tts-audio-playback-coordinator.service.spec.ts +117 -0
- package/src/app/providers/tts-audio-playback-coordinator.service.ts +39 -16
- package/src/app/providers/url-preview.service.ts +82 -0
- package/src/app/providers/voice/audio.types.ts +6 -0
- package/src/app/providers/voice/voice-streaming.service.spec.ts +23 -0
- package/src/app/providers/voice/voice-streaming.service.ts +710 -0
- package/src/app/providers/voice/voice-streaming.types.ts +113 -0
- package/src/app/providers/voice/voice.service.spec.ts +203 -3
- package/src/app/providers/voice/voice.service.ts +517 -13
- package/src/app/sass/_variables.scss +1 -1
- package/src/app/sass/animations.scss +19 -1
- package/src/app/utils/globals.ts +4 -0
- package/src/app/utils/json-sources-utils.ts +27 -0
- package/src/app/utils/url-utils.ts +98 -0
- package/src/app/utils/utils-resources.ts +1 -1
- package/src/assets/i18n/en.json +26 -1
- package/src/assets/i18n/es.json +106 -101
- package/src/assets/i18n/fr.json +106 -101
- package/src/assets/i18n/it.json +106 -99
- package/src/assets/twp/index-dev.html +18 -0
- package/src/assets/twp/tiledesk_widget_files/widget-css-override-example.css +14 -0
- package/src/chat21-core/providers/chat-manager.spec.ts +72 -0
- package/src/chat21-core/providers/scripts/script.service.spec.ts +12 -2
- package/src/chat21-core/utils/constants.ts +4 -0
- package/src/chat21-core/utils/utils-message.ts +23 -1
- package/src/widget-config-template.json +3 -1
- package/src/widget-config.json +28 -27
- package/test-results/.last-run.json +4 -0
- package/tests/widget-form-rich.spec.ts +67 -0
- package/tests/widget-index-dev-settings.spec.ts +52 -0
- package/tests/widget-twp-iframe.spec.ts +39 -0
|
@@ -12,11 +12,12 @@ import {
|
|
|
12
12
|
import { Subscription } from 'rxjs';
|
|
13
13
|
import { MessageModel } from 'src/chat21-core/models/message';
|
|
14
14
|
import { TtsAudioPlaybackCoordinator } from 'src/app/providers/tts-audio-playback-coordinator.service';
|
|
15
|
-
import { Globals } from 'src/app/utils/globals';
|
|
16
15
|
import { VoiceService } from 'src/app/providers/voice/voice.service';
|
|
16
|
+
import { Globals } from 'src/app/utils/globals';
|
|
17
17
|
|
|
18
18
|
/** HAVE_METADATA: metadati già disponibili (tipico audio servito da cache). */
|
|
19
19
|
const HAVE_METADATA = 1;
|
|
20
|
+
const BROWSER_TTS_OUTPUT_FORMAT = 'mp3_44100_128';
|
|
20
21
|
|
|
21
22
|
@Component({
|
|
22
23
|
selector: 'chat-audio-sync',
|
|
@@ -50,11 +51,10 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
50
51
|
private destroyed = false;
|
|
51
52
|
private playbackRequested = false;
|
|
52
53
|
private playbackStarted = false;
|
|
53
|
-
private micInterrupted = false;
|
|
54
54
|
private streamAbort?: AbortController;
|
|
55
55
|
private mediaSourceObjectUrl?: string;
|
|
56
|
-
private
|
|
57
|
-
private
|
|
56
|
+
private stopAllSub?: Subscription;
|
|
57
|
+
private preemptSub?: Subscription;
|
|
58
58
|
|
|
59
59
|
constructor(
|
|
60
60
|
private readonly cdr: ChangeDetectorRef,
|
|
@@ -93,28 +93,6 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
93
93
|
(this.message?.uid && String(this.message.uid).trim()) ||
|
|
94
94
|
`tts-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
95
95
|
|
|
96
|
-
// Se l’utente parla al microfono mentre sta ascoltando, interrompi TUTTO (corrente + coda).
|
|
97
|
-
this.micSpeechSub = this.voiceService.speechStart$.subscribe(() => {
|
|
98
|
-
if (this.destroyed) {
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
// interrompi solo se questo messaggio era in riproduzione o in attesa
|
|
102
|
-
if (this.playbackStarted || this.playbackRequested) {
|
|
103
|
-
this.micInterrupted = true;
|
|
104
|
-
this.ttsPlayback.cancelAll();
|
|
105
|
-
this.interruptPlaybackAndRevealText();
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
// Stop globale (es. mic) notificato dal coordinatore: ogni istanza deve fermarsi e mostrare testo intero.
|
|
110
|
-
this.cancelAllSub = this.ttsPlayback.cancelAll$.subscribe(() => {
|
|
111
|
-
if (this.destroyed) {
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
this.micInterrupted = true;
|
|
115
|
-
this.interruptPlaybackAndRevealText();
|
|
116
|
-
});
|
|
117
|
-
|
|
118
96
|
this.onPlaybackEnded = () => {
|
|
119
97
|
this.playbackStarted = false;
|
|
120
98
|
this.cleanupStreaming();
|
|
@@ -166,19 +144,12 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
166
144
|
this.cdr.detectChanges();
|
|
167
145
|
|
|
168
146
|
setTimeout(() => {
|
|
169
|
-
if (this.playbackRequested || this.destroyed
|
|
170
|
-
if (this.micInterrupted) {
|
|
171
|
-
this.markAllWordsPast();
|
|
172
|
-
if (this.message) {
|
|
173
|
-
this.message.isJustRecived = false;
|
|
174
|
-
}
|
|
175
|
-
this.cdr.detectChanges();
|
|
176
|
-
}
|
|
147
|
+
if (this.playbackRequested || this.destroyed) {
|
|
177
148
|
return;
|
|
178
149
|
}
|
|
179
150
|
this.playbackRequested = true;
|
|
180
151
|
this.ttsPlayback.requestStart(this.playbackOwnerId, () => {
|
|
181
|
-
if (this.destroyed
|
|
152
|
+
if (this.destroyed) {
|
|
182
153
|
this.ttsPlayback.releaseIfCurrent(this.playbackOwnerId);
|
|
183
154
|
return;
|
|
184
155
|
}
|
|
@@ -188,14 +159,59 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
188
159
|
this.startPlayback(audio);
|
|
189
160
|
});
|
|
190
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
|
+
});
|
|
191
205
|
}
|
|
192
206
|
|
|
193
207
|
ngOnDestroy(): void {
|
|
194
208
|
this.destroyed = true;
|
|
195
209
|
this.playbackStarted = false;
|
|
196
210
|
this.cleanupStreaming();
|
|
197
|
-
this.
|
|
198
|
-
this.
|
|
211
|
+
this.stopAllSub?.unsubscribe();
|
|
212
|
+
this.stopAllSub = undefined;
|
|
213
|
+
this.preemptSub?.unsubscribe();
|
|
214
|
+
this.preemptSub = undefined;
|
|
199
215
|
|
|
200
216
|
const audio = this.audioRef?.nativeElement;
|
|
201
217
|
if (audio) {
|
|
@@ -219,34 +235,29 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
219
235
|
}
|
|
220
236
|
}
|
|
221
237
|
|
|
222
|
-
private
|
|
223
|
-
this.
|
|
224
|
-
this.cleanupStreaming();
|
|
238
|
+
private startPlayback(audio: HTMLAudioElement): void {
|
|
239
|
+
const messageSrc = (this.message as any)?.metadata?.src as string | undefined;
|
|
225
240
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
audio
|
|
231
|
-
|
|
232
|
-
/* ignore */
|
|
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;
|
|
233
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;
|
|
234
258
|
}
|
|
235
259
|
|
|
236
|
-
|
|
237
|
-
this.ttsPlayback.releaseIfCurrent(this.playbackOwnerId);
|
|
238
|
-
|
|
239
|
-
// Mostra tutto il testo (niente "future" invisibili).
|
|
240
|
-
this.markAllWordsPast();
|
|
241
|
-
if (this.message) {
|
|
242
|
-
this.message.isJustRecived = false;
|
|
243
|
-
}
|
|
244
|
-
this.cdr.detectChanges();
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
private startPlayback(audio: HTMLAudioElement): void {
|
|
248
|
-
const src = (this.message as any)?.metadata?.src as string | undefined;
|
|
249
|
-
if (!src) {
|
|
260
|
+
if (!messageSrc) {
|
|
250
261
|
this.playbackStarted = false;
|
|
251
262
|
this.ttsPlayback.releaseIfCurrent(this.playbackOwnerId);
|
|
252
263
|
this.markAllWordsPast();
|
|
@@ -257,11 +268,10 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
257
268
|
return;
|
|
258
269
|
}
|
|
259
270
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
271
|
+
this.playDirectUrl(audio, messageSrc);
|
|
272
|
+
}
|
|
264
273
|
|
|
274
|
+
private playDirectUrl(audio: HTMLAudioElement, src: string): void {
|
|
265
275
|
audio.src = src;
|
|
266
276
|
try {
|
|
267
277
|
audio.currentTime = 0;
|
|
@@ -271,16 +281,40 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
271
281
|
audio.play().catch(() => this.handlePlaybackError());
|
|
272
282
|
}
|
|
273
283
|
|
|
274
|
-
private startStreamingFromEndpoint(
|
|
284
|
+
private startStreamingFromEndpoint(
|
|
285
|
+
audio: HTMLAudioElement,
|
|
286
|
+
endpoint: string,
|
|
287
|
+
fullFileEndpoint?: string | null,
|
|
288
|
+
directFallbackSrc?: string,
|
|
289
|
+
): void {
|
|
275
290
|
this.cleanupStreaming();
|
|
276
291
|
|
|
277
292
|
const jwt = this.getJwtToken();
|
|
278
293
|
const voiceSettings = this.getVoiceSettingsBody();
|
|
279
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
|
+
|
|
280
314
|
// <audio src="..."> non può inviare header/body: serve fetch().
|
|
281
315
|
const hasMse = typeof (window as any).MediaSource !== 'undefined';
|
|
282
316
|
if (!hasMse) {
|
|
283
|
-
|
|
317
|
+
fallback();
|
|
284
318
|
return;
|
|
285
319
|
}
|
|
286
320
|
|
|
@@ -298,10 +332,8 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
298
332
|
try {
|
|
299
333
|
const headers: Record<string, string> = {
|
|
300
334
|
'Content-Type': 'application/json',
|
|
335
|
+
'Authorization': `${jwt}`
|
|
301
336
|
};
|
|
302
|
-
if (jwt) {
|
|
303
|
-
headers['Authorization'] = jwt;
|
|
304
|
-
}
|
|
305
337
|
|
|
306
338
|
const response = await fetch(endpoint, {
|
|
307
339
|
method: 'POST',
|
|
@@ -314,14 +346,15 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
314
346
|
}
|
|
315
347
|
|
|
316
348
|
const headerType = (response.headers.get('content-type') || '').split(';')[0].trim();
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
349
|
+
if (headerType && !MediaSourceCtor.isTypeSupported(headerType)) {
|
|
350
|
+
// Fallback: fetch completo e play via blob (no streaming).
|
|
351
|
+
fallback();
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
320
354
|
|
|
355
|
+
const mime = headerType || 'audio/mpeg';
|
|
321
356
|
if (!MediaSourceCtor.isTypeSupported(mime)) {
|
|
322
|
-
|
|
323
|
-
// Fallback: fetch completo e play via blob (no streaming).
|
|
324
|
-
this.fetchAsBlobAndPlay(audio, endpoint, jwt, requestBody);
|
|
357
|
+
fallback();
|
|
325
358
|
return;
|
|
326
359
|
}
|
|
327
360
|
|
|
@@ -362,15 +395,14 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
362
395
|
) as ArrayBuffer;
|
|
363
396
|
sourceBuffer.appendBuffer(ab);
|
|
364
397
|
} catch {
|
|
365
|
-
|
|
366
|
-
this.fetchAsBlobAndPlay(audio, endpoint, jwt, requestBody);
|
|
398
|
+
fallback();
|
|
367
399
|
}
|
|
368
400
|
};
|
|
369
401
|
|
|
370
402
|
sourceBuffer.addEventListener('updateend', () => {
|
|
371
403
|
if (!started && this.playbackStarted && !this.destroyed) {
|
|
372
404
|
started = true;
|
|
373
|
-
audio.play().catch(() =>
|
|
405
|
+
audio.play().catch(() => fallback());
|
|
374
406
|
}
|
|
375
407
|
pump();
|
|
376
408
|
});
|
|
@@ -394,7 +426,7 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
394
426
|
tryEndOfStream();
|
|
395
427
|
} catch {
|
|
396
428
|
if (!abort.signal.aborted) {
|
|
397
|
-
|
|
429
|
+
fallback();
|
|
398
430
|
}
|
|
399
431
|
}
|
|
400
432
|
};
|
|
@@ -463,10 +495,8 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
463
495
|
try {
|
|
464
496
|
const headers: Record<string, string> = {
|
|
465
497
|
'Content-Type': 'application/json',
|
|
498
|
+
'Authorization': `${jwt}`
|
|
466
499
|
};
|
|
467
|
-
if (jwt) {
|
|
468
|
-
headers['Authorization'] = jwt;
|
|
469
|
-
}
|
|
470
500
|
|
|
471
501
|
const response = await fetch(endpoint, {
|
|
472
502
|
method: 'POST',
|
|
@@ -493,16 +523,33 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
493
523
|
}
|
|
494
524
|
}
|
|
495
525
|
|
|
496
|
-
private
|
|
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 {
|
|
497
534
|
const text = this.message?.text ?? '';
|
|
498
535
|
if (
|
|
499
536
|
voiceSettings &&
|
|
500
537
|
typeof voiceSettings === 'object' &&
|
|
501
538
|
!Array.isArray(voiceSettings)
|
|
502
539
|
) {
|
|
503
|
-
return {
|
|
540
|
+
return {
|
|
541
|
+
outputFormat: BROWSER_TTS_OUTPUT_FORMAT,
|
|
542
|
+
...(voiceSettings as Record<string, unknown>),
|
|
543
|
+
text,
|
|
544
|
+
streaming,
|
|
545
|
+
};
|
|
504
546
|
}
|
|
505
|
-
return {
|
|
547
|
+
return {
|
|
548
|
+
voiceSettings,
|
|
549
|
+
text,
|
|
550
|
+
streaming,
|
|
551
|
+
outputFormat: BROWSER_TTS_OUTPUT_FORMAT,
|
|
552
|
+
};
|
|
506
553
|
}
|
|
507
554
|
|
|
508
555
|
private markAllWordsPast(): void {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<div class="c21-icon-avatar">
|
|
2
2
|
<div class="c21-avatar-image profile_image">
|
|
3
3
|
<!-- is a BOT -->
|
|
4
|
-
<img *ngIf="senderID?.indexOf('bot_') !== -1 || senderFullname.toLowerCase().includes('bot')" [src]="url" (error)="onBotImgError($event)" (load)="onLoadedBot($event)"/>
|
|
4
|
+
<img *ngIf="senderID?.indexOf('bot_') !== -1 || senderFullname.toLowerCase().includes('bot')" [src]="url" [attr.alt]="senderFullname || 'Bot'" role="img" (error)="onBotImgError($event)" (load)="onLoadedBot($event)"/>
|
|
5
5
|
<!-- is a HUMAN -->
|
|
6
|
-
<img *ngIf="senderID?.indexOf('bot_') === -1 && !senderFullname.toLowerCase().includes('bot')" [src]="url" (error)="onHumanImgError($event)" (load)="onLoadedHuman($event)"/>
|
|
6
|
+
<img *ngIf="senderID?.indexOf('bot_') === -1 && !senderFullname.toLowerCase().includes('bot')" [src]="url" [attr.alt]="senderFullname || 'User'" role="img" (error)="onHumanImgError($event)" (load)="onLoadedHuman($event)"/>
|
|
7
7
|
</div>
|
|
8
8
|
</div>
|
|
9
9
|
|
|
@@ -1,27 +1,119 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Injectable } from '@angular/core';
|
|
2
|
+
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
|
2
3
|
import { ImageRepoService } from '../../../../chat21-core/providers/abstract/image-repo.service';
|
|
3
4
|
|
|
4
5
|
import { AvatarComponent } from './avatar.component';
|
|
5
6
|
|
|
7
|
+
@Injectable()
|
|
8
|
+
class ImageRepoStub extends ImageRepoService {
|
|
9
|
+
getImagePhotoUrl(uid: string): string {
|
|
10
|
+
return `https://cdn.test/photo/${uid}`;
|
|
11
|
+
}
|
|
12
|
+
checkImageExists(_uid: string, _cb: (exist: boolean) => void): void {}
|
|
13
|
+
}
|
|
14
|
+
|
|
6
15
|
describe('AvatarComponent', () => {
|
|
7
16
|
let component: AvatarComponent;
|
|
8
17
|
let fixture: ComponentFixture<AvatarComponent>;
|
|
9
18
|
|
|
10
|
-
beforeEach(
|
|
19
|
+
beforeEach(waitForAsync(() => {
|
|
11
20
|
TestBed.configureTestingModule({
|
|
12
|
-
declarations: [
|
|
13
|
-
providers: [ ImageRepoService ]
|
|
14
|
-
})
|
|
15
|
-
.compileComponents();
|
|
21
|
+
declarations: [AvatarComponent],
|
|
22
|
+
providers: [{ provide: ImageRepoService, useClass: ImageRepoStub }],
|
|
23
|
+
}).compileComponents();
|
|
16
24
|
}));
|
|
17
25
|
|
|
18
26
|
beforeEach(() => {
|
|
19
27
|
fixture = TestBed.createComponent(AvatarComponent);
|
|
20
28
|
component = fixture.componentInstance;
|
|
21
|
-
|
|
29
|
+
component.baseLocation = 'https://app.test';
|
|
22
30
|
});
|
|
23
31
|
|
|
24
32
|
it('should create', () => {
|
|
33
|
+
component.senderID = 'user_1';
|
|
34
|
+
component.senderFullname = 'Alice';
|
|
35
|
+
fixture.detectChanges();
|
|
25
36
|
expect(component).toBeTruthy();
|
|
26
37
|
});
|
|
38
|
+
|
|
39
|
+
it('ngOnInit should prefer remote photo when checkImageExists returns true', () => {
|
|
40
|
+
const remote = 'https://cdn.test/photo/bot_1';
|
|
41
|
+
spyOn(AvatarComponent.prototype as any, 'checkImageExists').and.callFake((_url: string, cb: (b: boolean) => void) => {
|
|
42
|
+
cb(true);
|
|
43
|
+
});
|
|
44
|
+
component.senderID = 'bot_1';
|
|
45
|
+
component.senderFullname = 'Support Bot';
|
|
46
|
+
component.ngOnInit();
|
|
47
|
+
expect(component.url).toBe(remote);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('ngOnInit should keep default bot asset when remote image missing', () => {
|
|
51
|
+
spyOn(AvatarComponent.prototype as any, 'checkImageExists').and.callFake((_url: string, cb: (b: boolean) => void) => {
|
|
52
|
+
cb(false);
|
|
53
|
+
});
|
|
54
|
+
component.senderID = 'bot_1';
|
|
55
|
+
component.senderFullname = 'Bot';
|
|
56
|
+
component.ngOnInit();
|
|
57
|
+
expect(component.url).toBe(component.baseLocation + '/assets/images/tommy_bot_tiledesk.svg');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('ngOnInit should use human default when not bot and photo missing', () => {
|
|
61
|
+
spyOn(AvatarComponent.prototype as any, 'checkImageExists').and.callFake((_url: string, cb: (b: boolean) => void) => {
|
|
62
|
+
cb(false);
|
|
63
|
+
});
|
|
64
|
+
component.senderID = 'user_99';
|
|
65
|
+
component.senderFullname = 'Bob';
|
|
66
|
+
component.ngOnInit();
|
|
67
|
+
expect(component.url).toBe(component.baseLocation + '/assets/images/chat_human_avatar.svg');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('checkImageExists should invoke callback on image load', (done) => {
|
|
71
|
+
const imgCtor = window.Image;
|
|
72
|
+
(window as any).Image = function MockImage(this: any) {
|
|
73
|
+
setTimeout(() => this.onload && this.onload(), 0);
|
|
74
|
+
return this;
|
|
75
|
+
} as any;
|
|
76
|
+
component.checkImageExists('https://x', (ok) => {
|
|
77
|
+
(window as any).Image = imgCtor;
|
|
78
|
+
expect(ok).toBe(true);
|
|
79
|
+
done();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('checkImageExists should invoke callback false on error', (done) => {
|
|
84
|
+
const imgCtor = window.Image;
|
|
85
|
+
(window as any).Image = function MockImage(this: any) {
|
|
86
|
+
setTimeout(() => this.onerror && this.onerror(), 0);
|
|
87
|
+
return this;
|
|
88
|
+
} as any;
|
|
89
|
+
component.checkImageExists('https://bad', (ok) => {
|
|
90
|
+
(window as any).Image = imgCtor;
|
|
91
|
+
expect(ok).toBe(false);
|
|
92
|
+
done();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('onBotImgError should swap to local bot svg', () => {
|
|
97
|
+
const target = { src: 'bad' } as any;
|
|
98
|
+
component.baseLocation = 'https://host';
|
|
99
|
+
component.onBotImgError({ target });
|
|
100
|
+
expect(target.src).toContain('tommy_bot_tiledesk.svg');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('onHumanImgError should swap to local human svg', () => {
|
|
104
|
+
const target = { src: 'bad' } as any;
|
|
105
|
+
component.baseLocation = 'https://host';
|
|
106
|
+
component.onHumanImgError({ target });
|
|
107
|
+
expect(target.src).toContain('chat_human_avatar.svg');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('template should expose accessible name on avatar image (bot)', () => {
|
|
111
|
+
spyOn(AvatarComponent.prototype as any, 'checkImageExists').and.stub();
|
|
112
|
+
component.senderID = 'bot_x';
|
|
113
|
+
component.senderFullname = 'Helper Bot';
|
|
114
|
+
fixture.detectChanges();
|
|
115
|
+
const img = (fixture.nativeElement as HTMLElement).querySelector('img');
|
|
116
|
+
expect(img?.getAttribute('alt')).toBeTruthy();
|
|
117
|
+
expect(img?.getAttribute('role')).toBe('img');
|
|
118
|
+
});
|
|
27
119
|
});
|
|
@@ -1,26 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
<!-- ngStyle]="{'padding': (isImage(message) || isFrame(message))?'1px':'0 8px'}" -->
|
|
3
|
-
<!-- 'width': (isImage(message) || isFrame(message))? sizeImage?.width + 'px': null -->
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
<!-- <div id="bubble-message" *ngIf="isAudio(message)" [ngStyle]="{'padding': '0'}" class="messages primary-color">
|
|
8
|
-
<div>
|
|
9
|
-
<chat-audio-track *ngIf="isAudio(message)"
|
|
10
|
-
[metadata]="message.metadata"
|
|
11
|
-
></chat-audio-track>
|
|
12
|
-
</div>
|
|
13
|
-
</div>
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
[ngStyle]="{'padding': (isImage(message) || isFrame(message) || isAudio(message))?'0 0px':'0 8px'}"
|
|
17
|
-
-->
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
<div id="bubble-message" class="messages primary-color">
|
|
1
|
+
<div class="bubble-message messages primary-color">
|
|
24
2
|
<div>
|
|
25
3
|
|
|
26
4
|
<div *ngIf="messageType(MESSAGE_TYPE_OTHERS, message) && !isSameSender"
|
|
@@ -28,17 +6,7 @@
|
|
|
28
6
|
[ngStyle]="{'margin': (isImage(message) || isFrame(message))? '12px 16px 8px 16px': '12px 16px 0px 16px'}" class="message_sender_fullname">
|
|
29
7
|
{{message?.sender_fullname}}
|
|
30
8
|
</div>
|
|
31
|
-
<!-- message type:: image -->
|
|
32
|
-
<!-- <div *ngIf="message.type == 'image' && message.metadata" [ngStyle] = "{ 'max-width': getSizeImg(message).width, 'max-height': getSizeImg(message).height }">
|
|
33
|
-
<img class="message-contentX message-content-imageX" [src]="message.metadata.src" />
|
|
34
|
-
</div> -->
|
|
35
|
-
|
|
36
|
-
<!-- <img *ngIf="message.type == 'image' && message.metadata" class="message-contentX message-content-imageX"
|
|
37
|
-
[src]="message.metadata.src" [width]="getSizeImg(message).width"
|
|
38
|
-
[height]="getSizeImg(message).height" /> -->
|
|
39
9
|
|
|
40
|
-
<!-- [width]="getMetadataSize(message.metadata).width"
|
|
41
|
-
[height]="getMetadataSize(message.metadata).height" -->
|
|
42
10
|
<chat-image *ngIf="isImage(message)"
|
|
43
11
|
[metadata]="message.metadata"
|
|
44
12
|
[width]="sizeImage?.width"
|
|
@@ -53,22 +21,42 @@
|
|
|
53
21
|
(onElementRendered)="onElementRenderedFN($event)">
|
|
54
22
|
</chat-frame>
|
|
55
23
|
|
|
56
|
-
<!-- <chat-audio *ngIf="isAudio(message)"
|
|
57
|
-
[metadata]="message.metadata"
|
|
58
|
-
(onElementRendered)="onElementRenderedFN($event)">
|
|
59
|
-
</chat-audio> -->
|
|
60
24
|
|
|
61
|
-
<chat-audio
|
|
25
|
+
<chat-audio *ngIf="isAudio(message)"
|
|
62
26
|
[metadata]="message.metadata"
|
|
63
27
|
[color]="fontColor"
|
|
64
|
-
[stylesMap]="stylesMap"
|
|
28
|
+
[stylesMap]="stylesMap"
|
|
29
|
+
[translationMap]="translationMap">
|
|
65
30
|
</chat-audio>
|
|
66
|
-
|
|
67
|
-
|
|
31
|
+
|
|
32
|
+
<!-- Json sources -->
|
|
33
|
+
<chat-json-sources *ngIf="jsonSources !== null && jsonSources.length > 0"
|
|
34
|
+
[items]="jsonSources"
|
|
35
|
+
(onElementRendered)="onElementRenderedFN($event)">
|
|
36
|
+
</chat-json-sources>
|
|
37
|
+
<!-- TTS player: only when voice proxy is NOT active (avoids double playback)
|
|
38
|
+
and the message was not already played by the proxy (avoids replay on session end) -->
|
|
39
|
+
<chat-audio-sync *ngIf="isAudioTTS(message) && !(voiceService.isWssVoiceActive$ | async) && !voiceService.wasProxyHandled(message?.uid)"
|
|
68
40
|
[message]="message"
|
|
69
41
|
[color]="fontColor">
|
|
70
42
|
</chat-audio-sync>
|
|
71
43
|
|
|
44
|
+
<!-- Karaoke display for TTS messages while a WSS voice session is active -->
|
|
45
|
+
<p *ngIf="isAudioTTS(message) && (voiceService.isWssVoiceActive$ | async) && _wssKaraokeWords$"
|
|
46
|
+
class="wss-karaoke"
|
|
47
|
+
[style.color]="fontColor">
|
|
48
|
+
<span *ngFor="let w of (_wssKaraokeWords$ | async); trackBy: trackKaraokeWord"
|
|
49
|
+
class="wss-word"
|
|
50
|
+
[class.future]="w.state === 'future'"
|
|
51
|
+
[class.active]="w.state === 'active'"
|
|
52
|
+
[class.past]="w.state === 'past'">{{ w.text }} </span>
|
|
53
|
+
</p>
|
|
54
|
+
|
|
55
|
+
<!-- Text fallback for TTS messages after the voice proxy session ended -->
|
|
56
|
+
<chat-text *ngIf="isAudioTTS(message) && !(voiceService.isWssVoiceActive$ | async) && voiceService.wasProxyHandled(message?.uid) && message?.text"
|
|
57
|
+
[text]="message?.text"
|
|
58
|
+
[color]="fontColor">
|
|
59
|
+
</chat-text>
|
|
72
60
|
|
|
73
61
|
<!-- <chat-frame *ngIf="message.metadata && message.metadata.type && message.metadata.type.includes('video')"
|
|
74
62
|
[metadata]="message.metadata"
|
|
@@ -80,7 +68,14 @@
|
|
|
80
68
|
<!-- <div *ngIf="message.type == 'text'"> -->
|
|
81
69
|
|
|
82
70
|
<!-- tooltip="{{message.timestamp | dateAgo}} ({{message.timestamp | date:'shortDate'}} {{message.timestamp | date:'HH:mm:ss'}})" placement="bottom" -->
|
|
83
|
-
<div *ngIf="message?.text && (!isAudio(message) && !isAudioTTS(message))" >
|
|
71
|
+
<div *ngIf="message?.text && (!isAudio(message) && !isAudioTTS(message)) && !isJsonSources(message)" >
|
|
72
|
+
|
|
73
|
+
<!-- Word-by-word streaming reveal during an active voice session -->
|
|
74
|
+
<p *ngIf="_isStreaming" class="streaming-text" [style.color]="fontColor">
|
|
75
|
+
<span *ngFor="let w of _streamingWords; trackBy: trackWord"
|
|
76
|
+
class="stream-word"
|
|
77
|
+
[style.animation-delay]="(w.index * 80) + 'ms'">{{w.word}} </span>
|
|
78
|
+
</p>
|
|
84
79
|
|
|
85
80
|
<!-- [htmlEnabled]="(message?.type==='html')? true : false" -->
|
|
86
81
|
<chat-text *ngIf="message?.type !=='html'"
|
|
@@ -90,7 +85,7 @@
|
|
|
90
85
|
(onAfterMessageRender)="onAfterMessageRenderFN($event)">
|
|
91
86
|
</chat-text>
|
|
92
87
|
|
|
93
|
-
<chat-html *ngIf="message?.type==='html'"
|
|
88
|
+
<chat-html *ngIf="jsonSources === null && message?.type==='html'"
|
|
94
89
|
[htmlText]="message?.text"
|
|
95
90
|
[fontSize]="stylesMap.get('buttonFontSize')"
|
|
96
91
|
[themeColor]="stylesMap.get('themeColor')"
|
|
@@ -98,13 +93,5 @@
|
|
|
98
93
|
</chat-html>
|
|
99
94
|
|
|
100
95
|
</div>
|
|
101
|
-
|
|
102
96
|
</div>
|
|
103
|
-
|
|
104
|
-
</div>
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
97
|
+
</div>
|