@chat21/chat21-web-widget 5.1.27-rc1 → 5.1.30-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 +9 -0
- package/docs/changelog/badge_Bot_Umano.md +85 -0
- package/package.json +1 -1
- package/src/app/app.component.ts +16 -3
- package/src/app/component/conversation-detail/conversation/conversation.component.ts +22 -127
- package/src/app/component/conversation-detail/conversation-header/conversation-header.component.html +1 -1
- package/src/app/component/last-message/last-message.component.ts +4 -1
- package/src/app/providers/global-settings.service.ts +2 -14
- package/src/app/utils/conversation-sender-classifier.ts +116 -0
- package/src/app/utils/globals.ts +25 -3
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,15 @@
|
|
|
6
6
|
### **Copyrigth**:
|
|
7
7
|
*Tiledesk SRL*
|
|
8
8
|
|
|
9
|
+
# 5.1.30-rc1
|
|
10
|
+
- **bug fixed**: startHidden is not working properly
|
|
11
|
+
|
|
12
|
+
# 5.1.27-rc3
|
|
13
|
+
- **bug fixed**: fixed Bot/Human conversation detection by correctly classifying bot replies
|
|
14
|
+
|
|
15
|
+
# 5.1.27-rc2
|
|
16
|
+
- **bug fixed**: centralized fullscreen management on mobile and handled the case of the closed widget that remained fullscreen
|
|
17
|
+
|
|
9
18
|
# 5.1.27-rc1
|
|
10
19
|
- **added**: closeChatInConversation parameters
|
|
11
20
|
- **added**: close chat button under textarea footer component
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Questo branch: UX bot/umano + disaccoppiamento callout
|
|
2
|
+
|
|
3
|
+
## Contesto
|
|
4
|
+
|
|
5
|
+
Questo branch migliora il feedback in conversazione e rende il comportamento del callout indipendente dal sign-in del widget.
|
|
6
|
+
|
|
7
|
+
## Modifiche incluse
|
|
8
|
+
|
|
9
|
+
- Aggiunto un **badge Bot/Umano** nella vista conversazione.
|
|
10
|
+
- All'apertura della conversazione (e ad ogni nuovo messaggio) analizza i messaggi e determina il tipo conversazione.
|
|
11
|
+
|
|
12
|
+
## Come viene identificato Bot/Umano
|
|
13
|
+
|
|
14
|
+
La logica vive in `ConversationComponent` e in un modulo pure di supporto (`src/app/utils/conversation-sender-classifier.ts`).
|
|
15
|
+
|
|
16
|
+
### Regole (in ordine)
|
|
17
|
+
|
|
18
|
+
1) **Selezione del messaggio “server” più recente**
|
|
19
|
+
- Si considerano solo i messaggi **non inviati dal client corrente** (si scartano quelli con `sender === senderId`).
|
|
20
|
+
- L’“ultimo” messaggio server viene determinato in modo robusto usando `timestamp` (equivalente a ordinare per timestamp decrescente).
|
|
21
|
+
|
|
22
|
+
2) **Regola speciale: handoff a operatore (Umano)**
|
|
23
|
+
- Se l’ultimo messaggio server è `system` e rappresenta un handoff verso umano:
|
|
24
|
+
- `attributes.subtype === "info"`
|
|
25
|
+
- `attributes.updateconversation === true`
|
|
26
|
+
- `attributes.messagelabel.key === "MEMBER_JOINED_GROUP"`
|
|
27
|
+
- `attributes.messagelabel.parameters.member_id` **non** è `system`, **non** inizia con `bot_`, e **non** coincide con il `senderId` del client
|
|
28
|
+
- allora la conversazione viene forzata a **Umano**.
|
|
29
|
+
|
|
30
|
+
3) **Se l’ultimo messaggio server non è system**
|
|
31
|
+
- viene classificato come **Bot** se:
|
|
32
|
+
- è presente `attributes.flowAttributes.chatbot_id` (segnale forte, anche se diverso da `sender`), oppure
|
|
33
|
+
- euristiche legacy: `sender` contiene `bot_` oppure `sender_fullname` contiene “bot”
|
|
34
|
+
- altrimenti come **Umano**.
|
|
35
|
+
|
|
36
|
+
4) **Se l’ultimo messaggio server è system (ma non handoff umano)**
|
|
37
|
+
- si cerca il messaggio server **precedente non-system** e si applica la classificazione Bot/Umano su quello.
|
|
38
|
+
|
|
39
|
+
### Risultato in UI
|
|
40
|
+
- Il badge mostra **Bot** o **Umano** in base all’ultimo responder server **non-system**, con precedenza della regola handoff.
|
|
41
|
+
|
|
42
|
+
- Aggiunto il feedback temporaneo **"sto pensando..."** dopo l'invio del messaggio da parte del client.
|
|
43
|
+
- Mostrato solo quando la conversazione e' classificata come **Bot**.
|
|
44
|
+
- Nascosto alla prima risposta server (nessuna durata minima).
|
|
45
|
+
|
|
46
|
+
- Rimossa l'implementazione temporanea del toast "ciao" nel footer e relativo wiring.
|
|
47
|
+
|
|
48
|
+
- Abilitato il percorso di avvio widget per i casi guidati da bot quando sono presenti `botsRules`.
|
|
49
|
+
|
|
50
|
+
## Disaccoppiamento callout (step completati)
|
|
51
|
+
|
|
52
|
+
- **Step 1**: rimossa la dipendenza da `g.senderId` nel rendering del componente callout in `app.component.html`.
|
|
53
|
+
- Prima: render solo con `g.senderId && !g.isOpenNewMessage`
|
|
54
|
+
- Dopo: render con `!g.isOpenNewMessage`
|
|
55
|
+
|
|
56
|
+
- **Step 2**: scheduling del callout al caricamento delle impostazioni widget in `AppComponent`.
|
|
57
|
+
- Aggiunto `scheduleCalloutFromSettings()` basato su `g.calloutTimer`.
|
|
58
|
+
- Invocato subito dopo la disponibilita' delle settings (non legato al login).
|
|
59
|
+
- Aggiunta la pulizia del timeout in `ngOnDestroy()`.
|
|
60
|
+
|
|
61
|
+
- **Step 3**: introdotte precedenze UI e rimossa la duplicazione dello scheduling callout.
|
|
62
|
+
- Aggiunta guardia `canShowCalloutNow()` in `AppComponent`:
|
|
63
|
+
- widget chiuso
|
|
64
|
+
- nessuna preview nuovo messaggio attiva
|
|
65
|
+
- stato callout abilitato
|
|
66
|
+
- callout presente nella configurazione widget
|
|
67
|
+
- Aggiornato `showCallout()` per aprire il callout solo quando le guardie passano e il componente esiste.
|
|
68
|
+
- Rimosso il timer interno (`openIfCallOutTimer`) da `EyeeyeCatcherCardComponent` per evitare doppi trigger.
|
|
69
|
+
|
|
70
|
+
## Comportamento atteso dopo questo branch
|
|
71
|
+
|
|
72
|
+
- Il callout puo' essere innescato da configurazione anche senza sign-in.
|
|
73
|
+
- Il callout non compare quando il widget e' aperto o quando la preview nuovo messaggio e' attiva.
|
|
74
|
+
- La UI della conversazione indica chiaramente se l'ultimo responder e' bot o umano.
|
|
75
|
+
- "Sto pensando..." compare solo nelle conversazioni bot e ha un comportamento prevedibile.
|
|
76
|
+
|
|
77
|
+
## Checklist regressioni (bot/umano)
|
|
78
|
+
- **Bot con `flowAttributes.chatbot_id` diverso da `sender`** (caso reale):
|
|
79
|
+
- Atteso: classificazione **Bot** (non Human).
|
|
80
|
+
- **System → bot joined** (`MEMBER_JOINED_GROUP` con `member_id` che inizia per `bot_`):
|
|
81
|
+
- Atteso: non forzare Umano (non è handoff verso operatore).
|
|
82
|
+
- **System → handoff umano** (`MEMBER_JOINED_GROUP` con `member_id` umano):
|
|
83
|
+
- Atteso: forzare **Umano** anche se messaggi precedenti indicavano bot.
|
|
84
|
+
- **Conversazione con soli messaggi system** (dopo aver escluso quelli del client):
|
|
85
|
+
- Atteso: nessun crash; badge Bot/Umano può essere nascosto, ma la classificazione dell’ultimo messaggio server deve restare coerente.
|
package/package.json
CHANGED
package/src/app/app.component.ts
CHANGED
|
@@ -169,13 +169,13 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
|
|
|
169
169
|
if (conversation.attributes && conversation.attributes['subtype'] === 'info') {
|
|
170
170
|
return;
|
|
171
171
|
}
|
|
172
|
-
if (conversation.is_new &&
|
|
172
|
+
if (conversation.is_new && that.isInitialized) {
|
|
173
173
|
that.manageTabNotification(false, 'conv-added')
|
|
174
174
|
// this.soundMessage();
|
|
175
175
|
}
|
|
176
|
-
if(this.g.isOpen === false){
|
|
177
|
-
that.lastConversation = conversation;
|
|
176
|
+
if(this.g.isOpen === false && conversation.sender !== this.g.senderId && !isInfo(conversation)){
|
|
178
177
|
that.g.isOpenNewMessage = true;
|
|
178
|
+
that.lastConversation = conversation;
|
|
179
179
|
}
|
|
180
180
|
} else {
|
|
181
181
|
//widget closed
|
|
@@ -223,6 +223,7 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
|
|
|
223
223
|
that.lastConversation = conversation;
|
|
224
224
|
that.g.isOpenNewMessage = true;
|
|
225
225
|
that.logger.debug('[APP-COMP] lastconversationnn', that.lastConversation)
|
|
226
|
+
that.logger.debug('[APP-COMP] lastconversationnn message' + JSON.stringify(that.lastConversation?.attributes?.commands))
|
|
226
227
|
}
|
|
227
228
|
let badgeNewConverstionNumber = that.conversationsHandlerService.countIsNew()
|
|
228
229
|
that.g.setParameter('conversationsBadge', badgeNewConverstionNumber);
|
|
@@ -452,6 +453,7 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
|
|
|
452
453
|
}
|
|
453
454
|
|
|
454
455
|
const autoStart = this.g.autoStart;
|
|
456
|
+
const startHidden = this.g.startHidden;
|
|
455
457
|
that.stateLoggedUser = state;
|
|
456
458
|
if (state && state === AUTH_STATE_ONLINE) {
|
|
457
459
|
/** sono loggato */
|
|
@@ -524,8 +526,10 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
|
|
|
524
526
|
this.g.onPageChangeVisibilityMobile === 'open' ||
|
|
525
527
|
(Array.isArray(this.g.botsRules) && this.g.botsRules.length > 0)
|
|
526
528
|
// || this.g.hasCalloutInWidgetConfig;
|
|
529
|
+
console.log('[APP-COMP] shouldAutoAuthenticate', shouldAutoAuthenticate, startHidden)
|
|
527
530
|
if (shouldAutoAuthenticate) {
|
|
528
531
|
that.authenticate();
|
|
532
|
+
if(startHidden){ that.hideWidget(); }
|
|
529
533
|
} else {
|
|
530
534
|
that.logger.debug('[APP-COMP] Skip auto-auth: startup conditions not met, show launcher only');
|
|
531
535
|
}
|
|
@@ -1253,6 +1257,7 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
|
|
|
1253
1257
|
const senderId = this.g.senderId;
|
|
1254
1258
|
this.logger.debug('[APP-COMP] f21_open senderId: ', senderId);
|
|
1255
1259
|
if (senderId) {
|
|
1260
|
+
this.enforceMobileFullscreenOnOpen();
|
|
1256
1261
|
// chiudo callout
|
|
1257
1262
|
this.g.setParameter('displayEyeCatcherCard', 'none');
|
|
1258
1263
|
// this.g.isOpen = true; // !this.isOpen;
|
|
@@ -1708,6 +1713,7 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
|
|
|
1708
1713
|
this.logger.debug('[APP-COMP] openCloseWidget', recipientId, this.g.isOpen, this.g.startFromHome);
|
|
1709
1714
|
|
|
1710
1715
|
if (this.g.isOpen === false) {
|
|
1716
|
+
this.enforceMobileFullscreenOnOpen();
|
|
1711
1717
|
if(this.forceDisconnect){
|
|
1712
1718
|
this.logger.log('[FORCE] onOpenCloseWidget --> reconnect', this.forceDisconnect)
|
|
1713
1719
|
this.messagingAuthService.createCustomToken(this.g.tiledeskToken)
|
|
@@ -2174,6 +2180,13 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
|
|
|
2174
2180
|
}
|
|
2175
2181
|
}
|
|
2176
2182
|
|
|
2183
|
+
private enforceMobileFullscreenOnOpen() {
|
|
2184
|
+
if (this.g?.isMobile) {
|
|
2185
|
+
this.g.fullscreenMode = true;
|
|
2186
|
+
this.g.size = 'max';
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2177
2190
|
/**
|
|
2178
2191
|
* MODAL RATING WIDGET:
|
|
2179
2192
|
* close modal page
|
|
@@ -40,6 +40,7 @@ import { LoggerInstance } from 'src/chat21-core/providers/logger/loggerInstance'
|
|
|
40
40
|
import { TiledeskRequestsService } from 'src/chat21-core/providers/tiledesk/tiledesk-requests.service';
|
|
41
41
|
import { ConversationContentComponent } from '../conversation-content/conversation-content.component';
|
|
42
42
|
import { checkAcceptedFile } from 'src/app/utils/utils';
|
|
43
|
+
import { computeConversationBadgeState } from 'src/app/utils/conversation-sender-classifier';
|
|
43
44
|
// import { TranslateService } from '@ngx-translate/core';
|
|
44
45
|
|
|
45
46
|
@Component({
|
|
@@ -115,12 +116,13 @@ export class ConversationComponent implements OnInit, AfterViewInit, OnChanges {
|
|
|
115
116
|
|
|
116
117
|
// Temporary "thinking" state after a client message is sent.
|
|
117
118
|
public showThinkingMessage: boolean = false;
|
|
118
|
-
private waitingServerReply: boolean = false;
|
|
119
119
|
|
|
120
120
|
// Badge "ultimo messaggio ricevuto dal server" (bot/umano)
|
|
121
121
|
public showLastServerSenderBadge: boolean = false;
|
|
122
122
|
public lastServerSenderKind: 'bot' | 'human' | null = null;
|
|
123
123
|
public lastServerSenderBadgeText: string = '';
|
|
124
|
+
// Diagnostics/internal state: kind of the latest *server* message (including system).
|
|
125
|
+
public latestServerMessageKind: 'bot' | 'human' | 'system' | 'unknown' = 'unknown';
|
|
124
126
|
|
|
125
127
|
|
|
126
128
|
CLIENT_BROWSER: string = navigator.userAgent;
|
|
@@ -367,129 +369,20 @@ export class ConversationComponent implements OnInit, AfterViewInit, OnChanges {
|
|
|
367
369
|
|
|
368
370
|
}
|
|
369
371
|
|
|
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
372
|
/**
|
|
399
|
-
*
|
|
400
|
-
*
|
|
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.
|
|
373
|
+
* Backward-compat wrappers: keep component API stable while delegating
|
|
374
|
+
* the sender classification logic to a pure utility module.
|
|
430
375
|
*/
|
|
431
376
|
private refreshLastServerSenderBadge() {
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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' : '');
|
|
377
|
+
const state = computeConversationBadgeState(this.messages || [], this.senderId);
|
|
378
|
+
this.latestServerMessageKind = state.latestServerMessageKind;
|
|
379
|
+
this.lastServerSenderKind = state.latestNonSystemResponderKind;
|
|
380
|
+
this.showLastServerSenderBadge = state.showBadge;
|
|
381
|
+
this.lastServerSenderBadgeText = state.badgeText;
|
|
467
382
|
}
|
|
468
383
|
|
|
469
|
-
|
|
470
|
-
this.waitingServerReply = true;
|
|
471
|
-
this.showThinkingMessage = true;
|
|
472
|
-
}
|
|
384
|
+
// (Implementation moved to src/app/utils/conversation-sender-classifier.ts)
|
|
473
385
|
|
|
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
386
|
|
|
494
387
|
/**
|
|
495
388
|
* do per scontato che this.userId esiste!!!
|
|
@@ -506,7 +399,6 @@ export class ConversationComponent implements OnInit, AfterViewInit, OnChanges {
|
|
|
506
399
|
// After loading/connecting, compute "ultimo messaggio ricevuto dal server"
|
|
507
400
|
// (excluding messages sent by the client).
|
|
508
401
|
this.refreshLastServerSenderBadge();
|
|
509
|
-
setTimeout(() => this.refreshLastServerSenderBadge(), 300);
|
|
510
402
|
|
|
511
403
|
this.logger.debug('[CONV-COMP] ------ 4: initializeChatManager ------ ');
|
|
512
404
|
//this.initializeChatManager();
|
|
@@ -928,7 +820,7 @@ export class ConversationComponent implements OnInit, AfterViewInit, OnChanges {
|
|
|
928
820
|
this.logger.debug('[CONV-COMP] ***** DETAIL messageAdded *****', msg);
|
|
929
821
|
if (msg) {
|
|
930
822
|
if (msg.sender !== this.senderId) {
|
|
931
|
-
this.
|
|
823
|
+
this.showThinkingMessage = false;
|
|
932
824
|
}
|
|
933
825
|
|
|
934
826
|
that.newMessageAdded(msg);
|
|
@@ -1463,13 +1355,17 @@ export class ConversationComponent implements OnInit, AfterViewInit, OnChanges {
|
|
|
1463
1355
|
onAfterSendMessageFN(message: MessageModel){
|
|
1464
1356
|
// Manage thinking state only for messages sent by the current client.
|
|
1465
1357
|
// Do not force-hide here for other message types/events.
|
|
1358
|
+
this.logger.debug('[CONV-COMP] onAfterSendMessageFN::::')
|
|
1466
1359
|
if (message && message.sender === this.senderId) {
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1360
|
+
this.logger.debug('[CONV-COMP] onAfterSendMessageFN:::: message', message)
|
|
1361
|
+
// if (this.shouldShowThinkingForBot()) {
|
|
1362
|
+
// this.logger.debug('[CONV-COMP] shouldShowThinkingForBot::::', true)
|
|
1363
|
+
// this.startThinkingMessage();
|
|
1364
|
+
// } else {
|
|
1365
|
+
// this.logger.debug('[CONV-COMP] shouldShowThinkingForBot::::', false)
|
|
1366
|
+
// this.showThinkingMessage = false;
|
|
1367
|
+
// }
|
|
1368
|
+
this.showThinkingMessage = true;
|
|
1473
1369
|
}
|
|
1474
1370
|
this.onAfterSendMessage.emit(message)
|
|
1475
1371
|
}
|
|
@@ -1529,7 +1425,6 @@ export class ConversationComponent implements OnInit, AfterViewInit, OnChanges {
|
|
|
1529
1425
|
this.isConversationArchived = false;
|
|
1530
1426
|
this.hideTextAreaContent = false;
|
|
1531
1427
|
this.showThinkingMessage = false;
|
|
1532
|
-
this.waitingServerReply = false;
|
|
1533
1428
|
this.conversationFooter.textInputTextArea='';
|
|
1534
1429
|
this.hideFooterTextReply = false;
|
|
1535
1430
|
this.footerMessagePlaceholder = '';
|
package/src/app/component/conversation-detail/conversation-header/conversation-header.component.html
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
</button>
|
|
17
17
|
|
|
18
18
|
<!-- ICON MENU OPTION -->
|
|
19
|
-
<button
|
|
19
|
+
<button [attr.disabled]="(isButtonsDisabled)?true:null" tabindex="-1" class="c21-header-button c21-right c21-button-clean" [ngStyle]="{'display': (hideHeaderConversationOptionsMenu)?'none':'flex'}" (click)="toggleMenu()" >
|
|
20
20
|
<svg aria-labelledby="altIconTitle" [ngStyle]="{'fill': stylesMap?.get('foregroundColor') }" xmlns="http://www.w3.org/2000/svg"
|
|
21
21
|
width="24" height="24" viewBox="0 0 24 24">
|
|
22
22
|
<path fill="none" d="M0 0h24v24H0V0z" />
|
|
@@ -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)
|
|
@@ -324,13 +324,13 @@ export class GlobalSettingsService {
|
|
|
324
324
|
}
|
|
325
325
|
/** set button colors */
|
|
326
326
|
this.setButtonColors();
|
|
327
|
+
// Detect mobile before loading persisted values so storage policies can depend on it.
|
|
327
328
|
this.globals.setParameter('isMobile', detectIfIsMobile(this.globals.windowContext));
|
|
328
329
|
|
|
329
330
|
this.setVariableFromStorage(this.globals);
|
|
330
331
|
this.setVariablesFromSettings(this.globals);
|
|
331
332
|
this.setVariablesFromAttributeHtml(this.globals, this.el);
|
|
332
333
|
this.setVariablesFromUrlParameters(this.globals);
|
|
333
|
-
this.enforceMobileFullscreenPolicy(this.globals);
|
|
334
334
|
|
|
335
335
|
this.setDepartmentFromExternal();
|
|
336
336
|
/** set color with gradient from theme's colors */
|
|
@@ -342,18 +342,6 @@ export class GlobalSettingsService {
|
|
|
342
342
|
this.obsSettingsService.next(true);
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
-
/**
|
|
346
|
-
* On mobile devices we always open the widget fullscreen.
|
|
347
|
-
* This also neutralizes any legacy `size` stored from previous sessions.
|
|
348
|
-
*/
|
|
349
|
-
private enforceMobileFullscreenPolicy(globals: Globals) {
|
|
350
|
-
if (!globals || globals.isMobile !== true) {
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
|
-
globals.fullscreenMode = true;
|
|
354
|
-
globals.size = 'max';
|
|
355
|
-
}
|
|
356
|
-
|
|
357
345
|
private setButtonColors() {
|
|
358
346
|
this.logger.debug('[GLOBAL-SET] ***** END SET PARAMETERS *****', this.globals);
|
|
359
347
|
const bubbleSentBackground = this.globals?.bubbleSentBackground;
|
|
@@ -1912,7 +1900,7 @@ export class GlobalSettingsService {
|
|
|
1912
1900
|
this.logger.debug('[GLOBAL-SET] setVariableFromStorage :::::::: SET VARIABLE ---------->', Object.keys(globals));
|
|
1913
1901
|
for (const key of Object.keys(globals)) {
|
|
1914
1902
|
if (globals.isMobile === true && key === 'size') {
|
|
1915
|
-
//
|
|
1903
|
+
// On mobile we always open fullscreen, so legacy/persisted widget size must be ignored.
|
|
1916
1904
|
try {
|
|
1917
1905
|
this.appStorageService.removeItem('size');
|
|
1918
1906
|
} catch (e) {
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { MessageModel } from 'src/chat21-core/models/message';
|
|
2
|
+
|
|
3
|
+
export type SenderKind = 'bot' | 'human' | 'system' | 'unknown';
|
|
4
|
+
export type Confidence = 'high' | 'medium' | 'low';
|
|
5
|
+
|
|
6
|
+
export interface SenderClassification {
|
|
7
|
+
kind: SenderKind;
|
|
8
|
+
confidence: Confidence;
|
|
9
|
+
reasons: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ConversationBadgeState {
|
|
13
|
+
/** Kind of the latest server message (including system). */
|
|
14
|
+
latestServerMessageKind: SenderKind;
|
|
15
|
+
/** Kind of the latest non-system server responder, used for Bot/Umano badge. */
|
|
16
|
+
latestNonSystemResponderKind: 'bot' | 'human' | null;
|
|
17
|
+
showBadge: boolean;
|
|
18
|
+
badgeText: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function classifyMessageSender(msg: MessageModel | null | undefined): SenderClassification {
|
|
22
|
+
if (!msg) return { kind: 'unknown', confidence: 'low', reasons: ['msg=null'] };
|
|
23
|
+
|
|
24
|
+
const sender = (msg as any).sender;
|
|
25
|
+
const senderFullname = (msg as any).sender_fullname;
|
|
26
|
+
const senderFullnameLower = (senderFullname || '').toString().toLowerCase();
|
|
27
|
+
|
|
28
|
+
if (sender === 'system' || senderFullnameLower === 'system') {
|
|
29
|
+
return { kind: 'system', confidence: 'high', reasons: ['sender=system'] };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const chatbotId = (msg as any)?.attributes?.flowAttributes?.chatbot_id;
|
|
33
|
+
if (chatbotId) {
|
|
34
|
+
return { kind: 'bot', confidence: 'high', reasons: ['flowAttributes.chatbot_id'] };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (sender && String(sender).includes('bot_')) {
|
|
38
|
+
return { kind: 'bot', confidence: 'medium', reasons: ['sender includes bot_'] };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (senderFullnameLower.includes('bot')) {
|
|
42
|
+
return { kind: 'bot', confidence: 'low', reasons: ['sender_fullname includes bot'] };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return { kind: 'human', confidence: 'low', reasons: ['fallback human'] };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function isHumanHandoffSystemMessage(msg: MessageModel | null | undefined, clientSenderId?: string): boolean {
|
|
49
|
+
if (!msg) return false;
|
|
50
|
+
if ((msg as any).sender !== 'system') return false;
|
|
51
|
+
|
|
52
|
+
const attrs: any = (msg as any).attributes || {};
|
|
53
|
+
const key = attrs?.messagelabel?.key;
|
|
54
|
+
const memberId = attrs?.messagelabel?.parameters?.member_id;
|
|
55
|
+
|
|
56
|
+
if (attrs?.subtype !== 'info') return false;
|
|
57
|
+
if (attrs?.updateconversation !== true) return false;
|
|
58
|
+
if (key !== 'MEMBER_JOINED_GROUP') return false;
|
|
59
|
+
if (!memberId || typeof memberId !== 'string') return false;
|
|
60
|
+
|
|
61
|
+
// Exclude system/bot/self joins.
|
|
62
|
+
if (memberId === 'system') return false;
|
|
63
|
+
if (memberId.startsWith('bot_')) return false;
|
|
64
|
+
if (clientSenderId && memberId === clientSenderId) return false;
|
|
65
|
+
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getTimestamp(msg: MessageModel | null | undefined): number {
|
|
70
|
+
const ts = msg && (msg as any).timestamp;
|
|
71
|
+
const n = ts != null ? Number(ts) : 0;
|
|
72
|
+
return Number.isFinite(n) ? n : 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function maxByTimestamp<T extends MessageModel>(items: T[]): T {
|
|
76
|
+
return items.reduce((acc, cur) => (getTimestamp(cur) >= getTimestamp(acc) ? cur : acc), items[0]);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isSystemMessage(msg: MessageModel | null | undefined): boolean {
|
|
80
|
+
if (!msg) return false;
|
|
81
|
+
const sender = (msg as any).sender;
|
|
82
|
+
const senderFullname = (msg as any).sender_fullname;
|
|
83
|
+
const senderFullnameLower = (senderFullname || '').toString().toLowerCase();
|
|
84
|
+
return sender === 'system' || senderFullnameLower === 'system';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function computeConversationBadgeState(messages: MessageModel[], clientSenderId?: string): ConversationBadgeState {
|
|
88
|
+
const msgs = messages || [];
|
|
89
|
+
const serverMsgs = msgs.filter(m => !!m && (!clientSenderId || (m as any).sender !== clientSenderId));
|
|
90
|
+
|
|
91
|
+
const latestServerMsg = serverMsgs.length > 0 ? maxByTimestamp(serverMsgs) : null;
|
|
92
|
+
const latestServerMessageKind = classifyMessageSender(latestServerMsg).kind;
|
|
93
|
+
|
|
94
|
+
let latestNonSystemResponderKind: 'bot' | 'human' | null = null;
|
|
95
|
+
|
|
96
|
+
// Priority rule: if the latest server message is a system handoff to a human, force "human".
|
|
97
|
+
if (isHumanHandoffSystemMessage(latestServerMsg, clientSenderId)) {
|
|
98
|
+
latestNonSystemResponderKind = 'human';
|
|
99
|
+
} else {
|
|
100
|
+
// Otherwise, use the latest non-system server message (by timestamp) as responder.
|
|
101
|
+
const nonSystemServerMsgs = serverMsgs.filter(m => !isSystemMessage(m));
|
|
102
|
+
const latestNonSystem = nonSystemServerMsgs.length > 0 ? maxByTimestamp(nonSystemServerMsgs) : null;
|
|
103
|
+
const kind = classifyMessageSender(latestNonSystem).kind;
|
|
104
|
+
if (kind === 'bot' || kind === 'human') {
|
|
105
|
+
latestNonSystemResponderKind = kind;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
latestServerMessageKind,
|
|
111
|
+
latestNonSystemResponderKind,
|
|
112
|
+
showBadge: latestNonSystemResponderKind !== null,
|
|
113
|
+
badgeText: latestNonSystemResponderKind === 'bot' ? 'Bot' : (latestNonSystemResponderKind === 'human' ? 'Umano' : ''),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
package/src/app/utils/globals.ts
CHANGED
|
@@ -616,13 +616,35 @@ export class Globals {
|
|
|
616
616
|
}
|
|
617
617
|
|
|
618
618
|
|
|
619
|
-
//
|
|
619
|
+
// On mobile, force fullscreen while open regardless of stored `size`.
|
|
620
620
|
if(isOpen && this.isMobile && divTiledeskWidget){
|
|
621
|
+
divTiledeskWidget.classList.remove('min-size')
|
|
622
|
+
divTiledeskWidget.classList.remove('max-size')
|
|
623
|
+
divTiledeskWidget.classList.remove('top-size')
|
|
624
|
+
divTiledeskWidget.classList.add('fullscreen')
|
|
625
|
+
divTiledeskWidget.style.left = '0px'
|
|
621
626
|
divTiledeskWidget.style.right = '0px'
|
|
627
|
+
divTiledeskWidget.style.top = '0px'
|
|
622
628
|
divTiledeskWidget.style.bottom = '0px'
|
|
629
|
+
divTiledeskWidget.style.width = '100%'
|
|
630
|
+
divTiledeskWidget.style.height = '100%'
|
|
631
|
+
divTiledeskWidget.style.maxWidth = 'none'
|
|
632
|
+
divTiledeskWidget.style.maxHeight = 'none'
|
|
623
633
|
} else if(!isOpen && this.isMobile && divTiledeskWidget){
|
|
624
|
-
divTiledeskWidget.
|
|
625
|
-
|
|
634
|
+
divTiledeskWidget.classList.remove('fullscreen')
|
|
635
|
+
divTiledeskWidget.style.removeProperty('top')
|
|
636
|
+
divTiledeskWidget.style.removeProperty('width')
|
|
637
|
+
divTiledeskWidget.style.removeProperty('height')
|
|
638
|
+
divTiledeskWidget.style.removeProperty('max-width')
|
|
639
|
+
divTiledeskWidget.style.removeProperty('max-height')
|
|
640
|
+
divTiledeskWidget.style.bottom = this.mobileMarginY
|
|
641
|
+
if (this.align === 'left') {
|
|
642
|
+
divTiledeskWidget.style.left = this.mobileMarginX
|
|
643
|
+
divTiledeskWidget.style.removeProperty('right')
|
|
644
|
+
} else {
|
|
645
|
+
divTiledeskWidget.style.right = this.mobileMarginX
|
|
646
|
+
divTiledeskWidget.style.removeProperty('left')
|
|
647
|
+
}
|
|
626
648
|
}
|
|
627
649
|
|
|
628
650
|
//customize position for 'tiledeskdiv' for desktop if fullscreenMode is not active
|