@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
|
@@ -6,7 +6,6 @@ import { BehaviorSubject, Observable } from 'rxjs';
|
|
|
6
6
|
import { Globals } from '../utils/globals';
|
|
7
7
|
import { convertColorToRGBA, detectIfIsMobile, getImageUrlThumb, getParameterByName, stringToBoolean, stringToNumber } from '../utils/utils';
|
|
8
8
|
|
|
9
|
-
import { TemplateBindingParseResult } from '@angular/compiler';
|
|
10
9
|
import { AppStorageService } from '../../chat21-core/providers/abstract/app-storage.service';
|
|
11
10
|
import { LoggerService } from '../../chat21-core/providers/abstract/logger.service';
|
|
12
11
|
import { LoggerInstance } from '../../chat21-core/providers/logger/loggerInstance';
|
|
@@ -339,6 +338,8 @@ export class GlobalSettingsService {
|
|
|
339
338
|
this.setCssIframe();
|
|
340
339
|
/** set main style */
|
|
341
340
|
this.setStyle();
|
|
341
|
+
/** external CSS override: last stylesheet in document head (max cascade priority vs bundle) */
|
|
342
|
+
this.applyCustomCssOverrideFromGlobals();
|
|
342
343
|
this.obsSettingsService.next(true);
|
|
343
344
|
}
|
|
344
345
|
|
|
@@ -419,6 +420,28 @@ export class GlobalSettingsService {
|
|
|
419
420
|
|
|
420
421
|
document.documentElement.style.setProperty('--font-family', family);
|
|
421
422
|
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Loads `globals.cssSource` (set only from tiledeskSettings) as the last stylesheet in head
|
|
426
|
+
* so rules with the same specificity override local / bundled CSS.
|
|
427
|
+
*/
|
|
428
|
+
private applyCustomCssOverrideFromGlobals(): void {
|
|
429
|
+
const id = 'tiledesk-widget-css-override';
|
|
430
|
+
document.getElementById(id)?.remove();
|
|
431
|
+
|
|
432
|
+
const href = (this.globals.cssSource || '').trim();
|
|
433
|
+
console.log('href', href);
|
|
434
|
+
if (!href) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const link = document.createElement('link');
|
|
439
|
+
link.id = id;
|
|
440
|
+
link.rel = 'stylesheet';
|
|
441
|
+
link.href = href;
|
|
442
|
+
link.setAttribute('data-tiledesk-css-override', 'true');
|
|
443
|
+
document.head.appendChild(link);
|
|
444
|
+
}
|
|
422
445
|
/**
|
|
423
446
|
* A: setVariablesFromService
|
|
424
447
|
*/
|
|
@@ -573,6 +596,9 @@ export class GlobalSettingsService {
|
|
|
573
596
|
if (variables.hasOwnProperty('allowedUploadExtentions')) {
|
|
574
597
|
globals['fileUploadAccept'] = variables['allowedUploadExtentions'];
|
|
575
598
|
}
|
|
599
|
+
if(variables.hasOwnProperty('showAudioStreamFooterButton')) {
|
|
600
|
+
globals['showAudioStreamFooterButton'] = variables['showAudioStreamFooterButton'];
|
|
601
|
+
}
|
|
576
602
|
|
|
577
603
|
}
|
|
578
604
|
}
|
|
@@ -639,6 +665,11 @@ export class GlobalSettingsService {
|
|
|
639
665
|
let TEMP: any;
|
|
640
666
|
const tiledeskSettings = windowContext['tiledeskSettings'];
|
|
641
667
|
// this.logger.debug('[GLOBAL-SET] setVariablesFromSettings > tiledeskSettings: ', tiledeskSettings);
|
|
668
|
+
/** css override URL: solo tiledeskSettings, mai da URL / query params */
|
|
669
|
+
TEMP = tiledeskSettings['cssSource'];
|
|
670
|
+
if (TEMP !== undefined) {
|
|
671
|
+
globals.cssSource = TEMP;
|
|
672
|
+
}
|
|
642
673
|
TEMP = tiledeskSettings['tenant'];
|
|
643
674
|
// this.logger.debug('[GLOBAL-SET] setVariablesFromSettings > tenant:: ', TEMP);
|
|
644
675
|
if (TEMP !== undefined) {
|
|
@@ -702,7 +733,7 @@ export class GlobalSettingsService {
|
|
|
702
733
|
}
|
|
703
734
|
TEMP = tiledeskSettings['lang'];
|
|
704
735
|
// this.logger.debug('[GLOBAL-SET] setVariablesFromSettings > lang:: ', TEMP);
|
|
705
|
-
if (
|
|
736
|
+
if (TEMP !== undefined) {
|
|
706
737
|
globals.lang = TEMP;
|
|
707
738
|
// globals.setParameter('lang', TEMP);
|
|
708
739
|
}
|
|
@@ -919,6 +950,14 @@ export class GlobalSettingsService {
|
|
|
919
950
|
if (TEMP !== undefined) {
|
|
920
951
|
globals.soundEnabled = TEMP;
|
|
921
952
|
}
|
|
953
|
+
TEMP = tiledeskSettings['keyboardSoundVolume'];
|
|
954
|
+
if (TEMP !== undefined) {
|
|
955
|
+
globals.keyboardSoundVolume = +TEMP;
|
|
956
|
+
}
|
|
957
|
+
TEMP = tiledeskSettings['keyboardSoundFile'];
|
|
958
|
+
if (TEMP !== undefined) {
|
|
959
|
+
globals.keyboardSoundFile = TEMP;
|
|
960
|
+
}
|
|
922
961
|
TEMP = tiledeskSettings['openExternalLinkButton'];
|
|
923
962
|
// this.logger.debug('[GLOBAL-SET] setVariablesFromSettings > openExternalLinkButton:: ', TEMP]);
|
|
924
963
|
if (TEMP !== undefined) {
|
|
@@ -1308,6 +1347,14 @@ export class GlobalSettingsService {
|
|
|
1308
1347
|
if (TEMP !== null) {
|
|
1309
1348
|
this.globals.soundEnabled = TEMP;
|
|
1310
1349
|
}
|
|
1350
|
+
TEMP = el.nativeElement.getAttribute('keyboardSoundVolume');
|
|
1351
|
+
if (TEMP !== null) {
|
|
1352
|
+
this.globals.keyboardSoundVolume = +TEMP;
|
|
1353
|
+
}
|
|
1354
|
+
TEMP = el.nativeElement.getAttribute('keyboardSoundFile');
|
|
1355
|
+
if (TEMP !== null) {
|
|
1356
|
+
this.globals.keyboardSoundFile = TEMP;
|
|
1357
|
+
}
|
|
1311
1358
|
TEMP = el.nativeElement.getAttribute('openExternalLinkButton');
|
|
1312
1359
|
if (TEMP !== null) {
|
|
1313
1360
|
this.globals.openExternalLinkButton = TEMP;
|
|
@@ -1707,6 +1754,16 @@ export class GlobalSettingsService {
|
|
|
1707
1754
|
globals.soundEnabled = stringToBoolean(TEMP);
|
|
1708
1755
|
}
|
|
1709
1756
|
|
|
1757
|
+
TEMP = getParameterByName(windowContext, 'tiledesk_keyboardSoundVolume');
|
|
1758
|
+
if (TEMP) {
|
|
1759
|
+
globals.keyboardSoundVolume = +TEMP;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
TEMP = getParameterByName(windowContext, 'tiledesk_keyboardSoundFile');
|
|
1763
|
+
if (TEMP) {
|
|
1764
|
+
globals.keyboardSoundFile = TEMP;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1710
1767
|
TEMP = getParameterByName(windowContext, 'tiledesk_openExternalLinkButton');
|
|
1711
1768
|
if (TEMP) {
|
|
1712
1769
|
globals.openExternalLinkButton = stringToBoolean(TEMP);
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { Injectable } from '@angular/core';
|
|
2
|
+
import { JSON_SOURCE_FIELD_TITLE, JSON_SOURCE_FIELD_URL } from 'src/chat21-core/utils/constants';
|
|
3
|
+
import { UrlPreviewService } from 'src/app/providers/url-preview.service';
|
|
4
|
+
import { extractUrlsFromText } from 'src/app/utils/url-utils';
|
|
5
|
+
import { JsonSourceItem } from 'src/app/component/message/json-sources/json-sources.component';
|
|
6
|
+
import { mergeJsonSourcesMissingFields } from 'src/app/utils/json-sources-utils';
|
|
7
|
+
|
|
8
|
+
export type UrlPreviewMessage = {
|
|
9
|
+
type?: string; // "url_preview"
|
|
10
|
+
text?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Parse and enrich "url_preview" messages into `JsonSourceItem[]`.
|
|
15
|
+
*
|
|
16
|
+
* Rules:
|
|
17
|
+
* - The payload is always read from `msg.text`, regardless of `activeMode`.
|
|
18
|
+
* - `msg.text` may be either:
|
|
19
|
+
* - a JSON array of source objects (`{source_name, source_file_name, ...}`), or
|
|
20
|
+
* - a plain string from which URLs are extracted (split by whitespace/punctuation).
|
|
21
|
+
* - After building the initial array, `url-preview` is called only for items that miss
|
|
22
|
+
* title or description, and missing fields are merged in (never overwriting).
|
|
23
|
+
*/
|
|
24
|
+
@Injectable({ providedIn: 'root' })
|
|
25
|
+
export class JsonSourcesParserService {
|
|
26
|
+
constructor(private urlPreviewService: UrlPreviewService) {}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Parse-only: returns sources immediately (no url-preview calls).
|
|
30
|
+
* Use this to render the list instantly, then call `enrichSources()` in background.
|
|
31
|
+
*/
|
|
32
|
+
parseBaseFromMessage(messageLike?: any): JsonSourceItem[] | null {
|
|
33
|
+
const payload = this.getUrlPreviewPayload(messageLike);
|
|
34
|
+
return this.parseBaseJsonSources(payload);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Parse + enrich: kept for backward compatibility with older callers.
|
|
39
|
+
* If you need instant rendering, prefer `parseBaseFromMessage()` + `enrichSources()`.
|
|
40
|
+
*/
|
|
41
|
+
/**
|
|
42
|
+
* Best-practice entrypoint for UI components:
|
|
43
|
+
* accepts a full `MessageModel`/message-like object, and supports url_preview payload
|
|
44
|
+
* living either on the root message OR inside `metadata` OR inside `attributes`.
|
|
45
|
+
*/
|
|
46
|
+
async parseFromMessage(messageLike?: any): Promise<JsonSourceItem[] | null> {
|
|
47
|
+
const base = this.parseBaseFromMessage(messageLike);
|
|
48
|
+
return this.enrichSources(base);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async enrichSources(baseSources?: JsonSourceItem[] | null): Promise<JsonSourceItem[] | null> {
|
|
52
|
+
const sources = (baseSources || []).filter((s) => !!s?.link);
|
|
53
|
+
if (sources.length === 0) return baseSources || null;
|
|
54
|
+
|
|
55
|
+
// Only call url-preview for items missing the most relevant fields.
|
|
56
|
+
const incompleteUrls = sources
|
|
57
|
+
.filter(s => !!s.link && (!s.title || !s.description))
|
|
58
|
+
.map(s => s.link!)
|
|
59
|
+
.slice(0, 10);
|
|
60
|
+
|
|
61
|
+
if (incompleteUrls.length === 0) return sources;
|
|
62
|
+
|
|
63
|
+
const previews = await this.urlPreviewService.previewUrls(incompleteUrls);
|
|
64
|
+
const previewItems: JsonSourceItem[] = (previews || []).map(p => ({
|
|
65
|
+
link: p.url,
|
|
66
|
+
title: p.title || p.siteName || p.url,
|
|
67
|
+
description: p.description,
|
|
68
|
+
image: p.image,
|
|
69
|
+
favicon: p.favicon,
|
|
70
|
+
favicon_hd: p.favicon_hd
|
|
71
|
+
}));
|
|
72
|
+
|
|
73
|
+
if (previewItems.length === 0) return sources;
|
|
74
|
+
return mergeJsonSourcesMissingFields(sources, previewItems);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async parseJsonSources(msg?: UrlPreviewMessage | null): Promise<JsonSourceItem[] | null> {
|
|
78
|
+
const base = this.parseBaseJsonSources(msg);
|
|
79
|
+
return this.enrichSources(base);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private getUrlPreviewPayload(messageLike?: any): UrlPreviewMessage | null {
|
|
83
|
+
if (!messageLike) return null;
|
|
84
|
+
const candidates: any[] = [
|
|
85
|
+
messageLike,
|
|
86
|
+
(messageLike?.metadata && typeof messageLike.metadata === 'object') ? messageLike.metadata : null,
|
|
87
|
+
(messageLike?.attributes && typeof messageLike.attributes === 'object') ? messageLike.attributes : null
|
|
88
|
+
].filter(Boolean);
|
|
89
|
+
return (candidates.find((c) => c?.type === 'url_preview') || null) as UrlPreviewMessage | null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private parseBaseJsonSources(msg?: UrlPreviewMessage | null): JsonSourceItem[] | null {
|
|
93
|
+
if (!msg || msg.type !== 'url_preview') return null;
|
|
94
|
+
|
|
95
|
+
// Regardless of `activeMode`, the payload is always read from `msg.text`.
|
|
96
|
+
// It can be either a JSON array of source objects, or a plain string with URLs.
|
|
97
|
+
return this.isJsonArrayOfObjects(msg.text)
|
|
98
|
+
? this.mapTextToSources(msg.text)
|
|
99
|
+
: this.mapListToSources(msg.text);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private mapListToSources(listValue?: string): JsonSourceItem[] | null {
|
|
103
|
+
const urls = extractUrlsFromText((listValue || '').toString(), 10);
|
|
104
|
+
return urls.length ? urls.map(u => ({ link: u, title: u })) : null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private isJsonArrayOfObjects(text?: string): boolean {
|
|
108
|
+
if (!text) return false;
|
|
109
|
+
try {
|
|
110
|
+
const parsed = this.parseJsonLenient(text);
|
|
111
|
+
return Array.isArray(parsed) && parsed.some(it => it && typeof it === 'object' && !Array.isArray(it));
|
|
112
|
+
} catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private mapTextToSources(text?: string): JsonSourceItem[] | null {
|
|
118
|
+
if (!text) return null;
|
|
119
|
+
try {
|
|
120
|
+
const parsed = this.parseJsonLenient(text);
|
|
121
|
+
return this.mapSourcesArray(parsed);
|
|
122
|
+
} catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private mapSourcesArray(input: any): JsonSourceItem[] | null {
|
|
128
|
+
const arr = Array.isArray(input) ? input : null;
|
|
129
|
+
if (!arr || arr.length === 0) return null;
|
|
130
|
+
const mapped = arr
|
|
131
|
+
.filter((s: any) => s && typeof s === 'object' && typeof s[JSON_SOURCE_FIELD_URL] === 'string')
|
|
132
|
+
.map((s: any): JsonSourceItem | null => {
|
|
133
|
+
const rawUrl = (s[JSON_SOURCE_FIELD_URL] || '').toString().trim();
|
|
134
|
+
const normalized = extractUrlsFromText(rawUrl, 1)[0];
|
|
135
|
+
if (!normalized) return null;
|
|
136
|
+
return {
|
|
137
|
+
link: normalized,
|
|
138
|
+
title: (s[JSON_SOURCE_FIELD_TITLE] || rawUrl).toString(),
|
|
139
|
+
description: typeof s.source_description === 'string' ? s.source_description : undefined,
|
|
140
|
+
image: typeof s.source_image === 'string' ? s.source_image : undefined
|
|
141
|
+
};
|
|
142
|
+
})
|
|
143
|
+
.filter((x: JsonSourceItem | null): x is JsonSourceItem => !!x && !!x.link);
|
|
144
|
+
return mapped.length ? mapped : null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private parseJsonLenient(input: string): any {
|
|
148
|
+
const trimmed = (input || '').trim();
|
|
149
|
+
try {
|
|
150
|
+
const parsed = JSON.parse(trimmed);
|
|
151
|
+
if (typeof parsed === 'string') {
|
|
152
|
+
const inner = parsed.trim();
|
|
153
|
+
if ((inner.startsWith('[') && inner.endsWith(']')) || (inner.startsWith('{') && inner.endsWith('}'))) {
|
|
154
|
+
return this.parseJsonLenient(inner);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return parsed;
|
|
158
|
+
} catch {
|
|
159
|
+
const cleaned = trimmed
|
|
160
|
+
.replace(/^```(?:json)?\s*/i, '')
|
|
161
|
+
.replace(/```$/i, '')
|
|
162
|
+
.trim()
|
|
163
|
+
.replace(/,\s*([}\]])/g, '$1');
|
|
164
|
+
const parsed = JSON.parse(cleaned);
|
|
165
|
+
if (typeof parsed === 'string') {
|
|
166
|
+
const inner = parsed.trim();
|
|
167
|
+
if ((inner.startsWith('[') && inner.endsWith(']')) || (inner.startsWith('{') && inner.endsWith('}'))) {
|
|
168
|
+
return this.parseJsonLenient(inner);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return parsed;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
@@ -234,12 +234,27 @@ export class TranslatorService {
|
|
|
234
234
|
this._translate.use(lang);
|
|
235
235
|
this.logger.debug(`[TRANSLATOR-SERV] »»»» initI18n - »»» loadRemoteTranslations ?`, environment.loadRemoteTranslations);
|
|
236
236
|
this._translate.setTranslation(lang, data, true);
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
237
|
+
this.syncDocumentLang(lang);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Synchronize the document `<html lang="...">` attribute with the active i18n language
|
|
242
|
+
* (WCAG 3.1.1 Language of Page, EN 301 549 § 9.3.1.1).
|
|
243
|
+
*
|
|
244
|
+
* The widget runs inside its own iframe, so we update the document of that iframe.
|
|
245
|
+
* If the widget is also embedded in a parent page (Tiledesk launcher), the parent
|
|
246
|
+
* page already declares its own `lang`, which we intentionally leave untouched.
|
|
247
|
+
*/
|
|
248
|
+
private syncDocumentLang(lang: string) {
|
|
249
|
+
if (!lang) { return; }
|
|
250
|
+
const normalized = lang.toLowerCase().substring(0, 2);
|
|
251
|
+
try {
|
|
252
|
+
if (typeof document !== 'undefined' && document.documentElement) {
|
|
253
|
+
document.documentElement.setAttribute('lang', normalized);
|
|
254
|
+
}
|
|
255
|
+
} catch (e) {
|
|
256
|
+
this.logger.warn('[TRANSLATOR-SERV] syncDocumentLang error', e);
|
|
257
|
+
}
|
|
243
258
|
}
|
|
244
259
|
|
|
245
260
|
/** */
|
|
@@ -302,6 +317,8 @@ export class TranslatorService {
|
|
|
302
317
|
'CLOSED',
|
|
303
318
|
'LABEL_PREVIEW',
|
|
304
319
|
'MAX_ATTACHMENT',
|
|
320
|
+
'EMOJI',
|
|
321
|
+
'BUTTON_OPEN_CHAT',
|
|
305
322
|
'MAX_ATTACHMENT_ERROR',
|
|
306
323
|
'EMOJI'
|
|
307
324
|
];
|
|
@@ -361,6 +378,7 @@ export class TranslatorService {
|
|
|
361
378
|
globals.MAX_ATTACHMENT = res['MAX_ATTACHMENT']
|
|
362
379
|
globals.MAX_ATTACHMENT_ERROR = res['MAX_ATTACHMENT_ERROR']
|
|
363
380
|
globals.EMOJI = res['EMOJI']
|
|
381
|
+
globals.BUTTON_OPEN_CHAT = res['BUTTON_OPEN_CHAT']
|
|
364
382
|
|
|
365
383
|
|
|
366
384
|
if(globals.WELCOME_TITLE === 'WELLCOME_TITLE') globals.WELCOME_TITLE = res['WELCOME_TITLE']
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { TestBed } from '@angular/core/testing';
|
|
2
|
+
import { TtsAudioPlaybackCoordinator } from './tts-audio-playback-coordinator.service';
|
|
3
|
+
|
|
4
|
+
describe('TtsAudioPlaybackCoordinator', () => {
|
|
5
|
+
let coordinator: TtsAudioPlaybackCoordinator;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
TestBed.configureTestingModule({ providers: [TtsAudioPlaybackCoordinator] });
|
|
9
|
+
coordinator = TestBed.inject(TtsAudioPlaybackCoordinator);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
// ── Basic lifecycle ───────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
it('should start playing immediately when nothing is active', () => {
|
|
15
|
+
const start = jasmine.createSpy('start');
|
|
16
|
+
coordinator.requestStart('msg-1', start);
|
|
17
|
+
expect(start).toHaveBeenCalledTimes(1);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('isTTSPlaying$ should be true while playing and false after release', () => {
|
|
21
|
+
const states: boolean[] = [];
|
|
22
|
+
coordinator.isTTSPlaying$.subscribe((v) => states.push(v));
|
|
23
|
+
|
|
24
|
+
coordinator.requestStart('msg-1', () => {});
|
|
25
|
+
coordinator.releaseIfCurrent('msg-1');
|
|
26
|
+
|
|
27
|
+
expect(states).toEqual([false, true, false]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('stopAll clears the queue, sets playing=false, and emits stopAllPlayback$', () => {
|
|
31
|
+
const stopNextSpy = spyOn((coordinator as any)._stopAll$, 'next').and.callThrough();
|
|
32
|
+
|
|
33
|
+
coordinator.requestStart('msg-1', () => {});
|
|
34
|
+
coordinator.stopAll();
|
|
35
|
+
|
|
36
|
+
expect(stopNextSpy).toHaveBeenCalledTimes(1);
|
|
37
|
+
const states: boolean[] = [];
|
|
38
|
+
coordinator.isTTSPlaying$.subscribe((v) => states.push(v));
|
|
39
|
+
expect(states).toEqual([false]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// ── Preemption tests (SPEC-002) ───────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
it('requestStart while playing preempts old owner: new start() is called immediately', () => {
|
|
45
|
+
const start1 = jasmine.createSpy('start1');
|
|
46
|
+
const start2 = jasmine.createSpy('start2');
|
|
47
|
+
|
|
48
|
+
coordinator.requestStart('msg-1', start1);
|
|
49
|
+
coordinator.requestStart('msg-2', start2);
|
|
50
|
+
|
|
51
|
+
expect(start1).toHaveBeenCalledTimes(1);
|
|
52
|
+
expect(start2).toHaveBeenCalledTimes(1); // started immediately, not queued
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('preemptPlayback$ emits evicted ownerId only (not the new owner)', () => {
|
|
56
|
+
const preempted: string[] = [];
|
|
57
|
+
coordinator.preemptPlayback$.subscribe((id) => preempted.push(id));
|
|
58
|
+
|
|
59
|
+
coordinator.requestStart('msg-1', () => {});
|
|
60
|
+
coordinator.requestStart('msg-2', () => {}); // preempts msg-1
|
|
61
|
+
|
|
62
|
+
expect(preempted).toEqual(['msg-1']);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('preemptPlayback$ does NOT emit the new owner id', () => {
|
|
66
|
+
const preempted: string[] = [];
|
|
67
|
+
coordinator.preemptPlayback$.subscribe((id) => preempted.push(id));
|
|
68
|
+
|
|
69
|
+
coordinator.requestStart('msg-1', () => {});
|
|
70
|
+
coordinator.requestStart('msg-2', () => {});
|
|
71
|
+
|
|
72
|
+
expect(preempted).not.toContain('msg-2');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('isTTSPlaying$ stays true after preemption until new owner releases', () => {
|
|
76
|
+
const states: boolean[] = [];
|
|
77
|
+
coordinator.isTTSPlaying$.subscribe((v) => states.push(v));
|
|
78
|
+
|
|
79
|
+
coordinator.requestStart('msg-1', () => {}); // true
|
|
80
|
+
coordinator.requestStart('msg-2', () => {}); // still true (preemption, new owner active)
|
|
81
|
+
coordinator.releaseIfCurrent('msg-2'); // false
|
|
82
|
+
|
|
83
|
+
expect(states).toEqual([false, true, false]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('releaseIfCurrent for an evicted owner is a no-op', () => {
|
|
87
|
+
const states: boolean[] = [];
|
|
88
|
+
coordinator.isTTSPlaying$.subscribe((v) => states.push(v));
|
|
89
|
+
|
|
90
|
+
coordinator.requestStart('msg-1', () => {});
|
|
91
|
+
coordinator.requestStart('msg-2', () => {}); // msg-1 evicted
|
|
92
|
+
|
|
93
|
+
// Old owner calls release after being preempted — should not affect playing state
|
|
94
|
+
coordinator.releaseIfCurrent('msg-1');
|
|
95
|
+
|
|
96
|
+
expect(states).toEqual([false, true]); // no extra false emission
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('chain of preemptions: each new requestStart immediately evicts the current owner', () => {
|
|
100
|
+
const preempted: string[] = [];
|
|
101
|
+
coordinator.preemptPlayback$.subscribe((id) => preempted.push(id));
|
|
102
|
+
|
|
103
|
+
coordinator.requestStart('msg-1', () => {});
|
|
104
|
+
coordinator.requestStart('msg-2', () => {});
|
|
105
|
+
coordinator.requestStart('msg-3', () => {});
|
|
106
|
+
|
|
107
|
+
expect(preempted).toEqual(['msg-1', 'msg-2']);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('requestStart is idempotent for the current owner', () => {
|
|
111
|
+
const start = jasmine.createSpy('start');
|
|
112
|
+
coordinator.requestStart('msg-1', start);
|
|
113
|
+
coordinator.requestStart('msg-1', start); // same owner — should be ignored
|
|
114
|
+
|
|
115
|
+
expect(start).toHaveBeenCalledTimes(1);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -1,17 +1,35 @@
|
|
|
1
1
|
import { Injectable } from '@angular/core';
|
|
2
|
+
import { BehaviorSubject, Observable, Subject } from 'rxjs';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Garantisce un solo messaggio TTS in riproduzione alla volta.
|
|
5
|
-
*
|
|
6
|
+
* Quando arriva un nuovo messaggio TTS mentre un altro è in corso, quello vecchio viene
|
|
7
|
+
* interrotto immediatamente (preemption) e il nuovo parte subito.
|
|
6
8
|
*/
|
|
7
9
|
@Injectable({ providedIn: 'root' })
|
|
8
10
|
export class TtsAudioPlaybackCoordinator {
|
|
9
11
|
private currentOwnerId: string | null = null;
|
|
10
12
|
private readonly queue: Array<{ ownerId: string; start: () => void }> = [];
|
|
11
13
|
|
|
14
|
+
/** Emits true while any TTS is playing or queued; false when the queue is fully drained. */
|
|
15
|
+
private readonly _isTTSPlaying$ = new BehaviorSubject<boolean>(false);
|
|
16
|
+
readonly isTTSPlaying$ = this._isTTSPlaying$.asObservable();
|
|
17
|
+
|
|
18
|
+
/** Emits once when stopAll() is called — signals every AudioSyncComponent to abort immediately. */
|
|
19
|
+
private readonly _stopAll$ = new Subject<void>();
|
|
20
|
+
readonly stopAllPlayback$: Observable<void> = this._stopAll$.asObservable();
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Emits the ownerId of the component being preempted (stopped mid-playback by a newer message).
|
|
24
|
+
* Only the component whose ownerId matches should react — unlike stopAll$ which targets everyone.
|
|
25
|
+
*/
|
|
26
|
+
private readonly _preemptCurrent$ = new Subject<string>();
|
|
27
|
+
readonly preemptPlayback$: Observable<string> = this._preemptCurrent$.asObservable();
|
|
28
|
+
|
|
12
29
|
/**
|
|
13
30
|
* Richiede l'avvio della riproduzione TTS per `ownerId`.
|
|
14
|
-
* Se
|
|
31
|
+
* Se un altro TTS è già in corso, viene interrotto immediatamente (preemption) e
|
|
32
|
+
* `ownerId` parte subito. Qualsiasi coda pendente viene svuotata.
|
|
15
33
|
*/
|
|
16
34
|
requestStart(ownerId: string, start: () => void): void {
|
|
17
35
|
const id = (ownerId || '').trim();
|
|
@@ -21,14 +39,22 @@ export class TtsAudioPlaybackCoordinator {
|
|
|
21
39
|
if (this.currentOwnerId === id) {
|
|
22
40
|
return;
|
|
23
41
|
}
|
|
24
|
-
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
42
|
+
|
|
27
43
|
if (this.currentOwnerId) {
|
|
28
|
-
|
|
29
|
-
|
|
44
|
+
// Preempt: signal only the evicted owner to stop (not a broadcast stopAll).
|
|
45
|
+
// This avoids stopping the component that is about to start playing.
|
|
46
|
+
const evicted = this.currentOwnerId;
|
|
47
|
+
this.queue.length = 0;
|
|
48
|
+
this.currentOwnerId = null;
|
|
49
|
+
this._preemptCurrent$.next(evicted);
|
|
50
|
+
} else {
|
|
51
|
+
this.queue.length = 0;
|
|
30
52
|
}
|
|
53
|
+
|
|
31
54
|
this.currentOwnerId = id;
|
|
55
|
+
if (!this._isTTSPlaying$.getValue()) {
|
|
56
|
+
this._isTTSPlaying$.next(true);
|
|
57
|
+
}
|
|
32
58
|
try {
|
|
33
59
|
start();
|
|
34
60
|
} catch {
|
|
@@ -54,6 +80,7 @@ export class TtsAudioPlaybackCoordinator {
|
|
|
54
80
|
this.currentOwnerId = null;
|
|
55
81
|
const next = this.queue.shift();
|
|
56
82
|
if (!next) {
|
|
83
|
+
this._isTTSPlaying$.next(false);
|
|
57
84
|
return;
|
|
58
85
|
}
|
|
59
86
|
this.currentOwnerId = next.ownerId;
|
|
@@ -68,4 +95,15 @@ export class TtsAudioPlaybackCoordinator {
|
|
|
68
95
|
release(ownerId: string): void {
|
|
69
96
|
this.releaseIfCurrent(ownerId);
|
|
70
97
|
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Stops all TTS playback immediately and clears the queue.
|
|
101
|
+
* Broadcasts on stopAllPlayback$ so every AudioSyncComponent can abort its stream and reveal all text.
|
|
102
|
+
*/
|
|
103
|
+
stopAll(): void {
|
|
104
|
+
this.queue.length = 0;
|
|
105
|
+
this.currentOwnerId = null;
|
|
106
|
+
this._isTTSPlaying$.next(false);
|
|
107
|
+
this._stopAll$.next();
|
|
108
|
+
}
|
|
71
109
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
|
2
|
+
import { Injectable } from '@angular/core';
|
|
3
|
+
import { firstValueFrom } from 'rxjs';
|
|
4
|
+
import { AppConfigService } from './app-config.service';
|
|
5
|
+
import { Globals } from '../utils/globals';
|
|
6
|
+
|
|
7
|
+
export type UrlPreviewItem = {
|
|
8
|
+
url: string;
|
|
9
|
+
title?: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
image?: string;
|
|
12
|
+
siteName?: string;
|
|
13
|
+
favicon?: string;
|
|
14
|
+
favicon_hd?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
@Injectable({
|
|
18
|
+
providedIn: 'root'
|
|
19
|
+
})
|
|
20
|
+
export class UrlPreviewService {
|
|
21
|
+
constructor(
|
|
22
|
+
private http: HttpClient,
|
|
23
|
+
private appConfigService: AppConfigService,
|
|
24
|
+
private g: Globals
|
|
25
|
+
) {}
|
|
26
|
+
|
|
27
|
+
async previewUrls(urls: string[]): Promise<UrlPreviewItem[]> {
|
|
28
|
+
const apiUrl = this.appConfigService.getConfig()?.apiUrl;
|
|
29
|
+
const projectId = this.g.projectid;
|
|
30
|
+
|
|
31
|
+
const cleaned = (urls || []).map((u) => (u || '').trim()).filter(Boolean).slice(0, 10);
|
|
32
|
+
if (!apiUrl || !projectId || cleaned.length === 0) return [];
|
|
33
|
+
|
|
34
|
+
const base = apiUrl.endsWith('/') ? apiUrl : apiUrl + '/';
|
|
35
|
+
const url = `${base}${projectId}/url-preview`;
|
|
36
|
+
|
|
37
|
+
const token = this.g.tiledeskToken;
|
|
38
|
+
const headers = new HttpHeaders({
|
|
39
|
+
Accept: 'application/json',
|
|
40
|
+
'Content-Type': 'application/json',
|
|
41
|
+
Authorization: token || ''
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const body = { urls: cleaned };
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const res = await firstValueFrom(
|
|
48
|
+
this.http.post<any>(url, body, { headers })
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// Expected response:
|
|
52
|
+
// [
|
|
53
|
+
// { url: "...", success: true, data: { url,title,description,image,siteName,... } },
|
|
54
|
+
// ...
|
|
55
|
+
// ]
|
|
56
|
+
// But we stay liberal and accept some alternative wrappers.
|
|
57
|
+
const items: any[] = Array.isArray(res)
|
|
58
|
+
? res
|
|
59
|
+
: (Array.isArray(res?.items) ? res.items : (Array.isArray(res?.data) ? res.data : []));
|
|
60
|
+
|
|
61
|
+
return (items || [])
|
|
62
|
+
.filter((x) => x && typeof x === 'object')
|
|
63
|
+
.filter((x) => x.success !== false) // keep true/undefined, drop explicit failures
|
|
64
|
+
.map((x) => {
|
|
65
|
+
const d = x.data && typeof x.data === 'object' ? x.data : x;
|
|
66
|
+
return {
|
|
67
|
+
url: (d.url || x.url || x.link || '').toString(),
|
|
68
|
+
title: typeof d.title === 'string' ? d.title : (typeof x.title === 'string' ? x.title : undefined),
|
|
69
|
+
description: typeof d.description === 'string' ? d.description : (typeof x.description === 'string' ? x.description : undefined),
|
|
70
|
+
image: typeof d.image === 'string' ? d.image : (typeof x.image === 'string' ? x.image : undefined),
|
|
71
|
+
siteName: typeof d.siteName === 'string' ? d.siteName : undefined,
|
|
72
|
+
favicon: typeof d.favicon === 'string' ? d.favicon : (typeof x.favicon === 'string' ? x.favicon : undefined),
|
|
73
|
+
favicon_hd: typeof d.favicon_hd === 'string' ? d.favicon_hd : (typeof x.favicon_hd === 'string' ? x.favicon_hd : undefined)
|
|
74
|
+
} as UrlPreviewItem;
|
|
75
|
+
})
|
|
76
|
+
.filter((x) => !!x.url);
|
|
77
|
+
} catch {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tipi condivisi per cattura microfono, VAD e registrazione (WebM).
|
|
3
3
|
*/
|
|
4
|
+
import type { VoiceStreamingSessionConfig } from './voice-streaming.types';
|
|
4
5
|
|
|
5
6
|
export const DEFAULT_VOICE_AUDIO_CONSTRAINTS: MediaTrackConstraints = {
|
|
6
7
|
echoCancellation: true,
|
|
@@ -31,4 +32,9 @@ export interface VoiceSessionStartOptions {
|
|
|
31
32
|
constraints?: MediaStreamConstraints;
|
|
32
33
|
/** Default `true`. Se `false`, non viene chiamato lo STT sul segmento. */
|
|
33
34
|
enableTranscription?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Con `voiceIngressStream`: solo streaming WSS — niente VAD locale; transcript e TTS dal server.
|
|
37
|
+
* Senza: MicVAD + segmenti e upload/STT lato client.
|
|
38
|
+
*/
|
|
39
|
+
voiceIngressStream?: VoiceStreamingSessionConfig | null;
|
|
34
40
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { TestBed } from '@angular/core/testing';
|
|
2
|
+
import { AppConfigService } from 'src/app/providers/app-config.service';
|
|
3
|
+
import { VoiceStreamingService } from './voice-streaming.service';
|
|
4
|
+
|
|
5
|
+
describe('VoiceStreamingService', () => {
|
|
6
|
+
let service: VoiceStreamingService;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
TestBed.configureTestingModule({
|
|
10
|
+
providers: [
|
|
11
|
+
{
|
|
12
|
+
provide: AppConfigService,
|
|
13
|
+
useValue: { getConfig: () => ({ voiceProxyWsUrl: '' }) },
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
});
|
|
17
|
+
service = TestBed.inject(VoiceStreamingService);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should be created', () => {
|
|
21
|
+
expect(service).toBeTruthy();
|
|
22
|
+
});
|
|
23
|
+
});
|