@chat21/chat21-web-widget 5.1.27-rc1 → 5.1.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/docker-community-push-latest.yml +13 -23
- package/.github/workflows/docker-image-tag-community-tag-push.yml +12 -22
- package/CHANGELOG.md +28 -82
- package/Dockerfile +5 -4
- package/angular.json +1 -2
- package/deploy_amazon_beta.sh +7 -17
- package/deploy_amazon_prod.sh +41 -0
- package/docs/changelog/badge_Bot_Umano.md +85 -0
- package/docs/changelog/this-branch.md +35 -24
- package/package.json +1 -1
- package/src/app/app.component.ts +17 -6
- package/src/app/component/conversation-detail/conversation/conversation.component.html +1 -3
- package/src/app/component/conversation-detail/conversation/conversation.component.scss +2 -2
- package/src/app/component/conversation-detail/conversation/conversation.component.ts +27 -153
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.scss +1 -1
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.html +80 -103
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss +13 -40
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts +0 -7
- package/src/app/component/conversation-detail/conversation-header/conversation-header.component.html +1 -1
- package/src/app/providers/global-settings.service.ts +2 -25
- package/src/app/providers/translator.service.ts +0 -2
- package/src/app/sass/_variables.scss +0 -1
- package/src/app/utils/conversation-sender-classifier.ts +116 -0
- package/src/app/utils/globals.ts +26 -7
- package/src/assets/i18n/en.json +0 -1
- package/src/assets/i18n/es.json +0 -1
- package/src/assets/i18n/fr.json +0 -1
- package/src/assets/i18n/it.json +0 -1
- package/src/chat21-core/providers/tiledesk/tiledesk-requests.service.ts +1 -1
- package/src/chat21-core/utils/utils.ts +2 -5
|
@@ -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;
|
|
@@ -1142,12 +1130,6 @@ export class GlobalSettingsService {
|
|
|
1142
1130
|
if (TEMP !== undefined) {
|
|
1143
1131
|
globals.size = TEMP;
|
|
1144
1132
|
}
|
|
1145
|
-
|
|
1146
|
-
TEMP = tiledeskSettings['closeChatInConversation'];
|
|
1147
|
-
// this.logger.debug('[GLOBAL-SET] setVariablesFromSettings > closeChatInConversation:: ', TEMP]);
|
|
1148
|
-
if (TEMP !== undefined) {
|
|
1149
|
-
globals.closeChatInConversation = (TEMP === true) ? true : false;
|
|
1150
|
-
}
|
|
1151
1133
|
}
|
|
1152
1134
|
|
|
1153
1135
|
/**
|
|
@@ -1894,11 +1876,6 @@ export class GlobalSettingsService {
|
|
|
1894
1876
|
if (TEMP) {
|
|
1895
1877
|
globals.size = TEMP;
|
|
1896
1878
|
}
|
|
1897
|
-
|
|
1898
|
-
TEMP = getParameterByName(windowContext, 'tiledesk_closeChatInConversation');
|
|
1899
|
-
if (TEMP) {
|
|
1900
|
-
globals.closeChatInConversation = stringToBoolean(TEMP);
|
|
1901
|
-
}
|
|
1902
1879
|
|
|
1903
1880
|
}
|
|
1904
1881
|
|
|
@@ -1912,7 +1889,7 @@ export class GlobalSettingsService {
|
|
|
1912
1889
|
this.logger.debug('[GLOBAL-SET] setVariableFromStorage :::::::: SET VARIABLE ---------->', Object.keys(globals));
|
|
1913
1890
|
for (const key of Object.keys(globals)) {
|
|
1914
1891
|
if (globals.isMobile === true && key === 'size') {
|
|
1915
|
-
//
|
|
1892
|
+
// On mobile we always open fullscreen, so legacy/persisted widget size must be ignored.
|
|
1916
1893
|
try {
|
|
1917
1894
|
this.appStorageService.removeItem('size');
|
|
1918
1895
|
} catch (e) {
|
|
@@ -302,7 +302,6 @@ export class TranslatorService {
|
|
|
302
302
|
'CLOSED',
|
|
303
303
|
'LABEL_PREVIEW',
|
|
304
304
|
'MAX_ATTACHMENT',
|
|
305
|
-
'MAX_ATTACHMENT_ERROR',
|
|
306
305
|
'EMOJI'
|
|
307
306
|
];
|
|
308
307
|
|
|
@@ -359,7 +358,6 @@ export class TranslatorService {
|
|
|
359
358
|
globals.LABEL_PREVIEW = res['LABEL_PREVIEW']
|
|
360
359
|
globals.LABEL_ERROR_FIELD_REQUIRED= res['LABEL_ERROR_FIELD_REQUIRED']
|
|
361
360
|
globals.MAX_ATTACHMENT = res['MAX_ATTACHMENT']
|
|
362
|
-
globals.MAX_ATTACHMENT_ERROR = res['MAX_ATTACHMENT_ERROR']
|
|
363
361
|
globals.EMOJI = res['EMOJI']
|
|
364
362
|
|
|
365
363
|
|
|
@@ -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
|
@@ -227,8 +227,6 @@ export class Globals {
|
|
|
227
227
|
fontFamilySource: string; // ******* new ********
|
|
228
228
|
|
|
229
229
|
size: 'min' | 'max' | 'top'; // ******* new ********
|
|
230
|
-
|
|
231
|
-
closeChatInConversation: boolean; // ******* new ********
|
|
232
230
|
constructor(
|
|
233
231
|
) { }
|
|
234
232
|
|
|
@@ -445,8 +443,7 @@ export class Globals {
|
|
|
445
443
|
this.hasCalloutInWidgetConfig = false;
|
|
446
444
|
/** set widget size from 3 different positions: min, max, top */
|
|
447
445
|
this.size = 'min';
|
|
448
|
-
|
|
449
|
-
this.closeChatInConversation = false;
|
|
446
|
+
|
|
450
447
|
// ============ END: SET EXTERNAL PARAMETERS ==============//
|
|
451
448
|
|
|
452
449
|
|
|
@@ -616,13 +613,35 @@ export class Globals {
|
|
|
616
613
|
}
|
|
617
614
|
|
|
618
615
|
|
|
619
|
-
//
|
|
616
|
+
// On mobile, force fullscreen while open regardless of stored `size`.
|
|
620
617
|
if(isOpen && this.isMobile && divTiledeskWidget){
|
|
618
|
+
divTiledeskWidget.classList.remove('min-size')
|
|
619
|
+
divTiledeskWidget.classList.remove('max-size')
|
|
620
|
+
divTiledeskWidget.classList.remove('top-size')
|
|
621
|
+
divTiledeskWidget.classList.add('fullscreen')
|
|
622
|
+
divTiledeskWidget.style.left = '0px'
|
|
621
623
|
divTiledeskWidget.style.right = '0px'
|
|
624
|
+
divTiledeskWidget.style.top = '0px'
|
|
622
625
|
divTiledeskWidget.style.bottom = '0px'
|
|
626
|
+
divTiledeskWidget.style.width = '100%'
|
|
627
|
+
divTiledeskWidget.style.height = '100%'
|
|
628
|
+
divTiledeskWidget.style.maxWidth = 'none'
|
|
629
|
+
divTiledeskWidget.style.maxHeight = 'none'
|
|
623
630
|
} else if(!isOpen && this.isMobile && divTiledeskWidget){
|
|
624
|
-
divTiledeskWidget.
|
|
625
|
-
|
|
631
|
+
divTiledeskWidget.classList.remove('fullscreen')
|
|
632
|
+
divTiledeskWidget.style.removeProperty('top')
|
|
633
|
+
divTiledeskWidget.style.removeProperty('width')
|
|
634
|
+
divTiledeskWidget.style.removeProperty('height')
|
|
635
|
+
divTiledeskWidget.style.removeProperty('max-width')
|
|
636
|
+
divTiledeskWidget.style.removeProperty('max-height')
|
|
637
|
+
divTiledeskWidget.style.bottom = this.mobileMarginY
|
|
638
|
+
if (this.align === 'left') {
|
|
639
|
+
divTiledeskWidget.style.left = this.mobileMarginX
|
|
640
|
+
divTiledeskWidget.style.removeProperty('right')
|
|
641
|
+
} else {
|
|
642
|
+
divTiledeskWidget.style.right = this.mobileMarginX
|
|
643
|
+
divTiledeskWidget.style.removeProperty('left')
|
|
644
|
+
}
|
|
626
645
|
}
|
|
627
646
|
|
|
628
647
|
//customize position for 'tiledeskdiv' for desktop if fullscreenMode is not active
|
package/src/assets/i18n/en.json
CHANGED
|
@@ -97,6 +97,5 @@
|
|
|
97
97
|
"EMOJI_NOT_ELLOWED":"Emoji not allowed",
|
|
98
98
|
"DOMAIN_NOT_ALLOWED":"URL contains a non-allowed domain",
|
|
99
99
|
"MAX_ATTACHMENT": "Max allowed size {{FILE_SIZE_LIMIT}}Mb",
|
|
100
|
-
"MAX_ATTACHMENT_ERROR": "The file exceeds the maximum allowed size",
|
|
101
100
|
"EMOJI": "Emoji"
|
|
102
101
|
}
|
package/src/assets/i18n/es.json
CHANGED
|
@@ -97,6 +97,5 @@
|
|
|
97
97
|
"EMOJI_NOT_ELLOWED":"Emoji no permitido",
|
|
98
98
|
"DOMAIN_NOT_ALLOWED":"La URL contiene un dominio no permitido",
|
|
99
99
|
"MAX_ATTACHMENT": "Tamaño máximo permitido {{FILE_SIZE_LIMIT}}Mb",
|
|
100
|
-
"MAX_ATTACHMENT_ERROR": "El archivo supera el tamaño máximo permitido",
|
|
101
100
|
"EMOJI": "Emoji"
|
|
102
101
|
}
|
package/src/assets/i18n/fr.json
CHANGED
|
@@ -97,6 +97,5 @@
|
|
|
97
97
|
"EMOJI_NOT_ELLOWED":"Emoji non autorisé",
|
|
98
98
|
"DOMAIN_NOT_ALLOWED":"L'URL contient un domaine non autorisé",
|
|
99
99
|
"MAX_ATTACHMENT": "Taille maximale autorisée {{FILE_SIZE_LIMIT}}Mo",
|
|
100
|
-
"MAX_ATTACHMENT_ERROR": "Le fichier dépasse la taille maximale autorisée",
|
|
101
100
|
"EMOJI": "Emoji"
|
|
102
101
|
}
|
package/src/assets/i18n/it.json
CHANGED
|
@@ -95,6 +95,5 @@
|
|
|
95
95
|
"EMOJI_NOT_ELLOWED":"Emoji non consentiti",
|
|
96
96
|
"DOMAIN_NOT_ALLOWED":"L'URL contiene un dominio non consentito",
|
|
97
97
|
"MAX_ATTACHMENT": "Dimensione massima consentita {{FILE_SIZE_LIMIT}}Mb",
|
|
98
|
-
"MAX_ATTACHMENT_ERROR": "Il file supera la dimensione massima consentita",
|
|
99
98
|
"EMOJI": "Emoji"
|
|
100
99
|
}
|
|
@@ -71,7 +71,7 @@ export class TiledeskRequestsService {
|
|
|
71
71
|
|
|
72
72
|
public getMyRequests(): Promise<{ requests: Array<any>}> {
|
|
73
73
|
this.tiledeskToken = this.appStorage.getItem('tiledeskToken')
|
|
74
|
-
const url = this.URL_TILEDESK_REQUEST + 'me?preflight=true'
|
|
74
|
+
const url = this.URL_TILEDESK_REQUEST + '/me?preflight=true'
|
|
75
75
|
this.logger.log('[TILEDESK-SERVICE] - GET REQUEST url ', url);
|
|
76
76
|
const httpOptions = {
|
|
77
77
|
headers: new HttpHeaders({
|
|
@@ -773,11 +773,6 @@ export function isAllowedUrlInText(text: string, allowedUrls: string[]) {
|
|
|
773
773
|
return nonWhitelistedDomains.length === 0;
|
|
774
774
|
}
|
|
775
775
|
|
|
776
|
-
// function extractUrls(text: string): string[] {
|
|
777
|
-
// const urlRegex = /https?:\/\/[^\s]+/g;
|
|
778
|
-
// return text.match(urlRegex) || [];
|
|
779
|
-
// }
|
|
780
|
-
|
|
781
776
|
function extractUrls(text: string): string[] {
|
|
782
777
|
// Rileva URL con o senza protocollo (http/https)
|
|
783
778
|
const urlRegex = /\b((https?:\/\/)?(www\.)?[a-z0-9.-]+\.[a-z]{2,})(\/[^\s]*)?/gi;
|
|
@@ -792,3 +787,5 @@ function extractUrls(text: string): string[] {
|
|
|
792
787
|
}
|
|
793
788
|
|
|
794
789
|
|
|
790
|
+
|
|
791
|
+
|