@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.
- package/dist/all-external-dependencies.js.map +1 -1
- package/dist/array.full.js +1 -1
- package/dist/array.full.js.map +1 -1
- package/dist/array.js +1 -1
- package/dist/array.js.map +1 -1
- package/dist/array.no-external.js +1 -1
- package/dist/array.no-external.js.map +1 -1
- package/dist/chat.js +1 -1
- package/dist/chat.js.map +1 -1
- package/dist/extensions/chat/chat-wrapper.d.ts +25 -1
- package/dist/extensions/chat/chat.d.ts +17 -19
- package/dist/extensions/chat/types.d.ts +3 -1
- package/dist/external-scripts-loader.js +1 -1
- package/dist/external-scripts-loader.js.map +1 -1
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/module.d.ts +43 -0
- package/dist/module.js +1 -1
- package/dist/module.js.map +1 -1
- package/dist/module.no-external.d.ts +43 -0
- package/dist/module.no-external.js +1 -1
- package/dist/module.no-external.js.map +1 -1
- package/dist/recorder.js.map +1 -1
- package/dist/utils/globals.d.ts +29 -0
- package/dist/web-vitals.js.map +1 -1
- package/lib/entrypoints/external-scripts-loader.js +1 -1
- package/lib/extensions/chat/chat-wrapper.d.ts +25 -1
- package/lib/extensions/chat/chat-wrapper.js +42 -0
- package/lib/extensions/chat/chat.d.ts +17 -19
- package/lib/extensions/chat/chat.js +828 -362
- package/lib/extensions/chat/types.d.ts +3 -1
- package/lib/utils/globals.d.ts +29 -0
- package/package.json +1 -1
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Lazy
|
|
4
|
-
*
|
|
5
|
-
* The actual chat widget implementation that is loaded on demand.
|
|
6
|
-
* This file is bundled into chat.js and loaded when chat is enabled.
|
|
7
|
-
*
|
|
8
|
-
* Uses Ably for real-time messaging.
|
|
3
|
+
* Chat Widget - Lazy loaded chat implementation using Ably for real-time messaging.
|
|
9
4
|
*/
|
|
10
5
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
11
6
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
@@ -21,18 +16,17 @@ const LOGGER_PREFIX = "[Chat]";
|
|
|
21
16
|
// ============================================================================
|
|
22
17
|
const DEFAULT_POSITION = "bottom-right";
|
|
23
18
|
const DEFAULT_THEME = {
|
|
24
|
-
primaryColor: "#
|
|
25
|
-
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
26
|
-
borderRadius: "
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
//
|
|
119
|
-
if (
|
|
120
|
-
this.
|
|
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
|
-
|
|
136
|
-
this.
|
|
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
|
|
166
|
-
if (!this._state.channel) {
|
|
167
|
-
|
|
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.
|
|
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(
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
457
|
+
const tokenResponse = await this._apiRequest(API.ablyToken, {
|
|
384
458
|
method: "POST",
|
|
385
459
|
body: JSON.stringify({
|
|
386
|
-
distinct_id: this.
|
|
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(
|
|
473
|
+
const refreshResponse = await this._apiRequest(API.ablyToken, {
|
|
401
474
|
method: "POST",
|
|
402
475
|
body: JSON.stringify({
|
|
403
|
-
distinct_id: this.
|
|
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
|
-
|
|
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
|
-
|
|
443
|
-
|
|
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
|
|
496
|
-
|
|
497
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
647
|
+
sender_id: this._distinctId,
|
|
634
648
|
sender_name: null,
|
|
635
649
|
is_typing: isTyping,
|
|
636
650
|
});
|
|
637
651
|
}
|
|
638
|
-
catch (
|
|
639
|
-
|
|
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
|
|
677
|
-
this.
|
|
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 =
|
|
689
|
-
|
|
690
|
-
|
|
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
|
-
|
|
698
|
-
var _a;
|
|
699
|
-
const
|
|
700
|
-
if (!
|
|
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
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
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
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
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
|
-
|
|
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: ${
|
|
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
|
|
1166
|
+
const primary = this._theme.primaryColor;
|
|
745
1167
|
return `
|
|
746
1168
|
width: 60px;
|
|
747
1169
|
height: 60px;
|
|
748
1170
|
border-radius: 50%;
|
|
749
|
-
background: ${
|
|
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
|
|
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
|
|
762
|
-
<path d="
|
|
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: -
|
|
768
|
-
right: -
|
|
769
|
-
background: #
|
|
1191
|
+
top: -4px;
|
|
1192
|
+
right: -4px;
|
|
1193
|
+
background: #E53935;
|
|
770
1194
|
color: white;
|
|
771
|
-
font-size:
|
|
772
|
-
font-weight:
|
|
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
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
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
|
|
799
|
-
const
|
|
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:
|
|
803
|
-
|
|
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:
|
|
1242
|
+
<div style="font-weight: 600; font-size: 17px; color: #000000;">${greeting}</div>
|
|
810
1243
|
<button class="vtilt-chat-close" style="
|
|
811
|
-
background:
|
|
1244
|
+
background: transparent;
|
|
812
1245
|
border: none;
|
|
813
|
-
color:
|
|
1246
|
+
color: #666666;
|
|
814
1247
|
cursor: pointer;
|
|
815
|
-
padding:
|
|
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="
|
|
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-
|
|
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
|
-
|
|
830
|
-
|
|
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
|
-
|
|
1283
|
+
z-index: 10;
|
|
837
1284
|
">
|
|
838
1285
|
<div style="
|
|
839
|
-
width:
|
|
840
|
-
height:
|
|
841
|
-
border:
|
|
842
|
-
border-top-color: ${
|
|
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
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
#vtilt-chat-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
.vtilt-chat-
|
|
902
|
-
|
|
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
|
-
|
|
905
|
-
|
|
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
|
|
914
|
-
// Check read status
|
|
1350
|
+
const isAi = message.sender_type === "ai";
|
|
915
1351
|
const isReadByAgent = isUser && this._isMessageReadByAgent(message.created_at);
|
|
916
|
-
const
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
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
|
|
934
|
-
|
|
1380
|
+
<div class="vtilt-msg" style="
|
|
1381
|
+
display: flex;
|
|
1382
|
+
gap: 10px;
|
|
1383
|
+
align-items: flex-end;
|
|
1384
|
+
">
|
|
935
1385
|
<div style="
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
border-radius:
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
${
|
|
946
|
-
|
|
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
|
-
|
|
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,
|