@chat21/chat21-web-widget 5.1.30 → 5.1.32-rc1
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/.github/workflows/docker-community-push-latest.yml +23 -13
- package/.github/workflows/docker-image-tag-community-tag-push.yml +22 -12
- package/CHANGELOG.md +38 -2
- package/Dockerfile +4 -5
- package/angular.json +5 -2
- package/docs/changelog/this-branch.md +36 -0
- package/package.json +4 -1
- package/src/app/app.component.ts +10 -9
- package/src/app/app.module.ts +9 -0
- package/src/app/component/conversation-detail/conversation/conversation.component.html +7 -1
- package/src/app/component/conversation-detail/conversation/conversation.component.scss +2 -2
- package/src/app/component/conversation-detail/conversation/conversation.component.ts +34 -5
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.html +2 -2
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.scss +1 -1
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.ts +2 -0
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.html +143 -78
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss +131 -13
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts +108 -7
- package/src/app/component/last-message/last-message.component.ts +4 -1
- package/src/app/component/message/audio/audio.component.ts +0 -5
- package/src/app/component/message/audio-sync/audio-sync.component.html +19 -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 +23 -0
- package/src/app/component/message/audio-sync/audio-sync.component.ts +197 -0
- package/src/app/component/message/bubble-message/bubble-message.component.html +6 -1
- package/src/app/component/message/bubble-message/bubble-message.component.ts +2 -1
- package/src/app/providers/global-settings.service.ts +21 -0
- package/src/app/providers/translator.service.ts +2 -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 +171 -0
- package/src/app/providers/voice/STT&TTS/speech-provider.abstract.ts +39 -0
- package/src/app/providers/voice/audio.types.ts +34 -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.service.spec.ts +60 -0
- package/src/app/providers/voice/voice.service.ts +264 -0
- package/src/app/sass/_variables.scss +1 -0
- package/src/app/shims/onnxruntime-web-wasm.ts +4 -0
- package/src/app/utils/conversation-sender-classifier.ts +21 -0
- package/src/app/utils/globals.ts +7 -1
- package/src/assets/i18n/en.json +1 -0
- package/src/assets/i18n/es.json +1 -0
- package/src/assets/i18n/fr.json +1 -0
- package/src/assets/i18n/it.json +1 -0
- 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/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/firebase/firebase-conversation-handler.ts +3 -2
- package/src/chat21-core/providers/mqtt/mqtt-conversation-handler.ts +12 -0
- package/src/chat21-core/providers/tiledesk/tiledesk-requests.service.ts +1 -1
- package/src/chat21-core/utils/utils-message.ts +7 -0
- package/src/chat21-core/utils/utils.ts +5 -2
- package/tsconfig.json +5 -0
package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.html
CHANGED
|
@@ -12,96 +12,161 @@
|
|
|
12
12
|
<div tabindex="-1" class="alertText">{{translationMap.get('EMOJI_NOT_ELLOWED')}}</div>
|
|
13
13
|
</div>
|
|
14
14
|
|
|
15
|
+
<!-- STREAM AUDIO: cerchio con onde animate -->
|
|
16
|
+
<div id="streamAudioAlert" *ngIf="!hideTextAreaContent && isStreamAudioActive" class="fade-in-bottom stream-audio-alert" [class.hideTextReply]="hideTextReply" role="status" [attr.aria-label]="translationMap?.get('STREAM_AUDIO_LISTENING') || 'Stream audio attivo'">
|
|
17
|
+
<div class="stream-audio-alert__orb" [ngStyle]="{ color: stylesMap?.get('themeColor') }">
|
|
18
|
+
<svg class="stream-audio-alert__svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
19
|
+
<circle cx="50" cy="50" r="46" fill="currentColor" opacity="0.14"/>
|
|
20
|
+
<g class="stream-audio-alert__waves" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round">
|
|
21
|
+
<g class="stream-audio-alert__wave-layer stream-audio-alert__wave-layer--1">
|
|
22
|
+
<path [attr.d]="wavePath1"></path>
|
|
23
|
+
</g>
|
|
24
|
+
|
|
25
|
+
<g class="stream-audio-alert__wave-layer stream-audio-alert__wave-layer--2">
|
|
26
|
+
<path [attr.d]="wavePath2"></path>
|
|
27
|
+
</g>
|
|
28
|
+
|
|
29
|
+
<g class="stream-audio-alert__wave-layer stream-audio-alert__wave-layer--3">
|
|
30
|
+
<path [attr.d]="wavePath3"></path>
|
|
31
|
+
</g>
|
|
32
|
+
</g>
|
|
33
|
+
</svg>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
15
37
|
</div>
|
|
16
38
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
39
|
+
<div class="textarea-container-wrapper" *ngIf="!hideTextAreaContent && !hideTextReply">
|
|
40
|
+
<!-- TEXTAREA + ICONS: conv active-->
|
|
41
|
+
<div class="textarea-container">
|
|
42
|
+
|
|
43
|
+
<div *ngIf="!isStopRec" class="icons-container">
|
|
44
|
+
<!-- ICON ATTACHMENT -->
|
|
45
|
+
<label *ngIf="showAttachmentFooterButton" tabindex="1502" aria-label="allegati" for="chat21-file" class="chat21-textarea-button" [class.active]="!isFilePendingToUpload && !hideTextReply" id="chat21-start-upload-doc">
|
|
46
|
+
<span class="v-align-center">
|
|
47
|
+
<svg role="img" aria-labelledby="altIconTitle" xmlns="http://www.w3.org/2000/svg" width="24px" height="24" viewBox="0 0 24 24" fill="currentColor">
|
|
48
|
+
<path d="M9.9,22.7c0,0-.1,0-.2,0-1.9.3-3.7-.2-5.2-1.4-3-2.3-3.6-6.4-1.4-9.5L9.5,2.5c.4-.5,1.1-.6,1.6-.3.5.4.6,1.1.3,1.6l-6.5,9.4c-1.4,2-1,4.8.9,6.3,1,.8,2.2,1.1,3.5.9,1.3-.2,2.4-.9,3.1-1.9l6-8.7c.9-1.2.6-3-.6-3.9-.6-.5-1.4-.6-2.1-.5-.8.1-1.4.5-1.9,1.1l-5.8,8.2c-.3.5-.2,1.1.2,1.5.2.2.5.3.8.2.3,0,.6-.2.7-.4l4.7-6.2c.4-.5,1.1-.6,1.6-.2.5.4.6,1.1.2,1.6l-4.7,6.2c-.5.7-1.4,1.2-2.3,1.3-.9.1-1.8-.2-2.5-.7-1.4-1.1-1.6-3.1-.6-4.6l5.8-8.2c.8-1.1,2-1.9,3.4-2.1,1.4-.2,2.7.1,3.8,1,2.2,1.7,2.7,4.8,1.1,7.1l-6,8.7c-1.1,1.5-2.6,2.5-4.4,2.8h0Z"/>
|
|
49
|
+
<title id="altIconTitle">{{ 'MAX_ATTACHMENT' | translate: { FILE_SIZE_LIMIT: file_size_limit } }}</title>
|
|
50
|
+
</svg>
|
|
51
|
+
|
|
52
|
+
</span>
|
|
53
|
+
<input
|
|
54
|
+
[attr.disabled] = "(isFilePendingToUpload || isConversationArchived || hideTextReply)? true : null"
|
|
55
|
+
tabindex="1503"
|
|
56
|
+
type="file"
|
|
57
|
+
aria-label="seleziona allegato"
|
|
58
|
+
[accept]="fileUploadAccept"
|
|
59
|
+
name="chat21-file"
|
|
60
|
+
id="chat21-file"
|
|
61
|
+
#chat21_file
|
|
62
|
+
class="inputfile"
|
|
63
|
+
[ngStyle]="{'display': 'block', height:'1px', width:'1px', overflow: 'hidden' }"
|
|
64
|
+
(change)="detectFiles($event)"/>
|
|
65
|
+
</label>
|
|
66
|
+
<!-- ICON EMOJII -->
|
|
67
|
+
<label *ngIf="showEmojiFooterButton" tabindex="1504" aria-label="emojii" for="chat21-emojii" class="chat21-textarea-button" [class.active]="!isFilePendingToUpload && !hideTextReply" id="chat21-emoticon-picker" (click)="onEmojiiPickerClicked()">
|
|
68
|
+
<span class="v-align-center">
|
|
69
|
+
<svg role="img" aria-labelledby="altIconTitle" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
|
|
70
|
+
<path stroke-width=".4px" stroke="currentColor" d="M12,20.8c-5.1,0-9.3-4.2-9.3-9.3S6.9,2.2,12,2.2s9.3,4.2,9.3,9.3-4.2,9.3-9.3,9.3ZM12,3.6c-4.4,0-7.9,3.6-7.9,7.9s3.6,7.9,7.9,7.9,7.9-3.6,7.9-7.9-3.6-7.9-7.9-7.9Z"/>
|
|
71
|
+
<path stroke-width=".4px" stroke="currentColor" d="M12,17.2c-2.7,0-4.3-1.9-4.6-2.3-.2-.3-.2-.7.1-1s.7-.2,1,.1c.1.2,1.4,1.8,3.5,1.8s2.2,0,3.5-1.8c.2-.3.7-.4,1-.1s.4.7.1,1c-1.7,2.2-4.1,2.3-4.6,2.3Z"/>
|
|
72
|
+
<path d="M8.7,10.9c-.9,0-1.6-.7-1.6-1.6s.7-1.6,1.6-1.6,1.6.7,1.6,1.6-.7,1.6-1.6,1.6Z"/>
|
|
73
|
+
<path d="M15.5,10.9c-.9,0-1.6-.7-1.6-1.6s.7-1.6,1.6-1.6,1.6.7,1.6,1.6-.7,1.6-1.6,1.6Z"/>
|
|
74
|
+
<title id="altIconTitle">{{ translationMap?.get('EMOJI') }}</title>
|
|
75
|
+
|
|
76
|
+
<!-- <path d="M0,0H20.57V20.57H0V0Z" fill="none"/>
|
|
77
|
+
<circle cx="15.02" cy="9.86" r="1.29"/>
|
|
78
|
+
<circle cx="9.02" cy="9.86" r="1.29"/>
|
|
79
|
+
<path d="M12.02,15.43c-1.27,0-2.36-.69-2.96-1.71h-1.43c.69,1.76,2.39,3,4.39,3s3.7-1.24,4.39-3h-1.43c-.6,1.02-1.69,1.71-2.96,1.71Zm0-12C7.28,3.43,3.45,7.27,3.45,12s3.83,8.57,8.56,8.57,8.58-3.84,8.58-8.57S16.75,3.43,12.01,3.43Zm0,15.43c-3.79,0-6.86-3.07-6.86-6.86s3.07-6.86,6.86-6.86,6.86,3.07,6.86,6.86-3.07,6.86-6.86,6.86Z"/> -->
|
|
80
|
+
</svg>
|
|
81
|
+
</span>
|
|
82
|
+
</label>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
<div *ngIf="!isStopRec" class="visible-text-area" [class.hasError]="showAlertEmoji" [class.disabled] = "( isConversationArchived || hideTextReply)? true : null">
|
|
89
|
+
<!-- isFilePendingToUpload || -->
|
|
90
|
+
<textarea
|
|
91
|
+
[attr.disabled] = "(hideTextReply)? true : null"
|
|
92
|
+
[attr.placeholder] ="(footerMessagePlaceholder)? footerMessagePlaceholder : translationMap?.get('LABEL_PLACEHOLDER')"
|
|
93
|
+
start-focus-chat21-conversation-component
|
|
94
|
+
inputTextArea
|
|
95
|
+
#textbox
|
|
96
|
+
tabindex="1501"
|
|
97
|
+
aria-labelledby="altTextArea"
|
|
98
|
+
rows="1"
|
|
99
|
+
id="chat21-main-message-context"
|
|
100
|
+
class='f21textarea c21-button-clean'
|
|
101
|
+
[(ngModel)]="textInputTextArea"
|
|
102
|
+
(ngModelChange)="onTextAreaChange()"
|
|
103
|
+
(keypress)="onkeypress($event)"
|
|
104
|
+
(keydown)="onkeydown($event)"
|
|
105
|
+
(paste)="onPaste($event)">
|
|
106
|
+
</textarea>
|
|
107
|
+
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<!-- ICON SEND -->
|
|
111
|
+
<div *ngIf="(textInputTextArea !== '' && !isStopRec) || !showAudioRecorderFooterButton" tabindex="-1" class="chat21-textarea-button" [class.disabled]="showAlertEmoji" [class.active]="textInputTextArea && !hideTextReply" id="chat21-button-send" (click)="onSendPressed($event)">
|
|
23
112
|
<span class="v-align-center">
|
|
24
|
-
<svg
|
|
25
|
-
<path d="
|
|
26
|
-
<title id="altIconTitle">{{ 'MAX_ATTACHMENT' | translate: { FILE_SIZE_LIMIT: file_size_limit } }}</title>
|
|
113
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="24" width="24" viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve" fill="currentColor">
|
|
114
|
+
<path d="M1.8,20.6V3.4l20.2,8.6L1.8,20.6ZM3.9,17.3l12.6-5.4L3.9,6.6v3.7l6.4,1.6-6.4,1.6v3.8ZM3.9,17.3V6.6v10.7Z"/>
|
|
27
115
|
</svg>
|
|
28
|
-
|
|
29
116
|
</span>
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<!-- ICON REC -->
|
|
120
|
+
<div *ngIf="showAudioRecorderFooterButton && !textInputTextArea" tabindex="-1" class="chat21-audio-button" [class.active]="isStopRec" id="chat21-button-rec">
|
|
121
|
+
<chat-audio-recorder
|
|
122
|
+
(startRecordingEvent)="onStartRecording()"
|
|
123
|
+
(deleteRecordingEvent)="onDeleteRecording()"
|
|
124
|
+
(endRecordingEvent)="onEndRecording($event)"
|
|
125
|
+
(sendRecordingEvent)="onSendRecording($event)"
|
|
126
|
+
[stylesMap]="stylesMap">
|
|
127
|
+
</chat-audio-recorder>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<div class="close-chat-container" *ngIf="closeChatInConversation">
|
|
132
|
+
<button tabindex="1040" aflistconv #aflistconv class="c21-button-primary c21-close" (click)="onCloseChat($event)" [ngStyle]="{'background-color': stylesMap.get('themeColor'), 'border-color': stylesMap.get('themeColor'), 'color': stylesMap?.get('foregroundColor')}">
|
|
45
133
|
<span class="v-align-center">
|
|
46
|
-
<svg
|
|
47
|
-
<path
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
<
|
|
55
|
-
<circle cx="9.02" cy="9.86" r="1.29"/>
|
|
56
|
-
<path d="M12.02,15.43c-1.27,0-2.36-.69-2.96-1.71h-1.43c.69,1.76,2.39,3,4.39,3s3.7-1.24,4.39-3h-1.43c-.6,1.02-1.69,1.71-2.96,1.71Zm0-12C7.28,3.43,3.45,7.27,3.45,12s3.83,8.57,8.56,8.57,8.58-3.84,8.58-8.57S16.75,3.43,12.01,3.43Zm0,15.43c-3.79,0-6.86-3.07-6.86-6.86s3.07-6.86,6.86-6.86,6.86,3.07,6.86,6.86-3.07,6.86-6.86,6.86Z"/> -->
|
|
134
|
+
<!-- <svg [ngStyle]="{'fill': 'yellow' }" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24">
|
|
135
|
+
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" [ngStyle]="{'fill': stylesMap?.get('foregroundColor')}"/>
|
|
136
|
+
</svg> -->
|
|
137
|
+
<svg [ngStyle]="{'stroke': stylesMap?.get('foregroundColor'), 'fill': stylesMap?.get('foregroundColor') }" role="img" id="archive" aria-labelledby="altIconTitle" class="icon-menu" xmlns="http://www.w3.org/2000/svg"
|
|
138
|
+
width="15px" height="15px" viewBox="0 0 512 512">
|
|
139
|
+
<path d="M80 152v256a40.12 40.12 0 0040 40h272a40.12 40.12 0 0040-40V152" stroke-linecap="round" stroke-linejoin="round" stroke-width="50px" fill="none"></path>
|
|
140
|
+
<rect x="48" y="64" width="416" height="80" rx="28" ry="28" stroke-linejoin="round" stroke-width="50px" fill="none" ></rect>
|
|
141
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M320 304l-64 64-64-64M256 345.89V224" stroke-width="50px" fill="none"></path>
|
|
142
|
+
<title id="altIconTitle">{{ translationMap?.get('CLOSE_CHAT') }}</title>
|
|
57
143
|
</svg>
|
|
58
144
|
</span>
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
<div *ngIf="!isStopRec" class="visible-text-area" [class.hasError]="showAlertEmoji" [class.disabled] = "( isConversationArchived || hideTextReply)? true : null">
|
|
66
|
-
<!-- isFilePendingToUpload || -->
|
|
67
|
-
<textarea
|
|
68
|
-
[attr.disabled] = "(hideTextReply)? true : null"
|
|
69
|
-
[attr.placeholder] ="(footerMessagePlaceholder)? footerMessagePlaceholder : translationMap?.get('LABEL_PLACEHOLDER')"
|
|
70
|
-
start-focus-chat21-conversation-component
|
|
71
|
-
inputTextArea
|
|
72
|
-
#textbox
|
|
73
|
-
tabindex="1501"
|
|
74
|
-
aria-labelledby="altTextArea"
|
|
75
|
-
rows="1"
|
|
76
|
-
id="chat21-main-message-context"
|
|
77
|
-
class='f21textarea c21-button-clean'
|
|
78
|
-
[(ngModel)]="textInputTextArea"
|
|
79
|
-
(ngModelChange)="onTextAreaChange()"
|
|
80
|
-
(keypress)="onkeypress($event)"
|
|
81
|
-
(keydown)="onkeydown($event)"
|
|
82
|
-
(paste)="onPaste($event)">
|
|
83
|
-
</textarea>
|
|
84
|
-
|
|
145
|
+
<span class="v-align-center c21-label-button">
|
|
146
|
+
{{translationMap?.get('CLOSE_CHAT')}}
|
|
147
|
+
</span>
|
|
148
|
+
<div class="clear"></div>
|
|
149
|
+
</button>
|
|
85
150
|
</div>
|
|
86
151
|
|
|
87
|
-
<!-- ICON
|
|
88
|
-
<div *ngIf="
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
152
|
+
<!-- ICON STREAM / CHIUDI STREAM (cerchio, icone bianche su iconColor) -->
|
|
153
|
+
<div *ngIf="showAudioStreamFooterButton" tabindex="-1" id="chat21-button-stream"
|
|
154
|
+
class="chat21-textarea-button chat21-stream-button" [class.active]="isStreamAudioActive || (!textInputTextArea && !hideTextReply)"
|
|
155
|
+
(click)="onStreamPressed($event)" [attr.aria-label]="isStreamAudioActive ? (translationMap?.get('CLOSE') || 'Chiudi stream') : (translationMap?.get('STREAM_AUDIO') || 'Stream audio')"
|
|
156
|
+
[ngStyle]="{ 'background-color': stylesMap?.get('iconColor') || stylesMap?.get('themeColor') }">
|
|
157
|
+
<span class="v-align-center chat21-stream-button__icon" *ngIf="!isStreamAudioActive">
|
|
158
|
+
<svg role="img" xmlns="http://www.w3.org/2000/svg" version="1.1" width="24" height="24" viewBox="0 0 30 30" fill="#ffffff" aria-hidden="true" preserveAspectRatio="xMidYMid meet">
|
|
159
|
+
<path class="s0" d="m5.21 7.41c-1.21 0-2.21 0.99-2.21 2.21v8.14c0 1.21 0.99 2.21 2.21 2.21 1.22 0 2.21-0.99 2.21-2.21v-8.14c0-1.21-0.99-2.21-2.21-2.21z"/>
|
|
160
|
+
<path class="s0" d="m11.64 3.01c-1.22 0-2.21 0.99-2.21 2.2v16.94c0 1.21 0.99 2.2 2.21 2.2 1.22 0 2.21-0.98 2.21-2.2v-16.94c0-1.21-0.99-2.21-2.21-2.21z"/>
|
|
161
|
+
<path class="s0" d="m15.86 9.25v8.88c0 1.21 0.99 2.21 2.21 2.21 1.22 0 2.21-0.99 2.21-2.21v-8.88c0-1.22-0.99-2.21-2.21-2.21-1.22 0-2.21 0.99-2.21 2.21z"/>
|
|
162
|
+
<path class="s0" d="m24.5 8.97c-1.22 0-2.21 0.99-2.21 2.21v5.02c0 1.22 0.99 2.21 2.21 2.21 1.22 0 2.21-0.99 2.21-2.21v-5.02c0-1.21-0.99-2.21-2.21-2.21z"/>
|
|
163
|
+
</svg>
|
|
164
|
+
</span>
|
|
165
|
+
<span class="v-align-center chat21-stream-button__icon" *ngIf="isStreamAudioActive">
|
|
166
|
+
<svg role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#ffffff" aria-hidden="true">
|
|
167
|
+
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/>
|
|
92
168
|
</svg>
|
|
93
169
|
</span>
|
|
94
|
-
</div>
|
|
95
|
-
|
|
96
|
-
<!-- ICON REC -->
|
|
97
|
-
<div *ngIf="showAudioRecorderFooterButton && !textInputTextArea" tabindex="-1" class="chat21-audio-button" [class.active]="isStopRec" id="chat21-button-rec">
|
|
98
|
-
<chat-audio-recorder
|
|
99
|
-
(startRecordingEvent)="onStartRecording()"
|
|
100
|
-
(deleteRecordingEvent)="onDeleteRecording()"
|
|
101
|
-
(endRecordingEvent)="onEndRecording($event)"
|
|
102
|
-
(sendRecordingEvent)="onSendRecording($event)"
|
|
103
|
-
[stylesMap]="stylesMap">
|
|
104
|
-
</chat-audio-recorder>
|
|
105
170
|
</div>
|
|
106
171
|
</div>
|
|
107
172
|
|
package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
|
-
|
|
1
|
+
.textarea-container-wrapper{
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: 8px;
|
|
5
|
+
}
|
|
2
6
|
.textarea-container{
|
|
3
|
-
// padding: 8px 34px;
|
|
4
|
-
// padding-left: 70px;
|
|
5
|
-
// padding-right: 45px;
|
|
6
7
|
display: flex;
|
|
7
|
-
// width: 100%;
|
|
8
8
|
align-items: center;
|
|
9
9
|
justify-content: space-between;
|
|
10
10
|
gap: 8px;
|
|
11
|
+
}
|
|
12
|
+
.close-chat-container{
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-direction: column;
|
|
15
|
+
align-items: center;
|
|
16
|
+
justify-content: center;
|
|
17
|
+
gap: 8px;
|
|
11
18
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
19
|
+
.c21-close{
|
|
20
|
+
height: 30px !important;
|
|
21
|
+
margin: 0px !important;
|
|
15
22
|
}
|
|
16
|
-
//if attachment icon AND emoji icon is not in DOM -> increment textarea width
|
|
17
|
-
&:has(:not(#chat21-start-upload-doc)):has(:not(#chat21-emoticon-picker)) .visible-text-area {
|
|
18
|
-
width: 90%;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
.icons-container{
|
|
@@ -82,6 +84,30 @@
|
|
|
82
84
|
border-radius: 50%;
|
|
83
85
|
}
|
|
84
86
|
|
|
87
|
+
/** Stream audio: cerchio pieno, glyph bianco su sfondo iconColor (stylesMap) */
|
|
88
|
+
.chat21-stream-button.chat21-textarea-button {
|
|
89
|
+
width: 36px;
|
|
90
|
+
height: 36px;
|
|
91
|
+
min-width: 36px;
|
|
92
|
+
border-radius: 50%;
|
|
93
|
+
box-sizing: border-box;
|
|
94
|
+
flex-shrink: 0;
|
|
95
|
+
color: #ffffff;
|
|
96
|
+
|
|
97
|
+
.chat21-stream-button__icon svg {
|
|
98
|
+
width: 20px;
|
|
99
|
+
height: 20px;
|
|
100
|
+
path {
|
|
101
|
+
fill: #ffffff;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
&.chat21-textarea-button span svg:hover {
|
|
106
|
+
background: rgba(255, 255, 255, 0.2) !important;
|
|
107
|
+
border-radius: 50%;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
85
111
|
textarea,
|
|
86
112
|
textarea:visited,
|
|
87
113
|
textarea:focus,
|
|
@@ -364,6 +390,73 @@ textarea:active{
|
|
|
364
390
|
}
|
|
365
391
|
}
|
|
366
392
|
|
|
393
|
+
#streamAudioAlert {
|
|
394
|
+
bottom: 100%;
|
|
395
|
+
width: 100%;
|
|
396
|
+
min-height: 96px;
|
|
397
|
+
display: flex;
|
|
398
|
+
align-items: center;
|
|
399
|
+
justify-content: center;
|
|
400
|
+
background-color: var(--content-background-color);
|
|
401
|
+
position: absolute;
|
|
402
|
+
padding: 10px 0;
|
|
403
|
+
|
|
404
|
+
&.hideTextReply {
|
|
405
|
+
position: unset;
|
|
406
|
+
min-height: auto;
|
|
407
|
+
padding: 16px 0;
|
|
408
|
+
box-shadow: none;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.stream-audio-alert__orb {
|
|
413
|
+
display: flex;
|
|
414
|
+
align-items: center;
|
|
415
|
+
justify-content: center;
|
|
416
|
+
width: 88px;
|
|
417
|
+
height: 88px;
|
|
418
|
+
border-radius: 50%;
|
|
419
|
+
border: 2px solid currentColor;
|
|
420
|
+
background: var(--content-background-color);
|
|
421
|
+
box-shadow: inset 0 0 24px rgba(0, 0, 0, 0.04);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
.stream-audio-alert__svg {
|
|
425
|
+
width: 72px;
|
|
426
|
+
height: 72px;
|
|
427
|
+
display: block;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
.stream-audio-alert__wave-layer {
|
|
431
|
+
transform-origin: 50px 50px;
|
|
432
|
+
transform-box: fill-box;
|
|
433
|
+
animation: stream-wave-float 1.35s ease-in-out infinite;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.stream-audio-alert__wave-layer--1 {
|
|
437
|
+
animation-delay: 0s;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
.stream-audio-alert__wave-layer--2 {
|
|
441
|
+
animation-delay: 0.18s;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
.stream-audio-alert__wave-layer--3 {
|
|
445
|
+
animation-delay: 0.36s;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
@keyframes stream-wave-float {
|
|
449
|
+
0%,
|
|
450
|
+
100% {
|
|
451
|
+
transform: translateY(0);
|
|
452
|
+
opacity: 0.85;
|
|
453
|
+
}
|
|
454
|
+
50% {
|
|
455
|
+
transform: translateY(-6px);
|
|
456
|
+
opacity: 1;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
367
460
|
#textAlert{
|
|
368
461
|
bottom: 100%;
|
|
369
462
|
width: 100%;
|
|
@@ -419,3 +512,28 @@ textarea:active{
|
|
|
419
512
|
border: none;
|
|
420
513
|
// margin: -2px -2px 0px;
|
|
421
514
|
}
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
// aggiungi un'animazione di fade in e fade out quando .star-rating-widget è visibile con transition
|
|
518
|
+
.star-rating-widget {
|
|
519
|
+
transition: all 0.5s ease-in-out;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
.star-rating-widget {
|
|
523
|
+
position: absolute;
|
|
524
|
+
left: 0;
|
|
525
|
+
right: 0;
|
|
526
|
+
bottom: -52px;
|
|
527
|
+
height: 100%;
|
|
528
|
+
width: 100%;
|
|
529
|
+
flex-direction: row;
|
|
530
|
+
justify-content: center;
|
|
531
|
+
background-color: rgb(255, 255, 255);
|
|
532
|
+
flex-wrap: nowrap;
|
|
533
|
+
&.active {
|
|
534
|
+
bottom: 0px;
|
|
535
|
+
}
|
|
536
|
+
&.inactive {
|
|
537
|
+
bottom: -52px;
|
|
538
|
+
}
|
|
539
|
+
}
|
package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Component, ComponentFactoryResolver, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild, ViewContainerRef } from '@angular/core';
|
|
1
|
+
import { Component, ComponentFactoryResolver, ElementRef, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges, ViewChild, ViewContainerRef } from '@angular/core';
|
|
2
2
|
import { error } from 'console';
|
|
3
3
|
import { FILE_SIZE_LIMIT } from 'src/app/utils/constants';
|
|
4
4
|
import { Globals } from 'src/app/utils/globals';
|
|
@@ -15,13 +15,15 @@ import { TYPE_MSG_FILE, TYPE_MSG_IMAGE, TYPE_MSG_TEXT } from 'src/chat21-core/ut
|
|
|
15
15
|
import { convertColorToRGBA, isAllowedUrlInText, isEmoji } from 'src/chat21-core/utils/utils';
|
|
16
16
|
import { findAndRemoveEmoji, isImage } from 'src/chat21-core/utils/utils-message';
|
|
17
17
|
import { ProjectModel } from 'src/models/project';
|
|
18
|
+
import { Subscription } from 'rxjs';
|
|
19
|
+
import { VoiceService } from 'src/app/providers/voice/voice.service';
|
|
18
20
|
|
|
19
21
|
@Component({
|
|
20
22
|
selector: 'chat-conversation-footer',
|
|
21
23
|
templateUrl: './conversation-footer.component.html',
|
|
22
24
|
styleUrls: ['./conversation-footer.component.scss']
|
|
23
25
|
})
|
|
24
|
-
export class ConversationFooterComponent implements OnInit, OnChanges {
|
|
26
|
+
export class ConversationFooterComponent implements OnInit, OnChanges, OnDestroy {
|
|
25
27
|
|
|
26
28
|
@Input() conversationWith: string;
|
|
27
29
|
@Input() attributes: string;
|
|
@@ -32,8 +34,9 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
|
|
|
32
34
|
@Input() userFullname: string;
|
|
33
35
|
@Input() userEmail: string;
|
|
34
36
|
@Input() showAttachmentFooterButton: boolean;
|
|
35
|
-
@Input() showEmojiFooterButton: boolean
|
|
36
|
-
@Input() showAudioRecorderFooterButton: boolean
|
|
37
|
+
@Input() showEmojiFooterButton: boolean;
|
|
38
|
+
@Input() showAudioRecorderFooterButton: boolean;
|
|
39
|
+
@Input() showAudioStreamFooterButton: boolean;
|
|
37
40
|
// @Input() showContinueConversationButton: boolean;
|
|
38
41
|
@Input() isConversationArchived: boolean;
|
|
39
42
|
@Input() hideTextAreaContent: boolean;
|
|
@@ -42,6 +45,7 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
|
|
|
42
45
|
@Input() isEmojiiPickerShow: boolean;
|
|
43
46
|
@Input() footerMessagePlaceholder: string;
|
|
44
47
|
@Input() fileUploadAccept: string;
|
|
48
|
+
@Input() closeChatInConversation: boolean;
|
|
45
49
|
@Input() dropEvent: Event;
|
|
46
50
|
@Input() poweredBy: string;
|
|
47
51
|
@Input() stylesMap: Map<string, string>
|
|
@@ -52,6 +56,8 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
|
|
|
52
56
|
@Output() onChangeTextArea = new EventEmitter<any>();
|
|
53
57
|
@Output() onAttachmentFileButtonClicked = new EventEmitter<any>();
|
|
54
58
|
@Output() onNewConversationButtonClicked = new EventEmitter();
|
|
59
|
+
@Output() onStreamAudioActiveChange = new EventEmitter<boolean>();
|
|
60
|
+
@Output() onCloseChatButtonClicked = new EventEmitter();
|
|
55
61
|
|
|
56
62
|
@ViewChild('chat21_file') public chat21_file: ElementRef;
|
|
57
63
|
// @ViewChild('emojii_container', {read: ViewContainerRef}) selector;
|
|
@@ -85,15 +91,28 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
|
|
|
85
91
|
|
|
86
92
|
showAlertEmoji: boolean = false
|
|
87
93
|
|
|
94
|
+
/** Stream audio UI: icona equalizer → X; alert con onde animate sopra il footer */
|
|
95
|
+
isStreamAudioActive = false;
|
|
96
|
+
/** Sottoscrizione ai segmenti audio (VAD → WebM) dal {@link VoiceService}. */
|
|
97
|
+
private voiceAudioSubscription?: Subscription;
|
|
98
|
+
/** Sottoscrizione al volume audio (real-time) dal {@link VoiceService}. */
|
|
99
|
+
private voiceVolumeSubscription?: Subscription;
|
|
100
|
+
currentVolume = 0;
|
|
101
|
+
wavePath1 = '';
|
|
102
|
+
wavePath2 = '';
|
|
103
|
+
wavePath3 = '';
|
|
104
|
+
|
|
88
105
|
file_size_limit = FILE_SIZE_LIMIT;
|
|
89
106
|
attachmentTooltip: string = '';
|
|
107
|
+
isErrorNetwork: boolean = false;
|
|
90
108
|
|
|
91
109
|
|
|
92
110
|
convertColorToRGBA = convertColorToRGBA;
|
|
93
111
|
private logger: LoggerService = LoggerInstance.getInstance()
|
|
94
112
|
constructor(private chatManager: ChatManager,
|
|
95
113
|
private typingService: TypingService,
|
|
96
|
-
private uploadService: UploadService
|
|
114
|
+
private uploadService: UploadService,
|
|
115
|
+
private voiceService: VoiceService) { }
|
|
97
116
|
|
|
98
117
|
ngOnInit() {
|
|
99
118
|
// this.updateAttachmentTooltip();
|
|
@@ -103,6 +122,8 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
|
|
|
103
122
|
ngOnChanges(changes: SimpleChanges){
|
|
104
123
|
if(changes['conversationWith'] && changes['conversationWith'].currentValue !== undefined){
|
|
105
124
|
this.conversationHandlerService = this.chatManager.getConversationHandlerByConversationId(this.conversationWith);
|
|
125
|
+
this.isStreamAudioActive = false;
|
|
126
|
+
void this.stopVoice();
|
|
106
127
|
}
|
|
107
128
|
if(changes['hideTextReply'] && changes['hideTextReply'].currentValue !== undefined){
|
|
108
129
|
this.restoreTextArea();
|
|
@@ -142,6 +163,59 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
|
|
|
142
163
|
// }, 500);
|
|
143
164
|
// }
|
|
144
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Microfono + VAD: ogni fine parlato il servizio emette su `audioSegment$` → upload.
|
|
168
|
+
*/
|
|
169
|
+
async initVoice() {
|
|
170
|
+
this.voiceAudioSubscription?.unsubscribe();
|
|
171
|
+
this.voiceVolumeSubscription?.unsubscribe();
|
|
172
|
+
|
|
173
|
+
this.voiceAudioSubscription = this.voiceService.audioSegment$.subscribe((rec) => {
|
|
174
|
+
console.log('[CONV-FOOTER] audioSegment$', rec);
|
|
175
|
+
this.prepareAndUpload(rec.blob);
|
|
176
|
+
});
|
|
177
|
+
this.voiceVolumeSubscription = this.voiceService.volume$.subscribe((volume) => {
|
|
178
|
+
this.currentVolume = volume;
|
|
179
|
+
this.updateWave(volume);
|
|
180
|
+
});
|
|
181
|
+
await this.voiceService.startSession();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async stopVoice() {
|
|
185
|
+
this.voiceAudioSubscription?.unsubscribe();
|
|
186
|
+
this.voiceAudioSubscription = undefined;
|
|
187
|
+
|
|
188
|
+
this.voiceVolumeSubscription?.unsubscribe();
|
|
189
|
+
this.voiceVolumeSubscription = undefined;
|
|
190
|
+
|
|
191
|
+
await this.voiceService.stopSession();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
updateWave(volume: number) {
|
|
195
|
+
const intensity = Math.min(volume / 80, 1); // più sensibile
|
|
196
|
+
|
|
197
|
+
const amp1 = 4 + intensity * 22;
|
|
198
|
+
const amp2 = 2 + intensity * 16;
|
|
199
|
+
const amp3 = 1 + intensity * 12;
|
|
200
|
+
|
|
201
|
+
this.wavePath1 = this.buildWave(42, amp1);
|
|
202
|
+
this.wavePath2 = this.buildWave(50, amp2);
|
|
203
|
+
this.wavePath3 = this.buildWave(58, amp3);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
buildWave(y: number, amp: number): string {
|
|
207
|
+
return `
|
|
208
|
+
M6 ${y}
|
|
209
|
+
Q24 ${y - amp} 42 ${y}
|
|
210
|
+
T78 ${y}
|
|
211
|
+
T98 ${y}
|
|
212
|
+
`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
ngOnDestroy() {
|
|
216
|
+
void this.stopVoice();
|
|
217
|
+
}
|
|
218
|
+
|
|
145
219
|
// ========= begin:: functions send image ======= //
|
|
146
220
|
// START LOAD IMAGE //
|
|
147
221
|
/** load the selected image locally and open the pop up preview */
|
|
@@ -521,7 +595,7 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
|
|
|
521
595
|
}
|
|
522
596
|
}
|
|
523
597
|
|
|
524
|
-
prepareAndUpload(audioBlob: Blob) {
|
|
598
|
+
prepareAndUpload(audioBlob: Blob, text: string = '') {
|
|
525
599
|
|
|
526
600
|
this.isFilePendingToUpload = true;
|
|
527
601
|
|
|
@@ -551,7 +625,7 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
|
|
|
551
625
|
this.logger.log('[UPLOAD] metadata:', metadata);
|
|
552
626
|
|
|
553
627
|
// stesso metodo che già usi
|
|
554
|
-
this.uploadSingle(metadata, file,
|
|
628
|
+
this.uploadSingle(metadata, file, text);
|
|
555
629
|
}
|
|
556
630
|
|
|
557
631
|
// Funzione per convertire Blob in Base64 usando FileReader
|
|
@@ -658,6 +732,29 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
|
|
|
658
732
|
}
|
|
659
733
|
}
|
|
660
734
|
|
|
735
|
+
async onStreamPressed(event: Event) {
|
|
736
|
+
this.logger.log('[CONV-FOOTER] onStreamPressed:event', event);
|
|
737
|
+
event.preventDefault();
|
|
738
|
+
if (this.showAlertEmoji) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
const turningOn = !this.isStreamAudioActive;
|
|
742
|
+
if (turningOn) {
|
|
743
|
+
try {
|
|
744
|
+
await this.initVoice();
|
|
745
|
+
this.isStreamAudioActive = true;
|
|
746
|
+
} catch (e) {
|
|
747
|
+
this.logger.error('[CONV-FOOTER] onStreamPressed: initVoice failed', e);
|
|
748
|
+
this.isStreamAudioActive = false;
|
|
749
|
+
}
|
|
750
|
+
} else {
|
|
751
|
+
await this.stopVoice();
|
|
752
|
+
this.isStreamAudioActive = false;
|
|
753
|
+
}
|
|
754
|
+
this.onStreamAudioActiveChange.emit(this.isStreamAudioActive);
|
|
755
|
+
this.logger.log('[CONV-FOOTER] isStreamAudioActive', this.isStreamAudioActive);
|
|
756
|
+
}
|
|
757
|
+
|
|
661
758
|
async onEmojiiPickerClicked(){
|
|
662
759
|
// if(this.loadPickerModule){
|
|
663
760
|
// this.loadPickerModule = false;
|
|
@@ -709,6 +806,10 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
|
|
|
709
806
|
this.onNewConversationButtonClicked.emit();
|
|
710
807
|
}
|
|
711
808
|
|
|
809
|
+
onCloseChat(event){
|
|
810
|
+
this.onCloseChatButtonClicked.emit();
|
|
811
|
+
}
|
|
812
|
+
|
|
712
813
|
// onContinueConversation(){
|
|
713
814
|
// this.hideTextAreaContent = false;
|
|
714
815
|
// this.onBackButton.emit(false)
|
|
@@ -12,7 +12,7 @@ import { MIN_WIDTH_IMAGES } from 'src/app/utils/constants';
|
|
|
12
12
|
import { ConversationModel } from 'src/chat21-core/models/conversation';
|
|
13
13
|
import { LoggerService } from 'src/chat21-core/providers/abstract/logger.service';
|
|
14
14
|
import { LoggerInstance } from 'src/chat21-core/providers/logger/loggerInstance';
|
|
15
|
-
import { commandToMessage, conversationToMessage, isEmojii, isFrame, isImage, isSameSender } from 'src/chat21-core/utils/utils-message';
|
|
15
|
+
import { commandToMessage, conversationToMessage, isEmojii, isFrame, isImage, isMine, isSameSender, isSender } from 'src/chat21-core/utils/utils-message';
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
@Component({
|
|
@@ -59,6 +59,9 @@ export class LastMessageComponent implements OnInit, AfterViewInit, OnDestroy {
|
|
|
59
59
|
ngOnChanges(changes: SimpleChanges) {
|
|
60
60
|
this.logger.debug('[LASTMESSAGE] onChanges', changes)
|
|
61
61
|
if(this.conversation){
|
|
62
|
+
|
|
63
|
+
/** if the message is sent by the logged user, do not add it to the messages array */
|
|
64
|
+
if(isSender(this.conversation.sender, this.g.senderId)) return;
|
|
62
65
|
|
|
63
66
|
if(this.conversation.attributes && this.conversation.attributes.commands){
|
|
64
67
|
this.addCommandMessage(this.conversation)
|