@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 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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@chat21/chat21-web-widget",
3
3
  "author": "Tiledesk SRL",
4
- "version": "5.1.27-rc1",
4
+ "version": "5.1.30-rc1",
5
5
  "license": "MIT",
6
6
  "homepage": "https://www.tiledesk.com",
7
7
  "repository": {
@@ -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 && this.isInitialized) {
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
- * 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.
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 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' : '');
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
- private startThinkingMessage() {
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.stopThinkingMessageImmediately();
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
- if (this.shouldShowThinkingForBot()) {
1468
- this.startThinkingMessage();
1469
- } else {
1470
- this.showThinkingMessage = false;
1471
- this.waitingServerReply = false;
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 = '';
@@ -16,7 +16,7 @@
16
16
  </button>
17
17
 
18
18
  <!-- ICON MENU OPTION -->
19
- <button *ngIf="!isMobile" [attr.disabled]="(isButtonsDisabled)?true:null" tabindex="-1" class="c21-header-button c21-right c21-button-clean" [ngStyle]="{'display': (hideHeaderConversationOptionsMenu)?'none':'flex'}" (click)="toggleMenu()" >
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
- // Backward compatibility: ignore legacy stored size on mobile.
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
+
@@ -616,13 +616,35 @@ export class Globals {
616
616
  }
617
617
 
618
618
 
619
- //customize position for 'tiledeskdiv' for mobile
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.style.bottom = this.marginY
625
- this.align === 'left'? divTiledeskWidget.style.left = this.mobileMarginX : divTiledeskWidget.style.right = this.mobileMarginX;
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