@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.
- package/dist/array.full.js +1 -1
- package/dist/array.js +1 -1
- package/dist/chat.js +1 -1
- package/dist/chat.js.map +1 -1
- package/dist/extensions/chat/chat.d.ts +1 -31
- package/dist/external-scripts-loader.js +1 -1
- package/dist/module.js +1 -1
- package/lib/entrypoints/external-scripts-loader.js +1 -1
- package/lib/extensions/chat/chat.d.ts +1 -31
- package/lib/extensions/chat/chat.js +47 -219
- package/package.json +1 -1
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Lazy
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
578
|
-
|
|
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
|
|
631
|
-
|
|
632
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
761
|
-
|
|
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;
|
|
1000
|
-
<div style="font-size: 15px; color: #666666; margin-bottom: 28px; line-height: 1.5; max-width: 280px;
|
|
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
|
|
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
|
|
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
|
-
|
|
1229
|
-
|
|
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
|
-
|
|
1254
|
-
const
|
|
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
|
-
|
|
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
|
|
1289
|
-
if (!
|
|
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
|
-
|
|
1293
|
-
const
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
.
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
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;
|