@chat21/chat21-web-widget 5.1.26-rc1 → 5.1.27-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/CHANGELOG.md +22 -0
- package/docs/changelog/this-branch.md +36 -0
- package/package.json +1 -1
- package/src/app/app.component.html +1 -1
- package/src/app/app.component.ts +67 -7
- package/src/app/component/conversation-detail/conversation/conversation.component.html +13 -2
- package/src/app/component/conversation-detail/conversation/conversation.component.scss +30 -2
- package/src/app/component/conversation-detail/conversation/conversation.component.ts +172 -1
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.html +12 -9
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.scss +15 -1
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.ts +1 -1
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.html +103 -80
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss +15 -13
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts +6 -0
- package/src/app/component/conversation-detail/conversation-header/conversation-header.component.html +4 -4
- package/src/app/component/conversation-detail/conversation-header/conversation-header.component.ts +1 -0
- package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.ts +0 -18
- package/src/app/component/home/home.component.html +3 -3
- package/src/app/providers/global-settings.service.ts +38 -0
- package/src/app/sass/_variables.scss +1 -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/launch.js +61 -6
- package/src/launch_template.js +61 -6
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,27 @@
|
|
|
6
6
|
### **Copyrigth**:
|
|
7
7
|
*Tiledesk SRL*
|
|
8
8
|
|
|
9
|
+
# 5.1.27-rc1
|
|
10
|
+
- **added**: closeChatInConversation parameters
|
|
11
|
+
- **added**: close chat button under textarea footer component
|
|
12
|
+
|
|
13
|
+
# 5.1.26-rc6
|
|
14
|
+
- **changed**: mobile always opens fullscreen and ignores legacy stored size”.
|
|
15
|
+
- **changed**: changed user-typing
|
|
16
|
+
|
|
17
|
+
# 5.1.26-rc5
|
|
18
|
+
- **changed**: Hide the resize-widget button when on mobile
|
|
19
|
+
- **added**: added "I'm thinking" when the bot responds
|
|
20
|
+
|
|
21
|
+
# 5.1.26-rc4
|
|
22
|
+
- **changed**: decoupled callout from signin
|
|
23
|
+
|
|
24
|
+
# 5.1.26-rc3
|
|
25
|
+
- **changed**: start with authentication if a proactive message has been set, so if a rule has been set on at one chatbot in the project
|
|
26
|
+
|
|
27
|
+
# 5.1.26-rc2
|
|
28
|
+
- **changed**: start with authentication if hasCalloutInWidgetConfig is true
|
|
29
|
+
|
|
9
30
|
# 5.1.26-rc1
|
|
10
31
|
- **bug fixed**: improved audio recording/upload flow by ignoring empty recorder chunks, preserving the recorder MIME type when creating the audio blob/file, and uploading audio directly without Base64 conversion
|
|
11
32
|
|
|
@@ -81,6 +102,7 @@
|
|
|
81
102
|
- **changed**: Force authentication if ageChangeVisibilityDesktop or PageChangeVisibilityMobile is OPEN
|
|
82
103
|
|
|
83
104
|
# 5.1.7-rc12
|
|
105
|
+
- **changed**: Force authentication if ageChangeVisibilityDesktop or PageChangeVisibilityMobile is OPEN
|
|
84
106
|
- **changed**: Set the default autoStart value to false
|
|
85
107
|
- **added**: Added the open widget loading spinner
|
|
86
108
|
- **changed**: Load the widget without authentication and display the speech bubble
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# This branch: identificazione bot o umano
|
|
2
|
+
|
|
3
|
+
## Obiettivo
|
|
4
|
+
|
|
5
|
+
In questo branch e' stata introdotta una logica esplicita per capire, all'apertura della conversazione, se l'ultimo responder lato server e' un **bot** oppure un **umano**.
|
|
6
|
+
|
|
7
|
+
## Come viene fatta l'identificazione
|
|
8
|
+
|
|
9
|
+
- La valutazione parte dai messaggi gia' caricati in conversazione.
|
|
10
|
+
- Viene cercato l'**ultimo messaggio ricevuto dal server** (non inviato dal client corrente).
|
|
11
|
+
- Quel messaggio viene classificato con una funzione dedicata (`classifyMessageSenderKind`) che usa piu' segnali:
|
|
12
|
+
- `attributes.flowAttributes.chatbot_id` (quando presente indica bot)
|
|
13
|
+
- pattern del mittente (es. `senderId` con prefisso bot, quando applicabile)
|
|
14
|
+
- informazioni del mittente (`sender_fullname` e metadati associati)
|
|
15
|
+
|
|
16
|
+
## Regola speciale per messaggi di sistema
|
|
17
|
+
|
|
18
|
+
Se l'ultimo messaggio utile e' di tipo `system`, viene fatto un controllo aggiuntivo:
|
|
19
|
+
|
|
20
|
+
- se in `attributes` e' presente un evento con `messagelabel.key = MEMBER_JOINED_GROUP`
|
|
21
|
+
- e rappresenta il passaggio della conversazione a un operatore
|
|
22
|
+
|
|
23
|
+
allora la conversazione viene forzata a **Umano** anche se altri indizi potrebbero suggerire bot.
|
|
24
|
+
|
|
25
|
+
## Risultato in UI
|
|
26
|
+
|
|
27
|
+
- In apertura conversazione viene mostrato un badge con stato:
|
|
28
|
+
- `Bot`
|
|
29
|
+
- `Umano`
|
|
30
|
+
- Questo stato viene ricalcolato al variare dei messaggi ricevuti.
|
|
31
|
+
|
|
32
|
+
## Effetto sui feedback utente
|
|
33
|
+
|
|
34
|
+
- Il messaggio temporaneo `"sto pensando..."` viene mostrato solo quando la conversazione risulta di tipo **Bot**.
|
|
35
|
+
- Alla ricezione della prima risposta dal server, `"sto pensando..."` viene nascosto **immediatamente**.
|
|
36
|
+
- Non e' previsto alcun tempo minimo di visualizzazione del messaggio.
|
package/package.json
CHANGED
|
@@ -120,7 +120,7 @@
|
|
|
120
120
|
******* EYE-CATCHER (aka CALLOUT) *********
|
|
121
121
|
*******************************************
|
|
122
122
|
tabindex -> 20 -->
|
|
123
|
-
<chat-eyeeye-catcher-card *ngIf="
|
|
123
|
+
<chat-eyeeye-catcher-card *ngIf="!g.isOpenNewMessage"
|
|
124
124
|
(onOpenChat)="onOpenCloseWidget($event)"
|
|
125
125
|
(onCloseEyeCatcherCard)="onCloseEyeCatcherCard($event)">
|
|
126
126
|
</chat-eyeeye-catcher-card>
|
package/src/app/app.component.ts
CHANGED
|
@@ -109,6 +109,7 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
|
|
|
109
109
|
//network status
|
|
110
110
|
isOnline: boolean = true;
|
|
111
111
|
loading: boolean = false;
|
|
112
|
+
private calloutScheduleTimeout: any = null;
|
|
112
113
|
|
|
113
114
|
// alert error message
|
|
114
115
|
isShowErrorMessage: boolean = false;
|
|
@@ -328,6 +329,10 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
|
|
|
328
329
|
}
|
|
329
330
|
}
|
|
330
331
|
|
|
332
|
+
// STEP-2: schedule callout after settings are loaded,
|
|
333
|
+
// independently from auth/sign-in.
|
|
334
|
+
this.scheduleCalloutFromSettings();
|
|
335
|
+
|
|
331
336
|
|
|
332
337
|
/**CHECK IF JWT IS IN URL PARAMETERS */
|
|
333
338
|
this.logger.debug('[APP-COMP] check if token is passed throw url: ', this.g.jwt);
|
|
@@ -367,6 +372,24 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
|
|
|
367
372
|
|
|
368
373
|
}
|
|
369
374
|
|
|
375
|
+
private scheduleCalloutFromSettings() {
|
|
376
|
+
if (this.calloutScheduleTimeout) {
|
|
377
|
+
clearTimeout(this.calloutScheduleTimeout);
|
|
378
|
+
this.calloutScheduleTimeout = null;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const calloutTimer = Number(this.g.calloutTimer);
|
|
382
|
+
if (isNaN(calloutTimer) || calloutTimer < 0) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const delayMs = calloutTimer * 1000;
|
|
387
|
+
this.calloutScheduleTimeout = setTimeout(() => {
|
|
388
|
+
this.calloutScheduleTimeout = null;
|
|
389
|
+
this.showCallout();
|
|
390
|
+
}, delayMs);
|
|
391
|
+
}
|
|
392
|
+
|
|
370
393
|
private initAll() {
|
|
371
394
|
this.addComponentToWindow(this.ngZone);
|
|
372
395
|
|
|
@@ -467,7 +490,8 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
|
|
|
467
490
|
|
|
468
491
|
// this.initConversationsHandler(this.g.tenant, that.g.senderId);
|
|
469
492
|
/* If singleConversation mode is active wait to showWidget: do it later in initConversationsHandler */
|
|
470
|
-
|
|
493
|
+
const hasBotsRules = Array.isArray(this.g.botsRules) && this.g.botsRules.length > 0;
|
|
494
|
+
if ((autoStart || hasBotsRules) && !that.g.singleConversation) {
|
|
471
495
|
that.showWidget();
|
|
472
496
|
}
|
|
473
497
|
|
|
@@ -495,8 +519,15 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
|
|
|
495
519
|
// that.hideWidget();
|
|
496
520
|
// that.g.setParameter('isShown', false, true);
|
|
497
521
|
that.triggerOnAuthStateChanged(that.stateLoggedUser);
|
|
498
|
-
|
|
522
|
+
const shouldAutoAuthenticate = autoStart ||
|
|
523
|
+
this.g.onPageChangeVisibilityDesktop === 'open' ||
|
|
524
|
+
this.g.onPageChangeVisibilityMobile === 'open' ||
|
|
525
|
+
(Array.isArray(this.g.botsRules) && this.g.botsRules.length > 0)
|
|
526
|
+
// || this.g.hasCalloutInWidgetConfig;
|
|
527
|
+
if (shouldAutoAuthenticate) {
|
|
499
528
|
that.authenticate();
|
|
529
|
+
} else {
|
|
530
|
+
that.logger.debug('[APP-COMP] Skip auto-auth: startup conditions not met, show launcher only');
|
|
500
531
|
}
|
|
501
532
|
} else if(state && state === AUTH_STATE_CLOSE ){
|
|
502
533
|
that.logger.info('[APP-COMP] CLOSE - CHANNEL CLOSED: ', this.chatManager);
|
|
@@ -1189,14 +1220,32 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
|
|
|
1189
1220
|
return this.signOut();
|
|
1190
1221
|
}
|
|
1191
1222
|
|
|
1223
|
+
private canShowCalloutNow(): boolean {
|
|
1224
|
+
if (this.g.isOpen !== false) {
|
|
1225
|
+
return false;
|
|
1226
|
+
}
|
|
1227
|
+
if (this.g.isOpenNewMessage) {
|
|
1228
|
+
return false;
|
|
1229
|
+
}
|
|
1230
|
+
if (!this.g.calloutStaus) {
|
|
1231
|
+
return false;
|
|
1232
|
+
}
|
|
1233
|
+
if (!this.g.hasCalloutInWidgetConfig) {
|
|
1234
|
+
return false;
|
|
1235
|
+
}
|
|
1236
|
+
return true;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1192
1239
|
/** show callout */
|
|
1193
1240
|
private showCallout() {
|
|
1194
|
-
if (this.
|
|
1195
|
-
|
|
1196
|
-
this.eyeeyeCatcherCardComponent.openEyeCatcher();
|
|
1197
|
-
this.g.setParameter('displayEyeCatcherCard', 'block');
|
|
1198
|
-
this.triggerOnOpenEyeCatcherEvent();
|
|
1241
|
+
if (!this.canShowCalloutNow()) {
|
|
1242
|
+
return;
|
|
1199
1243
|
}
|
|
1244
|
+
if (!this.eyeeyeCatcherCardComponent) {
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
// Delegate visibility logic to the eye-catcher component.
|
|
1248
|
+
this.eyeeyeCatcherCardComponent.openEyeCatcher();
|
|
1200
1249
|
}
|
|
1201
1250
|
|
|
1202
1251
|
/** open popup conversation */
|
|
@@ -2074,6 +2123,12 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
|
|
|
2074
2123
|
}
|
|
2075
2124
|
|
|
2076
2125
|
onWidgetSizeChange(mode: any) {
|
|
2126
|
+
if (this.g?.isMobile) {
|
|
2127
|
+
this.g.fullscreenMode = true;
|
|
2128
|
+
this.g.size = 'max';
|
|
2129
|
+
return;
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2077
2132
|
const normalize = (val: any): 'min' | 'max' | 'top' => {
|
|
2078
2133
|
const v = (typeof val === 'string') ? val.toLowerCase().trim() : '';
|
|
2079
2134
|
return (v === 'min' || v === 'max' || v === 'top') ? (v as any) : 'min';
|
|
@@ -2252,6 +2307,7 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
|
|
|
2252
2307
|
this.el.nativeElement.style.setProperty('--chat-header-height', this.g.hideHeaderConversation? '0px': null)
|
|
2253
2308
|
this.el.nativeElement.style.setProperty('--font-size-bubble-message', this.g.fontSize)
|
|
2254
2309
|
this.el.nativeElement.style.setProperty('--font-family-bubble-message', this.g.fontFamily)
|
|
2310
|
+
this.el.nativeElement.style.setProperty('--chat-footer-close-button-height', this.g.closeChatInConversation? '30px': '0px')
|
|
2255
2311
|
|
|
2256
2312
|
}
|
|
2257
2313
|
|
|
@@ -2301,6 +2357,10 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
|
|
|
2301
2357
|
/** elimino tutte le sottoscrizioni */
|
|
2302
2358
|
ngOnDestroy() {
|
|
2303
2359
|
this.logger.debug('[APP-COMP] this.subscriptions', this.subscriptions);
|
|
2360
|
+
if (this.calloutScheduleTimeout) {
|
|
2361
|
+
clearTimeout(this.calloutScheduleTimeout);
|
|
2362
|
+
this.calloutScheduleTimeout = null;
|
|
2363
|
+
}
|
|
2304
2364
|
const windowContext = this.g.windowContext;
|
|
2305
2365
|
if (windowContext && windowContext['tiledesk']) {
|
|
2306
2366
|
windowContext['tiledesk']['angularcomponent'] = null;
|
|
@@ -30,12 +30,21 @@
|
|
|
30
30
|
[isTypings]="isTypings"
|
|
31
31
|
[nameUserTypingNow]="nameUserTypingNow"
|
|
32
32
|
[typingLocation]="g?.typingLocation"
|
|
33
|
+
[isMobile]="g?.isMobile"
|
|
33
34
|
(onBack)="onBackHomeFN()"
|
|
34
35
|
(onCloseWidget)="onCloseWidgetFN()"
|
|
35
36
|
(onMenuOptionClick)="onMenuOptionClick($event)"
|
|
36
37
|
(onMenuOptionShow)="onMenuOption($event)">
|
|
37
38
|
</chat-conversation-header>
|
|
38
39
|
|
|
40
|
+
<!-- Badge: natura dell'ultimo messaggio ricevuto dal server -->
|
|
41
|
+
<!-- <div
|
|
42
|
+
*ngIf="showLastServerSenderBadge"
|
|
43
|
+
id="chat21-last-server-sender-badge"
|
|
44
|
+
[ngClass]="lastServerSenderKind">
|
|
45
|
+
{{ lastServerSenderBadgeText }}
|
|
46
|
+
</div> -->
|
|
47
|
+
|
|
39
48
|
<div id="dropZone_container" *ngIf="isHovering"
|
|
40
49
|
[class.hideTextReply]="hideFooterTextReply && g?.poweredBy">
|
|
41
50
|
<div class="drop">
|
|
@@ -54,6 +63,7 @@
|
|
|
54
63
|
[idUserTypingNow]="idUserTypingNow"
|
|
55
64
|
[nameUserTypingNow]="nameUserTypingNow"
|
|
56
65
|
[typingLocation]="g?.typingLocation"
|
|
66
|
+
[showThinkingMessage]="showThinkingMessage"
|
|
57
67
|
[fullscreenMode]="g?.fullscreenMode"
|
|
58
68
|
[translationMap]="translationMapContent"
|
|
59
69
|
[stylesMap]="stylesMap"
|
|
@@ -68,7 +78,6 @@
|
|
|
68
78
|
(dragleave)="drag($event)" >
|
|
69
79
|
</chat-conversation-content>
|
|
70
80
|
|
|
71
|
-
|
|
72
81
|
|
|
73
82
|
<!-- INTERNAL FRAME FOR SELF ACTION LINK BUTTONS-->
|
|
74
83
|
<chat-internal-frame *ngIf="isButtonUrl"
|
|
@@ -128,6 +137,7 @@
|
|
|
128
137
|
[isMobile]="g?.isMobile"
|
|
129
138
|
[isEmojiiPickerShow]="isEmojiiPickerShow"
|
|
130
139
|
[footerMessagePlaceholder]="footerMessagePlaceholder"
|
|
140
|
+
[closeChatInConversation]="g?.closeChatInConversation"
|
|
131
141
|
[fileUploadAccept]="g?.fileUploadAccept"
|
|
132
142
|
[dropEvent]="dropEvent"
|
|
133
143
|
[poweredBy]="g?.poweredBy"
|
|
@@ -138,7 +148,8 @@
|
|
|
138
148
|
(onAfterSendMessage)="onAfterSendMessageFN($event)"
|
|
139
149
|
(onChangeTextArea)="onChangeTextArea($event)"
|
|
140
150
|
(onAttachmentFileButtonClicked)="onAttachmentFileButtonClicked($event)"
|
|
141
|
-
(onNewConversationButtonClicked)="onNewConversationButtonClickedFN($event)"
|
|
151
|
+
(onNewConversationButtonClicked)="onNewConversationButtonClickedFN($event)"
|
|
152
|
+
(onCloseChatButtonClicked)="onCloseChatButtonClickedFN($event)">
|
|
142
153
|
</chat-conversation-footer>
|
|
143
154
|
|
|
144
155
|
</div>
|
|
@@ -137,7 +137,7 @@
|
|
|
137
137
|
#dropZone_container{
|
|
138
138
|
position: absolute;
|
|
139
139
|
top: 52px;
|
|
140
|
-
bottom: calc(var(--chat-footer-logo-height) + var(--chat-footer-height));
|
|
140
|
+
bottom: calc(var(--chat-footer-logo-height) + var(--chat-footer-height) + var(--chat-footer-close-button-height));
|
|
141
141
|
left: 0;
|
|
142
142
|
right: 0;
|
|
143
143
|
background-color: rgba(240,248,255,0.6);
|
|
@@ -153,6 +153,34 @@
|
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
#chat21-last-server-sender-badge{
|
|
157
|
+
position: absolute;
|
|
158
|
+
top: 58px;
|
|
159
|
+
right: 12px;
|
|
160
|
+
z-index: 25;
|
|
161
|
+
padding: 6px 10px;
|
|
162
|
+
border-radius: 999px;
|
|
163
|
+
font-size: 0.95em;
|
|
164
|
+
font-weight: 600;
|
|
165
|
+
line-height: 1.1;
|
|
166
|
+
border: 1px solid rgba(0,0,0,0.08);
|
|
167
|
+
color: rgba(0,0,0,0.65);
|
|
168
|
+
background: rgba(0,0,0,0.03);
|
|
169
|
+
user-select: none;
|
|
170
|
+
|
|
171
|
+
&.bot{
|
|
172
|
+
border-color: rgba(0, 150, 136, 0.45);
|
|
173
|
+
color: rgba(0, 150, 136, 1);
|
|
174
|
+
background: rgba(0, 150, 136, 0.12);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
&.human{
|
|
178
|
+
border-color: rgba(63, 81, 181, 0.45);
|
|
179
|
+
color: rgba(63, 81, 181, 1);
|
|
180
|
+
background: rgba(63, 81, 181, 0.12);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
156
184
|
dialog:-internal-dialog-in-top-layer{
|
|
157
185
|
border: 0px;
|
|
158
186
|
border-radius: 16px;
|
|
@@ -212,7 +240,7 @@ dialog:-internal-dialog-in-top-layer{
|
|
|
212
240
|
|
|
213
241
|
|
|
214
242
|
::ng-deep .chat21-sheet-content{
|
|
215
|
-
bottom: calc(var(--chat-footer-logo-height) + var(--chat-footer-height) + 34px)!important;
|
|
243
|
+
bottom: calc(var(--chat-footer-logo-height) + var(--chat-footer-height) + var(--chat-footer-close-button-height) + 34px)!important;
|
|
216
244
|
}
|
|
217
245
|
|
|
218
246
|
}
|
|
@@ -113,6 +113,15 @@ export class ConversationComponent implements OnInit, AfterViewInit, OnChanges {
|
|
|
113
113
|
// availableAgentsStatus = false; // indica quando è impostato lo stato degli agenti nel subscribe
|
|
114
114
|
messages: Array<MessageModel> = [];
|
|
115
115
|
|
|
116
|
+
// Temporary "thinking" state after a client message is sent.
|
|
117
|
+
public showThinkingMessage: boolean = false;
|
|
118
|
+
private waitingServerReply: boolean = false;
|
|
119
|
+
|
|
120
|
+
// Badge "ultimo messaggio ricevuto dal server" (bot/umano)
|
|
121
|
+
public showLastServerSenderBadge: boolean = false;
|
|
122
|
+
public lastServerSenderKind: 'bot' | 'human' | null = null;
|
|
123
|
+
public lastServerSenderBadgeText: string = '';
|
|
124
|
+
|
|
116
125
|
|
|
117
126
|
CLIENT_BROWSER: string = navigator.userAgent;
|
|
118
127
|
|
|
@@ -235,7 +244,8 @@ export class ConversationComponent implements OnInit, AfterViewInit, OnChanges {
|
|
|
235
244
|
'CONTINUE',
|
|
236
245
|
'EMOJI_NOT_ELLOWED',
|
|
237
246
|
'ATTACHMENT',
|
|
238
|
-
'EMOJI'
|
|
247
|
+
'EMOJI',
|
|
248
|
+
'CLOSE_CHAT'
|
|
239
249
|
];
|
|
240
250
|
|
|
241
251
|
const keysContent = [
|
|
@@ -252,6 +262,7 @@ export class ConversationComponent implements OnInit, AfterViewInit, OnChanges {
|
|
|
252
262
|
'LABEL_TODAY',
|
|
253
263
|
'LABEL_TOMORROW',
|
|
254
264
|
'LABEL_LOADING',
|
|
265
|
+
'LABEL_THINKING',
|
|
255
266
|
'LABEL_TO',
|
|
256
267
|
'ARRAY_DAYS',
|
|
257
268
|
];
|
|
@@ -356,6 +367,130 @@ export class ConversationComponent implements OnInit, AfterViewInit, OnChanges {
|
|
|
356
367
|
|
|
357
368
|
}
|
|
358
369
|
|
|
370
|
+
private classifyMessageSenderKind(msg: MessageModel | null | undefined): 'bot' | 'human' | 'system' | 'unknown' {
|
|
371
|
+
if (!msg) return 'unknown';
|
|
372
|
+
|
|
373
|
+
const sender = msg.sender;
|
|
374
|
+
const senderFullname = msg.sender_fullname;
|
|
375
|
+
const senderFullnameLower = (senderFullname || '').toString().toLowerCase();
|
|
376
|
+
|
|
377
|
+
// System messages are always from "system".
|
|
378
|
+
if (sender === 'system' || senderFullnameLower === 'system') {
|
|
379
|
+
return 'system';
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const chatbotId = msg?.attributes?.flowAttributes?.chatbot_id;
|
|
383
|
+
if (chatbotId && sender && String(chatbotId) === String(sender)) {
|
|
384
|
+
return 'bot';
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Fallback heuristics (used when chatbot_id is missing)
|
|
388
|
+
if (sender && String(sender).includes('bot_')) {
|
|
389
|
+
return 'bot';
|
|
390
|
+
}
|
|
391
|
+
if (senderFullnameLower.includes('bot')) {
|
|
392
|
+
return 'bot';
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return 'human';
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Detects explicit handoff-to-human system messages.
|
|
400
|
+
* Example:
|
|
401
|
+
* attributes.subtype = "info"
|
|
402
|
+
* attributes.updateconversation = true
|
|
403
|
+
* attributes.messagelabel.key = "MEMBER_JOINED_GROUP"
|
|
404
|
+
* attributes.messagelabel.parameters.member_id = "<human-agent-id>"
|
|
405
|
+
*/
|
|
406
|
+
private isHumanHandoffSystemMessage(msg: MessageModel | null | undefined): boolean {
|
|
407
|
+
if (!msg) return false;
|
|
408
|
+
if (msg.sender !== 'system') return false;
|
|
409
|
+
|
|
410
|
+
const attrs: any = msg.attributes || {};
|
|
411
|
+
const key = attrs?.messagelabel?.key;
|
|
412
|
+
const memberId = attrs?.messagelabel?.parameters?.member_id;
|
|
413
|
+
|
|
414
|
+
if (attrs?.subtype !== 'info') return false;
|
|
415
|
+
if (attrs?.updateconversation !== true) return false;
|
|
416
|
+
if (key !== 'MEMBER_JOINED_GROUP') return false;
|
|
417
|
+
if (!memberId || typeof memberId !== 'string') return false;
|
|
418
|
+
|
|
419
|
+
// Exclude system/bot/self joins.
|
|
420
|
+
if (memberId === 'system') return false;
|
|
421
|
+
if (memberId.startsWith('bot_')) return false;
|
|
422
|
+
if (this.senderId && memberId === this.senderId) return false;
|
|
423
|
+
|
|
424
|
+
return true;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Finds the last server message (sender != client) and classifies it as bot/human.
|
|
429
|
+
* If the last server message is "system", it scans backward to find the last non-system server message.
|
|
430
|
+
*/
|
|
431
|
+
private refreshLastServerSenderBadge() {
|
|
432
|
+
const senderId = this.senderId;
|
|
433
|
+
const msgs = this.messages || [];
|
|
434
|
+
|
|
435
|
+
let found: 'bot' | 'human' | null = null;
|
|
436
|
+
let latestServerMsg: MessageModel | null = null;
|
|
437
|
+
|
|
438
|
+
// messages are kept sorted by the handler, but we still scan from the end for "latest".
|
|
439
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
440
|
+
const m = msgs[i];
|
|
441
|
+
if (!m) continue;
|
|
442
|
+
|
|
443
|
+
// Skip messages sent by the current client/user.
|
|
444
|
+
if (senderId && m.sender === senderId) continue;
|
|
445
|
+
|
|
446
|
+
if (!latestServerMsg) {
|
|
447
|
+
latestServerMsg = m;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const kind = this.classifyMessageSenderKind(m);
|
|
451
|
+
if (kind === 'system') continue;
|
|
452
|
+
if (kind === 'bot' || kind === 'human') {
|
|
453
|
+
found = kind;
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Priority rule requested: if the latest server message is a system handoff message,
|
|
459
|
+
// consider the conversation as "human".
|
|
460
|
+
if (this.isHumanHandoffSystemMessage(latestServerMsg)) {
|
|
461
|
+
found = 'human';
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
this.lastServerSenderKind = found;
|
|
465
|
+
this.showLastServerSenderBadge = found !== null;
|
|
466
|
+
this.lastServerSenderBadgeText = found === 'bot' ? 'Bot' : (found === 'human' ? 'Umano' : '');
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
private startThinkingMessage() {
|
|
470
|
+
this.waitingServerReply = true;
|
|
471
|
+
this.showThinkingMessage = true;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
private stopThinkingMessageImmediately() {
|
|
475
|
+
if (!this.waitingServerReply) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
this.waitingServerReply = false;
|
|
479
|
+
this.showThinkingMessage = false;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
private shouldShowThinkingForBot(): boolean {
|
|
483
|
+
// Primary source: latest server-side classification already computed.
|
|
484
|
+
if (this.lastServerSenderKind === 'bot') {
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
487
|
+
// Safe fallback for bot-targeted direct conversations.
|
|
488
|
+
if (this.conversationWith && this.conversationWith.includes('bot_')) {
|
|
489
|
+
return true;
|
|
490
|
+
}
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
493
|
+
|
|
359
494
|
/**
|
|
360
495
|
* do per scontato che this.userId esiste!!!
|
|
361
496
|
*/
|
|
@@ -368,6 +503,11 @@ export class ConversationComponent implements OnInit, AfterViewInit, OnChanges {
|
|
|
368
503
|
// this.connectConversation();
|
|
369
504
|
await this.initConversationHandler();
|
|
370
505
|
|
|
506
|
+
// After loading/connecting, compute "ultimo messaggio ricevuto dal server"
|
|
507
|
+
// (excluding messages sent by the client).
|
|
508
|
+
this.refreshLastServerSenderBadge();
|
|
509
|
+
setTimeout(() => this.refreshLastServerSenderBadge(), 300);
|
|
510
|
+
|
|
371
511
|
this.logger.debug('[CONV-COMP] ------ 4: initializeChatManager ------ ');
|
|
372
512
|
//this.initializeChatManager();
|
|
373
513
|
|
|
@@ -787,8 +927,14 @@ export class ConversationComponent implements OnInit, AfterViewInit, OnChanges {
|
|
|
787
927
|
subscribtion = this.conversationHandlerService.messageAdded.pipe(takeUntil(this.unsubscribe$)).subscribe((msg: MessageModel) => {
|
|
788
928
|
this.logger.debug('[CONV-COMP] ***** DETAIL messageAdded *****', msg);
|
|
789
929
|
if (msg) {
|
|
930
|
+
if (msg.sender !== this.senderId) {
|
|
931
|
+
this.stopThinkingMessageImmediately();
|
|
932
|
+
}
|
|
790
933
|
|
|
791
934
|
that.newMessageAdded(msg);
|
|
935
|
+
// Update badge based on the latest message received from the server.
|
|
936
|
+
// We rely on `messages` being kept in-sync by the conversation handler.
|
|
937
|
+
that.refreshLastServerSenderBadge();
|
|
792
938
|
this.checkMessagesLegntForTranscriptDownloadMenuOption();
|
|
793
939
|
this.resetTimeout();
|
|
794
940
|
|
|
@@ -1102,6 +1248,13 @@ export class ConversationComponent implements OnInit, AfterViewInit, OnChanges {
|
|
|
1102
1248
|
}
|
|
1103
1249
|
/** CALLED BY: conv-header component */
|
|
1104
1250
|
onWidgetSizeChange(mode: any){
|
|
1251
|
+
if (this.g?.isMobile) {
|
|
1252
|
+
this.g.fullscreenMode = true;
|
|
1253
|
+
this.g.size = 'max';
|
|
1254
|
+
this.isMenuShow = false;
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1105
1258
|
const normalize = (val: any): 'min' | 'max' | 'top' => {
|
|
1106
1259
|
const v = (typeof val === 'string') ? val.toLowerCase().trim() : '';
|
|
1107
1260
|
return (v === 'min' || v === 'max' || v === 'top') ? (v as any) : 'min';
|
|
@@ -1308,6 +1461,16 @@ export class ConversationComponent implements OnInit, AfterViewInit, OnChanges {
|
|
|
1308
1461
|
}
|
|
1309
1462
|
/** CALLED BY: conv-footer component */
|
|
1310
1463
|
onAfterSendMessageFN(message: MessageModel){
|
|
1464
|
+
// Manage thinking state only for messages sent by the current client.
|
|
1465
|
+
// Do not force-hide here for other message types/events.
|
|
1466
|
+
if (message && message.sender === this.senderId) {
|
|
1467
|
+
if (this.shouldShowThinkingForBot()) {
|
|
1468
|
+
this.startThinkingMessage();
|
|
1469
|
+
} else {
|
|
1470
|
+
this.showThinkingMessage = false;
|
|
1471
|
+
this.waitingServerReply = false;
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1311
1474
|
this.onAfterSendMessage.emit(message)
|
|
1312
1475
|
}
|
|
1313
1476
|
/** CALLED BY: conv-footer component */
|
|
@@ -1339,6 +1502,12 @@ export class ConversationComponent implements OnInit, AfterViewInit, OnChanges {
|
|
|
1339
1502
|
this.logger.debug('[CONV-COMP] floating onNewConversationButtonClicked')
|
|
1340
1503
|
this.onNewConversationButtonClicked.emit()
|
|
1341
1504
|
}
|
|
1505
|
+
|
|
1506
|
+
/** CALLED BY: conv-footer component */
|
|
1507
|
+
onCloseChatButtonClickedFN(event){
|
|
1508
|
+
this.logger.debug('[CONV-COMP] onCloseChatButtonClicked::::', event)
|
|
1509
|
+
this.onCloseChat()
|
|
1510
|
+
}
|
|
1342
1511
|
// =========== END: event emitter function ====== //
|
|
1343
1512
|
|
|
1344
1513
|
|
|
@@ -1359,6 +1528,8 @@ export class ConversationComponent implements OnInit, AfterViewInit, OnChanges {
|
|
|
1359
1528
|
//this.storageService.removeItem('activeConversation');
|
|
1360
1529
|
this.isConversationArchived = false;
|
|
1361
1530
|
this.hideTextAreaContent = false;
|
|
1531
|
+
this.showThinkingMessage = false;
|
|
1532
|
+
this.waitingServerReply = false;
|
|
1362
1533
|
this.conversationFooter.textInputTextArea='';
|
|
1363
1534
|
this.hideFooterTextReply = false;
|
|
1364
1535
|
this.footerMessagePlaceholder = '';
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
[nameUserTypingNow]="nameUserTypingNow">
|
|
14
14
|
</user-typing>
|
|
15
15
|
</span>
|
|
16
|
-
|
|
17
16
|
|
|
18
17
|
<div id="chat21-sheet-content" class="chat21-sheet-content">
|
|
19
18
|
<div class="chat21-conversation-parts-container">
|
|
@@ -120,25 +119,21 @@
|
|
|
120
119
|
|
|
121
120
|
<!-- FILE PENDING UPLOAD -->
|
|
122
121
|
<!-- -->
|
|
123
|
-
<div *ngIf="showUploadProgress" class="msg_container base_sent" >
|
|
122
|
+
<div *ngIf="showUploadProgress && !showThinkingMessage" class="msg_container base_sent" >
|
|
124
123
|
<div class="chat21-spinner active" id="chat21-spinner" style="margin: 0px 6px 0px;">
|
|
125
124
|
<div class="chat21-bounce1" [ngStyle]="{'background-color': stylesMap.get('iconColor')}"></div>
|
|
126
125
|
<div class="chat21-bounce2" [ngStyle]="{'background-color': stylesMap.get('iconColor'), 'opacity': 0.4}"></div>
|
|
127
126
|
<div class="chat21-bounce3" [ngStyle]="{'background-color': stylesMap.get('iconColor'), 'opacity': 0.6}"></div>
|
|
128
127
|
</div>
|
|
129
|
-
|
|
130
|
-
|
|
131
128
|
</div>
|
|
132
129
|
|
|
133
130
|
|
|
134
|
-
<div *ngIf="isTypings && typingLocation==='content'
|
|
135
|
-
<!-- !isSameSender(idUserTypingNow, i) -->
|
|
131
|
+
<div *ngIf="isTypings && !showThinkingMessage && typingLocation==='content'" class="msg_container base_receive">
|
|
136
132
|
<chat-avatar-image *ngIf="idUserTypingNow " class="slide-in-left"
|
|
137
133
|
[senderID]="idUserTypingNow"
|
|
138
134
|
[senderFullname]="nameUserTypingNow"
|
|
139
135
|
[baseLocation]="baseLocation">
|
|
140
136
|
</chat-avatar-image>
|
|
141
|
-
|
|
142
137
|
<user-typing
|
|
143
138
|
[ngClass]="{'userTypingNowExist': !idUserTypingNow}"
|
|
144
139
|
[color]="stylesMap?.get('iconColor')"
|
|
@@ -147,7 +142,16 @@
|
|
|
147
142
|
[nameUserTypingNow]="nameUserTypingNow">
|
|
148
143
|
</user-typing>
|
|
149
144
|
</div>
|
|
150
|
-
|
|
145
|
+
|
|
146
|
+
<div *ngIf="showThinkingMessage" class="msg_container base_receive thinking_receive">
|
|
147
|
+
<user-typing class="loading thinking-dots"
|
|
148
|
+
[color]="stylesMap?.get('iconColor')"
|
|
149
|
+
[translationMap]="translationMap"
|
|
150
|
+
[idUserTypingNow]="idUserTypingNow"
|
|
151
|
+
[nameUserTypingNow]="nameUserTypingNow">
|
|
152
|
+
</user-typing>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
151
155
|
</div>
|
|
152
156
|
</div>
|
|
153
157
|
</div>
|
|
@@ -155,7 +159,6 @@
|
|
|
155
159
|
</div>
|
|
156
160
|
</div>
|
|
157
161
|
</div>
|
|
158
|
-
|
|
159
162
|
</div>
|
|
160
163
|
|
|
161
164
|
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
top: 0;
|
|
45
45
|
right: 0;
|
|
46
46
|
left: 0;
|
|
47
|
-
bottom: calc(var(--chat-footer-logo-height) + var(--chat-footer-height));
|
|
47
|
+
bottom: calc(var(--chat-footer-logo-height) + var(--chat-footer-height) + var(--chat-footer-close-button-height));
|
|
48
48
|
overflow: hidden;
|
|
49
49
|
.time{
|
|
50
50
|
margin-bottom: 20px;
|
|
@@ -122,6 +122,11 @@
|
|
|
122
122
|
text-align: center;
|
|
123
123
|
padding: 0px 0px 6px 0px
|
|
124
124
|
}
|
|
125
|
+
.thinking_receive {
|
|
126
|
+
.thinking-dots ::ng-deep > div.spinner{
|
|
127
|
+
margin: 25px 30px;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
125
130
|
/* ====== SET MESSAGES ====== */
|
|
126
131
|
.messages {
|
|
127
132
|
border-radius: var(--border-radius-bubble-message);
|
|
@@ -265,6 +270,15 @@
|
|
|
265
270
|
}// end c21-body-container
|
|
266
271
|
}// end c21-body
|
|
267
272
|
|
|
273
|
+
@keyframes thinking-dot {
|
|
274
|
+
0%, 80%, 100% {
|
|
275
|
+
opacity: 0.2;
|
|
276
|
+
}
|
|
277
|
+
40% {
|
|
278
|
+
opacity: 1;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
268
282
|
/* LOADING */
|
|
269
283
|
/*http://tobiasahlin.com/spinkit/*/
|
|
270
284
|
#chat21-spinner {
|