@v-tilt/browser 1.4.2 → 1.4.4

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.
@@ -1,11 +1,6 @@
1
1
  "use strict";
2
2
  /**
3
- * Lazy Loaded Chat Implementation
4
- *
5
- * The actual chat widget implementation that is loaded on demand.
6
- * This file is bundled into chat.js and loaded when chat is enabled.
7
- *
8
- * Uses Ably for real-time messaging.
3
+ * Chat Widget - Lazy loaded chat implementation using Ably for real-time messaging.
9
4
  */
10
5
  var __importDefault = (this && this.__importDefault) || function (mod) {
11
6
  return (mod && mod.__esModule) ? mod : { "default": mod };
@@ -51,13 +46,11 @@ class LazyLoadedChat {
51
46
  this._typingCallbacks = [];
52
47
  this._connectionCallbacks = [];
53
48
  // Timers
54
- this._typingTimeout = null;
55
49
  this._typingDebounce = null;
56
50
  this._isUserTyping = false;
57
51
  // Read tracking - initial position when widget opens (to show unread indicators)
58
52
  this._initialUserReadAt = null;
59
53
  this._isMarkingRead = false;
60
- this._previousView = "list";
61
54
  this._instance = instance;
62
55
  this._config = {
63
56
  enabled: true,
@@ -86,7 +79,6 @@ class LazyLoadedChat {
86
79
  // Initialize UI
87
80
  this._createUI();
88
81
  this._attachEventListeners();
89
- console.info(`${LOGGER_PREFIX} initialized`);
90
82
  }
91
83
  // ============================================================================
92
84
  // Public API - State (LazyLoadedChatInterface)
@@ -125,8 +117,9 @@ class LazyLoadedChat {
125
117
  // ============================================================================
126
118
  open() {
127
119
  var _a;
128
- if (this._state.isOpen)
120
+ if (this._state.isOpen) {
129
121
  return;
122
+ }
130
123
  this._state.isOpen = true;
131
124
  this._updateUI();
132
125
  // Add opening animation
@@ -138,17 +131,15 @@ class LazyLoadedChat {
138
131
  $page_url: (_a = globals_1.window === null || globals_1.window === void 0 ? void 0 : globals_1.window.location) === null || _a === void 0 ? void 0 : _a.href,
139
132
  $trigger: "api",
140
133
  });
141
- // Show channel list view first (multi-channel support)
142
- // Only fetch channels if not already loaded
134
+ // Fetch channels if not already loaded
143
135
  if (this._state.channels.length === 0) {
144
136
  this.getChannels();
145
137
  }
146
- // NOTE: Ably connection is now established only when entering conversation view
147
- // This saves connection minutes when user is just browsing the channel list
148
138
  }
149
139
  close() {
150
- if (!this._state.isOpen)
140
+ if (!this._state.isOpen) {
151
141
  return;
142
+ }
152
143
  const timeOpen = this._getTimeOpen();
153
144
  // Add closing animation
154
145
  if (this._widget) {
@@ -168,7 +159,6 @@ class LazyLoadedChat {
168
159
  $time_open_seconds: timeOpen,
169
160
  $messages_sent: this._state.messages.filter((m) => m.sender_type === "user").length,
170
161
  });
171
- // Disconnect Ably when widget closes to save connection minutes
172
162
  this._disconnectRealtime();
173
163
  }
174
164
  toggle() {
@@ -190,9 +180,6 @@ class LazyLoadedChat {
190
180
  // ============================================================================
191
181
  // Public API - Channel Management (Multi-channel support)
192
182
  // ============================================================================
193
- /**
194
- * Fetch/refresh the list of user's channels
195
- */
196
183
  async getChannels() {
197
184
  this._state.isLoading = true;
198
185
  this._updateUI();
@@ -214,9 +201,6 @@ class LazyLoadedChat {
214
201
  this._updateUI();
215
202
  }
216
203
  }
217
- /**
218
- * Select a channel and load its messages
219
- */
220
204
  async selectChannel(channelId) {
221
205
  this._state.isLoading = true;
222
206
  this._updateUI();
@@ -228,13 +212,10 @@ class LazyLoadedChat {
228
212
  this._state.channel = response.channel;
229
213
  this._state.messages = response.messages || [];
230
214
  this._state.currentView = "conversation";
231
- // Initialize read cursors from channel
232
215
  this._state.agentLastReadAt =
233
216
  response.channel.agent_last_read_at || null;
234
217
  this._initialUserReadAt = response.channel.user_last_read_at || null;
235
- // Connect to Ably for this channel
236
218
  this._connectRealtime();
237
- // Auto-mark unread messages as read if widget is open
238
219
  if (this._state.isOpen) {
239
220
  this._autoMarkAsRead();
240
221
  }
@@ -248,9 +229,6 @@ class LazyLoadedChat {
248
229
  this._updateUI();
249
230
  }
250
231
  }
251
- /**
252
- * Create a new channel and enter it
253
- */
254
232
  async createChannel() {
255
233
  var _a;
256
234
  this._state.isLoading = true;
@@ -268,11 +246,9 @@ class LazyLoadedChat {
268
246
  this._state.channel = response.channel;
269
247
  this._state.messages = response.messages || [];
270
248
  this._state.currentView = "conversation";
271
- // Initialize read cursors
272
249
  this._state.agentLastReadAt =
273
250
  response.channel.agent_last_read_at || null;
274
251
  this._initialUserReadAt = response.channel.user_last_read_at || null;
275
- // Add new channel to the list
276
252
  const newChannelSummary = {
277
253
  id: response.channel.id,
278
254
  status: response.channel.status,
@@ -285,13 +261,11 @@ class LazyLoadedChat {
285
261
  created_at: response.channel.created_at,
286
262
  };
287
263
  this._state.channels.unshift(newChannelSummary);
288
- // Track channel started
289
264
  this._trackEvent(types_1.CHAT_EVENTS.STARTED, {
290
265
  $channel_id: response.channel.id,
291
266
  $initiated_by: "user",
292
267
  $ai_mode: response.channel.ai_mode,
293
268
  });
294
- // Connect to Ably for this channel
295
269
  this._connectRealtime();
296
270
  }
297
271
  }
@@ -303,13 +277,8 @@ class LazyLoadedChat {
303
277
  this._updateUI();
304
278
  }
305
279
  }
306
- /**
307
- * Go back to channel list from conversation view
308
- */
309
280
  goToChannelList() {
310
- // Disconnect Ably when leaving conversation to save connection minutes
311
281
  this._disconnectRealtime();
312
- // Update the channel in the list with latest data
313
282
  if (this._state.channel) {
314
283
  const channelIndex = this._state.channels.findIndex((ch) => { var _a; return ch.id === ((_a = this._state.channel) === null || _a === void 0 ? void 0 : _a.id); });
315
284
  if (channelIndex !== -1) {
@@ -322,13 +291,13 @@ class LazyLoadedChat {
322
291
  ? this._state.messages[this._state.messages.length - 1].content.substring(0, 100)
323
292
  : this._state.channels[channelIndex].last_message_preview,
324
293
  last_message_sender: this._state.messages.length > 0
325
- ? this._state.messages[this._state.messages.length - 1].sender_type
294
+ ? this._state.messages[this._state.messages.length - 1]
295
+ .sender_type
326
296
  : this._state.channels[channelIndex].last_message_sender,
327
297
  unread_count: 0, // We just viewed it
328
298
  };
329
299
  }
330
300
  }
331
- // Clear current channel state
332
301
  this._state.channel = null;
333
302
  this._state.messages = [];
334
303
  this._state.currentView = "list";
@@ -341,8 +310,9 @@ class LazyLoadedChat {
341
310
  // ============================================================================
342
311
  async sendMessage(content) {
343
312
  var _a, _b, _c, _d;
344
- if (!content.trim())
313
+ if (!content.trim()) {
345
314
  return;
315
+ }
346
316
  // Ensure we're in conversation view with a channel
347
317
  if (!this._state.channel || this._state.currentView !== "conversation") {
348
318
  console.error(`${LOGGER_PREFIX} Cannot send message: not in conversation view`);
@@ -383,9 +353,6 @@ class LazyLoadedChat {
383
353
  if (index !== -1 && (response === null || response === void 0 ? void 0 : response.message)) {
384
354
  this._state.messages[index] = response.message;
385
355
  }
386
- // Note: AI response will come through Ably channel if AI mode is enabled
387
- // No need to handle it here since server publishes to Ably
388
- // Track event
389
356
  this._trackEvent(types_1.CHAT_EVENTS.MESSAGE_SENT, {
390
357
  $channel_id: channelId,
391
358
  $message_id: (_b = response === null || response === void 0 ? void 0 : response.message) === null || _b === void 0 ? void 0 : _b.id,
@@ -406,28 +373,22 @@ class LazyLoadedChat {
406
373
  markAsRead() {
407
374
  this._autoMarkAsRead();
408
375
  }
409
- /**
410
- * Automatically mark unread agent/AI messages as read
411
- * Called when widget opens or new messages arrive while open
412
- */
413
376
  _autoMarkAsRead() {
414
- if (!this._state.channel)
377
+ if (!this._state.channel || this._isMarkingRead) {
415
378
  return;
416
- if (this._isMarkingRead)
417
- return; // Already in progress
418
- // Get the latest message timestamp
379
+ }
419
380
  const latestMessage = this._state.messages[this._state.messages.length - 1];
420
- if (!latestMessage)
381
+ if (!latestMessage) {
421
382
  return;
422
- // Check if there are unread agent messages
383
+ }
423
384
  const hasUnreadAgentMessages = this._state.messages.some((m) => (m.sender_type === "agent" || m.sender_type === "ai") &&
424
385
  !this._isMessageReadByUser(m.created_at));
425
- if (!hasUnreadAgentMessages)
386
+ if (!hasUnreadAgentMessages) {
426
387
  return;
388
+ }
427
389
  this._state.unreadCount = 0;
428
390
  this._isMarkingRead = true;
429
391
  this._updateUI();
430
- // API call to update read cursor with latest message timestamp
431
392
  this._apiRequest(API.read, {
432
393
  method: "POST",
433
394
  body: JSON.stringify({
@@ -437,23 +398,19 @@ class LazyLoadedChat {
437
398
  }),
438
399
  })
439
400
  .then(() => {
440
- // Update initial read position after successful mark
441
401
  this._initialUserReadAt = latestMessage.created_at;
442
402
  this._isMarkingRead = false;
443
403
  this._updateUI();
444
404
  })
445
- .catch((err) => {
446
- console.error(`${LOGGER_PREFIX} Failed to mark as read:`, err);
405
+ .catch(() => {
447
406
  this._isMarkingRead = false;
448
407
  this._updateUI();
449
408
  });
450
409
  }
451
- /**
452
- * Check if a message has been read by the user (using initial cursor)
453
- */
454
410
  _isMessageReadByUser(messageCreatedAt) {
455
- if (!this._initialUserReadAt)
411
+ if (!this._initialUserReadAt) {
456
412
  return false;
413
+ }
457
414
  return new Date(messageCreatedAt) <= new Date(this._initialUserReadAt);
458
415
  }
459
416
  // ============================================================================
@@ -463,46 +420,44 @@ class LazyLoadedChat {
463
420
  this._messageCallbacks.push(callback);
464
421
  return () => {
465
422
  const index = this._messageCallbacks.indexOf(callback);
466
- if (index > -1)
423
+ if (index > -1) {
467
424
  this._messageCallbacks.splice(index, 1);
425
+ }
468
426
  };
469
427
  }
470
428
  onTyping(callback) {
471
429
  this._typingCallbacks.push(callback);
472
430
  return () => {
473
431
  const index = this._typingCallbacks.indexOf(callback);
474
- if (index > -1)
432
+ if (index > -1) {
475
433
  this._typingCallbacks.splice(index, 1);
434
+ }
476
435
  };
477
436
  }
478
437
  onConnectionChange(callback) {
479
438
  this._connectionCallbacks.push(callback);
480
439
  return () => {
481
440
  const index = this._connectionCallbacks.indexOf(callback);
482
- if (index > -1)
441
+ if (index > -1) {
483
442
  this._connectionCallbacks.splice(index, 1);
443
+ }
484
444
  };
485
445
  }
486
446
  // ============================================================================
487
447
  // Public API - Lifecycle
488
448
  // ============================================================================
489
449
  destroy() {
490
- // Disconnect Ably
450
+ var _a;
491
451
  this._disconnectRealtime();
492
- // Clear timers
493
- if (this._typingTimeout)
494
- clearTimeout(this._typingTimeout);
495
- if (this._typingDebounce)
452
+ if (this._typingDebounce) {
496
453
  clearTimeout(this._typingDebounce);
497
- // Remove DOM elements
498
- if (this._container && this._container.parentNode) {
454
+ }
455
+ if ((_a = this._container) === null || _a === void 0 ? void 0 : _a.parentNode) {
499
456
  this._container.parentNode.removeChild(this._container);
500
457
  }
501
- // Clear callbacks
502
458
  this._messageCallbacks = [];
503
459
  this._typingCallbacks = [];
504
460
  this._connectionCallbacks = [];
505
- console.info(`${LOGGER_PREFIX} destroyed`);
506
461
  }
507
462
  // ============================================================================
508
463
  // Private - Ably Realtime Connection
@@ -527,7 +482,6 @@ class LazyLoadedChat {
527
482
  this._connectionState = "error";
528
483
  return;
529
484
  }
530
- // Create Ably client with token auth
531
485
  this._ably = new ably_1.default.Realtime({
532
486
  authCallback: async (_, callback) => {
533
487
  var _a;
@@ -552,37 +506,25 @@ class LazyLoadedChat {
552
506
  },
553
507
  authMethod: "POST",
554
508
  });
555
- // Authenticate with initial token
556
509
  await this._ably.auth.authorize(tokenResponse.tokenRequest);
557
- // Get project ID from instance config
558
510
  const config = this._instance.getConfig();
559
511
  const projectId = config.projectId || this._extractProjectId(config.token || "");
560
- // Subscribe to chat channel (Ably channel)
561
512
  const ablyChannelName = `chat:${projectId}:${this._state.channel.id}`;
562
513
  this._ablyChannel = this._ably.channels.get(ablyChannelName);
563
- // Listen for new messages
564
514
  this._ablyChannel.subscribe("message", (msg) => {
565
- const message = msg.data;
566
- this._handleNewMessage(message);
567
- });
568
- // Subscribe to typing channel
569
- const typingChannelName = `${ablyChannelName}:typing`;
570
- this._typingChannel = this._ably.channels.get(typingChannelName);
571
- this._typingChannel.subscribe("typing", (msg) => {
572
- const event = msg.data;
573
- this._handleTypingEvent(event);
515
+ this._handleNewMessage(msg.data);
574
516
  });
575
- // Subscribe to read cursor events
576
517
  this._ablyChannel.subscribe("read", (msg) => {
577
- const event = msg.data;
578
- this._handleReadCursorEvent(event);
518
+ this._handleReadCursorEvent(msg.data);
519
+ });
520
+ this._typingChannel = this._ably.channels.get(`${ablyChannelName}:typing`);
521
+ this._typingChannel.subscribe("typing", (msg) => {
522
+ this._handleTypingEvent(msg.data);
579
523
  });
580
- // Handle connection state changes
581
524
  this._ably.connection.on("connected", () => {
582
525
  this._connectionState = "connected";
583
526
  this._state.isConnected = true;
584
527
  this._notifyConnectionChange(true);
585
- console.info(`${LOGGER_PREFIX} Connected to Ably`);
586
528
  });
587
529
  this._ably.connection.on("disconnected", () => {
588
530
  this._connectionState = "disconnected";
@@ -594,7 +536,6 @@ class LazyLoadedChat {
594
536
  this._state.isConnected = false;
595
537
  this._notifyConnectionChange(false);
596
538
  });
597
- // Initial connection
598
539
  this._ably.connect();
599
540
  }
600
541
  catch (error) {
@@ -624,14 +565,12 @@ class LazyLoadedChat {
624
565
  return parts[0] || "";
625
566
  }
626
567
  _handleNewMessage(message) {
627
- // Avoid duplicates by ID
628
- if (this._state.messages.some((m) => m.id === message.id))
568
+ if (this._state.messages.some((m) => m.id === message.id)) {
629
569
  return;
630
- // Skip user's own messages - we already have them from optimistic updates
631
- // The sender_id for user messages is the distinct_id
570
+ }
571
+ // Skip own messages but replace temp message if present
632
572
  if (message.sender_type === "user" &&
633
573
  message.sender_id === this._distinctId) {
634
- // But DO replace temp message with real one if present
635
574
  const tempIndex = this._state.messages.findIndex((m) => m.id.startsWith("temp-") &&
636
575
  m.content === message.content &&
637
576
  m.sender_type === "user");
@@ -641,19 +580,14 @@ class LazyLoadedChat {
641
580
  }
642
581
  return;
643
582
  }
644
- // Add to messages
645
583
  this._state.messages.push(message);
646
- // Update unread count if from agent/AI
647
584
  if (message.sender_type !== "user") {
648
585
  if (!this._state.isOpen) {
649
586
  this._state.unreadCount++;
650
587
  }
651
588
  else {
652
- // Widget is open, auto-mark as read after a short delay
653
- // to ensure UI updates first
654
589
  setTimeout(() => this._autoMarkAsRead(), 100);
655
590
  }
656
- // Track received event
657
591
  this._trackEvent(types_1.CHAT_EVENTS.MESSAGE_RECEIVED, {
658
592
  $channel_id: message.channel_id,
659
593
  $message_id: message.id,
@@ -661,32 +595,27 @@ class LazyLoadedChat {
661
595
  $sender_type: message.sender_type,
662
596
  });
663
597
  }
664
- // Clear typing indicator when message arrives
665
598
  this._state.isTyping = false;
666
599
  this._state.typingSender = null;
667
- // Notify callbacks
668
600
  this._messageCallbacks.forEach((cb) => cb(message));
669
601
  this._updateUI();
670
602
  }
671
603
  _handleTypingEvent(event) {
672
- // Only show typing for non-user senders
673
- if (event.sender_type === "user")
604
+ if (event.sender_type === "user") {
674
605
  return;
606
+ }
675
607
  const senderName = event.sender_name ||
676
608
  (event.sender_type === "ai" ? "AI Assistant" : "Agent");
677
609
  this._state.isTyping = event.is_typing;
678
610
  this._state.typingSender = event.is_typing ? senderName : null;
679
- // Notify callbacks
680
611
  this._typingCallbacks.forEach((cb) => cb(event.is_typing, senderName));
681
612
  this._updateUI();
682
613
  }
683
614
  _handleReadCursorEvent(event) {
684
- // Only handle agent read events (user's own reads are local)
685
- if (event.reader_type !== "agent")
615
+ if (event.reader_type !== "agent") {
686
616
  return;
687
- // Update the agent read cursor
617
+ }
688
618
  this._state.agentLastReadAt = event.read_at;
689
- // Update UI to show read status on user messages
690
619
  this._updateUI();
691
620
  }
692
621
  _notifyConnectionChange(connected) {
@@ -696,59 +625,49 @@ class LazyLoadedChat {
696
625
  // Private - UI
697
626
  // ============================================================================
698
627
  _createUI() {
699
- if (!globals_1.document)
628
+ if (!globals_1.document) {
700
629
  return;
701
- // Create container
630
+ }
702
631
  this._container = globals_1.document.createElement("div");
703
632
  this._container.id = "vtilt-chat-container";
704
633
  this._container.setAttribute("style", this._getContainerStyles());
705
- // Create bubble (launcher button)
706
634
  this._bubble = globals_1.document.createElement("div");
707
635
  this._bubble.id = "vtilt-chat-bubble";
708
636
  this._bubble.innerHTML = this._getBubbleHTML();
709
637
  this._bubble.setAttribute("style", this._getBubbleStyles());
710
638
  this._container.appendChild(this._bubble);
711
- // Create widget (chat window)
712
639
  this._widget = globals_1.document.createElement("div");
713
640
  this._widget.id = "vtilt-chat-widget";
714
641
  this._widget.innerHTML = this._getWidgetHTML();
715
642
  this._widget.setAttribute("style", this._getWidgetStyles());
716
643
  this._container.appendChild(this._widget);
717
- // Add to DOM
718
644
  globals_1.document.body.appendChild(this._container);
719
645
  }
720
646
  _attachEventListeners() {
721
- var _a, _b;
722
- // Bubble click
647
+ var _a, _b, _c;
723
648
  (_a = this._bubble) === null || _a === void 0 ? void 0 : _a.addEventListener("click", () => this.toggle());
724
- // Close button (initial attachment - re-attached in _updateHeader)
725
- const closeBtn = (_b = this._widget) === null || _b === void 0 ? void 0 : _b.querySelector(".vtilt-chat-close");
726
- closeBtn === null || closeBtn === void 0 ? void 0 : closeBtn.addEventListener("click", () => this.close());
727
- // Note: Channel list and conversation listeners are attached dynamically
728
- // in _attachChannelListListeners() and _attachConversationListeners()
649
+ (_c = (_b = this._widget) === null || _b === void 0 ? void 0 : _b.querySelector(".vtilt-chat-close")) === null || _c === void 0 ? void 0 : _c.addEventListener("click", () => this.close());
729
650
  }
730
651
  _handleUserTyping() {
731
- // Don't send typing if not connected to Ably
732
- if (!this._typingChannel)
652
+ if (!this._typingChannel) {
733
653
  return;
734
- // Send typing started if not already typing
654
+ }
735
655
  if (!this._isUserTyping) {
736
656
  this._isUserTyping = true;
737
657
  this._sendTypingIndicator(true);
738
658
  }
739
- // Clear existing debounce timer
740
659
  if (this._typingDebounce) {
741
660
  clearTimeout(this._typingDebounce);
742
661
  }
743
- // Set timer to send typing stopped after 2 seconds of no input
744
662
  this._typingDebounce = setTimeout(() => {
745
663
  this._isUserTyping = false;
746
664
  this._sendTypingIndicator(false);
747
665
  }, 2000);
748
666
  }
749
667
  _sendTypingIndicator(isTyping) {
750
- if (!this._typingChannel)
668
+ if (!this._typingChannel) {
751
669
  return;
670
+ }
752
671
  try {
753
672
  this._typingChannel.publish("typing", {
754
673
  sender_type: "user",
@@ -757,18 +676,18 @@ class LazyLoadedChat {
757
676
  is_typing: isTyping,
758
677
  });
759
678
  }
760
- catch (err) {
761
- console.error(`${LOGGER_PREFIX} Failed to send typing indicator:`, err);
679
+ catch (_a) {
680
+ // Silently fail
762
681
  }
763
682
  }
764
683
  _handleSend() {
765
684
  var _a;
766
685
  const input = (_a = this._widget) === null || _a === void 0 ? void 0 : _a.querySelector(".vtilt-chat-input");
767
- if (!input)
686
+ if (!input) {
768
687
  return;
688
+ }
769
689
  const content = input.value.trim();
770
690
  if (content) {
771
- // Stop typing indicator
772
691
  if (this._isUserTyping) {
773
692
  this._isUserTyping = false;
774
693
  this._sendTypingIndicator(false);
@@ -782,8 +701,9 @@ class LazyLoadedChat {
782
701
  }
783
702
  }
784
703
  _updateUI() {
785
- if (!this._container || !this._widget || !this._bubble)
704
+ if (!this._container || !this._widget || !this._bubble) {
786
705
  return;
706
+ }
787
707
  // Update visibility
788
708
  this._container.style.display = this._state.isVisible ? "block" : "none";
789
709
  // Update widget open state
@@ -795,18 +715,9 @@ class LazyLoadedChat {
795
715
  this._state.unreadCount > 0 ? "flex" : "none";
796
716
  badge.textContent = String(this._state.unreadCount);
797
717
  }
798
- // Detect view change for animation
799
- const viewChanged = this._previousView !== this._state.currentView;
800
- const animationClass = viewChanged
801
- ? this._state.currentView === "conversation"
802
- ? "vtilt-view-enter-right"
803
- : "vtilt-view-enter-left"
804
- : "";
805
718
  // Update content based on current view
806
719
  const contentContainer = this._widget.querySelector(".vtilt-chat-content");
807
720
  if (contentContainer) {
808
- // Remove previous animation classes
809
- contentContainer.classList.remove("vtilt-view-enter-right", "vtilt-view-enter-left");
810
721
  if (this._state.currentView === "list") {
811
722
  contentContainer.innerHTML = this._getChannelListHTML();
812
723
  this._attachChannelListListeners();
@@ -814,18 +725,9 @@ class LazyLoadedChat {
814
725
  else {
815
726
  contentContainer.innerHTML = this._getConversationHTML();
816
727
  this._attachConversationListeners();
817
- // Render messages in conversation view
818
728
  this._renderMessages();
819
729
  }
820
- // Add animation class if view changed
821
- if (animationClass) {
822
- // Force reflow to restart animation
823
- void contentContainer.offsetWidth;
824
- contentContainer.classList.add(animationClass);
825
- }
826
730
  }
827
- // Update previous view
828
- this._previousView = this._state.currentView;
829
731
  // Update header based on view
830
732
  this._updateHeader();
831
733
  // Update loading state
@@ -851,8 +753,9 @@ class LazyLoadedChat {
851
753
  _updateHeader() {
852
754
  var _a, _b;
853
755
  const header = (_a = this._widget) === null || _a === void 0 ? void 0 : _a.querySelector(".vtilt-chat-header");
854
- if (!header)
756
+ if (!header) {
855
757
  return;
758
+ }
856
759
  const greeting = this._config.greeting || "Messages";
857
760
  const primary = this._theme.primaryColor;
858
761
  if (this._state.currentView === "list") {
@@ -979,7 +882,6 @@ class LazyLoadedChat {
979
882
  justify-content: center;
980
883
  padding: 48px 24px;
981
884
  text-align: center;
982
- animation: vtilt-fadein 0.4s ease;
983
885
  ">
984
886
  <div style="
985
887
  width: 72px;
@@ -990,14 +892,13 @@ class LazyLoadedChat {
990
892
  display: flex;
991
893
  align-items: center;
992
894
  justify-content: center;
993
- animation: vtilt-bubble-pop 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275) 0.1s both;
994
895
  ">
995
896
  <svg width="36" height="36" viewBox="0 0 24 24" fill="white">
996
897
  <path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z"/>
997
898
  </svg>
998
899
  </div>
999
- <div style="font-size: 18px; font-weight: 600; color: #000000; margin-bottom: 8px; animation: vtilt-fadein 0.4s ease 0.15s both;">No conversations yet</div>
1000
- <div style="font-size: 15px; color: #666666; margin-bottom: 28px; line-height: 1.5; max-width: 280px; animation: vtilt-fadein 0.4s ease 0.2s both;">Questions? We're here to help. Start a conversation with us.</div>
900
+ <div style="font-size: 18px; font-weight: 600; color: #000000; margin-bottom: 8px;">No conversations yet</div>
901
+ <div style="font-size: 15px; color: #666666; margin-bottom: 28px; line-height: 1.5; max-width: 280px;">Questions? We're here to help. Start a conversation with us.</div>
1001
902
  <button class="vtilt-chat-new-channel" style="
1002
903
  background: ${primary};
1003
904
  color: white;
@@ -1013,7 +914,6 @@ class LazyLoadedChat {
1013
914
  align-items: center;
1014
915
  gap: 10px;
1015
916
  box-shadow: 0 2px 8px rgba(123, 104, 238, 0.3);
1016
- animation: vtilt-fadein 0.4s ease 0.25s both;
1017
917
  ">
1018
918
  Send us a message
1019
919
  <svg width="18" height="18" viewBox="0 0 24 24" fill="white">
@@ -1024,7 +924,7 @@ class LazyLoadedChat {
1024
924
  `;
1025
925
  }
1026
926
  const channelsHtml = this._state.channels
1027
- .map((ch, index) => this._getChannelItemHTML(ch, index))
927
+ .map((ch) => this._getChannelItemHTML(ch))
1028
928
  .join("");
1029
929
  return `
1030
930
  <div style="flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch;">
@@ -1061,13 +961,12 @@ class LazyLoadedChat {
1061
961
  </div>
1062
962
  `;
1063
963
  }
1064
- _getChannelItemHTML(channel, index = 0) {
964
+ _getChannelItemHTML(channel) {
1065
965
  const hasUnread = channel.unread_count > 0;
1066
966
  const timeStr = this._formatRelativeTime(channel.last_message_at || channel.created_at);
1067
967
  const preview = channel.last_message_preview || "No messages yet";
1068
968
  const primary = this._theme.primaryColor;
1069
969
  const senderPrefix = channel.last_message_sender === "user" ? "You: " : "";
1070
- const animationDelay = Math.min(index * 0.05, 0.3); // Max 300ms total delay
1071
970
  return `
1072
971
  <div class="vtilt-channel-item" data-channel-id="${channel.id}" style="
1073
972
  padding: 14px 16px;
@@ -1079,7 +978,6 @@ class LazyLoadedChat {
1079
978
  gap: 12px;
1080
979
  background: white;
1081
980
  border-bottom: 1px solid #EEEEEE;
1082
- animation: vtilt-item-in 0.3s cubic-bezier(0.16, 1, 0.3, 1) ${animationDelay}s both;
1083
981
  ">
1084
982
  <div style="
1085
983
  width: 48px;
@@ -1113,15 +1011,17 @@ class LazyLoadedChat {
1113
1011
  flex: 1;
1114
1012
  min-width: 0;
1115
1013
  line-height: 1.4;
1116
- ">${senderPrefix}${this._escapeHTML(preview)}${channel.status === "closed" ? ' · Closed' : ""}</div>
1117
- ${hasUnread ? `<div style="
1014
+ ">${senderPrefix}${this._escapeHTML(preview)}${channel.status === "closed" ? " · Closed" : ""}</div>
1015
+ ${hasUnread
1016
+ ? `<div style="
1118
1017
  min-width: 10px;
1119
1018
  width: 10px;
1120
1019
  height: 10px;
1121
1020
  background: ${primary};
1122
1021
  border-radius: 50%;
1123
1022
  flex-shrink: 0;
1124
- "></div>` : ""}
1023
+ "></div>`
1024
+ : ""}
1125
1025
  </div>
1126
1026
  </div>
1127
1027
  </div>
@@ -1224,47 +1124,28 @@ class LazyLoadedChat {
1224
1124
  `;
1225
1125
  }
1226
1126
  _attachChannelListListeners() {
1227
- var _a, _b;
1228
- // New channel button
1229
- const newChannelBtn = (_a = this._widget) === null || _a === void 0 ? void 0 : _a.querySelector(".vtilt-chat-new-channel");
1230
- newChannelBtn === null || newChannelBtn === void 0 ? void 0 : newChannelBtn.addEventListener("click", () => this.createChannel());
1231
- // Channel items
1232
- const channelItems = (_b = this._widget) === null || _b === void 0 ? void 0 : _b.querySelectorAll(".vtilt-channel-item");
1233
- const isTouchDevice = globals_1.window && ("ontouchstart" in globals_1.window || navigator.maxTouchPoints > 0);
1234
- channelItems === null || channelItems === void 0 ? void 0 : channelItems.forEach((item) => {
1127
+ var _a, _b, _c, _d;
1128
+ (_b = (_a = this._widget) === null || _a === void 0 ? void 0 : _a.querySelector(".vtilt-chat-new-channel")) === null || _b === void 0 ? void 0 : _b.addEventListener("click", () => this.createChannel());
1129
+ (_d = (_c = this._widget) === null || _c === void 0 ? void 0 : _c.querySelectorAll(".vtilt-channel-item")) === null || _d === void 0 ? void 0 : _d.forEach((item) => {
1235
1130
  item.addEventListener("click", () => {
1236
1131
  const channelId = item.getAttribute("data-channel-id");
1237
- if (channelId)
1132
+ if (channelId) {
1238
1133
  this.selectChannel(channelId);
1134
+ }
1239
1135
  });
1240
- // Hover effect (only on non-touch devices)
1241
- if (!isTouchDevice) {
1242
- item.addEventListener("mouseenter", () => {
1243
- item.style.background = "#F5F5F5";
1244
- });
1245
- item.addEventListener("mouseleave", () => {
1246
- item.style.background = "white";
1247
- });
1248
- }
1249
1136
  });
1250
1137
  }
1251
1138
  _attachConversationListeners() {
1252
- var _a, _b;
1253
- // Send button
1254
- const sendBtn = (_a = this._widget) === null || _a === void 0 ? void 0 : _a.querySelector(".vtilt-chat-send");
1255
- sendBtn === null || sendBtn === void 0 ? void 0 : sendBtn.addEventListener("click", () => this._handleSend());
1256
- // Input enter key and typing indicator
1257
- const input = (_b = this._widget) === null || _b === void 0 ? void 0 : _b.querySelector(".vtilt-chat-input");
1139
+ var _a, _b, _c;
1140
+ (_b = (_a = this._widget) === null || _a === void 0 ? void 0 : _a.querySelector(".vtilt-chat-send")) === null || _b === void 0 ? void 0 : _b.addEventListener("click", () => this._handleSend());
1141
+ const input = (_c = this._widget) === null || _c === void 0 ? void 0 : _c.querySelector(".vtilt-chat-input");
1258
1142
  input === null || input === void 0 ? void 0 : input.addEventListener("keypress", (e) => {
1259
1143
  if (e.key === "Enter" && !e.shiftKey) {
1260
1144
  e.preventDefault();
1261
1145
  this._handleSend();
1262
1146
  }
1263
1147
  });
1264
- // Send typing indicator on input
1265
- input === null || input === void 0 ? void 0 : input.addEventListener("input", () => {
1266
- this._handleUserTyping();
1267
- });
1148
+ input === null || input === void 0 ? void 0 : input.addEventListener("input", () => this._handleUserTyping());
1268
1149
  }
1269
1150
  _formatRelativeTime(isoString) {
1270
1151
  const date = new Date(isoString);
@@ -1273,52 +1154,43 @@ class LazyLoadedChat {
1273
1154
  const diffMins = Math.floor(diffMs / 60000);
1274
1155
  const diffHours = Math.floor(diffMs / 3600000);
1275
1156
  const diffDays = Math.floor(diffMs / 86400000);
1276
- if (diffMins < 1)
1157
+ if (diffMins < 1) {
1277
1158
  return "Just now";
1278
- if (diffMins < 60)
1159
+ }
1160
+ if (diffMins < 60) {
1279
1161
  return `${diffMins}m ago`;
1280
- if (diffHours < 24)
1162
+ }
1163
+ if (diffHours < 24) {
1281
1164
  return `${diffHours}h ago`;
1282
- if (diffDays < 7)
1165
+ }
1166
+ if (diffDays < 7) {
1283
1167
  return `${diffDays}d ago`;
1168
+ }
1284
1169
  return date.toLocaleDateString();
1285
1170
  }
1286
1171
  _renderMessages() {
1287
1172
  var _a;
1288
- const messagesContainer = (_a = this._widget) === null || _a === void 0 ? void 0 : _a.querySelector(".vtilt-chat-messages");
1289
- if (!messagesContainer)
1173
+ const container = (_a = this._widget) === null || _a === void 0 ? void 0 : _a.querySelector(".vtilt-chat-messages");
1174
+ if (!container) {
1290
1175
  return;
1176
+ }
1291
1177
  const primary = this._theme.primaryColor;
1292
- // Find first unread agent message index
1293
1178
  const firstUnreadIndex = this._state.messages.findIndex((m) => (m.sender_type === "agent" || m.sender_type === "ai") &&
1294
1179
  !this._isMessageReadByUser(m.created_at));
1295
- // Build HTML with unread divider
1296
- const messagesHtml = this._state.messages
1297
- .map((msg, index) => {
1298
- let html = "";
1299
- // Add unread divider before first unread message
1300
- if (index === firstUnreadIndex && firstUnreadIndex > 0) {
1301
- html += `
1302
- <div style="display: flex; align-items: center; gap: 12px; margin: 12px 0;">
1303
- <div style="flex: 1; height: 1px; background: #DDDDDD;"></div>
1304
- <span style="font-size: 12px; font-weight: 600; color: ${primary};">New</span>
1305
- <div style="flex: 1; height: 1px; background: #DDDDDD;"></div>
1306
- </div>
1307
- `;
1308
- }
1309
- html += this._getMessageHTML(msg);
1310
- return html;
1180
+ const html = this._state.messages
1181
+ .map((msg, i) => {
1182
+ const divider = i === firstUnreadIndex && firstUnreadIndex > 0
1183
+ ? `<div style="display:flex;align-items:center;gap:12px;margin:12px 0"><div style="flex:1;height:1px;background:#DDD"></div><span style="font-size:12px;font-weight:600;color:${primary}">New</span><div style="flex:1;height:1px;background:#DDD"></div></div>`
1184
+ : "";
1185
+ return divider + this._getMessageHTML(msg);
1311
1186
  })
1312
1187
  .join("");
1313
- messagesContainer.innerHTML = messagesHtml;
1314
- messagesContainer.scrollTop = messagesContainer.scrollHeight;
1188
+ container.innerHTML = html;
1189
+ container.scrollTop = container.scrollHeight;
1315
1190
  }
1316
1191
  // ============================================================================
1317
1192
  // Private - Styles & HTML
1318
1193
  // ============================================================================
1319
- _isMobile() {
1320
- return globals_1.window ? globals_1.window.innerWidth < 480 : false;
1321
- }
1322
1194
  _getContainerStyles() {
1323
1195
  const isRight = (this._config.position || DEFAULT_POSITION) === "bottom-right";
1324
1196
  return `
@@ -1544,7 +1416,9 @@ class LazyLoadedChat {
1544
1416
  </div>
1545
1417
  `;
1546
1418
  }
1547
- const senderLabel = isAi ? "AI Assistant" : (message.sender_name || "Support");
1419
+ const senderLabel = isAi
1420
+ ? "AI Assistant"
1421
+ : message.sender_name || "Support";
1548
1422
  return `
1549
1423
  <div class="vtilt-msg" style="
1550
1424
  display: flex;
@@ -1584,12 +1458,10 @@ class LazyLoadedChat {
1584
1458
  </div>
1585
1459
  `;
1586
1460
  }
1587
- /**
1588
- * Check if a message has been read by the agent using cursor comparison
1589
- */
1590
1461
  _isMessageReadByAgent(messageCreatedAt) {
1591
- if (!this._state.agentLastReadAt)
1462
+ if (!this._state.agentLastReadAt) {
1592
1463
  return false;
1464
+ }
1593
1465
  return new Date(messageCreatedAt) <= new Date(this._state.agentLastReadAt);
1594
1466
  }
1595
1467
  // ============================================================================
@@ -1628,8 +1500,9 @@ class LazyLoadedChat {
1628
1500
  return 0;
1629
1501
  }
1630
1502
  _escapeHTML(text) {
1631
- if (!globals_1.document)
1503
+ if (!globals_1.document) {
1632
1504
  return text;
1505
+ }
1633
1506
  const div = globals_1.document.createElement("div");
1634
1507
  div.textContent = text;
1635
1508
  return div.innerHTML;