@chat21/chat21-web-widget 5.1.32-rc8 → 5.1.33-rc11
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 +68 -3
- 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.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 +63 -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 +22 -9
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.scss +23 -1
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.spec.ts +242 -149
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.ts +7 -6
- 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 -61
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss +133 -16
- 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 +199 -79
- 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 +46 -19
- package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.scss +67 -10
- package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.ts +142 -12
- 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 +35 -3
- 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 -1
- 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 +134 -24
- 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 +59 -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 +152 -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 +201 -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/pipe/marked.pipe.ts +51 -41
- 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 +59 -2
- package/src/app/providers/json-sources-parser.service.ts +175 -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 +45 -7
- 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 +702 -0
- package/src/app/providers/voice/voice-streaming.types.ts +112 -0
- package/src/app/providers/voice/voice.service.spec.ts +170 -3
- package/src/app/providers/voice/voice.service.ts +695 -16
- package/src/app/sass/_variables.scss +1 -1
- package/src/app/sass/animations.scss +19 -1
- package/src/app/utils/globals.ts +14 -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 +106 -100
- package/src/assets/i18n/es.json +107 -101
- package/src/assets/i18n/fr.json +107 -101
- package/src/assets/i18n/it.json +107 -99
- package/src/assets/sounds/keyboard.mp3 +0 -0
- 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/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
|
@@ -1,23 +1,158 @@
|
|
|
1
1
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
2
|
+
import { LoggerInstance } from 'src/chat21-core/providers/logger/loggerInstance';
|
|
3
|
+
import { CustomLogger } from 'src/chat21-core/providers/logger/customLogger';
|
|
4
|
+
import { NGXLogger } from 'ngx-logger';
|
|
2
5
|
|
|
3
6
|
import { AudioComponent } from './audio.component';
|
|
4
7
|
|
|
5
|
-
describe('
|
|
8
|
+
describe('AudioComponent', () => {
|
|
6
9
|
let component: AudioComponent;
|
|
7
10
|
let fixture: ComponentFixture<AudioComponent>;
|
|
11
|
+
const ngxlogger = jasmine.createSpyObj('NGXLogger', ['log', 'trace', 'debug', 'warn', 'error', 'info']);
|
|
12
|
+
const customLogger = new CustomLogger(ngxlogger);
|
|
13
|
+
const arrayBuf = new ArrayBuffer(64);
|
|
14
|
+
|
|
15
|
+
const fakeBuffer = {
|
|
16
|
+
duration: 90,
|
|
17
|
+
getChannelData: () => new Float32Array(4000),
|
|
18
|
+
} as unknown as AudioBuffer;
|
|
8
19
|
|
|
9
20
|
beforeEach(async () => {
|
|
21
|
+
LoggerInstance.setInstance(customLogger);
|
|
22
|
+
spyOn(window, 'fetch').and.returnValue(
|
|
23
|
+
Promise.resolve({
|
|
24
|
+
arrayBuffer: () => Promise.resolve(arrayBuf),
|
|
25
|
+
} as Response),
|
|
26
|
+
);
|
|
27
|
+
spyOn(AudioContext.prototype, 'decodeAudioData').and.returnValue(Promise.resolve(fakeBuffer));
|
|
28
|
+
|
|
10
29
|
await TestBed.configureTestingModule({
|
|
11
|
-
declarations: [
|
|
30
|
+
declarations: [AudioComponent],
|
|
12
31
|
})
|
|
13
|
-
|
|
32
|
+
.overrideComponent(AudioComponent, {
|
|
33
|
+
set: {
|
|
34
|
+
template: `
|
|
35
|
+
<div class="audio-container">
|
|
36
|
+
<div class="audio-track"></div>
|
|
37
|
+
<div class="audio-player-custom">
|
|
38
|
+
<audio #audioElement></audio>
|
|
39
|
+
<canvas #canvasElement width="120" height="32"></canvas>
|
|
40
|
+
</div>
|
|
41
|
+
</div>`,
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
.compileComponents();
|
|
14
45
|
|
|
15
46
|
fixture = TestBed.createComponent(AudioComponent);
|
|
16
47
|
component = fixture.componentInstance;
|
|
17
|
-
|
|
48
|
+
component.stylesMap = new Map<string, string>([
|
|
49
|
+
['bubbleSentBackground', 'rgba(10, 20, 30, 1)'],
|
|
50
|
+
['bubbleSentTextColor', '#112233'],
|
|
51
|
+
]);
|
|
52
|
+
component.color = '#000000';
|
|
18
53
|
});
|
|
19
54
|
|
|
20
|
-
it('should create', () => {
|
|
55
|
+
it('should create', async () => {
|
|
56
|
+
const blob = new Blob([new Uint8Array(arrayBuf.byteLength)], { type: 'audio/wav' });
|
|
57
|
+
component.audioBlob = blob;
|
|
58
|
+
fixture.detectChanges();
|
|
59
|
+
await fixture.whenStable();
|
|
60
|
+
fixture.detectChanges();
|
|
21
61
|
expect(component).toBeTruthy();
|
|
22
62
|
});
|
|
63
|
+
|
|
64
|
+
it('formatTime should pad seconds under 10', () => {
|
|
65
|
+
expect(component.formatTime(0)).toBe('0:00');
|
|
66
|
+
expect(component.formatTime(9)).toBe('0:09');
|
|
67
|
+
expect(component.formatTime(70)).toBe('1:10');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('extractFirstColor should parse first rgba from gradient string', () => {
|
|
71
|
+
expect(component.extractFirstColor('linear-gradient(rgba(1, 2, 3, 0.5), red)')).toBe('rgba(1, 2, 3, 0.5)');
|
|
72
|
+
expect(component.extractFirstColor('no-color')).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('drawWaveform should return early when canvas context missing', () => {
|
|
76
|
+
const canvas = document.createElement('canvas');
|
|
77
|
+
spyOn(canvas, 'getContext').and.returnValue(null);
|
|
78
|
+
(component as any).waveformCanvas = { nativeElement: canvas };
|
|
79
|
+
(component as any).audioBuffer = fakeBuffer;
|
|
80
|
+
(component as any).audioDuration = 10;
|
|
81
|
+
(component as any).audioElement = {
|
|
82
|
+
nativeElement: { currentTime: 0, paused: true },
|
|
83
|
+
};
|
|
84
|
+
expect(() => component.drawWaveform(fakeBuffer)).not.toThrow();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('drawWaveform should render bars when context exists', () => {
|
|
88
|
+
const fillRect = jasmine.createSpy('fillRect');
|
|
89
|
+
const clearRect = jasmine.createSpy('clearRect');
|
|
90
|
+
const canvas = document.createElement('canvas');
|
|
91
|
+
canvas.width = 200;
|
|
92
|
+
canvas.height = 40;
|
|
93
|
+
spyOn(canvas, 'getContext').and.returnValue({ fillRect, clearRect } as any);
|
|
94
|
+
(component as any).waveformCanvas = { nativeElement: canvas };
|
|
95
|
+
(component as any).audioElement = {
|
|
96
|
+
nativeElement: { currentTime: 0, paused: true },
|
|
97
|
+
};
|
|
98
|
+
(component as any).audioDuration = 10;
|
|
99
|
+
component.drawWaveform(fakeBuffer);
|
|
100
|
+
expect(clearRect).toHaveBeenCalled();
|
|
101
|
+
expect(fillRect).toHaveBeenCalled();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('ngAfterViewInit with blob should wire object URL and CSS vars', async () => {
|
|
105
|
+
const blob = new Blob([new Uint8Array(128)], { type: 'audio/wav' });
|
|
106
|
+
component.audioBlob = blob;
|
|
107
|
+
spyOn(URL, 'createObjectURL').and.returnValue('blob:mock-audio');
|
|
108
|
+
fixture.detectChanges();
|
|
109
|
+
await fixture.whenStable();
|
|
110
|
+
fixture.detectChanges();
|
|
111
|
+
expect(component.rawAudioUrl).toBe('blob:mock-audio');
|
|
112
|
+
expect(URL.createObjectURL).toHaveBeenCalledWith(blob);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('ngAfterViewInit with metadata.src should fetch and decode', async () => {
|
|
116
|
+
component.audioBlob = null;
|
|
117
|
+
component.metadata = { src: 'blob:from-meta' };
|
|
118
|
+
fixture.detectChanges();
|
|
119
|
+
await fixture.whenStable();
|
|
120
|
+
fixture.detectChanges();
|
|
121
|
+
expect(window.fetch).toHaveBeenCalled();
|
|
122
|
+
expect(component.audioDuration).toBe(90);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('playPauseAudio should toggle play state when buffer ready', () => {
|
|
126
|
+
spyOn(window, 'requestAnimationFrame').and.stub();
|
|
127
|
+
(component as any).audioBuffer = fakeBuffer;
|
|
128
|
+
(component as any).audioDuration = 10;
|
|
129
|
+
const play = jasmine.createSpy('play').and.returnValue(Promise.resolve());
|
|
130
|
+
const pause = jasmine.createSpy('pause');
|
|
131
|
+
const canvas = document.createElement('canvas');
|
|
132
|
+
canvas.width = 120;
|
|
133
|
+
canvas.height = 32;
|
|
134
|
+
spyOn(canvas, 'getContext').and.returnValue({
|
|
135
|
+
fillRect: jasmine.createSpy(),
|
|
136
|
+
clearRect: jasmine.createSpy(),
|
|
137
|
+
} as any);
|
|
138
|
+
(component as any).waveformCanvas = { nativeElement: canvas };
|
|
139
|
+
(component as any).audioElement = {
|
|
140
|
+
nativeElement: { paused: true, currentTime: 0, play, pause, ontimeupdate: null as any, onended: null as any },
|
|
141
|
+
};
|
|
142
|
+
(component as any).audioContext = { resume: jasmine.createSpy().and.returnValue(Promise.resolve()) };
|
|
143
|
+
|
|
144
|
+
component.playPauseAudio();
|
|
145
|
+
expect(play).toHaveBeenCalled();
|
|
146
|
+
expect(component.isPlaying).toBe(true);
|
|
147
|
+
|
|
148
|
+
(component as any).audioElement.nativeElement.paused = false;
|
|
149
|
+
component.playPauseAudio();
|
|
150
|
+
expect(pause).toHaveBeenCalled();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('getAudioDuration should set audioDuration from decoded buffer', async () => {
|
|
154
|
+
component.metadata = { src: 'blob:x' };
|
|
155
|
+
await component.getAudioDuration();
|
|
156
|
+
expect(component.audioDuration).toBe(90);
|
|
157
|
+
});
|
|
23
158
|
});
|
|
@@ -18,6 +18,7 @@ export class AudioComponent implements AfterViewInit {
|
|
|
18
18
|
@Input() metadata: any | null = null;
|
|
19
19
|
@Input() color: string;
|
|
20
20
|
@Input() stylesMap: Map<string, string>;
|
|
21
|
+
@Input() translationMap: Map<string, string>;
|
|
21
22
|
|
|
22
23
|
audioUrl: SafeUrl | null = null;
|
|
23
24
|
rawAudioUrl: string | null = null;
|
|
@@ -1,14 +1,39 @@
|
|
|
1
1
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
2
3
|
|
|
3
4
|
import { AudioSyncComponent } from './audio-sync.component';
|
|
5
|
+
import { TtsAudioPlaybackCoordinator } from 'src/app/providers/tts-audio-playback-coordinator.service';
|
|
6
|
+
import { VoiceService } from 'src/app/providers/voice/voice.service';
|
|
7
|
+
import { Globals } from 'src/app/utils/globals';
|
|
4
8
|
|
|
5
9
|
describe('AudioSyncComponent', () => {
|
|
6
10
|
let component: AudioSyncComponent;
|
|
7
11
|
let fixture: ComponentFixture<AudioSyncComponent>;
|
|
12
|
+
let voiceService: { proxyTtsStreamUrl: string | null; proxyTtsUrl: string | null };
|
|
8
13
|
|
|
9
14
|
beforeEach(async () => {
|
|
15
|
+
voiceService = {
|
|
16
|
+
proxyTtsStreamUrl: 'https://speech.example.com/api/tts/stream',
|
|
17
|
+
proxyTtsUrl: 'https://speech.example.com/api/tts',
|
|
18
|
+
};
|
|
19
|
+
|
|
10
20
|
await TestBed.configureTestingModule({
|
|
11
|
-
|
|
21
|
+
declarations: [AudioSyncComponent],
|
|
22
|
+
imports: [CommonModule],
|
|
23
|
+
providers: [
|
|
24
|
+
{
|
|
25
|
+
provide: TtsAudioPlaybackCoordinator,
|
|
26
|
+
useValue: {
|
|
27
|
+
requestStart: (_ownerId: string, start: () => void) => start(),
|
|
28
|
+
releaseIfCurrent: jasmine.createSpy('releaseIfCurrent'),
|
|
29
|
+
release: jasmine.createSpy('release'),
|
|
30
|
+
stopAllPlayback$: { subscribe: () => ({ unsubscribe: () => undefined }) },
|
|
31
|
+
preemptPlayback$: { subscribe: () => ({ unsubscribe: () => undefined }) },
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
{ provide: Globals, useValue: { tiledeskToken: 'JWT test-token', jwt: '' } },
|
|
35
|
+
{ provide: VoiceService, useValue: voiceService },
|
|
36
|
+
],
|
|
12
37
|
})
|
|
13
38
|
.compileComponents();
|
|
14
39
|
|
|
@@ -20,4 +45,59 @@ describe('AudioSyncComponent', () => {
|
|
|
20
45
|
it('should create', () => {
|
|
21
46
|
expect(component).toBeTruthy();
|
|
22
47
|
});
|
|
48
|
+
|
|
49
|
+
it('starts TTS playback from the proxy streaming endpoint first', () => {
|
|
50
|
+
component.message = {
|
|
51
|
+
uid: 'm1',
|
|
52
|
+
type: 'tts',
|
|
53
|
+
text: 'hello',
|
|
54
|
+
metadata: {},
|
|
55
|
+
isJustRecived: true,
|
|
56
|
+
} as any;
|
|
57
|
+
const audio = document.createElement('audio');
|
|
58
|
+
const startStreaming = spyOn(component as any, 'startStreamingFromEndpoint').and.stub();
|
|
59
|
+
|
|
60
|
+
(component as any).startPlayback(audio);
|
|
61
|
+
|
|
62
|
+
expect(startStreaming).toHaveBeenCalledWith(
|
|
63
|
+
audio,
|
|
64
|
+
'https://speech.example.com/api/tts/stream',
|
|
65
|
+
'https://speech.example.com/api/tts',
|
|
66
|
+
undefined,
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('requests browser-compatible MP3 for proxy REST TTS by default', () => {
|
|
71
|
+
component.message = {
|
|
72
|
+
uid: 'm1',
|
|
73
|
+
type: 'tts',
|
|
74
|
+
text: 'hello',
|
|
75
|
+
metadata: {},
|
|
76
|
+
} as any;
|
|
77
|
+
|
|
78
|
+
const body = (component as any).buildTtsRequestBody({});
|
|
79
|
+
|
|
80
|
+
expect(body).toEqual({
|
|
81
|
+
text: 'hello',
|
|
82
|
+
streaming: true,
|
|
83
|
+
outputFormat: 'mp3_44100_128',
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('does not override an explicit TTS outputFormat from message voice settings', () => {
|
|
88
|
+
component.message = {
|
|
89
|
+
uid: 'm1',
|
|
90
|
+
type: 'tts',
|
|
91
|
+
text: 'hello',
|
|
92
|
+
metadata: {},
|
|
93
|
+
} as any;
|
|
94
|
+
|
|
95
|
+
const body = (component as any).buildTtsRequestBody({ outputFormat: 'pcm_16000' });
|
|
96
|
+
|
|
97
|
+
expect(body).toEqual({
|
|
98
|
+
text: 'hello',
|
|
99
|
+
streaming: true,
|
|
100
|
+
outputFormat: 'pcm_16000',
|
|
101
|
+
});
|
|
102
|
+
});
|
|
23
103
|
});
|
|
@@ -9,12 +9,15 @@ import {
|
|
|
9
9
|
SimpleChanges,
|
|
10
10
|
ViewChild,
|
|
11
11
|
} from '@angular/core';
|
|
12
|
+
import { Subscription } from 'rxjs';
|
|
12
13
|
import { MessageModel } from 'src/chat21-core/models/message';
|
|
13
14
|
import { TtsAudioPlaybackCoordinator } from 'src/app/providers/tts-audio-playback-coordinator.service';
|
|
15
|
+
import { VoiceService } from 'src/app/providers/voice/voice.service';
|
|
14
16
|
import { Globals } from 'src/app/utils/globals';
|
|
15
17
|
|
|
16
18
|
/** HAVE_METADATA: metadati già disponibili (tipico audio servito da cache). */
|
|
17
19
|
const HAVE_METADATA = 1;
|
|
20
|
+
const BROWSER_TTS_OUTPUT_FORMAT = 'mp3_44100_128';
|
|
18
21
|
|
|
19
22
|
@Component({
|
|
20
23
|
selector: 'chat-audio-sync',
|
|
@@ -50,11 +53,14 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
50
53
|
private playbackStarted = false;
|
|
51
54
|
private streamAbort?: AbortController;
|
|
52
55
|
private mediaSourceObjectUrl?: string;
|
|
56
|
+
private stopAllSub?: Subscription;
|
|
57
|
+
private preemptSub?: Subscription;
|
|
53
58
|
|
|
54
59
|
constructor(
|
|
55
60
|
private readonly cdr: ChangeDetectorRef,
|
|
56
61
|
private readonly ttsPlayback: TtsAudioPlaybackCoordinator,
|
|
57
62
|
private readonly globals: Globals,
|
|
63
|
+
private readonly voiceService: VoiceService,
|
|
58
64
|
) {}
|
|
59
65
|
|
|
60
66
|
/** `false` = messaggio già in storico: niente autoplay / karaoke. Da `message.isJustRecived`. */
|
|
@@ -153,12 +159,59 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
153
159
|
this.startPlayback(audio);
|
|
154
160
|
});
|
|
155
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
|
+
});
|
|
156
205
|
}
|
|
157
206
|
|
|
158
207
|
ngOnDestroy(): void {
|
|
159
208
|
this.destroyed = true;
|
|
160
209
|
this.playbackStarted = false;
|
|
161
210
|
this.cleanupStreaming();
|
|
211
|
+
this.stopAllSub?.unsubscribe();
|
|
212
|
+
this.stopAllSub = undefined;
|
|
213
|
+
this.preemptSub?.unsubscribe();
|
|
214
|
+
this.preemptSub = undefined;
|
|
162
215
|
|
|
163
216
|
const audio = this.audioRef?.nativeElement;
|
|
164
217
|
if (audio) {
|
|
@@ -183,8 +236,28 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
183
236
|
}
|
|
184
237
|
|
|
185
238
|
private startPlayback(audio: HTMLAudioElement): void {
|
|
186
|
-
const
|
|
187
|
-
|
|
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) {
|
|
188
261
|
this.playbackStarted = false;
|
|
189
262
|
this.ttsPlayback.releaseIfCurrent(this.playbackOwnerId);
|
|
190
263
|
this.markAllWordsPast();
|
|
@@ -195,11 +268,10 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
195
268
|
return;
|
|
196
269
|
}
|
|
197
270
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
271
|
+
this.playDirectUrl(audio, messageSrc);
|
|
272
|
+
}
|
|
202
273
|
|
|
274
|
+
private playDirectUrl(audio: HTMLAudioElement, src: string): void {
|
|
203
275
|
audio.src = src;
|
|
204
276
|
try {
|
|
205
277
|
audio.currentTime = 0;
|
|
@@ -209,16 +281,40 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
209
281
|
audio.play().catch(() => this.handlePlaybackError());
|
|
210
282
|
}
|
|
211
283
|
|
|
212
|
-
private startStreamingFromEndpoint(
|
|
284
|
+
private startStreamingFromEndpoint(
|
|
285
|
+
audio: HTMLAudioElement,
|
|
286
|
+
endpoint: string,
|
|
287
|
+
fullFileEndpoint?: string | null,
|
|
288
|
+
directFallbackSrc?: string,
|
|
289
|
+
): void {
|
|
213
290
|
this.cleanupStreaming();
|
|
214
291
|
|
|
215
292
|
const jwt = this.getJwtToken();
|
|
216
293
|
const voiceSettings = this.getVoiceSettingsBody();
|
|
217
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
|
+
|
|
218
314
|
// <audio src="..."> non può inviare header/body: serve fetch().
|
|
219
315
|
const hasMse = typeof (window as any).MediaSource !== 'undefined';
|
|
220
316
|
if (!hasMse) {
|
|
221
|
-
|
|
317
|
+
fallback();
|
|
222
318
|
return;
|
|
223
319
|
}
|
|
224
320
|
|
|
@@ -250,14 +346,15 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
250
346
|
}
|
|
251
347
|
|
|
252
348
|
const headerType = (response.headers.get('content-type') || '').split(';')[0].trim();
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
349
|
+
if (headerType && !MediaSourceCtor.isTypeSupported(headerType)) {
|
|
350
|
+
// Fallback: fetch completo e play via blob (no streaming).
|
|
351
|
+
fallback();
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
256
354
|
|
|
355
|
+
const mime = headerType || 'audio/mpeg';
|
|
257
356
|
if (!MediaSourceCtor.isTypeSupported(mime)) {
|
|
258
|
-
|
|
259
|
-
// Fallback: fetch completo e play via blob (no streaming).
|
|
260
|
-
this.fetchAsBlobAndPlay(audio, endpoint, jwt, requestBody);
|
|
357
|
+
fallback();
|
|
261
358
|
return;
|
|
262
359
|
}
|
|
263
360
|
|
|
@@ -298,15 +395,14 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
298
395
|
) as ArrayBuffer;
|
|
299
396
|
sourceBuffer.appendBuffer(ab);
|
|
300
397
|
} catch {
|
|
301
|
-
|
|
302
|
-
this.fetchAsBlobAndPlay(audio, endpoint, jwt, requestBody);
|
|
398
|
+
fallback();
|
|
303
399
|
}
|
|
304
400
|
};
|
|
305
401
|
|
|
306
402
|
sourceBuffer.addEventListener('updateend', () => {
|
|
307
403
|
if (!started && this.playbackStarted && !this.destroyed) {
|
|
308
404
|
started = true;
|
|
309
|
-
audio.play().catch(() =>
|
|
405
|
+
audio.play().catch(() => fallback());
|
|
310
406
|
}
|
|
311
407
|
pump();
|
|
312
408
|
});
|
|
@@ -330,7 +426,7 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
330
426
|
tryEndOfStream();
|
|
331
427
|
} catch {
|
|
332
428
|
if (!abort.signal.aborted) {
|
|
333
|
-
|
|
429
|
+
fallback();
|
|
334
430
|
}
|
|
335
431
|
}
|
|
336
432
|
};
|
|
@@ -402,9 +498,6 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
402
498
|
'Authorization': `${jwt}`
|
|
403
499
|
};
|
|
404
500
|
|
|
405
|
-
console.log('headers', headers);
|
|
406
|
-
console.log('requestBody', requestBody);
|
|
407
|
-
|
|
408
501
|
const response = await fetch(endpoint, {
|
|
409
502
|
method: 'POST',
|
|
410
503
|
headers,
|
|
@@ -430,16 +523,33 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
430
523
|
}
|
|
431
524
|
}
|
|
432
525
|
|
|
433
|
-
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 {
|
|
434
534
|
const text = this.message?.text ?? '';
|
|
435
535
|
if (
|
|
436
536
|
voiceSettings &&
|
|
437
537
|
typeof voiceSettings === 'object' &&
|
|
438
538
|
!Array.isArray(voiceSettings)
|
|
439
539
|
) {
|
|
440
|
-
return {
|
|
540
|
+
return {
|
|
541
|
+
outputFormat: BROWSER_TTS_OUTPUT_FORMAT,
|
|
542
|
+
...(voiceSettings as Record<string, unknown>),
|
|
543
|
+
text,
|
|
544
|
+
streaming,
|
|
545
|
+
};
|
|
441
546
|
}
|
|
442
|
-
return {
|
|
547
|
+
return {
|
|
548
|
+
voiceSettings,
|
|
549
|
+
text,
|
|
550
|
+
streaming,
|
|
551
|
+
outputFormat: BROWSER_TTS_OUTPUT_FORMAT,
|
|
552
|
+
};
|
|
443
553
|
}
|
|
444
554
|
|
|
445
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
|
|