@chat21/chat21-web-widget 5.1.26-rc1 → 5.1.27-rc1

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