@v-tilt/browser 1.4.2 → 1.4.3

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)
@@ -138,13 +130,10 @@ class LazyLoadedChat {
138
130
  $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
131
  $trigger: "api",
140
132
  });
141
- // Show channel list view first (multi-channel support)
142
- // Only fetch channels if not already loaded
133
+ // Fetch channels if not already loaded
143
134
  if (this._state.channels.length === 0) {
144
135
  this.getChannels();
145
136
  }
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
137
  }
149
138
  close() {
150
139
  if (!this._state.isOpen)
@@ -168,7 +157,6 @@ class LazyLoadedChat {
168
157
  $time_open_seconds: timeOpen,
169
158
  $messages_sent: this._state.messages.filter((m) => m.sender_type === "user").length,
170
159
  });
171
- // Disconnect Ably when widget closes to save connection minutes
172
160
  this._disconnectRealtime();
173
161
  }
174
162
  toggle() {
@@ -190,9 +178,6 @@ class LazyLoadedChat {
190
178
  // ============================================================================
191
179
  // Public API - Channel Management (Multi-channel support)
192
180
  // ============================================================================
193
- /**
194
- * Fetch/refresh the list of user's channels
195
- */
196
181
  async getChannels() {
197
182
  this._state.isLoading = true;
198
183
  this._updateUI();
@@ -214,9 +199,6 @@ class LazyLoadedChat {
214
199
  this._updateUI();
215
200
  }
216
201
  }
217
- /**
218
- * Select a channel and load its messages
219
- */
220
202
  async selectChannel(channelId) {
221
203
  this._state.isLoading = true;
222
204
  this._updateUI();
@@ -228,16 +210,11 @@ class LazyLoadedChat {
228
210
  this._state.channel = response.channel;
229
211
  this._state.messages = response.messages || [];
230
212
  this._state.currentView = "conversation";
231
- // Initialize read cursors from channel
232
- this._state.agentLastReadAt =
233
- response.channel.agent_last_read_at || null;
213
+ this._state.agentLastReadAt = response.channel.agent_last_read_at || null;
234
214
  this._initialUserReadAt = response.channel.user_last_read_at || null;
235
- // Connect to Ably for this channel
236
215
  this._connectRealtime();
237
- // Auto-mark unread messages as read if widget is open
238
- if (this._state.isOpen) {
216
+ if (this._state.isOpen)
239
217
  this._autoMarkAsRead();
240
- }
241
218
  }
242
219
  }
243
220
  catch (error) {
@@ -248,9 +225,6 @@ class LazyLoadedChat {
248
225
  this._updateUI();
249
226
  }
250
227
  }
251
- /**
252
- * Create a new channel and enter it
253
- */
254
228
  async createChannel() {
255
229
  var _a;
256
230
  this._state.isLoading = true;
@@ -268,11 +242,8 @@ class LazyLoadedChat {
268
242
  this._state.channel = response.channel;
269
243
  this._state.messages = response.messages || [];
270
244
  this._state.currentView = "conversation";
271
- // Initialize read cursors
272
- this._state.agentLastReadAt =
273
- response.channel.agent_last_read_at || null;
245
+ this._state.agentLastReadAt = response.channel.agent_last_read_at || null;
274
246
  this._initialUserReadAt = response.channel.user_last_read_at || null;
275
- // Add new channel to the list
276
247
  const newChannelSummary = {
277
248
  id: response.channel.id,
278
249
  status: response.channel.status,
@@ -285,13 +256,11 @@ class LazyLoadedChat {
285
256
  created_at: response.channel.created_at,
286
257
  };
287
258
  this._state.channels.unshift(newChannelSummary);
288
- // Track channel started
289
259
  this._trackEvent(types_1.CHAT_EVENTS.STARTED, {
290
260
  $channel_id: response.channel.id,
291
261
  $initiated_by: "user",
292
262
  $ai_mode: response.channel.ai_mode,
293
263
  });
294
- // Connect to Ably for this channel
295
264
  this._connectRealtime();
296
265
  }
297
266
  }
@@ -303,13 +272,8 @@ class LazyLoadedChat {
303
272
  this._updateUI();
304
273
  }
305
274
  }
306
- /**
307
- * Go back to channel list from conversation view
308
- */
309
275
  goToChannelList() {
310
- // Disconnect Ably when leaving conversation to save connection minutes
311
276
  this._disconnectRealtime();
312
- // Update the channel in the list with latest data
313
277
  if (this._state.channel) {
314
278
  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
279
  if (channelIndex !== -1) {
@@ -328,7 +292,6 @@ class LazyLoadedChat {
328
292
  };
329
293
  }
330
294
  }
331
- // Clear current channel state
332
295
  this._state.channel = null;
333
296
  this._state.messages = [];
334
297
  this._state.currentView = "list";
@@ -383,9 +346,6 @@ class LazyLoadedChat {
383
346
  if (index !== -1 && (response === null || response === void 0 ? void 0 : response.message)) {
384
347
  this._state.messages[index] = response.message;
385
348
  }
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
349
  this._trackEvent(types_1.CHAT_EVENTS.MESSAGE_SENT, {
390
350
  $channel_id: channelId,
391
351
  $message_id: (_b = response === null || response === void 0 ? void 0 : response.message) === null || _b === void 0 ? void 0 : _b.id,
@@ -406,28 +366,18 @@ class LazyLoadedChat {
406
366
  markAsRead() {
407
367
  this._autoMarkAsRead();
408
368
  }
409
- /**
410
- * Automatically mark unread agent/AI messages as read
411
- * Called when widget opens or new messages arrive while open
412
- */
413
369
  _autoMarkAsRead() {
414
- if (!this._state.channel)
370
+ if (!this._state.channel || this._isMarkingRead)
415
371
  return;
416
- if (this._isMarkingRead)
417
- return; // Already in progress
418
- // Get the latest message timestamp
419
372
  const latestMessage = this._state.messages[this._state.messages.length - 1];
420
373
  if (!latestMessage)
421
374
  return;
422
- // Check if there are unread agent messages
423
- const hasUnreadAgentMessages = this._state.messages.some((m) => (m.sender_type === "agent" || m.sender_type === "ai") &&
424
- !this._isMessageReadByUser(m.created_at));
375
+ const hasUnreadAgentMessages = this._state.messages.some((m) => (m.sender_type === "agent" || m.sender_type === "ai") && !this._isMessageReadByUser(m.created_at));
425
376
  if (!hasUnreadAgentMessages)
426
377
  return;
427
378
  this._state.unreadCount = 0;
428
379
  this._isMarkingRead = true;
429
380
  this._updateUI();
430
- // API call to update read cursor with latest message timestamp
431
381
  this._apiRequest(API.read, {
432
382
  method: "POST",
433
383
  body: JSON.stringify({
@@ -437,20 +387,15 @@ class LazyLoadedChat {
437
387
  }),
438
388
  })
439
389
  .then(() => {
440
- // Update initial read position after successful mark
441
390
  this._initialUserReadAt = latestMessage.created_at;
442
391
  this._isMarkingRead = false;
443
392
  this._updateUI();
444
393
  })
445
- .catch((err) => {
446
- console.error(`${LOGGER_PREFIX} Failed to mark as read:`, err);
394
+ .catch(() => {
447
395
  this._isMarkingRead = false;
448
396
  this._updateUI();
449
397
  });
450
398
  }
451
- /**
452
- * Check if a message has been read by the user (using initial cursor)
453
- */
454
399
  _isMessageReadByUser(messageCreatedAt) {
455
400
  if (!this._initialUserReadAt)
456
401
  return false;
@@ -487,22 +432,16 @@ class LazyLoadedChat {
487
432
  // Public API - Lifecycle
488
433
  // ============================================================================
489
434
  destroy() {
490
- // Disconnect Ably
435
+ var _a;
491
436
  this._disconnectRealtime();
492
- // Clear timers
493
- if (this._typingTimeout)
494
- clearTimeout(this._typingTimeout);
495
437
  if (this._typingDebounce)
496
438
  clearTimeout(this._typingDebounce);
497
- // Remove DOM elements
498
- if (this._container && this._container.parentNode) {
439
+ if ((_a = this._container) === null || _a === void 0 ? void 0 : _a.parentNode) {
499
440
  this._container.parentNode.removeChild(this._container);
500
441
  }
501
- // Clear callbacks
502
442
  this._messageCallbacks = [];
503
443
  this._typingCallbacks = [];
504
444
  this._connectionCallbacks = [];
505
- console.info(`${LOGGER_PREFIX} destroyed`);
506
445
  }
507
446
  // ============================================================================
508
447
  // Private - Ably Realtime Connection
@@ -527,7 +466,6 @@ class LazyLoadedChat {
527
466
  this._connectionState = "error";
528
467
  return;
529
468
  }
530
- // Create Ably client with token auth
531
469
  this._ably = new ably_1.default.Realtime({
532
470
  authCallback: async (_, callback) => {
533
471
  var _a;
@@ -552,37 +490,25 @@ class LazyLoadedChat {
552
490
  },
553
491
  authMethod: "POST",
554
492
  });
555
- // Authenticate with initial token
556
493
  await this._ably.auth.authorize(tokenResponse.tokenRequest);
557
- // Get project ID from instance config
558
494
  const config = this._instance.getConfig();
559
495
  const projectId = config.projectId || this._extractProjectId(config.token || "");
560
- // Subscribe to chat channel (Ably channel)
561
496
  const ablyChannelName = `chat:${projectId}:${this._state.channel.id}`;
562
497
  this._ablyChannel = this._ably.channels.get(ablyChannelName);
563
- // Listen for new messages
564
498
  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);
499
+ this._handleNewMessage(msg.data);
574
500
  });
575
- // Subscribe to read cursor events
576
501
  this._ablyChannel.subscribe("read", (msg) => {
577
- const event = msg.data;
578
- this._handleReadCursorEvent(event);
502
+ this._handleReadCursorEvent(msg.data);
503
+ });
504
+ this._typingChannel = this._ably.channels.get(`${ablyChannelName}:typing`);
505
+ this._typingChannel.subscribe("typing", (msg) => {
506
+ this._handleTypingEvent(msg.data);
579
507
  });
580
- // Handle connection state changes
581
508
  this._ably.connection.on("connected", () => {
582
509
  this._connectionState = "connected";
583
510
  this._state.isConnected = true;
584
511
  this._notifyConnectionChange(true);
585
- console.info(`${LOGGER_PREFIX} Connected to Ably`);
586
512
  });
587
513
  this._ably.connection.on("disconnected", () => {
588
514
  this._connectionState = "disconnected";
@@ -594,7 +520,6 @@ class LazyLoadedChat {
594
520
  this._state.isConnected = false;
595
521
  this._notifyConnectionChange(false);
596
522
  });
597
- // Initial connection
598
523
  this._ably.connect();
599
524
  }
600
525
  catch (error) {
@@ -624,36 +549,25 @@ class LazyLoadedChat {
624
549
  return parts[0] || "";
625
550
  }
626
551
  _handleNewMessage(message) {
627
- // Avoid duplicates by ID
628
552
  if (this._state.messages.some((m) => m.id === message.id))
629
553
  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
632
- if (message.sender_type === "user" &&
633
- message.sender_id === this._distinctId) {
634
- // But DO replace temp message with real one if present
635
- const tempIndex = this._state.messages.findIndex((m) => m.id.startsWith("temp-") &&
636
- m.content === message.content &&
637
- m.sender_type === "user");
554
+ // Skip own messages but replace temp message if present
555
+ if (message.sender_type === "user" && message.sender_id === this._distinctId) {
556
+ const tempIndex = this._state.messages.findIndex((m) => m.id.startsWith("temp-") && m.content === message.content && m.sender_type === "user");
638
557
  if (tempIndex !== -1) {
639
558
  this._state.messages[tempIndex] = message;
640
559
  this._updateUI();
641
560
  }
642
561
  return;
643
562
  }
644
- // Add to messages
645
563
  this._state.messages.push(message);
646
- // Update unread count if from agent/AI
647
564
  if (message.sender_type !== "user") {
648
565
  if (!this._state.isOpen) {
649
566
  this._state.unreadCount++;
650
567
  }
651
568
  else {
652
- // Widget is open, auto-mark as read after a short delay
653
- // to ensure UI updates first
654
569
  setTimeout(() => this._autoMarkAsRead(), 100);
655
570
  }
656
- // Track received event
657
571
  this._trackEvent(types_1.CHAT_EVENTS.MESSAGE_RECEIVED, {
658
572
  $channel_id: message.channel_id,
659
573
  $message_id: message.id,
@@ -661,32 +575,24 @@ class LazyLoadedChat {
661
575
  $sender_type: message.sender_type,
662
576
  });
663
577
  }
664
- // Clear typing indicator when message arrives
665
578
  this._state.isTyping = false;
666
579
  this._state.typingSender = null;
667
- // Notify callbacks
668
580
  this._messageCallbacks.forEach((cb) => cb(message));
669
581
  this._updateUI();
670
582
  }
671
583
  _handleTypingEvent(event) {
672
- // Only show typing for non-user senders
673
584
  if (event.sender_type === "user")
674
585
  return;
675
- const senderName = event.sender_name ||
676
- (event.sender_type === "ai" ? "AI Assistant" : "Agent");
586
+ const senderName = event.sender_name || (event.sender_type === "ai" ? "AI Assistant" : "Agent");
677
587
  this._state.isTyping = event.is_typing;
678
588
  this._state.typingSender = event.is_typing ? senderName : null;
679
- // Notify callbacks
680
589
  this._typingCallbacks.forEach((cb) => cb(event.is_typing, senderName));
681
590
  this._updateUI();
682
591
  }
683
592
  _handleReadCursorEvent(event) {
684
- // Only handle agent read events (user's own reads are local)
685
593
  if (event.reader_type !== "agent")
686
594
  return;
687
- // Update the agent read cursor
688
595
  this._state.agentLastReadAt = event.read_at;
689
- // Update UI to show read status on user messages
690
596
  this._updateUI();
691
597
  }
692
598
  _notifyConnectionChange(connected) {
@@ -698,49 +604,35 @@ class LazyLoadedChat {
698
604
  _createUI() {
699
605
  if (!globals_1.document)
700
606
  return;
701
- // Create container
702
607
  this._container = globals_1.document.createElement("div");
703
608
  this._container.id = "vtilt-chat-container";
704
609
  this._container.setAttribute("style", this._getContainerStyles());
705
- // Create bubble (launcher button)
706
610
  this._bubble = globals_1.document.createElement("div");
707
611
  this._bubble.id = "vtilt-chat-bubble";
708
612
  this._bubble.innerHTML = this._getBubbleHTML();
709
613
  this._bubble.setAttribute("style", this._getBubbleStyles());
710
614
  this._container.appendChild(this._bubble);
711
- // Create widget (chat window)
712
615
  this._widget = globals_1.document.createElement("div");
713
616
  this._widget.id = "vtilt-chat-widget";
714
617
  this._widget.innerHTML = this._getWidgetHTML();
715
618
  this._widget.setAttribute("style", this._getWidgetStyles());
716
619
  this._container.appendChild(this._widget);
717
- // Add to DOM
718
620
  globals_1.document.body.appendChild(this._container);
719
621
  }
720
622
  _attachEventListeners() {
721
- var _a, _b;
722
- // Bubble click
623
+ var _a, _b, _c;
723
624
  (_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()
625
+ (_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
626
  }
730
627
  _handleUserTyping() {
731
- // Don't send typing if not connected to Ably
732
628
  if (!this._typingChannel)
733
629
  return;
734
- // Send typing started if not already typing
735
630
  if (!this._isUserTyping) {
736
631
  this._isUserTyping = true;
737
632
  this._sendTypingIndicator(true);
738
633
  }
739
- // Clear existing debounce timer
740
- if (this._typingDebounce) {
634
+ if (this._typingDebounce)
741
635
  clearTimeout(this._typingDebounce);
742
- }
743
- // Set timer to send typing stopped after 2 seconds of no input
744
636
  this._typingDebounce = setTimeout(() => {
745
637
  this._isUserTyping = false;
746
638
  this._sendTypingIndicator(false);
@@ -757,8 +649,8 @@ class LazyLoadedChat {
757
649
  is_typing: isTyping,
758
650
  });
759
651
  }
760
- catch (err) {
761
- console.error(`${LOGGER_PREFIX} Failed to send typing indicator:`, err);
652
+ catch (_a) {
653
+ // Silently fail
762
654
  }
763
655
  }
764
656
  _handleSend() {
@@ -768,7 +660,6 @@ class LazyLoadedChat {
768
660
  return;
769
661
  const content = input.value.trim();
770
662
  if (content) {
771
- // Stop typing indicator
772
663
  if (this._isUserTyping) {
773
664
  this._isUserTyping = false;
774
665
  this._sendTypingIndicator(false);
@@ -795,18 +686,9 @@ class LazyLoadedChat {
795
686
  this._state.unreadCount > 0 ? "flex" : "none";
796
687
  badge.textContent = String(this._state.unreadCount);
797
688
  }
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
689
  // Update content based on current view
806
690
  const contentContainer = this._widget.querySelector(".vtilt-chat-content");
807
691
  if (contentContainer) {
808
- // Remove previous animation classes
809
- contentContainer.classList.remove("vtilt-view-enter-right", "vtilt-view-enter-left");
810
692
  if (this._state.currentView === "list") {
811
693
  contentContainer.innerHTML = this._getChannelListHTML();
812
694
  this._attachChannelListListeners();
@@ -814,18 +696,9 @@ class LazyLoadedChat {
814
696
  else {
815
697
  contentContainer.innerHTML = this._getConversationHTML();
816
698
  this._attachConversationListeners();
817
- // Render messages in conversation view
818
699
  this._renderMessages();
819
700
  }
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
701
  }
827
- // Update previous view
828
- this._previousView = this._state.currentView;
829
702
  // Update header based on view
830
703
  this._updateHeader();
831
704
  // Update loading state
@@ -979,7 +852,6 @@ class LazyLoadedChat {
979
852
  justify-content: center;
980
853
  padding: 48px 24px;
981
854
  text-align: center;
982
- animation: vtilt-fadein 0.4s ease;
983
855
  ">
984
856
  <div style="
985
857
  width: 72px;
@@ -990,14 +862,13 @@ class LazyLoadedChat {
990
862
  display: flex;
991
863
  align-items: center;
992
864
  justify-content: center;
993
- animation: vtilt-bubble-pop 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275) 0.1s both;
994
865
  ">
995
866
  <svg width="36" height="36" viewBox="0 0 24 24" fill="white">
996
867
  <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
868
  </svg>
998
869
  </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>
870
+ <div style="font-size: 18px; font-weight: 600; color: #000000; margin-bottom: 8px;">No conversations yet</div>
871
+ <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
872
  <button class="vtilt-chat-new-channel" style="
1002
873
  background: ${primary};
1003
874
  color: white;
@@ -1013,7 +884,6 @@ class LazyLoadedChat {
1013
884
  align-items: center;
1014
885
  gap: 10px;
1015
886
  box-shadow: 0 2px 8px rgba(123, 104, 238, 0.3);
1016
- animation: vtilt-fadein 0.4s ease 0.25s both;
1017
887
  ">
1018
888
  Send us a message
1019
889
  <svg width="18" height="18" viewBox="0 0 24 24" fill="white">
@@ -1024,7 +894,7 @@ class LazyLoadedChat {
1024
894
  `;
1025
895
  }
1026
896
  const channelsHtml = this._state.channels
1027
- .map((ch, index) => this._getChannelItemHTML(ch, index))
897
+ .map((ch) => this._getChannelItemHTML(ch))
1028
898
  .join("");
1029
899
  return `
1030
900
  <div style="flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch;">
@@ -1061,13 +931,12 @@ class LazyLoadedChat {
1061
931
  </div>
1062
932
  `;
1063
933
  }
1064
- _getChannelItemHTML(channel, index = 0) {
934
+ _getChannelItemHTML(channel) {
1065
935
  const hasUnread = channel.unread_count > 0;
1066
936
  const timeStr = this._formatRelativeTime(channel.last_message_at || channel.created_at);
1067
937
  const preview = channel.last_message_preview || "No messages yet";
1068
938
  const primary = this._theme.primaryColor;
1069
939
  const senderPrefix = channel.last_message_sender === "user" ? "You: " : "";
1070
- const animationDelay = Math.min(index * 0.05, 0.3); // Max 300ms total delay
1071
940
  return `
1072
941
  <div class="vtilt-channel-item" data-channel-id="${channel.id}" style="
1073
942
  padding: 14px 16px;
@@ -1079,7 +948,6 @@ class LazyLoadedChat {
1079
948
  gap: 12px;
1080
949
  background: white;
1081
950
  border-bottom: 1px solid #EEEEEE;
1082
- animation: vtilt-item-in 0.3s cubic-bezier(0.16, 1, 0.3, 1) ${animationDelay}s both;
1083
951
  ">
1084
952
  <div style="
1085
953
  width: 48px;
@@ -1224,47 +1092,27 @@ class LazyLoadedChat {
1224
1092
  `;
1225
1093
  }
1226
1094
  _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) => {
1095
+ var _a, _b, _c, _d;
1096
+ (_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());
1097
+ (_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
1098
  item.addEventListener("click", () => {
1236
1099
  const channelId = item.getAttribute("data-channel-id");
1237
1100
  if (channelId)
1238
1101
  this.selectChannel(channelId);
1239
1102
  });
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
1103
  });
1250
1104
  }
1251
1105
  _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");
1106
+ var _a, _b, _c;
1107
+ (_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());
1108
+ const input = (_c = this._widget) === null || _c === void 0 ? void 0 : _c.querySelector(".vtilt-chat-input");
1258
1109
  input === null || input === void 0 ? void 0 : input.addEventListener("keypress", (e) => {
1259
1110
  if (e.key === "Enter" && !e.shiftKey) {
1260
1111
  e.preventDefault();
1261
1112
  this._handleSend();
1262
1113
  }
1263
1114
  });
1264
- // Send typing indicator on input
1265
- input === null || input === void 0 ? void 0 : input.addEventListener("input", () => {
1266
- this._handleUserTyping();
1267
- });
1115
+ input === null || input === void 0 ? void 0 : input.addEventListener("input", () => this._handleUserTyping());
1268
1116
  }
1269
1117
  _formatRelativeTime(isoString) {
1270
1118
  const date = new Date(isoString);
@@ -1285,40 +1133,23 @@ class LazyLoadedChat {
1285
1133
  }
1286
1134
  _renderMessages() {
1287
1135
  var _a;
1288
- const messagesContainer = (_a = this._widget) === null || _a === void 0 ? void 0 : _a.querySelector(".vtilt-chat-messages");
1289
- if (!messagesContainer)
1136
+ const container = (_a = this._widget) === null || _a === void 0 ? void 0 : _a.querySelector(".vtilt-chat-messages");
1137
+ if (!container)
1290
1138
  return;
1291
1139
  const primary = this._theme.primaryColor;
1292
- // Find first unread agent message index
1293
- const firstUnreadIndex = this._state.messages.findIndex((m) => (m.sender_type === "agent" || m.sender_type === "ai") &&
1294
- !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;
1311
- })
1312
- .join("");
1313
- messagesContainer.innerHTML = messagesHtml;
1314
- messagesContainer.scrollTop = messagesContainer.scrollHeight;
1140
+ const firstUnreadIndex = this._state.messages.findIndex((m) => (m.sender_type === "agent" || m.sender_type === "ai") && !this._isMessageReadByUser(m.created_at));
1141
+ const html = this._state.messages.map((msg, i) => {
1142
+ const divider = i === firstUnreadIndex && firstUnreadIndex > 0
1143
+ ? `<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>`
1144
+ : "";
1145
+ return divider + this._getMessageHTML(msg);
1146
+ }).join("");
1147
+ container.innerHTML = html;
1148
+ container.scrollTop = container.scrollHeight;
1315
1149
  }
1316
1150
  // ============================================================================
1317
1151
  // Private - Styles & HTML
1318
1152
  // ============================================================================
1319
- _isMobile() {
1320
- return globals_1.window ? globals_1.window.innerWidth < 480 : false;
1321
- }
1322
1153
  _getContainerStyles() {
1323
1154
  const isRight = (this._config.position || DEFAULT_POSITION) === "bottom-right";
1324
1155
  return `
@@ -1584,9 +1415,6 @@ class LazyLoadedChat {
1584
1415
  </div>
1585
1416
  `;
1586
1417
  }
1587
- /**
1588
- * Check if a message has been read by the agent using cursor comparison
1589
- */
1590
1418
  _isMessageReadByAgent(messageCreatedAt) {
1591
1419
  if (!this._state.agentLastReadAt)
1592
1420
  return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@v-tilt/browser",
3
- "version": "1.4.2",
3
+ "version": "1.4.3",
4
4
  "description": "vTilt browser tracking library",
5
5
  "main": "dist/main.js",
6
6
  "module": "dist/module.js",