@chat21/chat21-web-widget 5.1.34 → 5.2.1
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/CHANGELOG.md +25 -0
- package/Dockerfile +4 -5
- package/README.md +1 -1
- package/angular.json +21 -3
- package/docs/ACCESSIBILITY-STATEMENT.md +388 -0
- package/docs/TILEDESK_WIDGET_ACCESSIBILITY_ALIGNMENT.md +60 -0
- package/docs/TILEDESK_WIDGET_ACCESSIBILITY_STATEMENT_COMPLETE.md +386 -0
- package/env.sample +3 -2
- package/mocks/voice-websocket-mock/server.cjs +245 -0
- package/package.json +10 -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 +13 -0
- package/src/app/component/conversation-detail/conversation/conversation.component.html +25 -11
- package/src/app/component/conversation-detail/conversation/conversation.component.scss +38 -0
- package/src/app/component/conversation-detail/conversation/conversation.component.spec.ts +644 -75
- package/src/app/component/conversation-detail/conversation/conversation.component.ts +70 -2
- 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 +23 -10
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.scss +18 -0
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.spec.ts +241 -149
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.ts +8 -5
- 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.html +203 -110
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss +212 -1
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.spec.ts +458 -78
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts +288 -76
- 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 -0
- package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.scss +83 -0
- package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.ts +192 -0
- 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 -5
- package/src/app/component/message/audio-sync/audio-sync.component.html +18 -0
- package/src/app/component/message/audio-sync/audio-sync.component.scss +65 -0
- package/src/app/component/message/audio-sync/audio-sync.component.spec.ts +112 -0
- package/src/app/component/message/audio-sync/audio-sync.component.ts +714 -0
- 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 +41 -51
- 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 +147 -57
- package/src/app/component/message/bubble-message/bubble-message.component.ts +95 -13
- 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 +6 -5
- package/src/app/component/message/json-sources/json-sources.component.scss +26 -18
- package/src/app/component/message/json-sources/json-sources.component.ts +41 -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 +40 -2
- package/src/app/providers/json-sources-parser.service.ts +13 -1
- package/src/app/providers/translator.service.ts +24 -7
- package/src/app/providers/tts-audio-playback-coordinator.service.spec.ts +116 -0
- package/src/app/providers/tts-audio-playback-coordinator.service.ts +122 -0
- package/src/app/providers/voice/STT&TTS/openai-voice.config.ts +12 -0
- package/src/app/providers/voice/STT&TTS/openai-voice.provider.ts +156 -0
- package/src/app/providers/voice/STT&TTS/speech-provider.abstract.ts +39 -0
- package/src/app/providers/voice/audio.types.ts +40 -0
- package/src/app/providers/voice/vad.service.spec.ts +28 -0
- package/src/app/providers/voice/vad.service.ts +70 -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 +227 -0
- package/src/app/providers/voice/voice.service.ts +969 -0
- package/src/app/sass/_variables.scss +2 -0
- package/src/app/sass/animations.scss +19 -1
- package/src/app/shims/onnxruntime-web-wasm.ts +4 -0
- package/src/app/utils/globals.ts +14 -0
- package/src/app/utils/utils-resources.ts +1 -1
- package/src/assets/i18n/en.json +128 -100
- package/src/assets/i18n/es.json +128 -100
- package/src/assets/i18n/fr.json +128 -100
- package/src/assets/i18n/it.json +128 -98
- package/src/assets/onnx/ort-wasm-simd-threaded.mjs +59 -0
- package/src/assets/onnx/ort-wasm-simd-threaded.wasm +0 -0
- package/src/assets/sounds/keyboard.mp3 +0 -0
- package/src/assets/vad/silero_vad_legacy.onnx +0 -0
- package/src/assets/vad/vad.worklet.bundle.min.js +1 -0
- package/src/chat21-core/models/message.ts +2 -1
- package/src/chat21-core/providers/chat-manager.spec.ts +72 -0
- package/src/chat21-core/providers/firebase/firebase-conversation-handler.ts +3 -2
- package/src/chat21-core/providers/firebase/firebase-init-service.ts +5 -5
- package/src/chat21-core/providers/mqtt/mqtt-conversation-handler.ts +12 -0
- package/src/chat21-core/providers/scripts/script.service.spec.ts +12 -2
- package/src/chat21-core/utils/utils-message.ts +7 -0
- 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
- package/tsconfig.json +5 -0
|
@@ -161,6 +161,11 @@ export class ConversationComponent implements OnInit, AfterViewInit, OnChanges {
|
|
|
161
161
|
membersConversation = ['SYSTEM'];
|
|
162
162
|
// ========== end:: typying =======
|
|
163
163
|
|
|
164
|
+
// ========== begin:: stream audio ======= //
|
|
165
|
+
public isStreamAudioActive = false;
|
|
166
|
+
public isStreamAudioConnecting = false;
|
|
167
|
+
// ========== end:: stream audio ======= //
|
|
168
|
+
|
|
164
169
|
@ViewChild(ConversationFooterComponent) conversationFooter: ConversationFooterComponent
|
|
165
170
|
@ViewChild(ConversationContentComponent) conversationContent: ConversationContentComponent
|
|
166
171
|
conversationHandlerService: ConversationHandlerService
|
|
@@ -246,7 +251,21 @@ export class ConversationComponent implements OnInit, AfterViewInit, OnChanges {
|
|
|
246
251
|
'CONTINUE',
|
|
247
252
|
'EMOJI_NOT_ELLOWED',
|
|
248
253
|
'ATTACHMENT',
|
|
249
|
-
'EMOJI'
|
|
254
|
+
'EMOJI',
|
|
255
|
+
'BUTTON_ATTACH_FILE',
|
|
256
|
+
'BUTTON_SEND_MESSAGE',
|
|
257
|
+
'BUTTON_RECORD_AUDIO',
|
|
258
|
+
'BUTTON_DELETE_AUDIO',
|
|
259
|
+
'BUTTON_SEND_AUDIO',
|
|
260
|
+
'BUTTON_PLAY_AUDIO',
|
|
261
|
+
'BUTTON_PAUSE_AUDIO',
|
|
262
|
+
'SKIP_TO_COMPOSER',
|
|
263
|
+
'CLOSE_CHAT',
|
|
264
|
+
'CLOSE',
|
|
265
|
+
'VOICE_CONNECTING',
|
|
266
|
+
'VOICE_LISTENING',
|
|
267
|
+
'VOICE_PROCESSING',
|
|
268
|
+
'STREAM_AUDIO'
|
|
250
269
|
];
|
|
251
270
|
|
|
252
271
|
const keysContent = [
|
|
@@ -266,13 +285,21 @@ export class ConversationComponent implements OnInit, AfterViewInit, OnChanges {
|
|
|
266
285
|
'LABEL_THINKING',
|
|
267
286
|
'LABEL_TO',
|
|
268
287
|
'ARRAY_DAYS',
|
|
288
|
+
'CONVERSATION_LOG_LABEL',
|
|
289
|
+
'BUTTON_SCROLL_TO_BOTTOM',
|
|
290
|
+
'CAROUSEL_PREVIOUS',
|
|
291
|
+
'CAROUSEL_NEXT',
|
|
292
|
+
'CAROUSEL_LABEL',
|
|
293
|
+
'CAROUSEL_SLIDE_LABEL'
|
|
269
294
|
];
|
|
270
295
|
|
|
271
296
|
const keysPreview= [
|
|
272
297
|
'BACK',
|
|
273
298
|
'CLOSE',
|
|
274
299
|
'LABEL_PLACEHOLDER',
|
|
275
|
-
'LABEL_PREVIEW'
|
|
300
|
+
'LABEL_PREVIEW',
|
|
301
|
+
'BUTTON_CLOSE_PREVIEW',
|
|
302
|
+
'BUTTON_SEND_MESSAGE'
|
|
276
303
|
];
|
|
277
304
|
|
|
278
305
|
const keysCloseChatDialog= [
|
|
@@ -822,6 +849,10 @@ export class ConversationComponent implements OnInit, AfterViewInit, OnChanges {
|
|
|
822
849
|
this.showThinkingMessage = false;
|
|
823
850
|
}
|
|
824
851
|
|
|
852
|
+
if (this.isStreamAudioActive && msg.sender !== this.senderId) {
|
|
853
|
+
this.conversationFooter?.interruptStreamDueToPeerMessage();
|
|
854
|
+
}
|
|
855
|
+
|
|
825
856
|
that.newMessageAdded(msg);
|
|
826
857
|
// Update badge based on the latest message received from the server.
|
|
827
858
|
// We rely on `messages` being kept in-sync by the conversation handler.
|
|
@@ -1032,6 +1063,21 @@ export class ConversationComponent implements OnInit, AfterViewInit, OnChanges {
|
|
|
1032
1063
|
|
|
1033
1064
|
|
|
1034
1065
|
|
|
1066
|
+
/**
|
|
1067
|
+
* Programmatically moves keyboard focus to the message composer textarea.
|
|
1068
|
+
* Wired to the visible-on-focus skip link in conversation.component.html (WCAG 2.4.1 Bypass Blocks).
|
|
1069
|
+
*/
|
|
1070
|
+
skipToCompose() {
|
|
1071
|
+
try {
|
|
1072
|
+
const textarea = document.getElementById('chat21-main-message-context') as HTMLTextAreaElement | null;
|
|
1073
|
+
if (textarea) {
|
|
1074
|
+
textarea.focus();
|
|
1075
|
+
}
|
|
1076
|
+
} catch(e) {
|
|
1077
|
+
this.logger.warn('[CONV-COMP] skipToCompose error', e);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1035
1081
|
scrollToBottom() {
|
|
1036
1082
|
this.conversationContent.scrollToBottom();
|
|
1037
1083
|
// const that = this;
|
|
@@ -1383,6 +1429,28 @@ export class ConversationComponent implements OnInit, AfterViewInit, OnChanges {
|
|
|
1383
1429
|
this.logger.debug('[CONV-COMP] floating onNewConversationButtonClicked')
|
|
1384
1430
|
this.onNewConversationButtonClicked.emit()
|
|
1385
1431
|
}
|
|
1432
|
+
|
|
1433
|
+
/** CALLED BY: conv-footer component */
|
|
1434
|
+
onStreamAudioActiveChange(event: boolean){
|
|
1435
|
+
this.isStreamAudioActive = event
|
|
1436
|
+
}
|
|
1437
|
+
/** CALLED BY: conv-footer when connecting state changes */
|
|
1438
|
+
onStreamAudioConnectingChange(event: boolean){
|
|
1439
|
+
this.isStreamAudioConnecting = event
|
|
1440
|
+
}
|
|
1441
|
+
/** CALLED BY: conv-footer component */
|
|
1442
|
+
onCloseChatButtonClickedFN(event){
|
|
1443
|
+
this.logger.debug('[CONV-COMP] onCloseChatButtonClicked::::', event)
|
|
1444
|
+
this.onCloseChat()
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
/**
|
|
1448
|
+
* True quando è visibile il pulsante chiudi stream (`.close-stream-button`, `isStreamAudioActive`).
|
|
1449
|
+
* Solo in quel caso il bottom del foglio include `--chat-footer-stream-button-height`.
|
|
1450
|
+
*/
|
|
1451
|
+
closeStreamButtonActiveForSheetBottom(): boolean {
|
|
1452
|
+
return !!(this.g?.showAudioStreamFooterButton && (this.isStreamAudioActive || this.isStreamAudioConnecting));
|
|
1453
|
+
}
|
|
1386
1454
|
// =========== END: event emitter function ====== //
|
|
1387
1455
|
|
|
1388
1456
|
|
|
@@ -1,32 +1,44 @@
|
|
|
1
1
|
<div class="audio-recorder">
|
|
2
|
-
<button *ngIf="audioUrl" (click)="deleteRecording()">
|
|
2
|
+
<button *ngIf="audioUrl" type="button" [attr.aria-label]="translationMap?.get('BUTTON_DELETE_AUDIO') || 'Delete recording'" (click)="deleteRecording()">
|
|
3
3
|
<span class="v-align-center">
|
|
4
|
-
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px">
|
|
4
|
+
<svg aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px">
|
|
5
5
|
<path d="M280-120q-33 0-56.5-23.5T200-200v-520h-40v-80h200v-40h240v40h200v80h-40v520q0 33-23.5 56.5T680-120H280Zm80-160h80v-360h-80v360Zm160 0h80v-360h-80v360Z"/>
|
|
6
6
|
</svg>
|
|
7
|
-
<!-- <i class="material-icons">delete_outline</i> -->
|
|
8
7
|
</span>
|
|
9
8
|
</button>
|
|
10
9
|
|
|
11
|
-
<chat-audio
|
|
12
|
-
[audioBlob]
|
|
10
|
+
<chat-audio class="test" *ngIf="audioBlob && audioUrl"
|
|
11
|
+
[audioBlob]="audioBlob"
|
|
13
12
|
[color]="'var(--chat-footer-color)'"
|
|
13
|
+
[translationMap]="translationMap"
|
|
14
14
|
[stylesMap]="stylesMap">
|
|
15
15
|
</chat-audio>
|
|
16
|
-
|
|
17
|
-
<button *ngIf="!audioUrl"
|
|
18
|
-
|
|
16
|
+
|
|
17
|
+
<button *ngIf="!audioUrl"
|
|
18
|
+
type="button"
|
|
19
|
+
class="mic-button"
|
|
20
|
+
[attr.aria-label]="translationMap?.get('BUTTON_RECORD_AUDIO') || 'Hold to record an audio message'"
|
|
21
|
+
[attr.aria-pressed]="isRecording ? 'true' : 'false'"
|
|
22
|
+
(mousedown)="startRecording($event)"
|
|
23
|
+
(mouseup)="stopRecording($event)"
|
|
24
|
+
(touchstart)="startRecording($event)"
|
|
25
|
+
(touchend)="stopRecording($event)"
|
|
26
|
+
(keydown.space)="$event.preventDefault(); !isRecording && startRecording($event)"
|
|
27
|
+
(keyup.space)="$event.preventDefault(); isRecording && stopRecording($event)">
|
|
28
|
+
<svg aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px">
|
|
19
29
|
<path d="M480-400q-50 0-85-35t-35-85v-240q0-50 35-85t85-35q50 0 85 35t35 85v240q0 50-35 85t-85 35Zm0-240Zm-40 520v-123q-104-14-172-93t-68-184h80q0 83 58.5 141.5T480-320q83 0 141.5-58.5T680-520h80q0 105-68 184t-172 93v123h-80Zm40-360q17 0 28.5-11.5T520-520v-240q0-17-11.5-28.5T480-800q-17 0-28.5 11.5T440-760v240q0 17 11.5 28.5T480-480Z"/>
|
|
20
30
|
</svg>
|
|
21
31
|
</button>
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
32
|
+
|
|
33
|
+
<button *ngIf="audioUrl"
|
|
34
|
+
type="button"
|
|
35
|
+
[attr.aria-label]="translationMap?.get('BUTTON_SEND_AUDIO') || 'Send audio message'"
|
|
36
|
+
(click)="sendMessage()">
|
|
25
37
|
<span class="v-align-center">
|
|
26
|
-
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
38
|
+
<svg aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="20" width="24" viewBox="0 0 24 20" xml:space="preserve">
|
|
27
39
|
<path d="M1.8,18.9V1.7L22,10.3L1.8,18.9z M3.9,15.6l12.6-5.4L3.9,4.9v3.7l6.4,1.6l-6.4,1.6V15.6z M3.9,15.6V4.9v7V15.6z"/>
|
|
28
40
|
</svg>
|
|
29
41
|
</span>
|
|
30
42
|
</button>
|
|
31
43
|
|
|
32
|
-
</div>
|
|
44
|
+
</div>
|
|
@@ -1,23 +1,141 @@
|
|
|
1
|
-
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
1
|
+
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
|
|
2
2
|
|
|
3
3
|
import { ConversationAudioRecorderComponent } from './conversation-audio-recorder.component';
|
|
4
4
|
|
|
5
|
-
describe('
|
|
5
|
+
describe('ConversationAudioRecorderComponent', () => {
|
|
6
6
|
let component: ConversationAudioRecorderComponent;
|
|
7
7
|
let fixture: ComponentFixture<ConversationAudioRecorderComponent>;
|
|
8
|
+
let stopListeners: { stop?: () => void; data?: (e: { data: Blob }) => void };
|
|
9
|
+
let mediaRecorderInstance: {
|
|
10
|
+
start: jasmine.Spy;
|
|
11
|
+
stop: jasmine.Spy;
|
|
12
|
+
mimeType: string;
|
|
13
|
+
addEventListener: jasmine.Spy;
|
|
14
|
+
};
|
|
8
15
|
|
|
9
16
|
beforeEach(async () => {
|
|
17
|
+
stopListeners = {};
|
|
18
|
+
mediaRecorderInstance = {
|
|
19
|
+
start: jasmine.createSpy('start'),
|
|
20
|
+
stop: jasmine.createSpy('stop').and.callFake(() => {
|
|
21
|
+
const fn = stopListeners.stop;
|
|
22
|
+
if (fn) {
|
|
23
|
+
fn();
|
|
24
|
+
}
|
|
25
|
+
}),
|
|
26
|
+
mimeType: 'audio/webm',
|
|
27
|
+
addEventListener: jasmine.createSpy('addEventListener').and.callFake((ev: string, fn: any) => {
|
|
28
|
+
if (ev === 'stop') {
|
|
29
|
+
stopListeners.stop = fn;
|
|
30
|
+
}
|
|
31
|
+
if (ev === 'dataavailable') {
|
|
32
|
+
stopListeners.data = fn;
|
|
33
|
+
}
|
|
34
|
+
}),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const stream = {
|
|
38
|
+
getTracks: () => [{ stop: jasmine.createSpy('trackStop') }],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
spyOn(window.navigator.mediaDevices, 'getUserMedia').and.returnValue(Promise.resolve(stream as any));
|
|
42
|
+
(window as any).MediaRecorder = jasmine.createSpy('MediaRecorder').and.returnValue(mediaRecorderInstance);
|
|
43
|
+
|
|
10
44
|
await TestBed.configureTestingModule({
|
|
11
|
-
declarations: [
|
|
12
|
-
})
|
|
13
|
-
.compileComponents();
|
|
45
|
+
declarations: [ConversationAudioRecorderComponent],
|
|
46
|
+
}).compileComponents();
|
|
14
47
|
|
|
15
48
|
fixture = TestBed.createComponent(ConversationAudioRecorderComponent);
|
|
16
49
|
component = fixture.componentInstance;
|
|
50
|
+
component.translationMap = new Map();
|
|
51
|
+
component.stylesMap = new Map();
|
|
52
|
+
spyOn(component.startRecordingEvent, 'emit');
|
|
53
|
+
spyOn(component.endRecordingEvent, 'emit');
|
|
17
54
|
fixture.detectChanges();
|
|
18
55
|
});
|
|
19
56
|
|
|
20
57
|
it('should create', () => {
|
|
21
58
|
expect(component).toBeTruthy();
|
|
22
59
|
});
|
|
60
|
+
|
|
61
|
+
describe('startRecording', () => {
|
|
62
|
+
it('should preventDefault on touchstart', fakeAsync(() => {
|
|
63
|
+
const ev = { type: 'touchstart', preventDefault: jasmine.createSpy('pd') } as any;
|
|
64
|
+
component.startRecording(ev);
|
|
65
|
+
tick();
|
|
66
|
+
expect(ev.preventDefault).toHaveBeenCalled();
|
|
67
|
+
expect(component.startRecordingEvent.emit).toHaveBeenCalled();
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
it('should request microphone and start MediaRecorder on mousedown', fakeAsync(() => {
|
|
71
|
+
const ev = new MouseEvent('mousedown');
|
|
72
|
+
component.startRecording(ev);
|
|
73
|
+
tick();
|
|
74
|
+
expect(navigator.mediaDevices.getUserMedia).toHaveBeenCalledWith({ audio: true });
|
|
75
|
+
expect(mediaRecorderInstance.start).toHaveBeenCalled();
|
|
76
|
+
expect(component.isRecording).toBe(true);
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
it('should log when getUserMedia fails', fakeAsync(() => {
|
|
80
|
+
(navigator.mediaDevices.getUserMedia as jasmine.Spy).and.returnValue(Promise.reject(new Error('denied')));
|
|
81
|
+
spyOn(console, 'error');
|
|
82
|
+
component.startRecording(new MouseEvent('mousedown'));
|
|
83
|
+
tick();
|
|
84
|
+
expect(console.error).toHaveBeenCalled();
|
|
85
|
+
}));
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('stopRecording', () => {
|
|
89
|
+
it('should discard very short press without stopping recorder', fakeAsync(() => {
|
|
90
|
+
component.startTime = Date.now();
|
|
91
|
+
component.stopRecording(new MouseEvent('mouseup'));
|
|
92
|
+
tick(400);
|
|
93
|
+
expect(mediaRecorderInstance.stop).not.toHaveBeenCalled();
|
|
94
|
+
}));
|
|
95
|
+
|
|
96
|
+
it('should stop recorder after long press', fakeAsync(() => {
|
|
97
|
+
component.mediaRecorder = mediaRecorderInstance as any;
|
|
98
|
+
component.startTime = Date.now() - 600;
|
|
99
|
+
component.stopRecording(new MouseEvent('mouseup'));
|
|
100
|
+
tick(400);
|
|
101
|
+
expect(mediaRecorderInstance.stop).toHaveBeenCalled();
|
|
102
|
+
}));
|
|
103
|
+
|
|
104
|
+
it('should preventDefault on touchend', () => {
|
|
105
|
+
const ev = { type: 'touchend', preventDefault: jasmine.createSpy('pd') } as any;
|
|
106
|
+
component.stopRecording(ev);
|
|
107
|
+
expect(ev.preventDefault).toHaveBeenCalled();
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('deleteRecording', () => {
|
|
112
|
+
it('should reset state and emit', () => {
|
|
113
|
+
spyOn(component.deleteRecordingEvent, 'emit');
|
|
114
|
+
component.audioUrl = {} as any;
|
|
115
|
+
component.audioBlob = new Blob();
|
|
116
|
+
component.deleteRecording();
|
|
117
|
+
expect(component.audioUrl).toBeNull();
|
|
118
|
+
expect(component.audioBlob).toBeNull();
|
|
119
|
+
expect(component.deleteRecordingEvent.emit).toHaveBeenCalledWith(null);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('sendMessage', () => {
|
|
124
|
+
it('should emit blob and clear url when recording exists', () => {
|
|
125
|
+
spyOn(component.sendRecordingEvent, 'emit');
|
|
126
|
+
const b = new Blob(['a'], { type: 'audio/webm' });
|
|
127
|
+
component.audioBlob = b;
|
|
128
|
+
component.audioUrl = {} as any;
|
|
129
|
+
component.sendMessage();
|
|
130
|
+
expect(component.sendRecordingEvent.emit).toHaveBeenCalledWith(b);
|
|
131
|
+
expect(component.audioUrl).toBeNull();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should no-op when there is no audioUrl', () => {
|
|
135
|
+
spyOn(component.sendRecordingEvent, 'emit');
|
|
136
|
+
component.audioUrl = null;
|
|
137
|
+
component.sendMessage();
|
|
138
|
+
expect(component.sendRecordingEvent.emit).not.toHaveBeenCalled();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
23
141
|
});
|
|
@@ -9,6 +9,7 @@ import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
|
|
|
9
9
|
export class ConversationAudioRecorderComponent {
|
|
10
10
|
|
|
11
11
|
@Input() stylesMap: Map<string, string>;
|
|
12
|
+
@Input() translationMap: Map<string, string>;
|
|
12
13
|
@Output() startRecordingEvent = new EventEmitter<void>();
|
|
13
14
|
@Output() deleteRecordingEvent = new EventEmitter<void>();
|
|
14
15
|
@Output() endRecordingEvent = new EventEmitter<Blob | null>();
|
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
<div class="c21-body-container">
|
|
4
4
|
|
|
5
|
-
<div class="c21-body-content"
|
|
5
|
+
<div class="c21-body-content"
|
|
6
|
+
role="log"
|
|
7
|
+
aria-live="polite"
|
|
8
|
+
aria-relevant="additions text"
|
|
9
|
+
aria-atomic="false"
|
|
10
|
+
[attr.aria-label]="translationMap?.get('CONVERSATION_LOG_LABEL') || 'Conversation messages'">
|
|
6
11
|
|
|
7
12
|
<!-- USER TYPING (WAIT MESSAGE) -->
|
|
8
13
|
<span *ngIf="messages && this.messages.length === 0 && !isTypings">
|
|
@@ -19,21 +24,22 @@
|
|
|
19
24
|
<div #scrollMe id="scroll-me" (scroll)="onScroll($event)">
|
|
20
25
|
|
|
21
26
|
<div id="{{idDivScroll}}" class="c21-contentScroll" > <!-- (resized)="onResized($event)" -->
|
|
22
|
-
<div *ngFor="let message of messages; let first = first; let last = last; let i = index"
|
|
23
|
-
|
|
27
|
+
<div *ngFor="let message of messages; let first = first; let last = last; let i = index" class="rowMsg">
|
|
28
|
+
|
|
24
29
|
<!-- message SENDER:: -->
|
|
25
|
-
<div role="
|
|
30
|
+
<div role="article" *ngIf="messageType(MESSAGE_TYPE_MINE, message) && !message.isJustRecived" class="msg_container base_sent">
|
|
26
31
|
|
|
27
32
|
<!--backgroundColor non viene ancora usato -->
|
|
28
33
|
<!-- class="messages msg_sent slide-in-right" -->
|
|
29
34
|
<chat-bubble-message class="messages msg_sent"
|
|
30
|
-
[class.no-background]="(isImage(message) || isFrame(message)) && ((message?.text && message?.text.trim() === '') || !message?.text)"
|
|
35
|
+
[class.no-background]="(isImage(message) || isFrame(message)) && ((message?.text && message?.text.trim() === '') || !message?.text)"
|
|
31
36
|
[class.emoticon]="isEmojii(message?.text)"
|
|
32
37
|
[ngStyle]="{'background': stylesMap.get('bubbleSentBackground'), 'color': stylesMap.get('bubbleSentTextColor')}"
|
|
33
38
|
[ngClass]="{'button-in-msg' : message?.metadata && message?.metadata?.button}"
|
|
34
39
|
[message]="message"
|
|
35
40
|
[fontColor]="stylesMap.get('bubbleSentTextColor')"
|
|
36
41
|
[stylesMap]="stylesMap"
|
|
42
|
+
[translationMap]="translationMap"
|
|
37
43
|
(onBeforeMessageRender)="onBeforeMessageRenderFN($event)"
|
|
38
44
|
(onAfterMessageRender)="onAfterMessageRenderFN($event)"
|
|
39
45
|
(onElementRendered)="onElementRenderedFN($event)">
|
|
@@ -47,9 +53,9 @@
|
|
|
47
53
|
</div>
|
|
48
54
|
|
|
49
55
|
<!-- message RECIPIENT:: -->
|
|
50
|
-
<div role="
|
|
56
|
+
<div role="article" *ngIf="messageType(MESSAGE_TYPE_OTHERS, message)" class="msg_container base_receive">
|
|
51
57
|
|
|
52
|
-
<chat-avatar-image *ngIf="!isSameSender(message?.sender, i)"
|
|
58
|
+
<chat-avatar-image *ngIf="!isSameSender(message?.sender, i) && !isStreamAudioActive"
|
|
53
59
|
[ngClass]="{'slide-in-left': false}"
|
|
54
60
|
[senderID]="message?.sender"
|
|
55
61
|
[senderFullname]="message?.sender_fullname"
|
|
@@ -60,14 +66,17 @@
|
|
|
60
66
|
<!-- [ngClass]="{'slide-in-left': !isFirstMessage(message?.sender, i)}" -->
|
|
61
67
|
<chat-bubble-message class="messages msg_receive"
|
|
62
68
|
[ngClass]="{'slide-in-left': false}"
|
|
63
|
-
[class.no-background]="(isImage(message) || isFrame(message) || isCarousel(message)) && ((message?.text && message?.text.trim() === '') || !message?.text)"
|
|
69
|
+
[class.no-background]="(isImage(message) || isFrame(message) || isCarousel(message)) && ((message?.text && message?.text.trim() === '') || !message?.text)"
|
|
64
70
|
[class.emoticon]="isEmojii(message?.text)"
|
|
65
|
-
[
|
|
71
|
+
[class.fullSizeMessage]="isStreamAudioActive"
|
|
72
|
+
[style.margin-left]="isSameSender(message?.sender, i) && !isStreamAudioActive ? 'calc(var(--avatar-width) + 10px)' : null"
|
|
66
73
|
[ngStyle]="{'background': stylesMap.get('bubbleReceivedBackground'), 'color': stylesMap.get('bubbleReceivedTextColor'), 'width':isFrame(message) ?'100%' : null}"
|
|
67
74
|
[isSameSender]="isSameSender(message?.sender, i)"
|
|
68
75
|
[message]="message"
|
|
69
76
|
[fontColor]="stylesMap.get('bubbleReceivedTextColor')"
|
|
70
77
|
[stylesMap]="stylesMap"
|
|
78
|
+
[translationMap]="translationMap"
|
|
79
|
+
[streamOnArrival]="false"
|
|
71
80
|
(onBeforeMessageRender)="onBeforeMessageRenderFN($event)"
|
|
72
81
|
(onAfterMessageRender)="onAfterMessageRenderFN($event)"
|
|
73
82
|
(onElementRendered)="onElementRenderedFN($event)">
|
|
@@ -110,6 +119,7 @@
|
|
|
110
119
|
[isConversationArchived]="isConversationArchived"
|
|
111
120
|
[isLastMessage] = "isLastMessage(message?.uid)"
|
|
112
121
|
[stylesMap]="stylesMap"
|
|
122
|
+
[translationMap]="translationMap"
|
|
113
123
|
(onElementRendered)="onElementRenderedFN($event)"
|
|
114
124
|
(onAttachmentButtonClicked)="onAttachmentButtonClickedFN($event)">
|
|
115
125
|
</chat-carousel>
|
|
@@ -134,9 +144,10 @@
|
|
|
134
144
|
[senderFullname]="nameUserTypingNow"
|
|
135
145
|
[baseLocation]="baseLocation">
|
|
136
146
|
</chat-avatar-image>
|
|
147
|
+
|
|
137
148
|
<user-typing
|
|
138
|
-
[ngClass]="{'userTypingNowExist': !idUserTypingNow}"
|
|
139
149
|
[color]="stylesMap?.get('iconColor')"
|
|
150
|
+
[ngClass]="{'userTypingNowExist': !idUserTypingNow}"
|
|
140
151
|
[translationMap]="translationMap"
|
|
141
152
|
[idUserTypingNow]="idUserTypingNow"
|
|
142
153
|
[nameUserTypingNow]="nameUserTypingNow">
|
|
@@ -145,7 +156,9 @@
|
|
|
145
156
|
|
|
146
157
|
<div *ngIf="showThinkingMessage && lastServerSenderKind === 'bot'" class="msg_container base_receive thinking_receive">
|
|
147
158
|
<user-typing class="loading thinking-dots"
|
|
159
|
+
[class.fullSize]="isStreamAudioActive"
|
|
148
160
|
[color]="stylesMap?.get('iconColor')"
|
|
161
|
+
[class.fullSize]="isStreamAudioActive"
|
|
149
162
|
[translationMap]="translationMap"
|
|
150
163
|
[idUserTypingNow]="idUserTypingNow"
|
|
151
164
|
[nameUserTypingNow]="nameUserTypingNow">
|
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
margin: 25px 50px
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
:host .loading.fullSize ::ng-deep > div.spinner{
|
|
31
|
+
margin: 50px 0px !important;
|
|
32
|
+
}
|
|
33
|
+
|
|
30
34
|
// ============= CSS c21-body ================= //
|
|
31
35
|
.c21-body {
|
|
32
36
|
// -webkit-box-shadow: inset 0 10px 10px -10px rgba(0,0,0,0.4);
|
|
@@ -242,6 +246,11 @@
|
|
|
242
246
|
height: fit-content;
|
|
243
247
|
width: auto;
|
|
244
248
|
|
|
249
|
+
&.fullSizeMessage {
|
|
250
|
+
max-width: 100%;
|
|
251
|
+
margin: auto 0 auto 0 !important;
|
|
252
|
+
}
|
|
253
|
+
|
|
245
254
|
}
|
|
246
255
|
.msg_receive.json-resources{
|
|
247
256
|
min-height: unset;
|
|
@@ -283,6 +292,15 @@
|
|
|
283
292
|
}// end c21-body-container
|
|
284
293
|
}// end c21-body
|
|
285
294
|
|
|
295
|
+
/* Solo con pulsante chiudi stream (stream in ascolto): altezza extra come #streamAudioAlert */
|
|
296
|
+
:host-context(#chat21-conversation-component.chat21-conversation--close-stream-active) .c21-body .c21-body-container .c21-body-content .chat21-sheet-content {
|
|
297
|
+
bottom: calc(
|
|
298
|
+
var(--chat-footer-logo-height) +
|
|
299
|
+
var(--chat-footer-height) +
|
|
300
|
+
var(--chat-footer-stream-button-height)
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
286
304
|
@keyframes thinking-dot {
|
|
287
305
|
0%, 80%, 100% {
|
|
288
306
|
opacity: 0.2;
|