@v-tilt/browser 1.4.1 → 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 };
@@ -21,18 +16,17 @@ const LOGGER_PREFIX = "[Chat]";
21
16
  // ============================================================================
22
17
  const DEFAULT_POSITION = "bottom-right";
23
18
  const DEFAULT_THEME = {
24
- primaryColor: "#6366f1",
25
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
26
- borderRadius: "12px",
27
- headerBgColor: "#6366f1",
28
- userBubbleColor: "#6366f1",
29
- agentBubbleColor: "#f3f4f6",
19
+ primaryColor: "#7B68EE", // Intercom-like purple
20
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
21
+ borderRadius: "16px",
22
+ };
23
+ // API endpoints
24
+ const API = {
25
+ channels: "/api/chat/widget/channels",
26
+ messages: "/api/chat/widget/messages",
27
+ read: "/api/chat/widget/read",
28
+ ablyToken: "/api/chat/ably-token",
30
29
  };
31
- // API endpoints (widget endpoints for SDK use)
32
- const API_WIDGET = "/api/chat/widget";
33
- const API_WIDGET_MESSAGES = "/api/chat/widget/messages";
34
- const API_WIDGET_READ = "/api/chat/widget/read";
35
- const API_ABLY_TOKEN = "/api/chat/ably-token";
36
30
  // ============================================================================
37
31
  // LazyLoadedChat Implementation
38
32
  // ============================================================================
@@ -52,7 +46,6 @@ class LazyLoadedChat {
52
46
  this._typingCallbacks = [];
53
47
  this._connectionCallbacks = [];
54
48
  // Timers
55
- this._typingTimeout = null;
56
49
  this._typingDebounce = null;
57
50
  this._isUserTyping = false;
58
51
  // Read tracking - initial position when widget opens (to show unread indicators)
@@ -73,6 +66,10 @@ class LazyLoadedChat {
73
66
  isConnected: false,
74
67
  isLoading: false,
75
68
  unreadCount: 0,
69
+ // Multi-channel support
70
+ currentView: "list",
71
+ channels: [],
72
+ // Current channel (when in conversation view)
76
73
  channel: null,
77
74
  messages: [],
78
75
  isTyping: false,
@@ -82,7 +79,6 @@ class LazyLoadedChat {
82
79
  // Initialize UI
83
80
  this._createUI();
84
81
  this._attachEventListeners();
85
- console.info(`${LOGGER_PREFIX} initialized`);
86
82
  }
87
83
  // ============================================================================
88
84
  // Public API - State (LazyLoadedChatInterface)
@@ -102,6 +98,20 @@ class LazyLoadedChat {
102
98
  get channel() {
103
99
  return this._state.channel;
104
100
  }
101
+ get channels() {
102
+ return this._state.channels;
103
+ }
104
+ get currentView() {
105
+ return this._state.currentView;
106
+ }
107
+ // Theme getter to avoid repeated DEFAULT_THEME fallback
108
+ get _theme() {
109
+ return this._config.theme || DEFAULT_THEME;
110
+ }
111
+ // Distinct ID getter for convenience
112
+ get _distinctId() {
113
+ return this._instance.getDistinctId() || "";
114
+ }
105
115
  // ============================================================================
106
116
  // Public API - Widget Control
107
117
  // ============================================================================
@@ -111,33 +121,43 @@ class LazyLoadedChat {
111
121
  return;
112
122
  this._state.isOpen = true;
113
123
  this._updateUI();
124
+ // Add opening animation
125
+ if (this._widget) {
126
+ this._widget.classList.remove("vtilt-closing");
127
+ this._widget.classList.add("vtilt-opening");
128
+ }
114
129
  this._trackEvent(types_1.CHAT_EVENTS.WIDGET_OPENED, {
115
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,
116
131
  $trigger: "api",
117
132
  });
118
- // Initialize channel if needed
119
- if (!this._state.channel) {
120
- this._initializeChannel();
121
- }
122
- else {
123
- // Channel exists, mark any unread messages as read
124
- this._autoMarkAsRead();
125
- }
126
- // Connect to Ably
127
- if (!this._state.isConnected) {
128
- this._connectRealtime();
133
+ // Fetch channels if not already loaded
134
+ if (this._state.channels.length === 0) {
135
+ this.getChannels();
129
136
  }
130
137
  }
131
138
  close() {
132
139
  if (!this._state.isOpen)
133
140
  return;
134
141
  const timeOpen = this._getTimeOpen();
135
- this._state.isOpen = false;
136
- this._updateUI();
142
+ // Add closing animation
143
+ if (this._widget) {
144
+ this._widget.classList.remove("vtilt-opening");
145
+ this._widget.classList.add("vtilt-closing");
146
+ // Hide after animation completes
147
+ setTimeout(() => {
148
+ this._state.isOpen = false;
149
+ this._updateUI();
150
+ }, 200);
151
+ }
152
+ else {
153
+ this._state.isOpen = false;
154
+ this._updateUI();
155
+ }
137
156
  this._trackEvent(types_1.CHAT_EVENTS.WIDGET_CLOSED, {
138
157
  $time_open_seconds: timeOpen,
139
158
  $messages_sent: this._state.messages.filter((m) => m.sender_type === "user").length,
140
159
  });
160
+ this._disconnectRealtime();
141
161
  }
142
162
  toggle() {
143
163
  if (this._state.isOpen) {
@@ -156,15 +176,140 @@ class LazyLoadedChat {
156
176
  this._updateUI();
157
177
  }
158
178
  // ============================================================================
179
+ // Public API - Channel Management (Multi-channel support)
180
+ // ============================================================================
181
+ async getChannels() {
182
+ this._state.isLoading = true;
183
+ this._updateUI();
184
+ try {
185
+ const response = await this._apiRequest(`${API.channels}?distinct_id=${encodeURIComponent(this._distinctId)}`, {
186
+ method: "GET",
187
+ });
188
+ if (response) {
189
+ this._state.channels = response.channels || [];
190
+ // Calculate total unread count from all channels
191
+ this._state.unreadCount = this._state.channels.reduce((sum, ch) => sum + (ch.unread_count || 0), 0);
192
+ }
193
+ }
194
+ catch (error) {
195
+ console.error(`${LOGGER_PREFIX} Failed to fetch channels:`, error);
196
+ }
197
+ finally {
198
+ this._state.isLoading = false;
199
+ this._updateUI();
200
+ }
201
+ }
202
+ async selectChannel(channelId) {
203
+ this._state.isLoading = true;
204
+ this._updateUI();
205
+ try {
206
+ const response = await this._apiRequest(`${API.channels}/${channelId}?distinct_id=${encodeURIComponent(this._distinctId)}`, {
207
+ method: "GET",
208
+ });
209
+ if (response) {
210
+ this._state.channel = response.channel;
211
+ this._state.messages = response.messages || [];
212
+ this._state.currentView = "conversation";
213
+ this._state.agentLastReadAt = response.channel.agent_last_read_at || null;
214
+ this._initialUserReadAt = response.channel.user_last_read_at || null;
215
+ this._connectRealtime();
216
+ if (this._state.isOpen)
217
+ this._autoMarkAsRead();
218
+ }
219
+ }
220
+ catch (error) {
221
+ console.error(`${LOGGER_PREFIX} Failed to select channel:`, error);
222
+ }
223
+ finally {
224
+ this._state.isLoading = false;
225
+ this._updateUI();
226
+ }
227
+ }
228
+ async createChannel() {
229
+ var _a;
230
+ this._state.isLoading = true;
231
+ this._updateUI();
232
+ try {
233
+ const response = await this._apiRequest(API.channels, {
234
+ method: "POST",
235
+ body: JSON.stringify({
236
+ distinct_id: this._distinctId,
237
+ 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,
238
+ page_title: globals_1.document === null || globals_1.document === void 0 ? void 0 : globals_1.document.title,
239
+ }),
240
+ });
241
+ if (response) {
242
+ this._state.channel = response.channel;
243
+ this._state.messages = response.messages || [];
244
+ this._state.currentView = "conversation";
245
+ this._state.agentLastReadAt = response.channel.agent_last_read_at || null;
246
+ this._initialUserReadAt = response.channel.user_last_read_at || null;
247
+ const newChannelSummary = {
248
+ id: response.channel.id,
249
+ status: response.channel.status,
250
+ ai_mode: response.channel.ai_mode,
251
+ last_message_at: response.channel.last_message_at,
252
+ last_message_preview: response.channel.last_message_preview,
253
+ last_message_sender: response.channel.last_message_sender,
254
+ unread_count: response.channel.unread_count,
255
+ user_last_read_at: response.channel.user_last_read_at,
256
+ created_at: response.channel.created_at,
257
+ };
258
+ this._state.channels.unshift(newChannelSummary);
259
+ this._trackEvent(types_1.CHAT_EVENTS.STARTED, {
260
+ $channel_id: response.channel.id,
261
+ $initiated_by: "user",
262
+ $ai_mode: response.channel.ai_mode,
263
+ });
264
+ this._connectRealtime();
265
+ }
266
+ }
267
+ catch (error) {
268
+ console.error(`${LOGGER_PREFIX} Failed to create channel:`, error);
269
+ }
270
+ finally {
271
+ this._state.isLoading = false;
272
+ this._updateUI();
273
+ }
274
+ }
275
+ goToChannelList() {
276
+ this._disconnectRealtime();
277
+ if (this._state.channel) {
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); });
279
+ if (channelIndex !== -1) {
280
+ this._state.channels[channelIndex] = {
281
+ ...this._state.channels[channelIndex],
282
+ last_message_at: this._state.messages.length > 0
283
+ ? this._state.messages[this._state.messages.length - 1].created_at
284
+ : this._state.channels[channelIndex].last_message_at,
285
+ last_message_preview: this._state.messages.length > 0
286
+ ? this._state.messages[this._state.messages.length - 1].content.substring(0, 100)
287
+ : this._state.channels[channelIndex].last_message_preview,
288
+ last_message_sender: this._state.messages.length > 0
289
+ ? this._state.messages[this._state.messages.length - 1].sender_type
290
+ : this._state.channels[channelIndex].last_message_sender,
291
+ unread_count: 0, // We just viewed it
292
+ };
293
+ }
294
+ }
295
+ this._state.channel = null;
296
+ this._state.messages = [];
297
+ this._state.currentView = "list";
298
+ this._state.isTyping = false;
299
+ this._state.typingSender = null;
300
+ this._updateUI();
301
+ }
302
+ // ============================================================================
159
303
  // Public API - Messaging
160
304
  // ============================================================================
161
305
  async sendMessage(content) {
162
306
  var _a, _b, _c, _d;
163
307
  if (!content.trim())
164
308
  return;
165
- // Ensure channel exists
166
- if (!this._state.channel) {
167
- await this._initializeChannel();
309
+ // Ensure we're in conversation view with a channel
310
+ if (!this._state.channel || this._state.currentView !== "conversation") {
311
+ console.error(`${LOGGER_PREFIX} Cannot send message: not in conversation view`);
312
+ return;
168
313
  }
169
314
  const channelId = (_a = this._state.channel) === null || _a === void 0 ? void 0 : _a.id;
170
315
  if (!channelId) {
@@ -176,7 +321,7 @@ class LazyLoadedChat {
176
321
  id: `temp-${Date.now()}`,
177
322
  channel_id: channelId,
178
323
  sender_type: "user",
179
- sender_id: this._instance.getDistinctId() || null,
324
+ sender_id: this._distinctId || null,
180
325
  sender_name: null,
181
326
  sender_avatar_url: null,
182
327
  content,
@@ -188,11 +333,11 @@ class LazyLoadedChat {
188
333
  this._updateUI();
189
334
  try {
190
335
  // Send to API
191
- const response = await this._apiRequest(`${API_WIDGET_MESSAGES}`, {
336
+ const response = await this._apiRequest(API.messages, {
192
337
  method: "POST",
193
338
  body: JSON.stringify({
194
339
  channel_id: channelId,
195
- distinct_id: this._instance.getDistinctId(),
340
+ distinct_id: this._distinctId,
196
341
  content,
197
342
  }),
198
343
  });
@@ -201,9 +346,6 @@ class LazyLoadedChat {
201
346
  if (index !== -1 && (response === null || response === void 0 ? void 0 : response.message)) {
202
347
  this._state.messages[index] = response.message;
203
348
  }
204
- // Note: AI response will come through Ably channel if AI mode is enabled
205
- // No need to handle it here since server publishes to Ably
206
- // Track event
207
349
  this._trackEvent(types_1.CHAT_EVENTS.MESSAGE_SENT, {
208
350
  $channel_id: channelId,
209
351
  $message_id: (_b = response === null || response === void 0 ? void 0 : response.message) === null || _b === void 0 ? void 0 : _b.id,
@@ -224,51 +366,36 @@ class LazyLoadedChat {
224
366
  markAsRead() {
225
367
  this._autoMarkAsRead();
226
368
  }
227
- /**
228
- * Automatically mark unread agent/AI messages as read
229
- * Called when widget opens or new messages arrive while open
230
- */
231
369
  _autoMarkAsRead() {
232
- if (!this._state.channel)
370
+ if (!this._state.channel || this._isMarkingRead)
233
371
  return;
234
- if (this._isMarkingRead)
235
- return; // Already in progress
236
- // Get the latest message timestamp
237
372
  const latestMessage = this._state.messages[this._state.messages.length - 1];
238
373
  if (!latestMessage)
239
374
  return;
240
- // Check if there are unread agent messages
241
- const hasUnreadAgentMessages = this._state.messages.some((m) => (m.sender_type === "agent" || m.sender_type === "ai") &&
242
- !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));
243
376
  if (!hasUnreadAgentMessages)
244
377
  return;
245
378
  this._state.unreadCount = 0;
246
379
  this._isMarkingRead = true;
247
380
  this._updateUI();
248
- // API call to update read cursor with latest message timestamp
249
- this._apiRequest(API_WIDGET_READ, {
381
+ this._apiRequest(API.read, {
250
382
  method: "POST",
251
383
  body: JSON.stringify({
252
384
  channel_id: this._state.channel.id,
253
- distinct_id: this._instance.getDistinctId(),
385
+ distinct_id: this._distinctId,
254
386
  read_at: latestMessage.created_at,
255
387
  }),
256
388
  })
257
389
  .then(() => {
258
- // Update initial read position after successful mark
259
390
  this._initialUserReadAt = latestMessage.created_at;
260
391
  this._isMarkingRead = false;
261
392
  this._updateUI();
262
393
  })
263
- .catch((err) => {
264
- console.error(`${LOGGER_PREFIX} Failed to mark as read:`, err);
394
+ .catch(() => {
265
395
  this._isMarkingRead = false;
266
396
  this._updateUI();
267
397
  });
268
398
  }
269
- /**
270
- * Check if a message has been read by the user (using initial cursor)
271
- */
272
399
  _isMessageReadByUser(messageCreatedAt) {
273
400
  if (!this._initialUserReadAt)
274
401
  return false;
@@ -305,69 +432,16 @@ class LazyLoadedChat {
305
432
  // Public API - Lifecycle
306
433
  // ============================================================================
307
434
  destroy() {
308
- // Disconnect Ably
435
+ var _a;
309
436
  this._disconnectRealtime();
310
- // Clear timers
311
- if (this._typingTimeout)
312
- clearTimeout(this._typingTimeout);
313
437
  if (this._typingDebounce)
314
438
  clearTimeout(this._typingDebounce);
315
- // Remove DOM elements
316
- if (this._container && this._container.parentNode) {
439
+ if ((_a = this._container) === null || _a === void 0 ? void 0 : _a.parentNode) {
317
440
  this._container.parentNode.removeChild(this._container);
318
441
  }
319
- // Clear callbacks
320
442
  this._messageCallbacks = [];
321
443
  this._typingCallbacks = [];
322
444
  this._connectionCallbacks = [];
323
- console.info(`${LOGGER_PREFIX} destroyed`);
324
- }
325
- // ============================================================================
326
- // Private - Channel Management
327
- // ============================================================================
328
- async _initializeChannel() {
329
- var _a;
330
- this._state.isLoading = true;
331
- this._updateUI();
332
- try {
333
- const response = await this._apiRequest(`${API_WIDGET}`, {
334
- method: "POST",
335
- body: JSON.stringify({
336
- distinct_id: this._instance.getDistinctId(),
337
- 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,
338
- page_title: globals_1.document === null || globals_1.document === void 0 ? void 0 : globals_1.document.title,
339
- }),
340
- });
341
- if (response) {
342
- this._state.channel = response.channel;
343
- this._state.messages = response.messages || [];
344
- // Initialize read cursors from channel
345
- this._state.agentLastReadAt =
346
- response.channel.agent_last_read_at || null;
347
- this._initialUserReadAt = response.channel.user_last_read_at || null;
348
- // Track channel started (only if new)
349
- if (response.messages.length === 0 || response.messages.length === 1) {
350
- this._trackEvent(types_1.CHAT_EVENTS.STARTED, {
351
- $channel_id: response.channel.id,
352
- $initiated_by: "user",
353
- $ai_mode: response.channel.ai_mode,
354
- });
355
- }
356
- // Connect to Ably now that we have channel ID
357
- this._connectRealtime();
358
- // Auto-mark unread messages as read if widget is open
359
- if (this._state.isOpen) {
360
- this._autoMarkAsRead();
361
- }
362
- }
363
- }
364
- catch (error) {
365
- console.error(`${LOGGER_PREFIX} Failed to initialize channel:`, error);
366
- }
367
- finally {
368
- this._state.isLoading = false;
369
- this._updateUI();
370
- }
371
445
  }
372
446
  // ============================================================================
373
447
  // Private - Ably Realtime Connection
@@ -380,10 +454,10 @@ class LazyLoadedChat {
380
454
  this._notifyConnectionChange(false);
381
455
  try {
382
456
  // Get Ably token from server
383
- const tokenResponse = await this._apiRequest(API_ABLY_TOKEN, {
457
+ const tokenResponse = await this._apiRequest(API.ablyToken, {
384
458
  method: "POST",
385
459
  body: JSON.stringify({
386
- distinct_id: this._instance.getDistinctId(),
460
+ distinct_id: this._distinctId,
387
461
  channel_id: this._state.channel.id,
388
462
  }),
389
463
  });
@@ -392,15 +466,14 @@ class LazyLoadedChat {
392
466
  this._connectionState = "error";
393
467
  return;
394
468
  }
395
- // Create Ably client with token auth
396
469
  this._ably = new ably_1.default.Realtime({
397
470
  authCallback: async (_, callback) => {
398
471
  var _a;
399
472
  try {
400
- const refreshResponse = await this._apiRequest(API_ABLY_TOKEN, {
473
+ const refreshResponse = await this._apiRequest(API.ablyToken, {
401
474
  method: "POST",
402
475
  body: JSON.stringify({
403
- distinct_id: this._instance.getDistinctId(),
476
+ distinct_id: this._distinctId,
404
477
  channel_id: (_a = this._state.channel) === null || _a === void 0 ? void 0 : _a.id,
405
478
  }),
406
479
  });
@@ -417,37 +490,25 @@ class LazyLoadedChat {
417
490
  },
418
491
  authMethod: "POST",
419
492
  });
420
- // Authenticate with initial token
421
493
  await this._ably.auth.authorize(tokenResponse.tokenRequest);
422
- // Get project ID from instance config
423
494
  const config = this._instance.getConfig();
424
495
  const projectId = config.projectId || this._extractProjectId(config.token || "");
425
- // Subscribe to chat channel (Ably channel)
426
496
  const ablyChannelName = `chat:${projectId}:${this._state.channel.id}`;
427
497
  this._ablyChannel = this._ably.channels.get(ablyChannelName);
428
- // Listen for new messages
429
498
  this._ablyChannel.subscribe("message", (msg) => {
430
- const message = msg.data;
431
- this._handleNewMessage(message);
499
+ this._handleNewMessage(msg.data);
432
500
  });
433
- // Subscribe to typing channel
434
- const typingChannelName = `${ablyChannelName}:typing`;
435
- this._typingChannel = this._ably.channels.get(typingChannelName);
436
- this._typingChannel.subscribe("typing", (msg) => {
437
- const event = msg.data;
438
- this._handleTypingEvent(event);
439
- });
440
- // Subscribe to read cursor events
441
501
  this._ablyChannel.subscribe("read", (msg) => {
442
- const event = msg.data;
443
- 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);
444
507
  });
445
- // Handle connection state changes
446
508
  this._ably.connection.on("connected", () => {
447
509
  this._connectionState = "connected";
448
510
  this._state.isConnected = true;
449
511
  this._notifyConnectionChange(true);
450
- console.info(`${LOGGER_PREFIX} Connected to Ably`);
451
512
  });
452
513
  this._ably.connection.on("disconnected", () => {
453
514
  this._connectionState = "disconnected";
@@ -459,7 +520,6 @@ class LazyLoadedChat {
459
520
  this._state.isConnected = false;
460
521
  this._notifyConnectionChange(false);
461
522
  });
462
- // Initial connection
463
523
  this._ably.connect();
464
524
  }
465
525
  catch (error) {
@@ -489,36 +549,25 @@ class LazyLoadedChat {
489
549
  return parts[0] || "";
490
550
  }
491
551
  _handleNewMessage(message) {
492
- // Avoid duplicates by ID
493
552
  if (this._state.messages.some((m) => m.id === message.id))
494
553
  return;
495
- // Skip user's own messages - we already have them from optimistic updates
496
- // The sender_id for user messages is the distinct_id
497
- if (message.sender_type === "user" &&
498
- message.sender_id === this._instance.getDistinctId()) {
499
- // But DO replace temp message with real one if present
500
- const tempIndex = this._state.messages.findIndex((m) => m.id.startsWith("temp-") &&
501
- m.content === message.content &&
502
- 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");
503
557
  if (tempIndex !== -1) {
504
558
  this._state.messages[tempIndex] = message;
505
559
  this._updateUI();
506
560
  }
507
561
  return;
508
562
  }
509
- // Add to messages
510
563
  this._state.messages.push(message);
511
- // Update unread count if from agent/AI
512
564
  if (message.sender_type !== "user") {
513
565
  if (!this._state.isOpen) {
514
566
  this._state.unreadCount++;
515
567
  }
516
568
  else {
517
- // Widget is open, auto-mark as read after a short delay
518
- // to ensure UI updates first
519
569
  setTimeout(() => this._autoMarkAsRead(), 100);
520
570
  }
521
- // Track received event
522
571
  this._trackEvent(types_1.CHAT_EVENTS.MESSAGE_RECEIVED, {
523
572
  $channel_id: message.channel_id,
524
573
  $message_id: message.id,
@@ -526,32 +575,24 @@ class LazyLoadedChat {
526
575
  $sender_type: message.sender_type,
527
576
  });
528
577
  }
529
- // Clear typing indicator when message arrives
530
578
  this._state.isTyping = false;
531
579
  this._state.typingSender = null;
532
- // Notify callbacks
533
580
  this._messageCallbacks.forEach((cb) => cb(message));
534
581
  this._updateUI();
535
582
  }
536
583
  _handleTypingEvent(event) {
537
- // Only show typing for non-user senders
538
584
  if (event.sender_type === "user")
539
585
  return;
540
- const senderName = event.sender_name ||
541
- (event.sender_type === "ai" ? "AI Assistant" : "Agent");
586
+ const senderName = event.sender_name || (event.sender_type === "ai" ? "AI Assistant" : "Agent");
542
587
  this._state.isTyping = event.is_typing;
543
588
  this._state.typingSender = event.is_typing ? senderName : null;
544
- // Notify callbacks
545
589
  this._typingCallbacks.forEach((cb) => cb(event.is_typing, senderName));
546
590
  this._updateUI();
547
591
  }
548
592
  _handleReadCursorEvent(event) {
549
- // Only handle agent read events (user's own reads are local)
550
593
  if (event.reader_type !== "agent")
551
594
  return;
552
- // Update the agent read cursor
553
595
  this._state.agentLastReadAt = event.read_at;
554
- // Update UI to show read status on user messages
555
596
  this._updateUI();
556
597
  }
557
598
  _notifyConnectionChange(connected) {
@@ -563,62 +604,35 @@ class LazyLoadedChat {
563
604
  _createUI() {
564
605
  if (!globals_1.document)
565
606
  return;
566
- // Create container
567
607
  this._container = globals_1.document.createElement("div");
568
608
  this._container.id = "vtilt-chat-container";
569
609
  this._container.setAttribute("style", this._getContainerStyles());
570
- // Create bubble (launcher button)
571
610
  this._bubble = globals_1.document.createElement("div");
572
611
  this._bubble.id = "vtilt-chat-bubble";
573
612
  this._bubble.innerHTML = this._getBubbleHTML();
574
613
  this._bubble.setAttribute("style", this._getBubbleStyles());
575
614
  this._container.appendChild(this._bubble);
576
- // Create widget (chat window)
577
615
  this._widget = globals_1.document.createElement("div");
578
616
  this._widget.id = "vtilt-chat-widget";
579
617
  this._widget.innerHTML = this._getWidgetHTML();
580
618
  this._widget.setAttribute("style", this._getWidgetStyles());
581
619
  this._container.appendChild(this._widget);
582
- // Add to DOM
583
620
  globals_1.document.body.appendChild(this._container);
584
621
  }
585
622
  _attachEventListeners() {
586
- var _a, _b, _c, _d;
587
- // Bubble click
623
+ var _a, _b, _c;
588
624
  (_a = this._bubble) === null || _a === void 0 ? void 0 : _a.addEventListener("click", () => this.toggle());
589
- // Close button
590
- const closeBtn = (_b = this._widget) === null || _b === void 0 ? void 0 : _b.querySelector(".vtilt-chat-close");
591
- closeBtn === null || closeBtn === void 0 ? void 0 : closeBtn.addEventListener("click", () => this.close());
592
- // Send button
593
- const sendBtn = (_c = this._widget) === null || _c === void 0 ? void 0 : _c.querySelector(".vtilt-chat-send");
594
- sendBtn === null || sendBtn === void 0 ? void 0 : sendBtn.addEventListener("click", () => this._handleSend());
595
- // Input enter key and typing indicator
596
- const input = (_d = this._widget) === null || _d === void 0 ? void 0 : _d.querySelector(".vtilt-chat-input");
597
- input === null || input === void 0 ? void 0 : input.addEventListener("keypress", (e) => {
598
- if (e.key === "Enter" && !e.shiftKey) {
599
- e.preventDefault();
600
- this._handleSend();
601
- }
602
- });
603
- // Send typing indicator on input
604
- input === null || input === void 0 ? void 0 : input.addEventListener("input", () => {
605
- this._handleUserTyping();
606
- });
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());
607
626
  }
608
627
  _handleUserTyping() {
609
- // Don't send typing if not connected to Ably
610
628
  if (!this._typingChannel)
611
629
  return;
612
- // Send typing started if not already typing
613
630
  if (!this._isUserTyping) {
614
631
  this._isUserTyping = true;
615
632
  this._sendTypingIndicator(true);
616
633
  }
617
- // Clear existing debounce timer
618
- if (this._typingDebounce) {
634
+ if (this._typingDebounce)
619
635
  clearTimeout(this._typingDebounce);
620
- }
621
- // Set timer to send typing stopped after 2 seconds of no input
622
636
  this._typingDebounce = setTimeout(() => {
623
637
  this._isUserTyping = false;
624
638
  this._sendTypingIndicator(false);
@@ -630,13 +644,13 @@ class LazyLoadedChat {
630
644
  try {
631
645
  this._typingChannel.publish("typing", {
632
646
  sender_type: "user",
633
- sender_id: this._instance.getDistinctId(),
647
+ sender_id: this._distinctId,
634
648
  sender_name: null,
635
649
  is_typing: isTyping,
636
650
  });
637
651
  }
638
- catch (err) {
639
- console.error(`${LOGGER_PREFIX} Failed to send typing indicator:`, err);
652
+ catch (_a) {
653
+ // Silently fail
640
654
  }
641
655
  }
642
656
  _handleSend() {
@@ -646,7 +660,6 @@ class LazyLoadedChat {
646
660
  return;
647
661
  const content = input.value.trim();
648
662
  if (content) {
649
- // Stop typing indicator
650
663
  if (this._isUserTyping) {
651
664
  this._isUserTyping = false;
652
665
  this._sendTypingIndicator(false);
@@ -673,8 +686,21 @@ class LazyLoadedChat {
673
686
  this._state.unreadCount > 0 ? "flex" : "none";
674
687
  badge.textContent = String(this._state.unreadCount);
675
688
  }
676
- // Update messages
677
- this._renderMessages();
689
+ // Update content based on current view
690
+ const contentContainer = this._widget.querySelector(".vtilt-chat-content");
691
+ if (contentContainer) {
692
+ if (this._state.currentView === "list") {
693
+ contentContainer.innerHTML = this._getChannelListHTML();
694
+ this._attachChannelListListeners();
695
+ }
696
+ else {
697
+ contentContainer.innerHTML = this._getConversationHTML();
698
+ this._attachConversationListeners();
699
+ this._renderMessages();
700
+ }
701
+ }
702
+ // Update header based on view
703
+ this._updateHeader();
678
704
  // Update loading state
679
705
  const loader = this._widget.querySelector(".vtilt-chat-loader");
680
706
  if (loader) {
@@ -682,104 +708,504 @@ class LazyLoadedChat {
682
708
  ? "flex"
683
709
  : "none";
684
710
  }
685
- // Update typing indicator
711
+ // Update typing indicator (only in conversation view)
686
712
  const typing = this._widget.querySelector(".vtilt-chat-typing");
687
713
  if (typing) {
688
- typing.style.display = this._state.isTyping
689
- ? "flex"
690
- : "none";
714
+ typing.style.display =
715
+ this._state.isTyping && this._state.currentView === "conversation"
716
+ ? "flex"
717
+ : "none";
691
718
  const typingText = typing.querySelector("span");
692
719
  if (typingText && this._state.typingSender) {
693
720
  typingText.textContent = `${this._state.typingSender} is typing...`;
694
721
  }
695
722
  }
696
723
  }
697
- _renderMessages() {
698
- var _a;
699
- const messagesContainer = (_a = this._widget) === null || _a === void 0 ? void 0 : _a.querySelector(".vtilt-chat-messages");
700
- if (!messagesContainer)
724
+ _updateHeader() {
725
+ var _a, _b;
726
+ const header = (_a = this._widget) === null || _a === void 0 ? void 0 : _a.querySelector(".vtilt-chat-header");
727
+ if (!header)
701
728
  return;
702
- const theme = this._config.theme || DEFAULT_THEME;
703
- // Find first unread agent message index
704
- const firstUnreadIndex = this._state.messages.findIndex((m) => (m.sender_type === "agent" || m.sender_type === "ai") &&
705
- !this._isMessageReadByUser(m.created_at));
706
- // Build HTML with unread divider
707
- const messagesHtml = this._state.messages
708
- .map((msg, index) => {
709
- let html = "";
710
- // Add unread divider before first unread message
711
- if (index === firstUnreadIndex && firstUnreadIndex > 0) {
712
- html += `
713
- <div style="display: flex; align-items: center; gap: 12px; margin: 8px 0;">
714
- <div style="flex: 1; height: 1px; background: ${theme.primaryColor}40;"></div>
715
- <span style="font-size: 11px; font-weight: 500; color: ${theme.primaryColor}; padding: 0 8px;">New messages</span>
716
- <div style="flex: 1; height: 1px; background: ${theme.primaryColor}40;"></div>
729
+ const greeting = this._config.greeting || "Messages";
730
+ const primary = this._theme.primaryColor;
731
+ if (this._state.currentView === "list") {
732
+ header.style.cssText = `
733
+ background: #ffffff;
734
+ border-bottom: 1px solid #E5E5E5;
735
+ padding: 18px 16px;
736
+ padding-top: max(18px, env(safe-area-inset-top, 18px));
737
+ display: flex;
738
+ align-items: center;
739
+ justify-content: space-between;
740
+ min-height: 60px;
741
+ box-sizing: border-box;
742
+ flex-shrink: 0;
743
+ `;
744
+ header.innerHTML = `
745
+ <div style="font-weight: 600; font-size: 17px; color: #000000;">${greeting}</div>
746
+ <button class="vtilt-chat-close" style="
747
+ background: transparent;
748
+ border: none;
749
+ color: #666666;
750
+ cursor: pointer;
751
+ padding: 6px;
752
+ margin: -6px;
753
+ border-radius: 4px;
754
+ display: flex;
755
+ align-items: center;
756
+ justify-content: center;
757
+ -webkit-tap-highlight-color: transparent;
758
+ ">
759
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
760
+ <path d="M18 6L6 18M6 6l12 12"></path>
761
+ </svg>
762
+ </button>
763
+ `;
764
+ }
765
+ else {
766
+ const isAiMode = (_b = this._state.channel) === null || _b === void 0 ? void 0 : _b.ai_mode;
767
+ header.style.cssText = `
768
+ background: #ffffff;
769
+ border-bottom: 1px solid #E5E5E5;
770
+ padding: 12px 16px;
771
+ padding-top: max(12px, env(safe-area-inset-top, 12px));
772
+ display: flex;
773
+ align-items: center;
774
+ gap: 12px;
775
+ min-height: 60px;
776
+ box-sizing: border-box;
777
+ flex-shrink: 0;
778
+ `;
779
+ header.innerHTML = `
780
+ <button class="vtilt-chat-back" style="
781
+ background: transparent;
782
+ border: none;
783
+ color: #666666;
784
+ cursor: pointer;
785
+ padding: 6px;
786
+ margin-left: -6px;
787
+ border-radius: 4px;
788
+ display: flex;
789
+ align-items: center;
790
+ justify-content: center;
791
+ -webkit-tap-highlight-color: transparent;
792
+ ">
793
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
794
+ <path d="M15 18l-6-6 6-6"></path>
795
+ </svg>
796
+ </button>
797
+ <div style="
798
+ width: 44px;
799
+ height: 44px;
800
+ border-radius: 50%;
801
+ background: ${isAiMode ? primary : "#DEDEDE"};
802
+ display: flex;
803
+ align-items: center;
804
+ justify-content: center;
805
+ flex-shrink: 0;
806
+ ">
807
+ ${isAiMode
808
+ ? `<svg width="22" height="22" viewBox="0 0 24 24" fill="white"><path d="M12 2a2 2 0 0 1 2 2c0 .74-.4 1.39-1 1.73V7h1a7 7 0 0 1 7 7h1a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-1v1a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-1H2a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h1a7 7 0 0 1 7-7h1V5.73c-.6-.34-1-.99-1-1.73a2 2 0 0 1 2-2M7.5 13A1.5 1.5 0 0 0 6 14.5A1.5 1.5 0 0 0 7.5 16A1.5 1.5 0 0 0 9 14.5A1.5 1.5 0 0 0 7.5 13m9 0a1.5 1.5 0 0 0-1.5 1.5a1.5 1.5 0 0 0 1.5 1.5a1.5 1.5 0 0 0 1.5-1.5a1.5 1.5 0 0 0-1.5-1.5"/></svg>`
809
+ : `<svg width="22" height="22" viewBox="0 0 24 24" fill="#666"><path d="M12 4a4 4 0 0 1 4 4a4 4 0 0 1-4 4a4 4 0 0 1-4-4a4 4 0 0 1 4-4m0 10c4.42 0 8 1.79 8 4v2H4v-2c0-2.21 3.58-4 8-4"/></svg>`}
810
+ </div>
811
+ <div style="flex: 1; min-width: 0;">
812
+ <div style="font-weight: 600; font-size: 16px; color: #000000;">${isAiMode ? "AI Assistant" : "Support"}</div>
813
+ <div style="font-size: 13px; color: #16A34A; display: flex; align-items: center; gap: 5px; margin-top: 1px;">
814
+ <span style="width: 7px; height: 7px; background: #16A34A; border-radius: 50%;"></span>
815
+ Online
816
+ </div>
817
+ </div>
818
+ <button class="vtilt-chat-close" style="
819
+ background: transparent;
820
+ border: none;
821
+ color: #666666;
822
+ cursor: pointer;
823
+ padding: 6px;
824
+ margin-right: -6px;
825
+ border-radius: 4px;
826
+ display: flex;
827
+ align-items: center;
828
+ justify-content: center;
829
+ -webkit-tap-highlight-color: transparent;
830
+ ">
831
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
832
+ <path d="M18 6L6 18M6 6l12 12"></path>
833
+ </svg>
834
+ </button>
835
+ `;
836
+ }
837
+ // Re-attach header event listeners
838
+ const closeBtn = header.querySelector(".vtilt-chat-close");
839
+ closeBtn === null || closeBtn === void 0 ? void 0 : closeBtn.addEventListener("click", () => this.close());
840
+ const backBtn = header.querySelector(".vtilt-chat-back");
841
+ backBtn === null || backBtn === void 0 ? void 0 : backBtn.addEventListener("click", () => this.goToChannelList());
842
+ }
843
+ _getChannelListHTML() {
844
+ const primary = this._theme.primaryColor;
845
+ if (this._state.channels.length === 0 && !this._state.isLoading) {
846
+ return `
847
+ <div style="
848
+ flex: 1;
849
+ display: flex;
850
+ flex-direction: column;
851
+ align-items: center;
852
+ justify-content: center;
853
+ padding: 48px 24px;
854
+ text-align: center;
855
+ ">
856
+ <div style="
857
+ width: 72px;
858
+ height: 72px;
859
+ margin-bottom: 24px;
860
+ background: ${primary};
861
+ border-radius: 50%;
862
+ display: flex;
863
+ align-items: center;
864
+ justify-content: center;
865
+ ">
866
+ <svg width="36" height="36" viewBox="0 0 24 24" fill="white">
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"/>
868
+ </svg>
869
+ </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>
872
+ <button class="vtilt-chat-new-channel" style="
873
+ background: ${primary};
874
+ color: white;
875
+ border: none;
876
+ border-radius: 100px;
877
+ padding: 14px 28px;
878
+ cursor: pointer;
879
+ font-weight: 500;
880
+ font-size: 15px;
881
+ -webkit-tap-highlight-color: transparent;
882
+ touch-action: manipulation;
883
+ display: flex;
884
+ align-items: center;
885
+ gap: 10px;
886
+ box-shadow: 0 2px 8px rgba(123, 104, 238, 0.3);
887
+ ">
888
+ Send us a message
889
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="white">
890
+ <path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
891
+ </svg>
892
+ </button>
893
+ </div>
894
+ `;
895
+ }
896
+ const channelsHtml = this._state.channels
897
+ .map((ch) => this._getChannelItemHTML(ch))
898
+ .join("");
899
+ return `
900
+ <div style="flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch;">
901
+ ${channelsHtml}
902
+ </div>
903
+ <div style="
904
+ padding: 16px;
905
+ padding-bottom: max(16px, env(safe-area-inset-bottom, 16px));
906
+ border-top: 1px solid #E5E5E5;
907
+ ">
908
+ <button class="vtilt-chat-new-channel" style="
909
+ width: 100%;
910
+ background: ${primary};
911
+ color: white;
912
+ border: none;
913
+ border-radius: 100px;
914
+ padding: 14px 24px;
915
+ cursor: pointer;
916
+ font-weight: 500;
917
+ font-size: 15px;
918
+ display: flex;
919
+ align-items: center;
920
+ justify-content: center;
921
+ gap: 10px;
922
+ -webkit-tap-highlight-color: transparent;
923
+ touch-action: manipulation;
924
+ box-shadow: 0 2px 8px rgba(123, 104, 238, 0.3);
925
+ ">
926
+ Send us a message
927
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="white">
928
+ <path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
929
+ </svg>
930
+ </button>
931
+ </div>
932
+ `;
933
+ }
934
+ _getChannelItemHTML(channel) {
935
+ const hasUnread = channel.unread_count > 0;
936
+ const timeStr = this._formatRelativeTime(channel.last_message_at || channel.created_at);
937
+ const preview = channel.last_message_preview || "No messages yet";
938
+ const primary = this._theme.primaryColor;
939
+ const senderPrefix = channel.last_message_sender === "user" ? "You: " : "";
940
+ return `
941
+ <div class="vtilt-channel-item" data-channel-id="${channel.id}" style="
942
+ padding: 14px 16px;
943
+ cursor: pointer;
944
+ -webkit-tap-highlight-color: transparent;
945
+ touch-action: manipulation;
946
+ display: flex;
947
+ align-items: center;
948
+ gap: 12px;
949
+ background: white;
950
+ border-bottom: 1px solid #EEEEEE;
951
+ ">
952
+ <div style="
953
+ width: 48px;
954
+ height: 48px;
955
+ border-radius: 50%;
956
+ background: ${channel.ai_mode ? primary : "#DEDEDE"};
957
+ display: flex;
958
+ align-items: center;
959
+ justify-content: center;
960
+ flex-shrink: 0;
961
+ ">
962
+ ${channel.ai_mode
963
+ ? `<svg width="24" height="24" viewBox="0 0 24 24" fill="white"><path d="M12 2a2 2 0 0 1 2 2c0 .74-.4 1.39-1 1.73V7h1a7 7 0 0 1 7 7h1a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-1v1a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-1H2a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h1a7 7 0 0 1 7-7h1V5.73c-.6-.34-1-.99-1-1.73a2 2 0 0 1 2-2M7.5 13A1.5 1.5 0 0 0 6 14.5A1.5 1.5 0 0 0 7.5 16A1.5 1.5 0 0 0 9 14.5A1.5 1.5 0 0 0 7.5 13m9 0a1.5 1.5 0 0 0-1.5 1.5a1.5 1.5 0 0 0 1.5 1.5a1.5 1.5 0 0 0 1.5-1.5a1.5 1.5 0 0 0-1.5-1.5"/></svg>`
964
+ : `<svg width="24" height="24" viewBox="0 0 24 24" fill="#666"><path d="M12 4a4 4 0 0 1 4 4a4 4 0 0 1-4 4a4 4 0 0 1-4-4a4 4 0 0 1 4-4m0 10c4.42 0 8 1.79 8 4v2H4v-2c0-2.21 3.58-4 8-4"/></svg>`}
965
+ </div>
966
+ <div style="flex: 1; min-width: 0;">
967
+ <div style="display: flex; justify-content: space-between; align-items: center; gap: 8px; margin-bottom: 4px;">
968
+ <div style="font-weight: ${hasUnread ? "600" : "500"}; font-size: 15px; color: #000000; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
969
+ ${channel.ai_mode ? "AI Assistant" : "Support"}
717
970
  </div>
718
- `;
971
+ <div style="font-size: 13px; color: #888888; white-space: nowrap; flex-shrink: 0;">${timeStr}</div>
972
+ </div>
973
+ <div style="display: flex; justify-content: space-between; align-items: center; gap: 8px;">
974
+ <div style="
975
+ font-size: 14px;
976
+ color: ${hasUnread ? "#333333" : "#888888"};
977
+ font-weight: 400;
978
+ white-space: nowrap;
979
+ overflow: hidden;
980
+ text-overflow: ellipsis;
981
+ flex: 1;
982
+ min-width: 0;
983
+ line-height: 1.4;
984
+ ">${senderPrefix}${this._escapeHTML(preview)}${channel.status === "closed" ? ' · Closed' : ""}</div>
985
+ ${hasUnread ? `<div style="
986
+ min-width: 10px;
987
+ width: 10px;
988
+ height: 10px;
989
+ background: ${primary};
990
+ border-radius: 50%;
991
+ flex-shrink: 0;
992
+ "></div>` : ""}
993
+ </div>
994
+ </div>
995
+ </div>
996
+ `;
997
+ }
998
+ _getConversationHTML() {
999
+ const primary = this._theme.primaryColor;
1000
+ return `
1001
+ <div class="vtilt-chat-messages" style="
1002
+ flex: 1;
1003
+ overflow-y: auto;
1004
+ -webkit-overflow-scrolling: touch;
1005
+ padding: 20px 16px;
1006
+ display: flex;
1007
+ flex-direction: column;
1008
+ gap: 12px;
1009
+ min-height: 0;
1010
+ background: #FAFAFA;
1011
+ "></div>
1012
+
1013
+ <div class="vtilt-chat-typing" style="
1014
+ display: none;
1015
+ padding: 0 16px 12px;
1016
+ background: #FAFAFA;
1017
+ align-items: center;
1018
+ ">
1019
+ <div style="
1020
+ display: inline-flex;
1021
+ align-items: center;
1022
+ gap: 4px;
1023
+ padding: 12px 16px;
1024
+ background: white;
1025
+ border-radius: 20px;
1026
+ ">
1027
+ <span style="width: 7px; height: 7px; background: #999; border-radius: 50%; animation: vtilt-typing 1.2s infinite 0s;"></span>
1028
+ <span style="width: 7px; height: 7px; background: #999; border-radius: 50%; animation: vtilt-typing 1.2s infinite 0.2s;"></span>
1029
+ <span style="width: 7px; height: 7px; background: #999; border-radius: 50%; animation: vtilt-typing 1.2s infinite 0.4s;"></span>
1030
+ </div>
1031
+ </div>
1032
+ <style>
1033
+ @keyframes vtilt-typing { 0%, 60%, 100% { opacity: 0.35; transform: translateY(0); } 30% { opacity: 1; transform: translateY(-2px); } }
1034
+ </style>
1035
+
1036
+ <div class="vtilt-chat-input-container" style="
1037
+ padding: 12px 16px;
1038
+ padding-bottom: max(12px, env(safe-area-inset-bottom, 12px));
1039
+ border-top: 1px solid #E5E5E5;
1040
+ display: flex;
1041
+ align-items: center;
1042
+ gap: 12px;
1043
+ flex-shrink: 0;
1044
+ background: #ffffff;
1045
+ ">
1046
+ <div style="flex: 1; min-width: 0;">
1047
+ <input
1048
+ type="text"
1049
+ class="vtilt-chat-input"
1050
+ placeholder="Message..."
1051
+ autocomplete="off"
1052
+ autocorrect="on"
1053
+ autocapitalize="sentences"
1054
+ style="
1055
+ width: 100%;
1056
+ box-sizing: border-box;
1057
+ border: 1px solid #DDDDDD;
1058
+ border-radius: 24px;
1059
+ padding: 12px 18px;
1060
+ font-size: 16px;
1061
+ line-height: 1.4;
1062
+ outline: none;
1063
+ background: #ffffff;
1064
+ -webkit-appearance: none;
1065
+ appearance: none;
1066
+ color: #000000;
1067
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
1068
+ "
1069
+ />
1070
+ </div>
1071
+ <button class="vtilt-chat-send" style="
1072
+ background: ${primary};
1073
+ color: white;
1074
+ border: none;
1075
+ border-radius: 50%;
1076
+ padding: 0;
1077
+ width: 44px;
1078
+ height: 44px;
1079
+ cursor: pointer;
1080
+ display: flex;
1081
+ align-items: center;
1082
+ justify-content: center;
1083
+ -webkit-tap-highlight-color: transparent;
1084
+ touch-action: manipulation;
1085
+ flex-shrink: 0;
1086
+ ">
1087
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="white">
1088
+ <path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
1089
+ </svg>
1090
+ </button>
1091
+ </div>
1092
+ `;
1093
+ }
1094
+ _attachChannelListListeners() {
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) => {
1098
+ item.addEventListener("click", () => {
1099
+ const channelId = item.getAttribute("data-channel-id");
1100
+ if (channelId)
1101
+ this.selectChannel(channelId);
1102
+ });
1103
+ });
1104
+ }
1105
+ _attachConversationListeners() {
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");
1109
+ input === null || input === void 0 ? void 0 : input.addEventListener("keypress", (e) => {
1110
+ if (e.key === "Enter" && !e.shiftKey) {
1111
+ e.preventDefault();
1112
+ this._handleSend();
719
1113
  }
720
- html += this._getMessageHTML(msg);
721
- return html;
722
- })
723
- .join("");
724
- messagesContainer.innerHTML = messagesHtml;
725
- // Scroll to bottom
726
- messagesContainer.scrollTop = messagesContainer.scrollHeight;
1114
+ });
1115
+ input === null || input === void 0 ? void 0 : input.addEventListener("input", () => this._handleUserTyping());
1116
+ }
1117
+ _formatRelativeTime(isoString) {
1118
+ const date = new Date(isoString);
1119
+ const now = new Date();
1120
+ const diffMs = now.getTime() - date.getTime();
1121
+ const diffMins = Math.floor(diffMs / 60000);
1122
+ const diffHours = Math.floor(diffMs / 3600000);
1123
+ const diffDays = Math.floor(diffMs / 86400000);
1124
+ if (diffMins < 1)
1125
+ return "Just now";
1126
+ if (diffMins < 60)
1127
+ return `${diffMins}m ago`;
1128
+ if (diffHours < 24)
1129
+ return `${diffHours}h ago`;
1130
+ if (diffDays < 7)
1131
+ return `${diffDays}d ago`;
1132
+ return date.toLocaleDateString();
1133
+ }
1134
+ _renderMessages() {
1135
+ var _a;
1136
+ const container = (_a = this._widget) === null || _a === void 0 ? void 0 : _a.querySelector(".vtilt-chat-messages");
1137
+ if (!container)
1138
+ return;
1139
+ const primary = this._theme.primaryColor;
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;
727
1149
  }
728
1150
  // ============================================================================
729
1151
  // Private - Styles & HTML
730
1152
  // ============================================================================
731
1153
  _getContainerStyles() {
732
- var _a;
733
- const position = this._config.position || DEFAULT_POSITION;
734
- const isRight = position === "bottom-right";
1154
+ const isRight = (this._config.position || DEFAULT_POSITION) === "bottom-right";
735
1155
  return `
736
1156
  position: fixed;
737
1157
  bottom: 20px;
738
1158
  ${isRight ? "right: 20px;" : "left: 20px;"}
739
1159
  z-index: 999999;
740
- font-family: ${((_a = this._config.theme) === null || _a === void 0 ? void 0 : _a.fontFamily) || DEFAULT_THEME.fontFamily};
1160
+ font-family: ${this._theme.fontFamily};
1161
+ -webkit-font-smoothing: antialiased;
1162
+ -moz-osx-font-smoothing: grayscale;
741
1163
  `;
742
1164
  }
743
1165
  _getBubbleStyles() {
744
- const theme = this._config.theme || DEFAULT_THEME;
1166
+ const primary = this._theme.primaryColor;
745
1167
  return `
746
1168
  width: 60px;
747
1169
  height: 60px;
748
1170
  border-radius: 50%;
749
- background: ${theme.primaryColor};
1171
+ background: ${primary};
750
1172
  cursor: pointer;
751
1173
  display: flex;
752
1174
  align-items: center;
753
1175
  justify-content: center;
754
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
755
- transition: transform 0.2s, box-shadow 0.2s;
1176
+ box-shadow: 0 4px 16px rgba(123, 104, 238, 0.4);
1177
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
756
1178
  position: relative;
1179
+ -webkit-tap-highlight-color: transparent;
1180
+ touch-action: manipulation;
757
1181
  `;
758
1182
  }
759
1183
  _getBubbleHTML() {
760
1184
  return `
761
- <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
762
- <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
1185
+ <svg width="28" height="28" viewBox="0 0 28 28" fill="white">
1186
+ <path d="M14 3C7.925 3 3 7.262 3 12.5c0 2.56 1.166 4.884 3.063 6.606L4.5 24l5.25-2.625C11.1 21.79 12.52 22 14 22c6.075 0 11-4.262 11-9.5S20.075 3 14 3z"/>
763
1187
  </svg>
764
1188
  <div class="vtilt-chat-badge" style="
765
1189
  display: none;
766
1190
  position: absolute;
767
- top: -5px;
768
- right: -5px;
769
- background: #ef4444;
1191
+ top: -4px;
1192
+ right: -4px;
1193
+ background: #E53935;
770
1194
  color: white;
771
- font-size: 12px;
772
- font-weight: 600;
1195
+ font-size: 11px;
1196
+ font-weight: 700;
773
1197
  min-width: 20px;
774
1198
  height: 20px;
775
1199
  border-radius: 10px;
776
1200
  align-items: center;
777
1201
  justify-content: center;
1202
+ padding: 0 6px;
1203
+ box-sizing: border-box;
1204
+ border: 2px solid white;
778
1205
  ">0</div>
779
1206
  `;
780
1207
  }
781
1208
  _getWidgetStyles() {
782
- const theme = this._config.theme || DEFAULT_THEME;
783
1209
  return `
784
1210
  display: none;
785
1211
  flex-direction: column;
@@ -787,170 +1213,208 @@ class LazyLoadedChat {
787
1213
  bottom: 80px;
788
1214
  right: 0;
789
1215
  width: 380px;
790
- height: 520px;
791
- background: white;
792
- border-radius: ${theme.borderRadius};
793
- box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
1216
+ max-width: calc(100vw - 40px);
1217
+ height: 600px;
1218
+ max-height: calc(100vh - 120px);
1219
+ max-height: calc(100dvh - 120px);
1220
+ background: #ffffff;
1221
+ border-radius: 16px;
1222
+ box-shadow: 0 5px 40px rgba(0, 0, 0, 0.16);
794
1223
  overflow: hidden;
795
1224
  `;
796
1225
  }
797
1226
  _getWidgetHTML() {
798
- const theme = this._config.theme || DEFAULT_THEME;
799
- const greeting = this._config.greeting || "How can we help you?";
1227
+ const greeting = this._config.greeting || "Messages";
1228
+ const primary = this._theme.primaryColor;
800
1229
  return `
801
1230
  <div class="vtilt-chat-header" style="
802
- background: ${theme.headerBgColor};
803
- color: white;
804
- padding: 16px;
1231
+ background: #ffffff;
1232
+ border-bottom: 1px solid #E5E5E5;
1233
+ padding: 18px 16px;
1234
+ padding-top: max(18px, env(safe-area-inset-top, 18px));
805
1235
  display: flex;
806
1236
  align-items: center;
807
1237
  justify-content: space-between;
1238
+ min-height: 60px;
1239
+ box-sizing: border-box;
1240
+ flex-shrink: 0;
808
1241
  ">
809
- <div style="font-weight: 600; font-size: 16px;">${greeting}</div>
1242
+ <div style="font-weight: 600; font-size: 17px; color: #000000;">${greeting}</div>
810
1243
  <button class="vtilt-chat-close" style="
811
- background: none;
1244
+ background: transparent;
812
1245
  border: none;
813
- color: white;
1246
+ color: #666666;
814
1247
  cursor: pointer;
815
- padding: 4px;
1248
+ padding: 6px;
1249
+ margin: -6px;
1250
+ border-radius: 4px;
1251
+ display: flex;
1252
+ align-items: center;
1253
+ justify-content: center;
1254
+ -webkit-tap-highlight-color: transparent;
1255
+ touch-action: manipulation;
816
1256
  ">
817
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1257
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
818
1258
  <path d="M18 6L6 18M6 6l12 12"></path>
819
1259
  </svg>
820
1260
  </button>
821
1261
  </div>
822
1262
 
823
- <div class="vtilt-chat-messages" style="
1263
+ <div class="vtilt-chat-content" style="
824
1264
  flex: 1;
825
- overflow-y: auto;
826
- padding: 16px;
827
1265
  display: flex;
828
1266
  flex-direction: column;
829
- gap: 12px;
830
- "></div>
1267
+ overflow: hidden;
1268
+ min-height: 0;
1269
+ background: #ffffff;
1270
+ ">
1271
+ </div>
831
1272
 
832
1273
  <div class="vtilt-chat-loader" style="
833
1274
  display: none;
1275
+ position: absolute;
1276
+ top: 0;
1277
+ left: 0;
1278
+ right: 0;
1279
+ bottom: 0;
1280
+ background: rgba(255, 255, 255, 0.95);
834
1281
  align-items: center;
835
1282
  justify-content: center;
836
- padding: 20px;
1283
+ z-index: 10;
837
1284
  ">
838
1285
  <div style="
839
- width: 24px;
840
- height: 24px;
841
- border: 2px solid #e5e7eb;
842
- border-top-color: ${theme.primaryColor};
1286
+ width: 32px;
1287
+ height: 32px;
1288
+ border: 3px solid #E5E5E5;
1289
+ border-top-color: ${primary};
843
1290
  border-radius: 50%;
844
1291
  animation: vtilt-spin 0.8s linear infinite;
845
1292
  "></div>
846
1293
  </div>
847
1294
 
848
- <div class="vtilt-chat-typing" style="
849
- display: none;
850
- padding: 8px 16px;
851
- color: #6b7280;
852
- font-size: 14px;
853
- ">
854
- <span style="animation: vtilt-pulse 1.5s infinite;">Agent is typing...</span>
855
- </div>
856
-
857
- <div class="vtilt-chat-input-container" style="
858
- padding: 16px;
859
- border-top: 1px solid #e5e7eb;
860
- display: flex;
861
- gap: 8px;
862
- ">
863
- <input
864
- type="text"
865
- class="vtilt-chat-input"
866
- placeholder="Type a message..."
867
- style="
868
- flex: 1;
869
- border: 1px solid #e5e7eb;
870
- border-radius: 8px;
871
- padding: 10px 14px;
872
- font-size: 14px;
873
- outline: none;
874
- transition: border-color 0.2s;
875
- "
876
- />
877
- <button class="vtilt-chat-send" style="
878
- background: ${theme.primaryColor};
879
- color: white;
880
- border: none;
881
- border-radius: 8px;
882
- padding: 10px 16px;
883
- cursor: pointer;
884
- font-weight: 500;
885
- transition: opacity 0.2s;
886
- ">Send</button>
887
- </div>
888
-
889
1295
  <style>
890
- @keyframes vtilt-spin {
891
- to { transform: rotate(360deg); }
892
- }
893
- @keyframes vtilt-pulse {
894
- 0%, 100% { opacity: 1; }
895
- 50% { opacity: 0.5; }
896
- }
897
- #vtilt-chat-bubble:hover {
898
- transform: scale(1.05);
899
- box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
900
- }
901
- .vtilt-chat-input:focus {
902
- border-color: ${theme.primaryColor} !important;
1296
+ @keyframes vtilt-spin { to { transform: rotate(360deg); } }
1297
+ @keyframes vtilt-fade { from { opacity: 0; } to { opacity: 1; } }
1298
+
1299
+ #vtilt-chat-bubble { transition: transform 0.15s ease, box-shadow 0.15s ease; }
1300
+ #vtilt-chat-bubble:hover { transform: scale(1.05); box-shadow: 0 6px 20px rgba(123, 104, 238, 0.45); }
1301
+ #vtilt-chat-bubble:active { transform: scale(0.95); }
1302
+
1303
+ #vtilt-chat-widget { transition: opacity 0.2s ease; }
1304
+ #vtilt-chat-widget.vtilt-opening { animation: vtilt-fade 0.2s ease forwards; }
1305
+ #vtilt-chat-widget.vtilt-closing { opacity: 0; }
1306
+
1307
+ .vtilt-chat-content { transition: opacity 0.15s ease; }
1308
+
1309
+ .vtilt-chat-close:hover { color: #000 !important; background: #F0F0F0 !important; }
1310
+ .vtilt-chat-back:hover { color: #000 !important; background: #F0F0F0 !important; }
1311
+
1312
+ .vtilt-chat-input { font-size: 16px !important; -webkit-text-size-adjust: 100%; transition: border-color 0.15s ease, box-shadow 0.15s ease; }
1313
+ .vtilt-chat-input:focus { border-color: ${primary} !important; box-shadow: 0 0 0 2px ${primary}20 !important; outline: none !important; }
1314
+ .vtilt-chat-input::placeholder { color: #999999; }
1315
+
1316
+ .vtilt-chat-send { transition: opacity 0.1s ease; }
1317
+ .vtilt-chat-send:hover { opacity: 0.85; }
1318
+ .vtilt-chat-send:active { opacity: 0.7; }
1319
+
1320
+ .vtilt-chat-new-channel { transition: opacity 0.1s ease; }
1321
+ .vtilt-chat-new-channel:hover { opacity: 0.9; }
1322
+ .vtilt-chat-new-channel:active { opacity: 0.8; }
1323
+
1324
+ .vtilt-channel-item { transition: background 0.1s ease; cursor: pointer; }
1325
+ .vtilt-channel-item:hover { background: #F5F5F5 !important; }
1326
+ .vtilt-channel-item:active { background: #EBEBEB !important; }
1327
+
1328
+ @media (max-width: 480px) {
1329
+ #vtilt-chat-container { bottom: 16px !important; right: 16px !important; }
1330
+ #vtilt-chat-bubble { width: 56px !important; height: 56px !important; }
1331
+ #vtilt-chat-bubble svg { width: 24px !important; height: 24px !important; }
1332
+ #vtilt-chat-widget {
1333
+ position: fixed !important;
1334
+ top: 0 !important; left: 0 !important; right: 0 !important; bottom: 0 !important;
1335
+ width: 100% !important; max-width: 100% !important;
1336
+ height: 100% !important; max-height: 100% !important;
1337
+ border-radius: 0 !important;
1338
+ z-index: 1000000 !important;
1339
+ }
903
1340
  }
904
- .vtilt-chat-send:hover {
905
- opacity: 0.9;
1341
+
1342
+ @media (prefers-reduced-motion: reduce) {
1343
+ * { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
906
1344
  }
907
1345
  </style>
908
1346
  `;
909
1347
  }
910
1348
  _getMessageHTML(message) {
911
- const theme = this._config.theme || DEFAULT_THEME;
912
1349
  const isUser = message.sender_type === "user";
913
- const isAgentOrAI = message.sender_type === "agent" || message.sender_type === "ai";
914
- // Check read status
1350
+ const isAi = message.sender_type === "ai";
915
1351
  const isReadByAgent = isUser && this._isMessageReadByAgent(message.created_at);
916
- const isUnread = isAgentOrAI && !this._isMessageReadByUser(message.created_at);
917
- const bubbleStyle = isUser
918
- ? `background: ${theme.userBubbleColor}; color: white; margin-left: auto;`
919
- : `background: ${theme.agentBubbleColor}; color: #1f2937; margin-right: auto;${isUnread && !this._isMarkingRead ? " box-shadow: 0 0 0 2px " + theme.primaryColor + "40;" : ""}`;
920
- const senderLabel = message.sender_type === "ai"
921
- ? "AI Assistant"
922
- : message.sender_type === "agent"
923
- ? message.sender_name || "Agent"
924
- : "";
925
- // Read receipt SVG icons
926
- const singleCheckSvg = `<svg width="14" height="14" viewBox="0 0 20 20" fill="currentColor" style="color: #9ca3af;"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>`;
927
- const doubleCheckSvg = `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="color: ${theme.primaryColor};"><path d="M18 7l-1.41-1.41-6.34 6.34 1.41 1.41L18 7zm4.24-1.41L11.66 16.17 7.48 12l-1.41 1.41L11.66 19l12-12-1.42-1.41zM.41 13.41L6 19l1.41-1.41L1.83 12 .41 13.41z"/></svg>`;
928
- // Unread badge for agent messages
929
- const unreadBadge = isUnread && !this._isMarkingRead
930
- ? `<span style="font-size: 10px; font-weight: 600; color: ${theme.primaryColor}; background: ${theme.primaryColor}20; padding: 2px 6px; border-radius: 9999px;">NEW</span>`
931
- : "";
1352
+ const primary = this._theme.primaryColor;
1353
+ if (isUser) {
1354
+ return `
1355
+ <div class="vtilt-msg" style="
1356
+ display: flex;
1357
+ flex-direction: column;
1358
+ align-items: flex-end;
1359
+ ">
1360
+ <div style="
1361
+ max-width: 80%;
1362
+ padding: 12px 16px;
1363
+ background: ${primary};
1364
+ color: white;
1365
+ border-radius: 20px 20px 4px 20px;
1366
+ font-size: 15px;
1367
+ line-height: 1.45;
1368
+ word-wrap: break-word;
1369
+ overflow-wrap: break-word;
1370
+ ">${this._escapeHTML(message.content)}</div>
1371
+ <div style="font-size: 12px; color: #888888; margin-top: 6px; display: flex; align-items: center; gap: 4px;">
1372
+ ${this._formatTime(message.created_at)}
1373
+ ${isReadByAgent ? `<svg width="14" height="14" viewBox="0 0 24 24" fill="${primary}"><path d="M18 7l-1.41-1.41-6.34 6.34 1.41 1.41L18 7zm4.24-1.41L11.66 16.17 7.48 12l-1.41 1.41L11.66 19l12-12-1.42-1.41zM.41 13.41L6 19l1.41-1.41L1.83 12 .41 13.41z"/></svg>` : ""}
1374
+ </div>
1375
+ </div>
1376
+ `;
1377
+ }
1378
+ const senderLabel = isAi ? "AI Assistant" : (message.sender_name || "Support");
932
1379
  return `
933
- <div style="display: flex; flex-direction: column; ${isUser ? "align-items: flex-end;" : "align-items: flex-start;"}${isUnread && !this._isMarkingRead ? " position: relative;" : ""}">
934
- ${!isUser && senderLabel ? `<div style="font-size: 12px; color: #6b7280; margin-bottom: 4px;">${senderLabel}</div>` : ""}
1380
+ <div class="vtilt-msg" style="
1381
+ display: flex;
1382
+ gap: 10px;
1383
+ align-items: flex-end;
1384
+ ">
935
1385
  <div style="
936
- max-width: 80%;
937
- padding: 10px 14px;
938
- border-radius: 12px;
939
- font-size: 14px;
940
- line-height: 1.4;
941
- transition: opacity 0.3s, box-shadow 0.3s;
942
- ${bubbleStyle}${this._isMarkingRead && isUnread ? " opacity: 0.7;" : ""}
943
- ">${this._escapeHTML(message.content)}</div>
944
- <div style="font-size: 11px; color: #9ca3af; margin-top: 4px; display: flex; align-items: center; gap: 4px;">
945
- ${this._formatTime(message.created_at)}
946
- ${isUser ? (isReadByAgent ? doubleCheckSvg : singleCheckSvg) : unreadBadge}
1386
+ width: 32px;
1387
+ height: 32px;
1388
+ border-radius: 50%;
1389
+ background: ${isAi ? primary : "#DEDEDE"};
1390
+ display: flex;
1391
+ align-items: center;
1392
+ justify-content: center;
1393
+ flex-shrink: 0;
1394
+ ">
1395
+ ${isAi
1396
+ ? `<svg width="16" height="16" viewBox="0 0 24 24" fill="white"><path d="M12 2a2 2 0 0 1 2 2c0 .74-.4 1.39-1 1.73V7h1a7 7 0 0 1 7 7h1a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-1v1a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-1H2a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h1a7 7 0 0 1 7-7h1V5.73c-.6-.34-1-.99-1-1.73a2 2 0 0 1 2-2M7.5 13A1.5 1.5 0 0 0 6 14.5A1.5 1.5 0 0 0 7.5 16A1.5 1.5 0 0 0 9 14.5A1.5 1.5 0 0 0 7.5 13m9 0a1.5 1.5 0 0 0-1.5 1.5a1.5 1.5 0 0 0 1.5 1.5a1.5 1.5 0 0 0 1.5-1.5a1.5 1.5 0 0 0-1.5-1.5"/></svg>`
1397
+ : `<svg width="16" height="16" viewBox="0 0 24 24" fill="#666"><path d="M12 4a4 4 0 0 1 4 4a4 4 0 0 1-4 4a4 4 0 0 1-4-4a4 4 0 0 1 4-4m0 10c4.42 0 8 1.79 8 4v2H4v-2c0-2.21 3.58-4 8-4"/></svg>`}
1398
+ </div>
1399
+ <div style="flex: 1; min-width: 0; display: flex; flex-direction: column; align-items: flex-start;">
1400
+ <div style="
1401
+ max-width: 85%;
1402
+ padding: 12px 16px;
1403
+ background: #ffffff;
1404
+ color: #000000;
1405
+ border-radius: 20px 20px 20px 4px;
1406
+ font-size: 15px;
1407
+ line-height: 1.45;
1408
+ word-wrap: break-word;
1409
+ overflow-wrap: break-word;
1410
+ ">${this._escapeHTML(message.content)}</div>
1411
+ <div style="font-size: 12px; color: #888888; margin-top: 6px; margin-left: 4px;">
1412
+ ${senderLabel} · ${this._formatTime(message.created_at)}
1413
+ </div>
947
1414
  </div>
948
1415
  </div>
949
1416
  `;
950
1417
  }
951
- /**
952
- * Check if a message has been read by the agent using cursor comparison
953
- */
954
1418
  _isMessageReadByAgent(messageCreatedAt) {
955
1419
  if (!this._state.agentLastReadAt)
956
1420
  return false;
@@ -963,7 +1427,9 @@ class LazyLoadedChat {
963
1427
  const config = this._instance.getConfig();
964
1428
  const apiHost = config.api_host || "";
965
1429
  const token = config.token || "";
966
- const url = `${apiHost}${endpoint}?token=${encodeURIComponent(token)}`;
1430
+ // Use & if endpoint already has query params, otherwise use ?
1431
+ const separator = endpoint.includes("?") ? "&" : "?";
1432
+ const url = `${apiHost}${endpoint}${separator}token=${encodeURIComponent(token)}`;
967
1433
  try {
968
1434
  const response = await fetch(url, {
969
1435
  ...options,