@chat21/chat21-web-widget 5.1.30 → 5.1.32-rc2

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.
Files changed (56) hide show
  1. package/.github/workflows/docker-community-push-latest.yml +23 -13
  2. package/.github/workflows/docker-image-tag-community-tag-push.yml +22 -12
  3. package/CHANGELOG.md +41 -2
  4. package/Dockerfile +4 -5
  5. package/angular.json +5 -2
  6. package/deploy_amazon_beta.sh +17 -7
  7. package/docs/changelog/this-branch.md +36 -0
  8. package/package.json +4 -1
  9. package/src/app/app.component.ts +10 -9
  10. package/src/app/app.module.ts +9 -0
  11. package/src/app/component/conversation-detail/conversation/conversation.component.html +7 -1
  12. package/src/app/component/conversation-detail/conversation/conversation.component.scss +2 -2
  13. package/src/app/component/conversation-detail/conversation/conversation.component.ts +34 -5
  14. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.html +2 -2
  15. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.scss +1 -1
  16. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.ts +2 -0
  17. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.html +146 -79
  18. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss +131 -13
  19. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts +108 -7
  20. package/src/app/component/last-message/last-message.component.ts +4 -1
  21. package/src/app/component/message/audio/audio.component.ts +0 -5
  22. package/src/app/component/message/audio-sync/audio-sync.component.html +19 -0
  23. package/src/app/component/message/audio-sync/audio-sync.component.scss +65 -0
  24. package/src/app/component/message/audio-sync/audio-sync.component.spec.ts +23 -0
  25. package/src/app/component/message/audio-sync/audio-sync.component.ts +197 -0
  26. package/src/app/component/message/bubble-message/bubble-message.component.html +6 -1
  27. package/src/app/component/message/bubble-message/bubble-message.component.ts +2 -1
  28. package/src/app/providers/global-settings.service.ts +21 -0
  29. package/src/app/providers/translator.service.ts +2 -0
  30. package/src/app/providers/voice/STT&TTS/openai-voice.config.ts +12 -0
  31. package/src/app/providers/voice/STT&TTS/openai-voice.provider.ts +171 -0
  32. package/src/app/providers/voice/STT&TTS/speech-provider.abstract.ts +39 -0
  33. package/src/app/providers/voice/audio.types.ts +34 -0
  34. package/src/app/providers/voice/vad.service.spec.ts +28 -0
  35. package/src/app/providers/voice/vad.service.ts +70 -0
  36. package/src/app/providers/voice/voice.service.spec.ts +60 -0
  37. package/src/app/providers/voice/voice.service.ts +264 -0
  38. package/src/app/sass/_variables.scss +1 -0
  39. package/src/app/shims/onnxruntime-web-wasm.ts +4 -0
  40. package/src/app/utils/conversation-sender-classifier.ts +21 -0
  41. package/src/app/utils/globals.ts +7 -1
  42. package/src/assets/i18n/en.json +1 -0
  43. package/src/assets/i18n/es.json +1 -0
  44. package/src/assets/i18n/fr.json +1 -0
  45. package/src/assets/i18n/it.json +1 -0
  46. package/src/assets/onnx/ort-wasm-simd-threaded.mjs +59 -0
  47. package/src/assets/onnx/ort-wasm-simd-threaded.wasm +0 -0
  48. package/src/assets/vad/silero_vad_legacy.onnx +0 -0
  49. package/src/assets/vad/vad.worklet.bundle.min.js +1 -0
  50. package/src/chat21-core/models/message.ts +2 -1
  51. package/src/chat21-core/providers/firebase/firebase-conversation-handler.ts +3 -2
  52. package/src/chat21-core/providers/mqtt/mqtt-conversation-handler.ts +12 -0
  53. package/src/chat21-core/providers/tiledesk/tiledesk-requests.service.ts +1 -1
  54. package/src/chat21-core/utils/utils-message.ts +7 -0
  55. package/src/chat21-core/utils/utils.ts +5 -2
  56. package/tsconfig.json +5 -0
@@ -12,97 +12,164 @@
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
- <!-- TEXTAREA + ICONS: conv active-->
18
- <div class="textarea-container" *ngIf="!hideTextAreaContent && !hideTextReply">
19
-
20
- <div *ngIf="!isStopRec" class="icons-container">
21
- <!-- ICON ATTACHMENT -->
22
- <label *ngIf="showAttachmentFooterButton" tabindex="1502" aria-label="allegati" for="chat21-file" class="chat21-textarea-button" [class.active]="!isFilePendingToUpload && !hideTextReply" id="chat21-start-upload-doc">
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 role="img" aria-labelledby="altIconTitle" xmlns="http://www.w3.org/2000/svg" width="24px" height="24" viewBox="0 0 24 24" fill="currentColor">
25
- <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"/>
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
- <input
31
- [attr.disabled] = "(isFilePendingToUpload || isConversationArchived || hideTextReply)? true : null"
32
- tabindex="1503"
33
- type="file"
34
- aria-label="seleziona allegato"
35
- [accept]="fileUploadAccept"
36
- name="chat21-file"
37
- id="chat21-file"
38
- #chat21_file
39
- class="inputfile"
40
- [ngStyle]="{'display': 'block', height:'1px', width:'1px', overflow: 'hidden' }"
41
- (change)="detectFiles($event)"/>
42
- </label>
43
- <!-- ICON EMOJII -->
44
- <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()">
45
- <span class="v-align-center">
46
- <svg role="img" aria-labelledby="altIconTitle" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
47
- <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"/>
48
- <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"/>
49
- <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"/>
50
- <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"/>
51
- <title id="altIconTitle">{{ translationMap?.get('EMOJI') }}</title>
52
-
53
- <!-- <path d="M0,0H20.57V20.57H0V0Z" fill="none"/>
54
- <circle cx="15.02" cy="9.86" r="1.29"/>
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"/> -->
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
+
130
+ <!-- ICON STREAM / CHIUDI STREAM (cerchio, icone bianche su iconColor) -->
131
+ <div *ngIf="showAudioStreamFooterButton" tabindex="-1" id="chat21-button-stream"
132
+ class="chat21-textarea-button chat21-stream-button" [class.active]="isStreamAudioActive || (!textInputTextArea && !hideTextReply)"
133
+ (click)="onStreamPressed($event)" [attr.aria-label]="isStreamAudioActive ? (translationMap?.get('CLOSE') || 'Chiudi stream') : (translationMap?.get('STREAM_AUDIO') || 'Stream audio')"
134
+ [ngStyle]="{ 'background-color': stylesMap?.get('iconColor') || stylesMap?.get('themeColor') }">
135
+ <span class="v-align-center chat21-stream-button__icon" *ngIf="!isStreamAudioActive">
136
+ <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">
137
+ <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"/>
138
+ <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"/>
139
+ <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"/>
140
+ <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"/>
57
141
  </svg>
58
142
  </span>
59
- </label>
143
+ <span class="v-align-center chat21-stream-button__icon" *ngIf="isStreamAudioActive">
144
+ <svg role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#ffffff" aria-hidden="true">
145
+ <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"/>
146
+ </svg>
147
+ </span>
148
+ </div>
60
149
  </div>
61
150
 
62
151
 
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
-
85
- </div>
86
-
87
- <!-- ICON SEND -->
88
- <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)">
89
- <span class="v-align-center">
90
- <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">
91
- <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"/>
92
- </svg>
93
- </span>
152
+ <div class="close-chat-container" *ngIf="closeChatInConversation">
153
+ <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')}">
154
+ <span class="v-align-center">
155
+ <!-- <svg [ngStyle]="{'fill': 'yellow' }" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24">
156
+ <path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" [ngStyle]="{'fill': stylesMap?.get('foregroundColor')}"/>
157
+ </svg> -->
158
+ <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"
159
+ width="15px" height="15px" viewBox="0 0 512 512">
160
+ <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>
161
+ <rect x="48" y="64" width="416" height="80" rx="28" ry="28" stroke-linejoin="round" stroke-width="50px" fill="none" ></rect>
162
+ <path stroke-linecap="round" stroke-linejoin="round" d="M320 304l-64 64-64-64M256 345.89V224" stroke-width="50px" fill="none"></path>
163
+ <title id="altIconTitle">{{ translationMap?.get('CLOSE_CHAT') }}</title>
164
+ </svg>
165
+ </span>
166
+ <span class="v-align-center c21-label-button">
167
+ {{translationMap?.get('CLOSE_CHAT')}}
168
+ </span>
169
+ <div class="clear"></div>
170
+ </button>
94
171
  </div>
95
172
 
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
- </div>
106
173
  </div>
107
174
 
108
175
 
@@ -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
- //if attachment icon OR emoji icon is not in DOM -> increment textarea width
13
- &:has(:not(#chat21-start-upload-doc), :not(#chat21-emoticon-picker)) .visible-text-area {
14
- width: 80%;
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
+ }
@@ -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)