@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.
Files changed (30) hide show
  1. package/.github/workflows/docker-community-push-latest.yml +13 -23
  2. package/.github/workflows/docker-image-tag-community-tag-push.yml +12 -22
  3. package/CHANGELOG.md +28 -82
  4. package/Dockerfile +5 -4
  5. package/angular.json +1 -2
  6. package/deploy_amazon_beta.sh +7 -17
  7. package/deploy_amazon_prod.sh +41 -0
  8. package/docs/changelog/badge_Bot_Umano.md +85 -0
  9. package/docs/changelog/this-branch.md +35 -24
  10. package/package.json +1 -1
  11. package/src/app/app.component.ts +17 -6
  12. package/src/app/component/conversation-detail/conversation/conversation.component.html +1 -3
  13. package/src/app/component/conversation-detail/conversation/conversation.component.scss +2 -2
  14. package/src/app/component/conversation-detail/conversation/conversation.component.ts +27 -153
  15. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.scss +1 -1
  16. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.html +80 -103
  17. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss +13 -40
  18. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts +0 -7
  19. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.html +1 -1
  20. package/src/app/providers/global-settings.service.ts +2 -25
  21. package/src/app/providers/translator.service.ts +0 -2
  22. package/src/app/sass/_variables.scss +0 -1
  23. package/src/app/utils/conversation-sender-classifier.ts +116 -0
  24. package/src/app/utils/globals.ts +26 -7
  25. package/src/assets/i18n/en.json +0 -1
  26. package/src/assets/i18n/es.json +0 -1
  27. package/src/assets/i18n/fr.json +0 -1
  28. package/src/assets/i18n/it.json +0 -1
  29. package/src/chat21-core/providers/tiledesk/tiledesk-requests.service.ts +1 -1
  30. 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
- // Backward compatibility: ignore legacy stored size on mobile.
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
 
@@ -36,7 +36,6 @@
36
36
 
37
37
  --chat-footer-height: 64px;
38
38
  --chat-footer-logo-height: 30px;
39
- --chat-footer-close-button-height: 30px;
40
39
  --chat-footer-border-radius: 16px;
41
40
  --chat-footer-background-color: #f6f7fb;
42
41
  --chat-footer-color: #1a1a1a;
@@ -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
+
@@ -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
- /** enable to close the chat in conversation */
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
- //customize position for 'tiledeskdiv' for mobile
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.style.bottom = this.marginY
625
- this.align === 'left'? divTiledeskWidget.style.left = this.mobileMarginX : divTiledeskWidget.style.right = this.mobileMarginX;
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
@@ -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
  }
@@ -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
  }
@@ -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
  }
@@ -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
+