@v-tilt/browser 1.6.0 → 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/dist/array.full.js +1 -1
  2. package/dist/array.full.js.map +1 -1
  3. package/dist/array.js +1 -1
  4. package/dist/array.js.map +1 -1
  5. package/dist/array.no-external.js +1 -1
  6. package/dist/array.no-external.js.map +1 -1
  7. package/dist/chat.js +1 -1
  8. package/dist/chat.js.map +1 -1
  9. package/dist/constants.d.ts +1 -1
  10. package/dist/entrypoints/server.es.d.ts +12 -0
  11. package/dist/external-scripts-loader.js +1 -1
  12. package/dist/main.js +1 -1
  13. package/dist/main.js.map +1 -1
  14. package/dist/module.d.ts +6 -3
  15. package/dist/module.js +1 -1
  16. package/dist/module.js.map +1 -1
  17. package/dist/module.no-external.d.ts +6 -3
  18. package/dist/module.no-external.js +1 -1
  19. package/dist/module.no-external.js.map +1 -1
  20. package/dist/server.d.ts +105 -0
  21. package/dist/server.js +2 -0
  22. package/dist/server.js.map +1 -0
  23. package/dist/types.d.ts +3 -1
  24. package/dist/vtilt.d.ts +3 -2
  25. package/package.json +1 -2
  26. package/lib/config.d.ts +0 -17
  27. package/lib/config.js +0 -76
  28. package/lib/constants.d.ts +0 -178
  29. package/lib/constants.js +0 -656
  30. package/lib/entrypoints/all-external-dependencies.d.ts +0 -8
  31. package/lib/entrypoints/all-external-dependencies.js +0 -10
  32. package/lib/entrypoints/array.d.ts +0 -2
  33. package/lib/entrypoints/array.full.d.ts +0 -17
  34. package/lib/entrypoints/array.full.js +0 -19
  35. package/lib/entrypoints/array.js +0 -4
  36. package/lib/entrypoints/array.no-external.d.ts +0 -1
  37. package/lib/entrypoints/array.no-external.js +0 -4
  38. package/lib/entrypoints/chat.d.ts +0 -22
  39. package/lib/entrypoints/chat.js +0 -32
  40. package/lib/entrypoints/external-scripts-loader.d.ts +0 -24
  41. package/lib/entrypoints/external-scripts-loader.js +0 -104
  42. package/lib/entrypoints/main.cjs.d.ts +0 -4
  43. package/lib/entrypoints/main.cjs.js +0 -29
  44. package/lib/entrypoints/module.es.d.ts +0 -4
  45. package/lib/entrypoints/module.es.js +0 -23
  46. package/lib/entrypoints/module.no-external.es.d.ts +0 -4
  47. package/lib/entrypoints/module.no-external.es.js +0 -23
  48. package/lib/entrypoints/recorder.d.ts +0 -23
  49. package/lib/entrypoints/recorder.js +0 -42
  50. package/lib/entrypoints/web-vitals.d.ts +0 -14
  51. package/lib/entrypoints/web-vitals.js +0 -29
  52. package/lib/extensions/chat/chat-wrapper.d.ts +0 -196
  53. package/lib/extensions/chat/chat-wrapper.js +0 -545
  54. package/lib/extensions/chat/chat.d.ts +0 -99
  55. package/lib/extensions/chat/chat.js +0 -1891
  56. package/lib/extensions/chat/index.d.ts +0 -10
  57. package/lib/extensions/chat/index.js +0 -27
  58. package/lib/extensions/chat/types.d.ts +0 -159
  59. package/lib/extensions/chat/types.js +0 -22
  60. package/lib/extensions/history-autocapture.d.ts +0 -17
  61. package/lib/extensions/history-autocapture.js +0 -105
  62. package/lib/extensions/replay/index.d.ts +0 -13
  63. package/lib/extensions/replay/index.js +0 -31
  64. package/lib/extensions/replay/session-recording-utils.d.ts +0 -92
  65. package/lib/extensions/replay/session-recording-utils.js +0 -212
  66. package/lib/extensions/replay/session-recording-wrapper.d.ts +0 -61
  67. package/lib/extensions/replay/session-recording-wrapper.js +0 -149
  68. package/lib/extensions/replay/session-recording.d.ts +0 -95
  69. package/lib/extensions/replay/session-recording.js +0 -700
  70. package/lib/extensions/replay/types.d.ts +0 -211
  71. package/lib/extensions/replay/types.js +0 -8
  72. package/lib/geolocation.d.ts +0 -5
  73. package/lib/geolocation.js +0 -31
  74. package/lib/rate-limiter.d.ts +0 -52
  75. package/lib/rate-limiter.js +0 -80
  76. package/lib/request-queue.d.ts +0 -78
  77. package/lib/request-queue.js +0 -156
  78. package/lib/request.d.ts +0 -54
  79. package/lib/request.js +0 -265
  80. package/lib/retry-queue.d.ts +0 -64
  81. package/lib/retry-queue.js +0 -182
  82. package/lib/session.d.ts +0 -66
  83. package/lib/session.js +0 -191
  84. package/lib/storage.d.ts +0 -117
  85. package/lib/storage.js +0 -438
  86. package/lib/types.d.ts +0 -350
  87. package/lib/types.js +0 -24
  88. package/lib/user-manager.d.ts +0 -154
  89. package/lib/user-manager.js +0 -589
  90. package/lib/utils/event-utils.d.ts +0 -52
  91. package/lib/utils/event-utils.js +0 -305
  92. package/lib/utils/globals.d.ts +0 -235
  93. package/lib/utils/globals.js +0 -30
  94. package/lib/utils/index.d.ts +0 -46
  95. package/lib/utils/index.js +0 -134
  96. package/lib/utils/patch.d.ts +0 -6
  97. package/lib/utils/patch.js +0 -39
  98. package/lib/utils/request-utils.d.ts +0 -17
  99. package/lib/utils/request-utils.js +0 -80
  100. package/lib/utils/type-utils.d.ts +0 -4
  101. package/lib/utils/type-utils.js +0 -9
  102. package/lib/utils/user-agent-utils.d.ts +0 -18
  103. package/lib/utils/user-agent-utils.js +0 -411
  104. package/lib/vtilt.d.ts +0 -359
  105. package/lib/vtilt.js +0 -1188
  106. package/lib/web-vitals.d.ts +0 -95
  107. package/lib/web-vitals.js +0 -380
@@ -1,1891 +0,0 @@
1
- "use strict";
2
- /**
3
- * Chat Widget - Lazy loaded chat implementation using Ably for real-time messaging.
4
- */
5
- var __importDefault = (this && this.__importDefault) || function (mod) {
6
- return (mod && mod.__esModule) ? mod : { "default": mod };
7
- };
8
- Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.LazyLoadedChat = void 0;
10
- const globals_1 = require("../../utils/globals");
11
- const types_1 = require("./types");
12
- const ably_1 = __importDefault(require("ably"));
13
- const LOGGER_PREFIX = "[Chat]";
14
- // ============================================================================
15
- // Constants
16
- // ============================================================================
17
- const DEFAULT_POSITION = "bottom-right";
18
- const DEFAULT_THEME = {
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",
29
- };
30
- // ============================================================================
31
- // LazyLoadedChat Implementation
32
- // ============================================================================
33
- class LazyLoadedChat {
34
- constructor(instance, config = {}) {
35
- // DOM elements
36
- this._container = null;
37
- this._widget = null;
38
- this._bubble = null;
39
- // Ably Realtime
40
- this._ably = null;
41
- this._ablyChannel = null;
42
- this._typingChannel = null;
43
- this._connectionState = "disconnected";
44
- // Callbacks
45
- this._messageCallbacks = [];
46
- this._typingCallbacks = [];
47
- this._connectionCallbacks = [];
48
- // Timers
49
- this._typingDebounce = null;
50
- this._isUserTyping = false;
51
- // Read tracking - initial position when widget opens (to show unread indicators)
52
- this._initialUserReadAt = null;
53
- this._isMarkingRead = false;
54
- this._instance = instance;
55
- this._config = {
56
- enabled: true,
57
- position: DEFAULT_POSITION,
58
- aiMode: true,
59
- preload: false,
60
- ...config,
61
- theme: { ...DEFAULT_THEME, ...config.theme },
62
- };
63
- this._state = {
64
- isOpen: false,
65
- isVisible: true,
66
- isConnected: false,
67
- isLoading: false,
68
- unreadCount: 0,
69
- // Multi-channel support
70
- currentView: "list",
71
- channels: [],
72
- // Current channel (when in conversation view)
73
- channel: null,
74
- messages: [],
75
- isTyping: false,
76
- typingSender: null,
77
- typingSenderType: null,
78
- agentLastReadAt: null, // Read cursor from agent
79
- };
80
- // Initialize UI
81
- this._createUI();
82
- this._attachEventListeners();
83
- }
84
- // ============================================================================
85
- // Public API - State (LazyLoadedChatInterface)
86
- // ============================================================================
87
- get isOpen() {
88
- return this._state.isOpen;
89
- }
90
- get isConnected() {
91
- return this._state.isConnected;
92
- }
93
- get isLoading() {
94
- return this._state.isLoading;
95
- }
96
- get unreadCount() {
97
- return this._state.unreadCount;
98
- }
99
- get channel() {
100
- return this._state.channel;
101
- }
102
- get channels() {
103
- return this._state.channels;
104
- }
105
- get currentView() {
106
- return this._state.currentView;
107
- }
108
- // Theme getter to avoid repeated DEFAULT_THEME fallback
109
- get _theme() {
110
- return this._config.theme || DEFAULT_THEME;
111
- }
112
- // Distinct ID getter for convenience
113
- get _distinctId() {
114
- return this._instance.getDistinctId() || "";
115
- }
116
- // ============================================================================
117
- // Public API - Widget Control
118
- // ============================================================================
119
- open() {
120
- var _a;
121
- if (this._state.isOpen) {
122
- return;
123
- }
124
- this._state.isOpen = true;
125
- this._updateUI();
126
- // Add opening animation
127
- if (this._widget) {
128
- this._widget.classList.remove("vtilt-closing");
129
- this._widget.classList.add("vtilt-opening");
130
- }
131
- this._trackEvent(types_1.CHAT_EVENTS.WIDGET_OPENED, {
132
- $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,
133
- $trigger: "api",
134
- });
135
- // Fetch channels if not already loaded
136
- if (this._state.channels.length === 0) {
137
- this.getChannels();
138
- }
139
- }
140
- close() {
141
- if (!this._state.isOpen) {
142
- return;
143
- }
144
- const timeOpen = this._getTimeOpen();
145
- // Add closing animation
146
- if (this._widget) {
147
- this._widget.classList.remove("vtilt-opening");
148
- this._widget.classList.add("vtilt-closing");
149
- // Hide after animation completes
150
- setTimeout(() => {
151
- this._state.isOpen = false;
152
- this._updateUI();
153
- }, 200);
154
- }
155
- else {
156
- this._state.isOpen = false;
157
- this._updateUI();
158
- }
159
- this._trackEvent(types_1.CHAT_EVENTS.WIDGET_CLOSED, {
160
- $time_open_seconds: timeOpen,
161
- $messages_sent: this._state.messages.filter((m) => m.sender_type === "user").length,
162
- });
163
- this._disconnectRealtime();
164
- }
165
- toggle() {
166
- if (this._state.isOpen) {
167
- this.close();
168
- }
169
- else {
170
- this.open();
171
- }
172
- }
173
- show() {
174
- this._state.isVisible = true;
175
- this._updateUI();
176
- }
177
- hide() {
178
- this._state.isVisible = false;
179
- this._updateUI();
180
- }
181
- // ============================================================================
182
- // Public API - Channel Management (Multi-channel support)
183
- // ============================================================================
184
- async getChannels() {
185
- this._state.isLoading = true;
186
- this._updateUI();
187
- try {
188
- const response = await this._apiRequest(`${API.channels}?distinct_id=${encodeURIComponent(this._distinctId)}`, {
189
- method: "GET",
190
- });
191
- if (response) {
192
- this._state.channels = response.channels || [];
193
- // For channel list, we need to calculate unread count based on user_last_read_at
194
- // But since we don't have messages here, we'll use unread_count from server
195
- // (which should be calculated server-side based on user_last_read_at)
196
- // For now, sum up the unread_count from channels
197
- this._state.unreadCount = this._state.channels.reduce((sum, ch) => sum + (ch.unread_count || 0), 0);
198
- }
199
- }
200
- catch (error) {
201
- console.error(`${LOGGER_PREFIX} Failed to fetch channels:`, error);
202
- }
203
- finally {
204
- this._state.isLoading = false;
205
- this._updateUI();
206
- }
207
- }
208
- async selectChannel(channelId) {
209
- this._state.isLoading = true;
210
- this._updateUI();
211
- try {
212
- const response = await this._apiRequest(`${API.channels}/${channelId}?distinct_id=${encodeURIComponent(this._distinctId)}`, {
213
- method: "GET",
214
- });
215
- if (response) {
216
- this._state.channel = response.channel;
217
- this._state.messages = response.messages || [];
218
- this._state.currentView = "conversation";
219
- this._state.agentLastReadAt =
220
- response.channel.agent_last_read_at || null;
221
- this._initialUserReadAt = response.channel.user_last_read_at || null;
222
- // Calculate unread count based on messages and user_last_read_at
223
- this._state.unreadCount = this._calculateUnreadCount();
224
- this._connectRealtime();
225
- if (this._state.isOpen) {
226
- this._autoMarkAsRead();
227
- }
228
- }
229
- }
230
- catch (error) {
231
- console.error(`${LOGGER_PREFIX} Failed to select channel:`, error);
232
- }
233
- finally {
234
- this._state.isLoading = false;
235
- this._updateUI();
236
- }
237
- }
238
- async createChannel() {
239
- var _a;
240
- this._state.isLoading = true;
241
- this._updateUI();
242
- try {
243
- const response = await this._apiRequest(API.channels, {
244
- method: "POST",
245
- body: JSON.stringify({
246
- distinct_id: this._distinctId,
247
- 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,
248
- page_title: globals_1.document === null || globals_1.document === void 0 ? void 0 : globals_1.document.title,
249
- }),
250
- });
251
- if (response) {
252
- this._state.channel = response.channel;
253
- this._state.messages = response.messages || [];
254
- this._state.currentView = "conversation";
255
- this._state.agentLastReadAt =
256
- response.channel.agent_last_read_at || null;
257
- this._initialUserReadAt = response.channel.user_last_read_at || null;
258
- // Calculate unread count based on messages and user_last_read_at
259
- this._state.unreadCount = this._calculateUnreadCount();
260
- const newChannelSummary = {
261
- id: response.channel.id,
262
- status: response.channel.status,
263
- ai_mode: response.channel.ai_mode,
264
- last_message_at: response.channel.last_message_at,
265
- last_message_preview: response.channel.last_message_preview,
266
- last_message_sender: response.channel.last_message_sender,
267
- unread_count: this._state.unreadCount, // Use calculated count
268
- user_last_read_at: response.channel.user_last_read_at,
269
- created_at: response.channel.created_at,
270
- };
271
- this._state.channels.unshift(newChannelSummary);
272
- this._trackEvent(types_1.CHAT_EVENTS.STARTED, {
273
- $channel_id: response.channel.id,
274
- $initiated_by: "user",
275
- $ai_mode: response.channel.ai_mode,
276
- });
277
- this._connectRealtime();
278
- }
279
- }
280
- catch (error) {
281
- console.error(`${LOGGER_PREFIX} Failed to create channel:`, error);
282
- }
283
- finally {
284
- this._state.isLoading = false;
285
- this._updateUI();
286
- }
287
- }
288
- goToChannelList() {
289
- this._disconnectRealtime();
290
- if (this._state.channel) {
291
- const channelIndex = this._state.channels.findIndex((ch) => { var _a; return ch.id === ((_a = this._state.channel) === null || _a === void 0 ? void 0 : _a.id); });
292
- if (channelIndex !== -1) {
293
- this._state.channels[channelIndex] = {
294
- ...this._state.channels[channelIndex],
295
- last_message_at: this._state.messages.length > 0
296
- ? this._state.messages[this._state.messages.length - 1].created_at
297
- : this._state.channels[channelIndex].last_message_at,
298
- last_message_preview: this._state.messages.length > 0
299
- ? this._state.messages[this._state.messages.length - 1].content.substring(0, 100)
300
- : this._state.channels[channelIndex].last_message_preview,
301
- last_message_sender: this._state.messages.length > 0
302
- ? this._state.messages[this._state.messages.length - 1]
303
- .sender_type
304
- : this._state.channels[channelIndex].last_message_sender,
305
- unread_count: 0, // We just viewed it
306
- };
307
- }
308
- }
309
- this._state.channel = null;
310
- this._state.messages = [];
311
- this._state.currentView = "list";
312
- this._state.isTyping = false;
313
- this._state.typingSender = null;
314
- this._state.typingSenderType = null;
315
- this._updateUI();
316
- }
317
- // ============================================================================
318
- // Public API - Messaging
319
- // ============================================================================
320
- async sendMessage(content) {
321
- var _a, _b, _c, _d;
322
- if (!content.trim()) {
323
- return;
324
- }
325
- // Ensure we're in conversation view with a channel
326
- if (!this._state.channel || this._state.currentView !== "conversation") {
327
- console.error(`${LOGGER_PREFIX} Cannot send message: not in conversation view`);
328
- return;
329
- }
330
- const channelId = (_a = this._state.channel) === null || _a === void 0 ? void 0 : _a.id;
331
- if (!channelId) {
332
- console.error(`${LOGGER_PREFIX} No channel to send message to`);
333
- return;
334
- }
335
- // Optimistic update
336
- const tempMessage = {
337
- id: `temp-${Date.now()}`,
338
- channel_id: channelId,
339
- sender_type: "user",
340
- sender_id: this._distinctId || null,
341
- sender_name: null,
342
- sender_avatar_url: null,
343
- content,
344
- content_type: "text",
345
- metadata: {},
346
- created_at: new Date().toISOString(),
347
- };
348
- this._state.messages.push(tempMessage);
349
- this._updateUI();
350
- try {
351
- // Send to API
352
- const config = this._instance.getConfig();
353
- const apiHost = config.api_host || "";
354
- const token = config.token || "";
355
- const separator = API.messages.includes("?") ? "&" : "?";
356
- const url = `${apiHost}${API.messages}${separator}token=${encodeURIComponent(token)}`;
357
- const response = await fetch(url, {
358
- method: "POST",
359
- headers: {
360
- "Content-Type": "application/json",
361
- },
362
- body: JSON.stringify({
363
- channel_id: channelId,
364
- distinct_id: this._distinctId,
365
- content,
366
- }),
367
- });
368
- if (!response.ok) {
369
- throw new Error(`API error: ${response.status}`);
370
- }
371
- // Check if response is a stream (AI enabled) or JSON (AI disabled)
372
- const contentType = response.headers.get("content-type") || "";
373
- const isStream = contentType.includes("text/plain");
374
- if (isStream) {
375
- // Handle streaming AI response
376
- await this._handleStreamingResponse(response, tempMessage, channelId);
377
- }
378
- else {
379
- // Handle JSON response (AI disabled)
380
- const data = await response.json();
381
- // Replace temp message with real one
382
- const index = this._state.messages.findIndex((m) => m.id === tempMessage.id);
383
- if (index !== -1 && (data === null || data === void 0 ? void 0 : data.message)) {
384
- this._state.messages[index] = data.message;
385
- }
386
- this._trackEvent(types_1.CHAT_EVENTS.MESSAGE_SENT, {
387
- $channel_id: channelId,
388
- $message_id: (_b = data === null || data === void 0 ? void 0 : data.message) === null || _b === void 0 ? void 0 : _b.id,
389
- $content_preview: content.substring(0, 100),
390
- $sender_type: "user",
391
- $ai_mode: (_d = (_c = this._state.channel) === null || _c === void 0 ? void 0 : _c.ai_mode) !== null && _d !== void 0 ? _d : true,
392
- $word_count: content.split(/\s+/).length,
393
- });
394
- this._updateUI();
395
- }
396
- }
397
- catch (error) {
398
- console.error(`${LOGGER_PREFIX} Failed to send message:`, error);
399
- // Remove temp message on error
400
- this._state.messages = this._state.messages.filter((m) => m.id !== tempMessage.id);
401
- this._updateUI();
402
- }
403
- }
404
- markAsRead() {
405
- this._autoMarkAsRead();
406
- }
407
- _autoMarkAsRead() {
408
- if (!this._state.channel || this._isMarkingRead) {
409
- return;
410
- }
411
- const latestMessage = this._state.messages[this._state.messages.length - 1];
412
- if (!latestMessage) {
413
- return;
414
- }
415
- const hasUnreadAgentMessages = this._state.messages.some((m) => (m.sender_type === "agent" || m.sender_type === "ai") &&
416
- !this._isMessageReadByUser(m.created_at));
417
- if (!hasUnreadAgentMessages) {
418
- return;
419
- }
420
- this._state.unreadCount = 0;
421
- this._isMarkingRead = true;
422
- this._updateUI();
423
- this._apiRequest(API.read, {
424
- method: "POST",
425
- body: JSON.stringify({
426
- channel_id: this._state.channel.id,
427
- distinct_id: this._distinctId,
428
- read_at: latestMessage.created_at,
429
- }),
430
- })
431
- .then(() => {
432
- this._initialUserReadAt = latestMessage.created_at;
433
- // Recalculate unread count after marking as read
434
- this._state.unreadCount = this._calculateUnreadCount();
435
- this._isMarkingRead = false;
436
- this._updateUI();
437
- })
438
- .catch(() => {
439
- this._isMarkingRead = false;
440
- this._updateUI();
441
- });
442
- }
443
- _isMessageReadByUser(messageCreatedAt) {
444
- if (!this._initialUserReadAt) {
445
- return false;
446
- }
447
- return new Date(messageCreatedAt) <= new Date(this._initialUserReadAt);
448
- }
449
- /**
450
- * Calculate unread count based on messages and user_last_read_at
451
- * (unread agent/AI messages for the user)
452
- */
453
- _calculateUnreadCount() {
454
- if (!this._state.channel || !this._initialUserReadAt) {
455
- return 0;
456
- }
457
- return this._state.messages.filter((m) => (m.sender_type === "agent" || m.sender_type === "ai") &&
458
- new Date(m.created_at) > new Date(this._initialUserReadAt)).length;
459
- }
460
- // ============================================================================
461
- // Public API - Events
462
- // ============================================================================
463
- onMessage(callback) {
464
- this._messageCallbacks.push(callback);
465
- return () => {
466
- const index = this._messageCallbacks.indexOf(callback);
467
- if (index > -1) {
468
- this._messageCallbacks.splice(index, 1);
469
- }
470
- };
471
- }
472
- onTyping(callback) {
473
- this._typingCallbacks.push(callback);
474
- return () => {
475
- const index = this._typingCallbacks.indexOf(callback);
476
- if (index > -1) {
477
- this._typingCallbacks.splice(index, 1);
478
- }
479
- };
480
- }
481
- onConnectionChange(callback) {
482
- this._connectionCallbacks.push(callback);
483
- return () => {
484
- const index = this._connectionCallbacks.indexOf(callback);
485
- if (index > -1) {
486
- this._connectionCallbacks.splice(index, 1);
487
- }
488
- };
489
- }
490
- // ============================================================================
491
- // Public API - Lifecycle
492
- // ============================================================================
493
- destroy() {
494
- var _a;
495
- this._disconnectRealtime();
496
- if (this._typingDebounce) {
497
- clearTimeout(this._typingDebounce);
498
- }
499
- if ((_a = this._container) === null || _a === void 0 ? void 0 : _a.parentNode) {
500
- this._container.parentNode.removeChild(this._container);
501
- }
502
- this._messageCallbacks = [];
503
- this._typingCallbacks = [];
504
- this._connectionCallbacks = [];
505
- }
506
- // ============================================================================
507
- // Private - Ably Realtime Connection
508
- // ============================================================================
509
- async _connectRealtime() {
510
- if (this._ably || !this._state.channel) {
511
- return;
512
- }
513
- this._connectionState = "connecting";
514
- this._notifyConnectionChange(false);
515
- try {
516
- // Get Ably token from server
517
- const tokenResponse = await this._apiRequest(API.ablyToken, {
518
- method: "POST",
519
- body: JSON.stringify({
520
- distinct_id: this._distinctId,
521
- channel_id: this._state.channel.id,
522
- }),
523
- });
524
- if (!(tokenResponse === null || tokenResponse === void 0 ? void 0 : tokenResponse.tokenRequest)) {
525
- console.warn(`${LOGGER_PREFIX} Failed to get Ably token`);
526
- this._connectionState = "error";
527
- return;
528
- }
529
- this._ably = new ably_1.default.Realtime({
530
- authCallback: async (_, callback) => {
531
- var _a;
532
- try {
533
- const refreshResponse = await this._apiRequest(API.ablyToken, {
534
- method: "POST",
535
- body: JSON.stringify({
536
- distinct_id: this._distinctId,
537
- channel_id: (_a = this._state.channel) === null || _a === void 0 ? void 0 : _a.id,
538
- }),
539
- });
540
- if (refreshResponse === null || refreshResponse === void 0 ? void 0 : refreshResponse.tokenRequest) {
541
- callback(null, refreshResponse.tokenRequest);
542
- }
543
- else {
544
- callback("Failed to refresh token", null);
545
- }
546
- }
547
- catch (err) {
548
- callback(String(err), null);
549
- }
550
- },
551
- authMethod: "POST",
552
- });
553
- await this._ably.auth.authorize(tokenResponse.tokenRequest);
554
- const config = this._instance.getConfig();
555
- const projectId = config.projectId || this._extractProjectId(config.token || "");
556
- const ablyChannelName = `chat:${projectId}:${this._state.channel.id}`;
557
- this._ablyChannel = this._ably.channels.get(ablyChannelName);
558
- this._ablyChannel.subscribe("message", (msg) => {
559
- this._handleNewMessage(msg.data);
560
- });
561
- this._ablyChannel.subscribe("read", (msg) => {
562
- this._handleReadCursorEvent(msg.data);
563
- });
564
- this._typingChannel = this._ably.channels.get(`${ablyChannelName}:typing`);
565
- this._typingChannel.subscribe("typing", (msg) => {
566
- this._handleTypingEvent(msg.data);
567
- });
568
- this._ably.connection.on("connected", () => {
569
- this._connectionState = "connected";
570
- this._state.isConnected = true;
571
- this._notifyConnectionChange(true);
572
- });
573
- this._ably.connection.on("disconnected", () => {
574
- this._connectionState = "disconnected";
575
- this._state.isConnected = false;
576
- this._notifyConnectionChange(false);
577
- });
578
- this._ably.connection.on("failed", () => {
579
- this._connectionState = "error";
580
- this._state.isConnected = false;
581
- this._notifyConnectionChange(false);
582
- });
583
- this._ably.connect();
584
- }
585
- catch (error) {
586
- console.error(`${LOGGER_PREFIX} Failed to connect to Ably:`, error);
587
- this._connectionState = "error";
588
- }
589
- }
590
- _disconnectRealtime() {
591
- if (this._ablyChannel) {
592
- this._ablyChannel.unsubscribe();
593
- this._ablyChannel = null;
594
- }
595
- if (this._typingChannel) {
596
- this._typingChannel.unsubscribe();
597
- this._typingChannel = null;
598
- }
599
- if (this._ably) {
600
- this._ably.close();
601
- this._ably = null;
602
- }
603
- this._connectionState = "disconnected";
604
- this._state.isConnected = false;
605
- }
606
- _extractProjectId(token) {
607
- // Token format: project_id.hash
608
- const parts = token.split(".");
609
- return parts[0] || "";
610
- }
611
- _handleNewMessage(message) {
612
- // If this is an AI message and we have a temp AI message, replace it
613
- if (message.sender_type === "ai") {
614
- const tempAiIndex = this._state.messages.findIndex((m) => m.id.startsWith("temp-ai-"));
615
- if (tempAiIndex !== -1) {
616
- // Replace temp message with real one
617
- this._state.messages[tempAiIndex] = message;
618
- // Continue processing (tracking, callbacks, etc.)
619
- }
620
- else if (this._state.messages.some((m) => m.id === message.id)) {
621
- // Already have this message, skip
622
- return;
623
- }
624
- else {
625
- // New AI message, add it
626
- this._state.messages.push(message);
627
- }
628
- }
629
- else if (this._state.messages.some((m) => m.id === message.id)) {
630
- // Already have this message, skip
631
- return;
632
- }
633
- else if (message.sender_type === "user" &&
634
- message.sender_id === this._distinctId) {
635
- // Skip own messages but replace temp message if present
636
- const tempIndex = this._state.messages.findIndex((m) => m.id.startsWith("temp-") &&
637
- m.content === message.content &&
638
- m.sender_type === "user");
639
- if (tempIndex !== -1) {
640
- this._state.messages[tempIndex] = message;
641
- this._updateUI();
642
- }
643
- return;
644
- }
645
- else {
646
- // New message, add it
647
- this._state.messages.push(message);
648
- }
649
- if (message.sender_type !== "user") {
650
- if (!this._state.isOpen) {
651
- this._state.unreadCount++;
652
- }
653
- else {
654
- setTimeout(() => this._autoMarkAsRead(), 100);
655
- }
656
- this._trackEvent(types_1.CHAT_EVENTS.MESSAGE_RECEIVED, {
657
- $channel_id: message.channel_id,
658
- $message_id: message.id,
659
- $content_preview: message.content.substring(0, 100),
660
- $sender_type: message.sender_type,
661
- });
662
- }
663
- this._state.isTyping = false;
664
- this._state.typingSender = null;
665
- this._state.typingSenderType = null;
666
- this._messageCallbacks.forEach((cb) => cb(message));
667
- this._updateUI();
668
- }
669
- _handleTypingEvent(event) {
670
- if (event.sender_type === "user") {
671
- return;
672
- }
673
- // Track sender type for proper display
674
- this._state.isTyping = event.is_typing;
675
- this._state.typingSenderType = event.is_typing
676
- ? event.sender_type
677
- : null;
678
- // Only set senderName for non-AI types (AI uses "AI is thinking" instead)
679
- const senderName = event.sender_type === "ai"
680
- ? null
681
- : event.sender_name || (event.sender_type === "agent" ? "Agent" : null);
682
- this._state.typingSender = event.is_typing ? senderName : null;
683
- this._typingCallbacks.forEach((cb) => cb(event.is_typing, senderName || ""));
684
- this._updateUI();
685
- }
686
- _handleReadCursorEvent(event) {
687
- if (event.reader_type !== "agent") {
688
- return;
689
- }
690
- this._state.agentLastReadAt = event.read_at;
691
- this._updateUI();
692
- }
693
- _notifyConnectionChange(connected) {
694
- this._connectionCallbacks.forEach((cb) => cb(connected));
695
- }
696
- // ============================================================================
697
- // Private - UI
698
- // ============================================================================
699
- _createUI() {
700
- if (!globals_1.document) {
701
- return;
702
- }
703
- this._container = globals_1.document.createElement("div");
704
- this._container.id = "vtilt-chat-container";
705
- this._container.setAttribute("style", this._getContainerStyles());
706
- this._bubble = globals_1.document.createElement("div");
707
- this._bubble.id = "vtilt-chat-bubble";
708
- this._bubble.innerHTML = this._getBubbleHTML();
709
- this._bubble.setAttribute("style", this._getBubbleStyles());
710
- this._container.appendChild(this._bubble);
711
- this._widget = globals_1.document.createElement("div");
712
- this._widget.id = "vtilt-chat-widget";
713
- this._widget.innerHTML = this._getWidgetHTML();
714
- this._widget.setAttribute("style", this._getWidgetStyles());
715
- this._container.appendChild(this._widget);
716
- globals_1.document.body.appendChild(this._container);
717
- }
718
- _attachEventListeners() {
719
- var _a, _b, _c;
720
- (_a = this._bubble) === null || _a === void 0 ? void 0 : _a.addEventListener("click", () => this.toggle());
721
- (_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());
722
- }
723
- _handleUserTyping() {
724
- if (!this._typingChannel) {
725
- return;
726
- }
727
- if (!this._isUserTyping) {
728
- this._isUserTyping = true;
729
- this._sendTypingIndicator(true);
730
- }
731
- if (this._typingDebounce) {
732
- clearTimeout(this._typingDebounce);
733
- }
734
- this._typingDebounce = setTimeout(() => {
735
- this._isUserTyping = false;
736
- this._sendTypingIndicator(false);
737
- }, 2000);
738
- }
739
- _sendTypingIndicator(isTyping) {
740
- if (!this._typingChannel) {
741
- return;
742
- }
743
- try {
744
- this._typingChannel.publish("typing", {
745
- sender_type: "user",
746
- sender_id: this._distinctId,
747
- sender_name: null,
748
- is_typing: isTyping,
749
- });
750
- }
751
- catch (_a) {
752
- // Silently fail
753
- }
754
- }
755
- _handleSend() {
756
- var _a, _b;
757
- const input = (_a = this._widget) === null || _a === void 0 ? void 0 : _a.querySelector(".vtilt-chat-input");
758
- if (!input) {
759
- return;
760
- }
761
- const content = input.value.trim();
762
- if (content) {
763
- if (this._isUserTyping) {
764
- this._isUserTyping = false;
765
- this._sendTypingIndicator(false);
766
- }
767
- if (this._typingDebounce) {
768
- clearTimeout(this._typingDebounce);
769
- this._typingDebounce = null;
770
- }
771
- this.sendMessage(content);
772
- input.value = "";
773
- // Reset textarea height after sending
774
- input.style.height = "auto";
775
- input.style.height = "60px";
776
- // Update send button state after sending
777
- const sendButton = (_b = this._widget) === null || _b === void 0 ? void 0 : _b.querySelector(".vtilt-chat-send");
778
- if (sendButton) {
779
- const primary = this._theme.primaryColor;
780
- sendButton.disabled = true;
781
- sendButton.style.background = "#E5E5E5";
782
- sendButton.style.color = "#999999";
783
- sendButton.style.cursor = "not-allowed";
784
- sendButton.style.opacity = "0.6";
785
- }
786
- }
787
- }
788
- _updateUI() {
789
- if (!this._container || !this._widget || !this._bubble) {
790
- return;
791
- }
792
- // Update visibility
793
- this._container.style.display = this._state.isVisible ? "block" : "none";
794
- // Update widget open state
795
- this._widget.style.display = this._state.isOpen ? "flex" : "none";
796
- // Update bubble badge
797
- const badge = this._bubble.querySelector(".vtilt-chat-badge");
798
- if (badge) {
799
- badge.style.display =
800
- this._state.unreadCount > 0 ? "flex" : "none";
801
- badge.textContent = String(this._state.unreadCount);
802
- }
803
- // Update content based on current view
804
- const contentContainer = this._widget.querySelector(".vtilt-chat-content");
805
- if (contentContainer) {
806
- if (this._state.currentView === "list") {
807
- contentContainer.innerHTML = this._getChannelListHTML();
808
- this._attachChannelListListeners();
809
- }
810
- else {
811
- contentContainer.innerHTML = this._getConversationHTML();
812
- this._attachConversationListeners();
813
- this._renderMessages();
814
- }
815
- }
816
- // Update header based on view
817
- this._updateHeader();
818
- // Update loading state
819
- const loader = this._widget.querySelector(".vtilt-chat-loader");
820
- if (loader) {
821
- loader.style.display = this._state.isLoading
822
- ? "flex"
823
- : "none";
824
- }
825
- // Update typing indicator (only in conversation view)
826
- const typing = this._widget.querySelector(".vtilt-chat-typing");
827
- if (typing) {
828
- typing.style.display =
829
- this._state.isTyping && this._state.currentView === "conversation"
830
- ? "flex"
831
- : "none";
832
- const typingText = typing.querySelector(".vtilt-chat-typing-text");
833
- if (typingText && this._state.isTyping) {
834
- // Show "AI is thinking..." for AI, otherwise show sender name
835
- if (this._state.typingSenderType === "ai") {
836
- typingText.textContent = "AI is thinking...";
837
- }
838
- else if (this._state.typingSender) {
839
- typingText.textContent = `${this._state.typingSender} is typing...`;
840
- }
841
- else {
842
- typingText.textContent = "Agent is typing...";
843
- }
844
- }
845
- }
846
- }
847
- _updateHeader() {
848
- var _a, _b;
849
- const header = (_a = this._widget) === null || _a === void 0 ? void 0 : _a.querySelector(".vtilt-chat-header");
850
- if (!header) {
851
- return;
852
- }
853
- const greeting = this._config.greeting || "Messages";
854
- const primary = this._theme.primaryColor;
855
- if (this._state.currentView === "list") {
856
- header.style.cssText = `
857
- background: #ffffff;
858
- border-bottom: 1px solid #E5E5E5;
859
- padding: 18px 16px;
860
- padding-top: max(18px, env(safe-area-inset-top, 18px));
861
- display: flex;
862
- align-items: center;
863
- justify-content: space-between;
864
- min-height: 60px;
865
- box-sizing: border-box;
866
- flex-shrink: 0;
867
- `;
868
- header.innerHTML = `
869
- <div style="font-weight: 600; font-size: 17px; color: #000000;">${greeting}</div>
870
- <button class="vtilt-chat-close" style="
871
- background: transparent;
872
- border: none;
873
- color: #666666;
874
- cursor: pointer;
875
- padding: 6px;
876
- margin: -6px;
877
- border-radius: 4px;
878
- display: flex;
879
- align-items: center;
880
- justify-content: center;
881
- -webkit-tap-highlight-color: transparent;
882
- ">
883
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
884
- <path d="M18 6L6 18M6 6l12 12"></path>
885
- </svg>
886
- </button>
887
- `;
888
- }
889
- else {
890
- const isAiMode = (_b = this._state.channel) === null || _b === void 0 ? void 0 : _b.ai_mode;
891
- header.style.cssText = `
892
- background: #ffffff;
893
- border-bottom: 1px solid #E5E5E5;
894
- padding: 12px 16px;
895
- padding-top: max(12px, env(safe-area-inset-top, 12px));
896
- display: flex;
897
- align-items: center;
898
- gap: 12px;
899
- min-height: 60px;
900
- box-sizing: border-box;
901
- flex-shrink: 0;
902
- `;
903
- header.innerHTML = `
904
- <button class="vtilt-chat-back" style="
905
- background: transparent;
906
- border: none;
907
- color: #666666;
908
- cursor: pointer;
909
- padding: 6px;
910
- margin-left: -6px;
911
- border-radius: 4px;
912
- display: flex;
913
- align-items: center;
914
- justify-content: center;
915
- -webkit-tap-highlight-color: transparent;
916
- ">
917
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
918
- <path d="M15 18l-6-6 6-6"></path>
919
- </svg>
920
- </button>
921
- <div style="
922
- width: 44px;
923
- height: 44px;
924
- border-radius: 50%;
925
- background: ${isAiMode ? primary : "#DEDEDE"};
926
- display: flex;
927
- align-items: center;
928
- justify-content: center;
929
- flex-shrink: 0;
930
- ">
931
- ${isAiMode
932
- ? `<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>`
933
- : `<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>`}
934
- </div>
935
- <div style="flex: 1; min-width: 0;">
936
- <div style="font-weight: 600; font-size: 16px; color: #000000;">${isAiMode ? "AI Assistant" : "Support"}</div>
937
- <div style="font-size: 13px; color: #16A34A; display: flex; align-items: center; gap: 5px; margin-top: 1px;">
938
- <span style="width: 7px; height: 7px; background: #16A34A; border-radius: 50%;"></span>
939
- Online
940
- </div>
941
- </div>
942
- <button class="vtilt-chat-close" style="
943
- background: transparent;
944
- border: none;
945
- color: #666666;
946
- cursor: pointer;
947
- padding: 6px;
948
- margin-right: -6px;
949
- border-radius: 4px;
950
- display: flex;
951
- align-items: center;
952
- justify-content: center;
953
- -webkit-tap-highlight-color: transparent;
954
- ">
955
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
956
- <path d="M18 6L6 18M6 6l12 12"></path>
957
- </svg>
958
- </button>
959
- `;
960
- }
961
- // Re-attach header event listeners
962
- const closeBtn = header.querySelector(".vtilt-chat-close");
963
- closeBtn === null || closeBtn === void 0 ? void 0 : closeBtn.addEventListener("click", () => this.close());
964
- const backBtn = header.querySelector(".vtilt-chat-back");
965
- backBtn === null || backBtn === void 0 ? void 0 : backBtn.addEventListener("click", () => this.goToChannelList());
966
- }
967
- _getChannelListHTML() {
968
- const primary = this._theme.primaryColor;
969
- if (this._state.channels.length === 0 && !this._state.isLoading) {
970
- return `
971
- <div style="
972
- flex: 1;
973
- display: flex;
974
- flex-direction: column;
975
- align-items: center;
976
- justify-content: center;
977
- padding: 48px 24px;
978
- text-align: center;
979
- ">
980
- <div style="
981
- width: 72px;
982
- height: 72px;
983
- margin-bottom: 24px;
984
- background: ${primary};
985
- border-radius: 50%;
986
- display: flex;
987
- align-items: center;
988
- justify-content: center;
989
- ">
990
- <svg width="36" height="36" viewBox="0 0 24 24" fill="white">
991
- <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"/>
992
- </svg>
993
- </div>
994
- <div style="font-size: 18px; font-weight: 600; color: #000000; margin-bottom: 8px;">No conversations yet</div>
995
- <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>
996
- <button class="vtilt-chat-new-channel" style="
997
- background: ${primary};
998
- color: white;
999
- border: none;
1000
- border-radius: 100px;
1001
- padding: 14px 28px;
1002
- cursor: pointer;
1003
- font-weight: 500;
1004
- font-size: 15px;
1005
- -webkit-tap-highlight-color: transparent;
1006
- touch-action: manipulation;
1007
- display: flex;
1008
- align-items: center;
1009
- gap: 10px;
1010
- box-shadow: 0 2px 8px rgba(123, 104, 238, 0.3);
1011
- ">
1012
- Send us a message
1013
- <svg width="18" height="18" viewBox="0 0 24 24" fill="white">
1014
- <path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
1015
- </svg>
1016
- </button>
1017
- </div>
1018
- `;
1019
- }
1020
- const channelsHtml = this._state.channels
1021
- .map((ch) => this._getChannelItemHTML(ch))
1022
- .join("");
1023
- return `
1024
- <div style="flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch;">
1025
- ${channelsHtml}
1026
- </div>
1027
- <div style="
1028
- padding: 16px;
1029
- padding-bottom: max(16px, env(safe-area-inset-bottom, 16px));
1030
- border-top: 1px solid #E5E5E5;
1031
- ">
1032
- <button class="vtilt-chat-new-channel" style="
1033
- width: 100%;
1034
- background: ${primary};
1035
- color: white;
1036
- border: none;
1037
- border-radius: 100px;
1038
- padding: 14px 24px;
1039
- cursor: pointer;
1040
- font-weight: 500;
1041
- font-size: 15px;
1042
- display: flex;
1043
- align-items: center;
1044
- justify-content: center;
1045
- gap: 10px;
1046
- -webkit-tap-highlight-color: transparent;
1047
- touch-action: manipulation;
1048
- box-shadow: 0 2px 8px rgba(123, 104, 238, 0.3);
1049
- ">
1050
- Send us a message
1051
- <svg width="18" height="18" viewBox="0 0 24 24" fill="white">
1052
- <path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
1053
- </svg>
1054
- </button>
1055
- </div>
1056
- `;
1057
- }
1058
- _getChannelItemHTML(channel) {
1059
- const hasUnread = channel.unread_count > 0;
1060
- const timeStr = this._formatRelativeTime(channel.last_message_at || channel.created_at);
1061
- const preview = channel.last_message_preview || "No messages yet";
1062
- const primary = this._theme.primaryColor;
1063
- const senderPrefix = channel.last_message_sender === "user" ? "You: " : "";
1064
- return `
1065
- <div class="vtilt-channel-item" data-channel-id="${channel.id}" style="
1066
- padding: 14px 16px;
1067
- cursor: pointer;
1068
- -webkit-tap-highlight-color: transparent;
1069
- touch-action: manipulation;
1070
- display: flex;
1071
- align-items: center;
1072
- gap: 12px;
1073
- background: white;
1074
- border-bottom: 1px solid #EEEEEE;
1075
- ">
1076
- <div style="
1077
- width: 48px;
1078
- height: 48px;
1079
- border-radius: 50%;
1080
- background: ${channel.ai_mode ? primary : "#DEDEDE"};
1081
- display: flex;
1082
- align-items: center;
1083
- justify-content: center;
1084
- flex-shrink: 0;
1085
- ">
1086
- ${channel.ai_mode
1087
- ? `<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>`
1088
- : `<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>`}
1089
- </div>
1090
- <div style="flex: 1; min-width: 0;">
1091
- <div style="display: flex; justify-content: space-between; align-items: center; gap: 8px; margin-bottom: 4px;">
1092
- <div style="font-weight: ${hasUnread ? "600" : "500"}; font-size: 15px; color: #000000; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
1093
- ${channel.ai_mode ? "AI Assistant" : "Support"}
1094
- </div>
1095
- <div style="font-size: 13px; color: #888888; white-space: nowrap; flex-shrink: 0;">${timeStr}</div>
1096
- </div>
1097
- <div style="display: flex; justify-content: space-between; align-items: center; gap: 8px;">
1098
- <div style="
1099
- font-size: 14px;
1100
- color: ${hasUnread ? "#333333" : "#888888"};
1101
- font-weight: 400;
1102
- white-space: nowrap;
1103
- overflow: hidden;
1104
- text-overflow: ellipsis;
1105
- flex: 1;
1106
- min-width: 0;
1107
- line-height: 1.4;
1108
- ">${senderPrefix}${this._escapeHTML(preview)}${channel.status === "closed" ? " · Closed" : ""}</div>
1109
- ${hasUnread
1110
- ? `<div style="
1111
- min-width: 10px;
1112
- width: 10px;
1113
- height: 10px;
1114
- background: ${primary};
1115
- border-radius: 50%;
1116
- flex-shrink: 0;
1117
- "></div>`
1118
- : ""}
1119
- </div>
1120
- </div>
1121
- </div>
1122
- `;
1123
- }
1124
- _getConversationHTML() {
1125
- const primary = this._theme.primaryColor;
1126
- return `
1127
- <div class="vtilt-chat-messages" style="
1128
- flex: 1;
1129
- overflow-y: auto;
1130
- -webkit-overflow-scrolling: touch;
1131
- padding: 20px 16px 24px 16px;
1132
- display: flex;
1133
- flex-direction: column;
1134
- gap: 12px;
1135
- min-height: 0;
1136
- background: #FAFAFA;
1137
- "></div>
1138
-
1139
- <div class="vtilt-chat-typing" style="
1140
- display: none;
1141
- padding: 12px 16px;
1142
- background: #FAFAFA;
1143
- align-items: center;
1144
- ">
1145
- <div style="
1146
- display: flex;
1147
- align-items: center;
1148
- gap: 6px;
1149
- ">
1150
- <span style="width: 7px; height: 7px; background: #999; border-radius: 50%; animation: vtilt-typing 1.2s infinite 0s;"></span>
1151
- <span style="width: 7px; height: 7px; background: #999; border-radius: 50%; animation: vtilt-typing 1.2s infinite 0.2s;"></span>
1152
- <span style="width: 7px; height: 7px; background: #999; border-radius: 50%; animation: vtilt-typing 1.2s infinite 0.4s;"></span>
1153
- <span class="vtilt-chat-typing-text" style="
1154
- font-size: 12px;
1155
- color: #666;
1156
- font-weight: 500;
1157
- white-space: nowrap;
1158
- margin-left: 2px;
1159
- ">Agent is typing...</span>
1160
- </div>
1161
- </div>
1162
- <style>
1163
- @keyframes vtilt-typing { 0%, 60%, 100% { opacity: 0.35; transform: translateY(0); } 30% { opacity: 1; transform: translateY(-2px); } }
1164
- </style>
1165
-
1166
- <div class="vtilt-chat-input-container" style="
1167
- padding: 8px 12px;
1168
- padding-bottom: max(8px, env(safe-area-inset-bottom, 8px));
1169
- border-top: 1px solid #E5E5E5;
1170
- flex-shrink: 0;
1171
- background: #ffffff;
1172
- ">
1173
- <div style="
1174
- position: relative;
1175
- border: 1px solid #DDDDDD;
1176
- border-radius: 12px;
1177
- background: #ffffff;
1178
- overflow: hidden;
1179
- ">
1180
- <textarea
1181
- class="vtilt-chat-input"
1182
- placeholder="Message..."
1183
- autocomplete="off"
1184
- autocorrect="on"
1185
- autocapitalize="sentences"
1186
- rows="1"
1187
- style="
1188
- width: 100%;
1189
- box-sizing: border-box;
1190
- border: none;
1191
- border-radius: 0;
1192
- padding: 12px 12px 36px 12px;
1193
- font-size: 15px;
1194
- line-height: 1.4;
1195
- outline: none;
1196
- background: transparent;
1197
- -webkit-appearance: none;
1198
- appearance: none;
1199
- color: #000000;
1200
- transition: none;
1201
- resize: none;
1202
- overflow-y: auto;
1203
- overflow-x: hidden;
1204
- min-height: 60px;
1205
- max-height: 120px;
1206
- font-family: inherit;
1207
- "
1208
- ></textarea>
1209
- <div style="
1210
- position: absolute;
1211
- left: 0;
1212
- bottom: 0;
1213
- display: flex;
1214
- align-items: center;
1215
- gap: 8px;
1216
- padding: 8px 0 8px 10px;
1217
- pointer-events: none;
1218
- z-index: 1;
1219
- ">
1220
- <button
1221
- class="vtilt-chat-attach"
1222
- style="
1223
- background: none;
1224
- border: none;
1225
- padding: 0;
1226
- cursor: pointer;
1227
- display: flex;
1228
- align-items: center;
1229
- justify-content: center;
1230
- width: 20px;
1231
- height: 20px;
1232
- pointer-events: auto;
1233
- color: #666666;
1234
- transition: color 0.15s ease;
1235
- "
1236
- title="Attach file"
1237
- >
1238
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1239
- <path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"></path>
1240
- </svg>
1241
- </button>
1242
- <button
1243
- class="vtilt-chat-mic"
1244
- style="
1245
- background: none;
1246
- border: none;
1247
- padding: 0;
1248
- cursor: pointer;
1249
- display: flex;
1250
- align-items: center;
1251
- justify-content: center;
1252
- width: 20px;
1253
- height: 20px;
1254
- pointer-events: auto;
1255
- color: #666666;
1256
- transition: color 0.15s ease;
1257
- "
1258
- title="Voice message"
1259
- >
1260
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1261
- <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
1262
- <path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
1263
- <line x1="12" y1="19" x2="12" y2="23"></line>
1264
- <line x1="8" y1="23" x2="16" y2="23"></line>
1265
- </svg>
1266
- </button>
1267
- </div>
1268
- <button class="vtilt-chat-send" disabled style="
1269
- position: absolute;
1270
- right: 0;
1271
- bottom: 0;
1272
- background: #E5E5E5;
1273
- color: #999999;
1274
- border: none;
1275
- border-radius: 50%;
1276
- padding: 0;
1277
- width: 32px;
1278
- height: 32px;
1279
- margin: 6px 8px 6px 6px;
1280
- cursor: not-allowed;
1281
- display: flex;
1282
- align-items: center;
1283
- justify-content: center;
1284
- -webkit-tap-highlight-color: transparent;
1285
- touch-action: manipulation;
1286
- z-index: 2;
1287
- transition: background-color 0.15s ease, color 0.15s ease, transform 0.1s ease, cursor 0.15s ease;
1288
- opacity: 0.6;
1289
- ">
1290
- <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
1291
- <path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
1292
- </svg>
1293
- </button>
1294
- </div>
1295
- </div>
1296
- `;
1297
- }
1298
- _attachChannelListListeners() {
1299
- var _a, _b, _c, _d;
1300
- (_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());
1301
- (_d = (_c = this._widget) === null || _c === void 0 ? void 0 : _c.querySelectorAll(".vtilt-channel-item")) === null || _d === void 0 ? void 0 : _d.forEach((item) => {
1302
- item.addEventListener("click", () => {
1303
- const channelId = item.getAttribute("data-channel-id");
1304
- if (channelId) {
1305
- this.selectChannel(channelId);
1306
- }
1307
- });
1308
- });
1309
- }
1310
- _attachConversationListeners() {
1311
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
1312
- const sendButton = (_a = this._widget) === null || _a === void 0 ? void 0 : _a.querySelector(".vtilt-chat-send");
1313
- sendButton === null || sendButton === void 0 ? void 0 : sendButton.addEventListener("click", () => {
1314
- if (!sendButton.disabled) {
1315
- this._handleSend();
1316
- }
1317
- });
1318
- const input = (_b = this._widget) === null || _b === void 0 ? void 0 : _b.querySelector(".vtilt-chat-input");
1319
- // Attachment button (placeholder for future functionality)
1320
- (_d = (_c = this._widget) === null || _c === void 0 ? void 0 : _c.querySelector(".vtilt-chat-attach")) === null || _d === void 0 ? void 0 : _d.addEventListener("click", (e) => {
1321
- e.preventDefault();
1322
- alert("File attachments coming soon!");
1323
- });
1324
- // Emoji button (placeholder for future functionality)
1325
- (_f = (_e = this._widget) === null || _e === void 0 ? void 0 : _e.querySelector(".vtilt-chat-emoji")) === null || _f === void 0 ? void 0 : _f.addEventListener("click", (e) => {
1326
- e.preventDefault();
1327
- // TODO: Implement emoji picker functionality
1328
- console.log("Emoji clicked - functionality coming soon");
1329
- });
1330
- // GIF button (placeholder for future functionality)
1331
- (_h = (_g = this._widget) === null || _g === void 0 ? void 0 : _g.querySelector(".vtilt-chat-gif")) === null || _h === void 0 ? void 0 : _h.addEventListener("click", (e) => {
1332
- e.preventDefault();
1333
- // TODO: Implement GIF picker functionality
1334
- console.log("GIF clicked - functionality coming soon");
1335
- });
1336
- // Microphone button (placeholder for future functionality)
1337
- (_k = (_j = this._widget) === null || _j === void 0 ? void 0 : _j.querySelector(".vtilt-chat-mic")) === null || _k === void 0 ? void 0 : _k.addEventListener("click", (e) => {
1338
- e.preventDefault();
1339
- alert("Voice messages coming soon!");
1340
- });
1341
- // Auto-resize textarea
1342
- const autoResize = () => {
1343
- if (input) {
1344
- input.style.height = "auto";
1345
- const scrollHeight = input.scrollHeight;
1346
- const minHeight = 60;
1347
- const maxHeight = 120;
1348
- input.style.height = `${Math.min(Math.max(scrollHeight, minHeight), maxHeight)}px`;
1349
- }
1350
- };
1351
- // Update send button state
1352
- const primary = this._theme.primaryColor;
1353
- const updateSendButton = () => {
1354
- var _a;
1355
- const sendButton = (_a = this._widget) === null || _a === void 0 ? void 0 : _a.querySelector(".vtilt-chat-send");
1356
- if (sendButton && input) {
1357
- const hasText = input.value.trim().length > 0;
1358
- sendButton.disabled = !hasText;
1359
- if (hasText) {
1360
- sendButton.style.background = primary || "#E5E5E5";
1361
- sendButton.style.color = "white";
1362
- sendButton.style.cursor = "pointer";
1363
- sendButton.style.opacity = "1";
1364
- }
1365
- else {
1366
- sendButton.style.background = "#E5E5E5";
1367
- sendButton.style.color = "#999999";
1368
- sendButton.style.cursor = "not-allowed";
1369
- sendButton.style.opacity = "0.6";
1370
- }
1371
- }
1372
- };
1373
- input === null || input === void 0 ? void 0 : input.addEventListener("input", () => {
1374
- autoResize();
1375
- this._handleUserTyping();
1376
- updateSendButton();
1377
- });
1378
- // Keyboard handling: Enter sends, Shift+Enter creates new line
1379
- input === null || input === void 0 ? void 0 : input.addEventListener("keydown", (e) => {
1380
- if (e.key === "Enter" && !e.shiftKey) {
1381
- e.preventDefault();
1382
- this._handleSend();
1383
- }
1384
- });
1385
- // Initial resize on mount
1386
- if (input) {
1387
- autoResize();
1388
- updateSendButton();
1389
- }
1390
- }
1391
- _formatRelativeTime(isoString) {
1392
- const date = new Date(isoString);
1393
- const now = new Date();
1394
- const diffMs = now.getTime() - date.getTime();
1395
- const diffMins = Math.floor(diffMs / 60000);
1396
- const diffHours = Math.floor(diffMs / 3600000);
1397
- const diffDays = Math.floor(diffMs / 86400000);
1398
- if (diffMins < 1) {
1399
- return "Just now";
1400
- }
1401
- if (diffMins < 60) {
1402
- return `${diffMins}m ago`;
1403
- }
1404
- if (diffHours < 24) {
1405
- return `${diffHours}h ago`;
1406
- }
1407
- if (diffDays < 7) {
1408
- return `${diffDays}d ago`;
1409
- }
1410
- return date.toLocaleDateString();
1411
- }
1412
- _renderMessages() {
1413
- var _a;
1414
- const container = (_a = this._widget) === null || _a === void 0 ? void 0 : _a.querySelector(".vtilt-chat-messages");
1415
- if (!container) {
1416
- return;
1417
- }
1418
- const primary = this._theme.primaryColor;
1419
- const firstUnreadIndex = this._state.messages.findIndex((m) => (m.sender_type === "agent" || m.sender_type === "ai") &&
1420
- !this._isMessageReadByUser(m.created_at));
1421
- const html = this._state.messages
1422
- .map((msg, i) => {
1423
- const divider = i === firstUnreadIndex && firstUnreadIndex > 0
1424
- ? `<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>`
1425
- : "";
1426
- return divider + this._getMessageHTML(msg);
1427
- })
1428
- .join("");
1429
- container.innerHTML = html;
1430
- container.scrollTop = container.scrollHeight;
1431
- }
1432
- // ============================================================================
1433
- // Private - Styles & HTML
1434
- // ============================================================================
1435
- _getContainerStyles() {
1436
- const isRight = (this._config.position || DEFAULT_POSITION) === "bottom-right";
1437
- return `
1438
- position: fixed;
1439
- bottom: 20px;
1440
- ${isRight ? "right: 20px;" : "left: 20px;"}
1441
- z-index: 999999;
1442
- font-family: ${this._theme.fontFamily};
1443
- -webkit-font-smoothing: antialiased;
1444
- -moz-osx-font-smoothing: grayscale;
1445
- `;
1446
- }
1447
- _getBubbleStyles() {
1448
- const primary = this._theme.primaryColor;
1449
- return `
1450
- width: 60px;
1451
- height: 60px;
1452
- border-radius: 50%;
1453
- background: ${primary};
1454
- cursor: pointer;
1455
- display: flex;
1456
- align-items: center;
1457
- justify-content: center;
1458
- box-shadow: 0 4px 16px rgba(123, 104, 238, 0.4);
1459
- transition: transform 0.2s ease, box-shadow 0.2s ease;
1460
- position: relative;
1461
- -webkit-tap-highlight-color: transparent;
1462
- touch-action: manipulation;
1463
- `;
1464
- }
1465
- _getBubbleHTML() {
1466
- return `
1467
- <svg width="28" height="28" viewBox="0 0 28 28" fill="white">
1468
- <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"/>
1469
- </svg>
1470
- <div class="vtilt-chat-badge" style="
1471
- display: none;
1472
- position: absolute;
1473
- top: -4px;
1474
- right: -4px;
1475
- background: #E53935;
1476
- color: white;
1477
- font-size: 11px;
1478
- font-weight: 700;
1479
- min-width: 20px;
1480
- height: 20px;
1481
- border-radius: 10px;
1482
- align-items: center;
1483
- justify-content: center;
1484
- padding: 0 6px;
1485
- box-sizing: border-box;
1486
- border: 2px solid white;
1487
- ">0</div>
1488
- `;
1489
- }
1490
- _getWidgetStyles() {
1491
- return `
1492
- display: none;
1493
- flex-direction: column;
1494
- position: absolute;
1495
- bottom: 80px;
1496
- right: 0;
1497
- width: 380px;
1498
- max-width: calc(100vw - 40px);
1499
- height: 600px;
1500
- max-height: calc(100vh - 120px);
1501
- max-height: calc(100dvh - 120px);
1502
- background: #ffffff;
1503
- border-radius: 16px;
1504
- box-shadow: 0 5px 40px rgba(0, 0, 0, 0.16);
1505
- overflow: hidden;
1506
- `;
1507
- }
1508
- _getWidgetHTML() {
1509
- const greeting = this._config.greeting || "Messages";
1510
- const primary = this._theme.primaryColor;
1511
- return `
1512
- <div class="vtilt-chat-header" style="
1513
- background: #ffffff;
1514
- border-bottom: 1px solid #E5E5E5;
1515
- padding: 18px 16px;
1516
- padding-top: max(18px, env(safe-area-inset-top, 18px));
1517
- display: flex;
1518
- align-items: center;
1519
- justify-content: space-between;
1520
- min-height: 60px;
1521
- box-sizing: border-box;
1522
- flex-shrink: 0;
1523
- ">
1524
- <div style="font-weight: 600; font-size: 17px; color: #000000;">${greeting}</div>
1525
- <button class="vtilt-chat-close" style="
1526
- background: transparent;
1527
- border: none;
1528
- color: #666666;
1529
- cursor: pointer;
1530
- padding: 6px;
1531
- margin: -6px;
1532
- border-radius: 4px;
1533
- display: flex;
1534
- align-items: center;
1535
- justify-content: center;
1536
- -webkit-tap-highlight-color: transparent;
1537
- touch-action: manipulation;
1538
- ">
1539
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1540
- <path d="M18 6L6 18M6 6l12 12"></path>
1541
- </svg>
1542
- </button>
1543
- </div>
1544
-
1545
- <div class="vtilt-chat-content" style="
1546
- flex: 1;
1547
- display: flex;
1548
- flex-direction: column;
1549
- overflow: hidden;
1550
- min-height: 0;
1551
- background: #ffffff;
1552
- ">
1553
- </div>
1554
-
1555
- <div class="vtilt-chat-loader" style="
1556
- display: none;
1557
- position: absolute;
1558
- top: 0;
1559
- left: 0;
1560
- right: 0;
1561
- bottom: 0;
1562
- background: rgba(255, 255, 255, 0.95);
1563
- align-items: center;
1564
- justify-content: center;
1565
- z-index: 10;
1566
- ">
1567
- <div style="
1568
- width: 32px;
1569
- height: 32px;
1570
- border: 3px solid #E5E5E5;
1571
- border-top-color: ${primary};
1572
- border-radius: 50%;
1573
- animation: vtilt-spin 0.8s linear infinite;
1574
- "></div>
1575
- </div>
1576
-
1577
- <style>
1578
- @keyframes vtilt-spin { to { transform: rotate(360deg); } }
1579
- @keyframes vtilt-fade { from { opacity: 0; } to { opacity: 1; } }
1580
-
1581
- #vtilt-chat-bubble { transition: transform 0.15s ease, box-shadow 0.15s ease; }
1582
- #vtilt-chat-bubble:hover { transform: scale(1.05); box-shadow: 0 6px 20px rgba(123, 104, 238, 0.45); }
1583
- #vtilt-chat-bubble:active { transform: scale(0.95); }
1584
-
1585
- #vtilt-chat-widget { transition: opacity 0.2s ease; }
1586
- #vtilt-chat-widget.vtilt-opening { animation: vtilt-fade 0.2s ease forwards; }
1587
- #vtilt-chat-widget.vtilt-closing { opacity: 0; }
1588
-
1589
- .vtilt-chat-content { transition: opacity 0.15s ease; }
1590
-
1591
- .vtilt-chat-close:hover { color: #000 !important; background: #F0F0F0 !important; }
1592
- .vtilt-chat-back:hover { color: #000 !important; background: #F0F0F0 !important; }
1593
-
1594
- .vtilt-chat-input-container > div:first-child:focus-within {
1595
- border-color: ${primary} !important;
1596
- box-shadow: 0 0 0 2px ${primary}20 !important;
1597
- }
1598
- .vtilt-chat-input {
1599
- font-size: 16px !important;
1600
- -webkit-text-size-adjust: 100%;
1601
- }
1602
- .vtilt-chat-input:focus {
1603
- outline: none !important;
1604
- }
1605
- .vtilt-chat-input::placeholder {
1606
- color: #999999;
1607
- }
1608
- .vtilt-chat-input::-webkit-scrollbar {
1609
- width: 4px;
1610
- }
1611
- .vtilt-chat-input::-webkit-scrollbar-track {
1612
- background: transparent;
1613
- }
1614
- .vtilt-chat-input::-webkit-scrollbar-thumb {
1615
- background: rgba(0, 0, 0, 0.2);
1616
- border-radius: 2px;
1617
- }
1618
- .vtilt-chat-input::-webkit-scrollbar-thumb:hover {
1619
- background: rgba(0, 0, 0, 0.3);
1620
- }
1621
- /* Firefox scrollbar */
1622
- .vtilt-chat-input {
1623
- scrollbar-width: thin;
1624
- scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
1625
- }
1626
- .vtilt-chat-attach:hover,
1627
- .vtilt-chat-emoji:hover,
1628
- .vtilt-chat-gif:hover,
1629
- .vtilt-chat-mic:hover {
1630
- color: #333333 !important;
1631
- }
1632
- .vtilt-chat-attach:active,
1633
- .vtilt-chat-emoji:active,
1634
- .vtilt-chat-gif:active,
1635
- .vtilt-chat-mic:active {
1636
- color: #000000 !important;
1637
- }
1638
- .vtilt-chat-send:not(:disabled):hover {
1639
- opacity: 0.9 !important;
1640
- transform: scale(1.05);
1641
- }
1642
- .vtilt-chat-send:not(:disabled):active {
1643
- opacity: 0.8 !important;
1644
- transform: scale(0.95);
1645
- }
1646
- .vtilt-chat-send:disabled {
1647
- cursor: not-allowed !important;
1648
- }
1649
-
1650
- .vtilt-chat-send { transition: opacity 0.1s ease; }
1651
-
1652
- .vtilt-chat-new-channel { transition: opacity 0.1s ease; }
1653
- .vtilt-chat-new-channel:hover { opacity: 0.9; }
1654
- .vtilt-chat-new-channel:active { opacity: 0.8; }
1655
-
1656
- .vtilt-channel-item { transition: background 0.1s ease; cursor: pointer; }
1657
- .vtilt-channel-item:hover { background: #F5F5F5 !important; }
1658
- .vtilt-channel-item:active { background: #EBEBEB !important; }
1659
-
1660
- @media (max-width: 480px) {
1661
- #vtilt-chat-container { bottom: 16px !important; right: 16px !important; }
1662
- #vtilt-chat-bubble { width: 56px !important; height: 56px !important; }
1663
- #vtilt-chat-bubble svg { width: 24px !important; height: 24px !important; }
1664
- #vtilt-chat-widget {
1665
- position: fixed !important;
1666
- top: 0 !important; left: 0 !important; right: 0 !important; bottom: 0 !important;
1667
- width: 100% !important; max-width: 100% !important;
1668
- height: 100% !important; max-height: 100% !important;
1669
- border-radius: 0 !important;
1670
- z-index: 1000000 !important;
1671
- }
1672
- }
1673
-
1674
- @media (prefers-reduced-motion: reduce) {
1675
- * { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
1676
- }
1677
- </style>
1678
- `;
1679
- }
1680
- _getMessageHTML(message) {
1681
- const isUser = message.sender_type === "user";
1682
- const isAi = message.sender_type === "ai";
1683
- const isReadByAgent = isUser && this._isMessageReadByAgent(message.created_at);
1684
- const primary = this._theme.primaryColor;
1685
- if (isUser) {
1686
- return `
1687
- <div class="vtilt-msg" style="
1688
- display: flex;
1689
- flex-direction: column;
1690
- align-items: flex-end;
1691
- ">
1692
- <div style="
1693
- max-width: 80%;
1694
- padding: 12px 16px;
1695
- background: ${primary};
1696
- color: white;
1697
- border-radius: 20px 20px 4px 20px;
1698
- font-size: 15px;
1699
- line-height: 1.45;
1700
- word-wrap: break-word;
1701
- overflow-wrap: break-word;
1702
- ">${this._escapeHTML(message.content)}</div>
1703
- <div style="font-size: 12px; color: #888888; margin-top: 6px; display: flex; align-items: center; gap: 4px;">
1704
- ${this._formatTime(message.created_at)}
1705
- ${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>` : ""}
1706
- </div>
1707
- </div>
1708
- `;
1709
- }
1710
- const senderLabel = isAi
1711
- ? "AI Assistant"
1712
- : message.sender_name || "Support";
1713
- return `
1714
- <div class="vtilt-msg" style="
1715
- display: flex;
1716
- gap: 10px;
1717
- align-items: flex-end;
1718
- ">
1719
- <div style="
1720
- width: 32px;
1721
- height: 32px;
1722
- border-radius: 50%;
1723
- background: ${isAi ? primary : "#DEDEDE"};
1724
- display: flex;
1725
- align-items: center;
1726
- justify-content: center;
1727
- flex-shrink: 0;
1728
- ">
1729
- ${isAi
1730
- ? `<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>`
1731
- : `<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>`}
1732
- </div>
1733
- <div style="flex: 1; min-width: 0; display: flex; flex-direction: column; align-items: flex-start;">
1734
- <div style="
1735
- max-width: 85%;
1736
- padding: 12px 16px;
1737
- background: #ffffff;
1738
- color: #000000;
1739
- border-radius: 20px 20px 20px 4px;
1740
- font-size: 15px;
1741
- line-height: 1.45;
1742
- word-wrap: break-word;
1743
- overflow-wrap: break-word;
1744
- ">${this._escapeHTML(message.content)}</div>
1745
- <div style="font-size: 12px; color: #888888; margin-top: 6px; margin-left: 4px;">
1746
- ${senderLabel} · ${this._formatTime(message.created_at)}
1747
- </div>
1748
- </div>
1749
- </div>
1750
- `;
1751
- }
1752
- _isMessageReadByAgent(messageCreatedAt) {
1753
- if (!this._state.agentLastReadAt) {
1754
- return false;
1755
- }
1756
- return new Date(messageCreatedAt) <= new Date(this._state.agentLastReadAt);
1757
- }
1758
- // ============================================================================
1759
- // Private - Utilities
1760
- // ============================================================================
1761
- /**
1762
- * Handle streaming AI response
1763
- * Industry standard: Real-time streaming with progressive UI updates
1764
- */
1765
- async _handleStreamingResponse(response, userMessage, channelId) {
1766
- var _a, _b, _c;
1767
- // Create temporary AI message for streaming
1768
- const tempAiMessage = {
1769
- id: `temp-ai-${Date.now()}`,
1770
- channel_id: channelId,
1771
- sender_type: "ai",
1772
- sender_id: null,
1773
- sender_name: "AI Assistant",
1774
- sender_avatar_url: null,
1775
- content: "",
1776
- content_type: "text",
1777
- metadata: {},
1778
- created_at: new Date().toISOString(),
1779
- };
1780
- this._state.messages.push(tempAiMessage);
1781
- this._updateUI();
1782
- const reader = (_a = response.body) === null || _a === void 0 ? void 0 : _a.getReader();
1783
- if (!reader) {
1784
- console.error(`${LOGGER_PREFIX} No response body reader available`);
1785
- this._removeTempMessage(tempAiMessage.id);
1786
- return;
1787
- }
1788
- const decoder = new TextDecoder();
1789
- let fullText = "";
1790
- let updateThrottle = null;
1791
- try {
1792
- while (true) {
1793
- const { done, value } = await reader.read();
1794
- if (done)
1795
- break;
1796
- // Decode chunk and append
1797
- const chunk = decoder.decode(value, { stream: true });
1798
- fullText += chunk;
1799
- // Throttle UI updates for performance (update every 50ms)
1800
- if (!updateThrottle) {
1801
- updateThrottle = setTimeout(() => {
1802
- const aiMessageIndex = this._state.messages.findIndex((m) => m.id === tempAiMessage.id);
1803
- if (aiMessageIndex !== -1) {
1804
- this._state.messages[aiMessageIndex].content = fullText;
1805
- this._updateUI();
1806
- }
1807
- updateThrottle = null;
1808
- }, 50);
1809
- }
1810
- }
1811
- // Final update with complete text
1812
- const aiMessageIndex = this._state.messages.findIndex((m) => m.id === tempAiMessage.id);
1813
- if (aiMessageIndex !== -1) {
1814
- this._state.messages[aiMessageIndex].content = fullText;
1815
- this._updateUI();
1816
- }
1817
- // Track event after streaming completes
1818
- this._trackEvent(types_1.CHAT_EVENTS.MESSAGE_SENT, {
1819
- $channel_id: channelId,
1820
- $message_id: userMessage.id,
1821
- $content_preview: userMessage.content.substring(0, 100),
1822
- $sender_type: "user",
1823
- $ai_mode: (_c = (_b = this._state.channel) === null || _b === void 0 ? void 0 : _b.ai_mode) !== null && _c !== void 0 ? _c : true,
1824
- $word_count: userMessage.content.split(/\s+/).length,
1825
- });
1826
- // Note: Complete AI message will arrive via Ably and replace temp message
1827
- }
1828
- catch (error) {
1829
- console.error(`${LOGGER_PREFIX} Failed to handle streaming response:`, error);
1830
- this._removeTempMessage(tempAiMessage.id);
1831
- }
1832
- finally {
1833
- reader.releaseLock();
1834
- if (updateThrottle) {
1835
- clearTimeout(updateThrottle);
1836
- }
1837
- }
1838
- }
1839
- /**
1840
- * Remove temporary message by ID
1841
- */
1842
- _removeTempMessage(messageId) {
1843
- this._state.messages = this._state.messages.filter((m) => m.id !== messageId);
1844
- this._updateUI();
1845
- }
1846
- async _apiRequest(endpoint, options = {}) {
1847
- const config = this._instance.getConfig();
1848
- const apiHost = config.api_host || "";
1849
- const token = config.token || "";
1850
- // Use & if endpoint already has query params, otherwise use ?
1851
- const separator = endpoint.includes("?") ? "&" : "?";
1852
- const url = `${apiHost}${endpoint}${separator}token=${encodeURIComponent(token)}`;
1853
- try {
1854
- const response = await fetch(url, {
1855
- ...options,
1856
- headers: {
1857
- "Content-Type": "application/json",
1858
- ...options.headers,
1859
- },
1860
- });
1861
- if (!response.ok) {
1862
- throw new Error(`API error: ${response.status}`);
1863
- }
1864
- return await response.json();
1865
- }
1866
- catch (error) {
1867
- console.error(`${LOGGER_PREFIX} API request failed:`, error);
1868
- return null;
1869
- }
1870
- }
1871
- _trackEvent(event, properties) {
1872
- this._instance.capture(event, properties);
1873
- }
1874
- _getTimeOpen() {
1875
- // TODO: Track actual open time
1876
- return 0;
1877
- }
1878
- _escapeHTML(text) {
1879
- if (!globals_1.document) {
1880
- return text;
1881
- }
1882
- const div = globals_1.document.createElement("div");
1883
- div.textContent = text;
1884
- return div.innerHTML;
1885
- }
1886
- _formatTime(isoString) {
1887
- const date = new Date(isoString);
1888
- return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
1889
- }
1890
- }
1891
- exports.LazyLoadedChat = LazyLoadedChat;