@v-tilt/browser 1.4.0 → 1.4.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.
- 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 +30 -2
- 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 +61 -19
- package/lib/extensions/chat/chat.d.ts +30 -2
- package/lib/extensions/chat/chat.js +966 -322
- package/lib/extensions/chat/types.d.ts +3 -1
- package/lib/utils/globals.d.ts +29 -0
- package/package.json +66 -66
|
@@ -21,18 +21,17 @@ const LOGGER_PREFIX = "[Chat]";
|
|
|
21
21
|
// ============================================================================
|
|
22
22
|
const DEFAULT_POSITION = "bottom-right";
|
|
23
23
|
const DEFAULT_THEME = {
|
|
24
|
-
primaryColor: "#
|
|
25
|
-
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
26
|
-
borderRadius: "
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
primaryColor: "#7B68EE", // Intercom-like purple
|
|
25
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
|
26
|
+
borderRadius: "16px",
|
|
27
|
+
};
|
|
28
|
+
// API endpoints
|
|
29
|
+
const API = {
|
|
30
|
+
channels: "/api/chat/widget/channels",
|
|
31
|
+
messages: "/api/chat/widget/messages",
|
|
32
|
+
read: "/api/chat/widget/read",
|
|
33
|
+
ablyToken: "/api/chat/ably-token",
|
|
30
34
|
};
|
|
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
35
|
// ============================================================================
|
|
37
36
|
// LazyLoadedChat Implementation
|
|
38
37
|
// ============================================================================
|
|
@@ -58,6 +57,7 @@ class LazyLoadedChat {
|
|
|
58
57
|
// Read tracking - initial position when widget opens (to show unread indicators)
|
|
59
58
|
this._initialUserReadAt = null;
|
|
60
59
|
this._isMarkingRead = false;
|
|
60
|
+
this._previousView = "list";
|
|
61
61
|
this._instance = instance;
|
|
62
62
|
this._config = {
|
|
63
63
|
enabled: true,
|
|
@@ -73,6 +73,10 @@ class LazyLoadedChat {
|
|
|
73
73
|
isConnected: false,
|
|
74
74
|
isLoading: false,
|
|
75
75
|
unreadCount: 0,
|
|
76
|
+
// Multi-channel support
|
|
77
|
+
currentView: "list",
|
|
78
|
+
channels: [],
|
|
79
|
+
// Current channel (when in conversation view)
|
|
76
80
|
channel: null,
|
|
77
81
|
messages: [],
|
|
78
82
|
isTyping: false,
|
|
@@ -102,6 +106,20 @@ class LazyLoadedChat {
|
|
|
102
106
|
get channel() {
|
|
103
107
|
return this._state.channel;
|
|
104
108
|
}
|
|
109
|
+
get channels() {
|
|
110
|
+
return this._state.channels;
|
|
111
|
+
}
|
|
112
|
+
get currentView() {
|
|
113
|
+
return this._state.currentView;
|
|
114
|
+
}
|
|
115
|
+
// Theme getter to avoid repeated DEFAULT_THEME fallback
|
|
116
|
+
get _theme() {
|
|
117
|
+
return this._config.theme || DEFAULT_THEME;
|
|
118
|
+
}
|
|
119
|
+
// Distinct ID getter for convenience
|
|
120
|
+
get _distinctId() {
|
|
121
|
+
return this._instance.getDistinctId() || "";
|
|
122
|
+
}
|
|
105
123
|
// ============================================================================
|
|
106
124
|
// Public API - Widget Control
|
|
107
125
|
// ============================================================================
|
|
@@ -111,34 +129,47 @@ class LazyLoadedChat {
|
|
|
111
129
|
return;
|
|
112
130
|
this._state.isOpen = true;
|
|
113
131
|
this._updateUI();
|
|
132
|
+
// Add opening animation
|
|
133
|
+
if (this._widget) {
|
|
134
|
+
this._widget.classList.remove("vtilt-closing");
|
|
135
|
+
this._widget.classList.add("vtilt-opening");
|
|
136
|
+
}
|
|
114
137
|
this._trackEvent(types_1.CHAT_EVENTS.WIDGET_OPENED, {
|
|
115
138
|
$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
139
|
$trigger: "api",
|
|
117
140
|
});
|
|
118
|
-
//
|
|
119
|
-
if
|
|
120
|
-
|
|
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();
|
|
141
|
+
// Show channel list view first (multi-channel support)
|
|
142
|
+
// Only fetch channels if not already loaded
|
|
143
|
+
if (this._state.channels.length === 0) {
|
|
144
|
+
this.getChannels();
|
|
129
145
|
}
|
|
146
|
+
// NOTE: Ably connection is now established only when entering conversation view
|
|
147
|
+
// This saves connection minutes when user is just browsing the channel list
|
|
130
148
|
}
|
|
131
149
|
close() {
|
|
132
150
|
if (!this._state.isOpen)
|
|
133
151
|
return;
|
|
134
152
|
const timeOpen = this._getTimeOpen();
|
|
135
|
-
|
|
136
|
-
this.
|
|
153
|
+
// Add closing animation
|
|
154
|
+
if (this._widget) {
|
|
155
|
+
this._widget.classList.remove("vtilt-opening");
|
|
156
|
+
this._widget.classList.add("vtilt-closing");
|
|
157
|
+
// Hide after animation completes
|
|
158
|
+
setTimeout(() => {
|
|
159
|
+
this._state.isOpen = false;
|
|
160
|
+
this._updateUI();
|
|
161
|
+
}, 200);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
this._state.isOpen = false;
|
|
165
|
+
this._updateUI();
|
|
166
|
+
}
|
|
137
167
|
this._trackEvent(types_1.CHAT_EVENTS.WIDGET_CLOSED, {
|
|
138
168
|
$time_open_seconds: timeOpen,
|
|
139
|
-
$messages_sent: this._state.messages.filter((m) => m.sender_type === "user")
|
|
140
|
-
.length,
|
|
169
|
+
$messages_sent: this._state.messages.filter((m) => m.sender_type === "user").length,
|
|
141
170
|
});
|
|
171
|
+
// Disconnect Ably when widget closes to save connection minutes
|
|
172
|
+
this._disconnectRealtime();
|
|
142
173
|
}
|
|
143
174
|
toggle() {
|
|
144
175
|
if (this._state.isOpen) {
|
|
@@ -157,15 +188,165 @@ class LazyLoadedChat {
|
|
|
157
188
|
this._updateUI();
|
|
158
189
|
}
|
|
159
190
|
// ============================================================================
|
|
191
|
+
// Public API - Channel Management (Multi-channel support)
|
|
192
|
+
// ============================================================================
|
|
193
|
+
/**
|
|
194
|
+
* Fetch/refresh the list of user's channels
|
|
195
|
+
*/
|
|
196
|
+
async getChannels() {
|
|
197
|
+
this._state.isLoading = true;
|
|
198
|
+
this._updateUI();
|
|
199
|
+
try {
|
|
200
|
+
const response = await this._apiRequest(`${API.channels}?distinct_id=${encodeURIComponent(this._distinctId)}`, {
|
|
201
|
+
method: "GET",
|
|
202
|
+
});
|
|
203
|
+
if (response) {
|
|
204
|
+
this._state.channels = response.channels || [];
|
|
205
|
+
// Calculate total unread count from all channels
|
|
206
|
+
this._state.unreadCount = this._state.channels.reduce((sum, ch) => sum + (ch.unread_count || 0), 0);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
console.error(`${LOGGER_PREFIX} Failed to fetch channels:`, error);
|
|
211
|
+
}
|
|
212
|
+
finally {
|
|
213
|
+
this._state.isLoading = false;
|
|
214
|
+
this._updateUI();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Select a channel and load its messages
|
|
219
|
+
*/
|
|
220
|
+
async selectChannel(channelId) {
|
|
221
|
+
this._state.isLoading = true;
|
|
222
|
+
this._updateUI();
|
|
223
|
+
try {
|
|
224
|
+
const response = await this._apiRequest(`${API.channels}/${channelId}?distinct_id=${encodeURIComponent(this._distinctId)}`, {
|
|
225
|
+
method: "GET",
|
|
226
|
+
});
|
|
227
|
+
if (response) {
|
|
228
|
+
this._state.channel = response.channel;
|
|
229
|
+
this._state.messages = response.messages || [];
|
|
230
|
+
this._state.currentView = "conversation";
|
|
231
|
+
// Initialize read cursors from channel
|
|
232
|
+
this._state.agentLastReadAt =
|
|
233
|
+
response.channel.agent_last_read_at || null;
|
|
234
|
+
this._initialUserReadAt = response.channel.user_last_read_at || null;
|
|
235
|
+
// Connect to Ably for this channel
|
|
236
|
+
this._connectRealtime();
|
|
237
|
+
// Auto-mark unread messages as read if widget is open
|
|
238
|
+
if (this._state.isOpen) {
|
|
239
|
+
this._autoMarkAsRead();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
console.error(`${LOGGER_PREFIX} Failed to select channel:`, error);
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
this._state.isLoading = false;
|
|
248
|
+
this._updateUI();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Create a new channel and enter it
|
|
253
|
+
*/
|
|
254
|
+
async createChannel() {
|
|
255
|
+
var _a;
|
|
256
|
+
this._state.isLoading = true;
|
|
257
|
+
this._updateUI();
|
|
258
|
+
try {
|
|
259
|
+
const response = await this._apiRequest(API.channels, {
|
|
260
|
+
method: "POST",
|
|
261
|
+
body: JSON.stringify({
|
|
262
|
+
distinct_id: this._distinctId,
|
|
263
|
+
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,
|
|
264
|
+
page_title: globals_1.document === null || globals_1.document === void 0 ? void 0 : globals_1.document.title,
|
|
265
|
+
}),
|
|
266
|
+
});
|
|
267
|
+
if (response) {
|
|
268
|
+
this._state.channel = response.channel;
|
|
269
|
+
this._state.messages = response.messages || [];
|
|
270
|
+
this._state.currentView = "conversation";
|
|
271
|
+
// Initialize read cursors
|
|
272
|
+
this._state.agentLastReadAt =
|
|
273
|
+
response.channel.agent_last_read_at || null;
|
|
274
|
+
this._initialUserReadAt = response.channel.user_last_read_at || null;
|
|
275
|
+
// Add new channel to the list
|
|
276
|
+
const newChannelSummary = {
|
|
277
|
+
id: response.channel.id,
|
|
278
|
+
status: response.channel.status,
|
|
279
|
+
ai_mode: response.channel.ai_mode,
|
|
280
|
+
last_message_at: response.channel.last_message_at,
|
|
281
|
+
last_message_preview: response.channel.last_message_preview,
|
|
282
|
+
last_message_sender: response.channel.last_message_sender,
|
|
283
|
+
unread_count: response.channel.unread_count,
|
|
284
|
+
user_last_read_at: response.channel.user_last_read_at,
|
|
285
|
+
created_at: response.channel.created_at,
|
|
286
|
+
};
|
|
287
|
+
this._state.channels.unshift(newChannelSummary);
|
|
288
|
+
// Track channel started
|
|
289
|
+
this._trackEvent(types_1.CHAT_EVENTS.STARTED, {
|
|
290
|
+
$channel_id: response.channel.id,
|
|
291
|
+
$initiated_by: "user",
|
|
292
|
+
$ai_mode: response.channel.ai_mode,
|
|
293
|
+
});
|
|
294
|
+
// Connect to Ably for this channel
|
|
295
|
+
this._connectRealtime();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
console.error(`${LOGGER_PREFIX} Failed to create channel:`, error);
|
|
300
|
+
}
|
|
301
|
+
finally {
|
|
302
|
+
this._state.isLoading = false;
|
|
303
|
+
this._updateUI();
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Go back to channel list from conversation view
|
|
308
|
+
*/
|
|
309
|
+
goToChannelList() {
|
|
310
|
+
// Disconnect Ably when leaving conversation to save connection minutes
|
|
311
|
+
this._disconnectRealtime();
|
|
312
|
+
// Update the channel in the list with latest data
|
|
313
|
+
if (this._state.channel) {
|
|
314
|
+
const channelIndex = this._state.channels.findIndex((ch) => { var _a; return ch.id === ((_a = this._state.channel) === null || _a === void 0 ? void 0 : _a.id); });
|
|
315
|
+
if (channelIndex !== -1) {
|
|
316
|
+
this._state.channels[channelIndex] = {
|
|
317
|
+
...this._state.channels[channelIndex],
|
|
318
|
+
last_message_at: this._state.messages.length > 0
|
|
319
|
+
? this._state.messages[this._state.messages.length - 1].created_at
|
|
320
|
+
: this._state.channels[channelIndex].last_message_at,
|
|
321
|
+
last_message_preview: this._state.messages.length > 0
|
|
322
|
+
? this._state.messages[this._state.messages.length - 1].content.substring(0, 100)
|
|
323
|
+
: this._state.channels[channelIndex].last_message_preview,
|
|
324
|
+
last_message_sender: this._state.messages.length > 0
|
|
325
|
+
? this._state.messages[this._state.messages.length - 1].sender_type
|
|
326
|
+
: this._state.channels[channelIndex].last_message_sender,
|
|
327
|
+
unread_count: 0, // We just viewed it
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// Clear current channel state
|
|
332
|
+
this._state.channel = null;
|
|
333
|
+
this._state.messages = [];
|
|
334
|
+
this._state.currentView = "list";
|
|
335
|
+
this._state.isTyping = false;
|
|
336
|
+
this._state.typingSender = null;
|
|
337
|
+
this._updateUI();
|
|
338
|
+
}
|
|
339
|
+
// ============================================================================
|
|
160
340
|
// Public API - Messaging
|
|
161
341
|
// ============================================================================
|
|
162
342
|
async sendMessage(content) {
|
|
163
343
|
var _a, _b, _c, _d;
|
|
164
344
|
if (!content.trim())
|
|
165
345
|
return;
|
|
166
|
-
// Ensure channel
|
|
167
|
-
if (!this._state.channel) {
|
|
168
|
-
|
|
346
|
+
// Ensure we're in conversation view with a channel
|
|
347
|
+
if (!this._state.channel || this._state.currentView !== "conversation") {
|
|
348
|
+
console.error(`${LOGGER_PREFIX} Cannot send message: not in conversation view`);
|
|
349
|
+
return;
|
|
169
350
|
}
|
|
170
351
|
const channelId = (_a = this._state.channel) === null || _a === void 0 ? void 0 : _a.id;
|
|
171
352
|
if (!channelId) {
|
|
@@ -177,7 +358,7 @@ class LazyLoadedChat {
|
|
|
177
358
|
id: `temp-${Date.now()}`,
|
|
178
359
|
channel_id: channelId,
|
|
179
360
|
sender_type: "user",
|
|
180
|
-
sender_id: this.
|
|
361
|
+
sender_id: this._distinctId || null,
|
|
181
362
|
sender_name: null,
|
|
182
363
|
sender_avatar_url: null,
|
|
183
364
|
content,
|
|
@@ -189,11 +370,11 @@ class LazyLoadedChat {
|
|
|
189
370
|
this._updateUI();
|
|
190
371
|
try {
|
|
191
372
|
// Send to API
|
|
192
|
-
const response = await this._apiRequest(
|
|
373
|
+
const response = await this._apiRequest(API.messages, {
|
|
193
374
|
method: "POST",
|
|
194
375
|
body: JSON.stringify({
|
|
195
376
|
channel_id: channelId,
|
|
196
|
-
distinct_id: this.
|
|
377
|
+
distinct_id: this._distinctId,
|
|
197
378
|
content,
|
|
198
379
|
}),
|
|
199
380
|
});
|
|
@@ -239,18 +420,19 @@ class LazyLoadedChat {
|
|
|
239
420
|
if (!latestMessage)
|
|
240
421
|
return;
|
|
241
422
|
// Check if there are unread agent messages
|
|
242
|
-
const hasUnreadAgentMessages = this._state.messages.some((m) => (m.sender_type === "agent" || m.sender_type === "ai") &&
|
|
423
|
+
const hasUnreadAgentMessages = this._state.messages.some((m) => (m.sender_type === "agent" || m.sender_type === "ai") &&
|
|
424
|
+
!this._isMessageReadByUser(m.created_at));
|
|
243
425
|
if (!hasUnreadAgentMessages)
|
|
244
426
|
return;
|
|
245
427
|
this._state.unreadCount = 0;
|
|
246
428
|
this._isMarkingRead = true;
|
|
247
429
|
this._updateUI();
|
|
248
430
|
// API call to update read cursor with latest message timestamp
|
|
249
|
-
this._apiRequest(
|
|
431
|
+
this._apiRequest(API.read, {
|
|
250
432
|
method: "POST",
|
|
251
433
|
body: JSON.stringify({
|
|
252
434
|
channel_id: this._state.channel.id,
|
|
253
|
-
distinct_id: this.
|
|
435
|
+
distinct_id: this._distinctId,
|
|
254
436
|
read_at: latestMessage.created_at,
|
|
255
437
|
}),
|
|
256
438
|
})
|
|
@@ -323,52 +505,6 @@ class LazyLoadedChat {
|
|
|
323
505
|
console.info(`${LOGGER_PREFIX} destroyed`);
|
|
324
506
|
}
|
|
325
507
|
// ============================================================================
|
|
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 = response.channel.agent_last_read_at || null;
|
|
346
|
-
this._initialUserReadAt = response.channel.user_last_read_at || null;
|
|
347
|
-
// Track channel started (only if new)
|
|
348
|
-
if (response.messages.length === 0 || response.messages.length === 1) {
|
|
349
|
-
this._trackEvent(types_1.CHAT_EVENTS.STARTED, {
|
|
350
|
-
$channel_id: response.channel.id,
|
|
351
|
-
$initiated_by: "user",
|
|
352
|
-
$ai_mode: response.channel.ai_mode,
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
// Connect to Ably now that we have channel ID
|
|
356
|
-
this._connectRealtime();
|
|
357
|
-
// Auto-mark unread messages as read if widget is open
|
|
358
|
-
if (this._state.isOpen) {
|
|
359
|
-
this._autoMarkAsRead();
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
catch (error) {
|
|
364
|
-
console.error(`${LOGGER_PREFIX} Failed to initialize channel:`, error);
|
|
365
|
-
}
|
|
366
|
-
finally {
|
|
367
|
-
this._state.isLoading = false;
|
|
368
|
-
this._updateUI();
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
// ============================================================================
|
|
372
508
|
// Private - Ably Realtime Connection
|
|
373
509
|
// ============================================================================
|
|
374
510
|
async _connectRealtime() {
|
|
@@ -379,10 +515,10 @@ class LazyLoadedChat {
|
|
|
379
515
|
this._notifyConnectionChange(false);
|
|
380
516
|
try {
|
|
381
517
|
// Get Ably token from server
|
|
382
|
-
const tokenResponse = await this._apiRequest(
|
|
518
|
+
const tokenResponse = await this._apiRequest(API.ablyToken, {
|
|
383
519
|
method: "POST",
|
|
384
520
|
body: JSON.stringify({
|
|
385
|
-
distinct_id: this.
|
|
521
|
+
distinct_id: this._distinctId,
|
|
386
522
|
channel_id: this._state.channel.id,
|
|
387
523
|
}),
|
|
388
524
|
});
|
|
@@ -396,10 +532,10 @@ class LazyLoadedChat {
|
|
|
396
532
|
authCallback: async (_, callback) => {
|
|
397
533
|
var _a;
|
|
398
534
|
try {
|
|
399
|
-
const refreshResponse = await this._apiRequest(
|
|
535
|
+
const refreshResponse = await this._apiRequest(API.ablyToken, {
|
|
400
536
|
method: "POST",
|
|
401
537
|
body: JSON.stringify({
|
|
402
|
-
distinct_id: this.
|
|
538
|
+
distinct_id: this._distinctId,
|
|
403
539
|
channel_id: (_a = this._state.channel) === null || _a === void 0 ? void 0 : _a.id,
|
|
404
540
|
}),
|
|
405
541
|
});
|
|
@@ -493,9 +629,12 @@ class LazyLoadedChat {
|
|
|
493
629
|
return;
|
|
494
630
|
// Skip user's own messages - we already have them from optimistic updates
|
|
495
631
|
// The sender_id for user messages is the distinct_id
|
|
496
|
-
if (message.sender_type === "user" &&
|
|
632
|
+
if (message.sender_type === "user" &&
|
|
633
|
+
message.sender_id === this._distinctId) {
|
|
497
634
|
// But DO replace temp message with real one if present
|
|
498
|
-
const tempIndex = this._state.messages.findIndex((m) => m.id.startsWith("temp-") &&
|
|
635
|
+
const tempIndex = this._state.messages.findIndex((m) => m.id.startsWith("temp-") &&
|
|
636
|
+
m.content === message.content &&
|
|
637
|
+
m.sender_type === "user");
|
|
499
638
|
if (tempIndex !== -1) {
|
|
500
639
|
this._state.messages[tempIndex] = message;
|
|
501
640
|
this._updateUI();
|
|
@@ -533,7 +672,8 @@ class LazyLoadedChat {
|
|
|
533
672
|
// Only show typing for non-user senders
|
|
534
673
|
if (event.sender_type === "user")
|
|
535
674
|
return;
|
|
536
|
-
const senderName = event.sender_name ||
|
|
675
|
+
const senderName = event.sender_name ||
|
|
676
|
+
(event.sender_type === "ai" ? "AI Assistant" : "Agent");
|
|
537
677
|
this._state.isTyping = event.is_typing;
|
|
538
678
|
this._state.typingSender = event.is_typing ? senderName : null;
|
|
539
679
|
// Notify callbacks
|
|
@@ -578,27 +718,14 @@ class LazyLoadedChat {
|
|
|
578
718
|
globals_1.document.body.appendChild(this._container);
|
|
579
719
|
}
|
|
580
720
|
_attachEventListeners() {
|
|
581
|
-
var _a, _b
|
|
721
|
+
var _a, _b;
|
|
582
722
|
// Bubble click
|
|
583
723
|
(_a = this._bubble) === null || _a === void 0 ? void 0 : _a.addEventListener("click", () => this.toggle());
|
|
584
|
-
// Close button
|
|
724
|
+
// Close button (initial attachment - re-attached in _updateHeader)
|
|
585
725
|
const closeBtn = (_b = this._widget) === null || _b === void 0 ? void 0 : _b.querySelector(".vtilt-chat-close");
|
|
586
726
|
closeBtn === null || closeBtn === void 0 ? void 0 : closeBtn.addEventListener("click", () => this.close());
|
|
587
|
-
//
|
|
588
|
-
|
|
589
|
-
sendBtn === null || sendBtn === void 0 ? void 0 : sendBtn.addEventListener("click", () => this._handleSend());
|
|
590
|
-
// Input enter key and typing indicator
|
|
591
|
-
const input = (_d = this._widget) === null || _d === void 0 ? void 0 : _d.querySelector(".vtilt-chat-input");
|
|
592
|
-
input === null || input === void 0 ? void 0 : input.addEventListener("keypress", (e) => {
|
|
593
|
-
if (e.key === "Enter" && !e.shiftKey) {
|
|
594
|
-
e.preventDefault();
|
|
595
|
-
this._handleSend();
|
|
596
|
-
}
|
|
597
|
-
});
|
|
598
|
-
// Send typing indicator on input
|
|
599
|
-
input === null || input === void 0 ? void 0 : input.addEventListener("input", () => {
|
|
600
|
-
this._handleUserTyping();
|
|
601
|
-
});
|
|
727
|
+
// Note: Channel list and conversation listeners are attached dynamically
|
|
728
|
+
// in _attachChannelListListeners() and _attachConversationListeners()
|
|
602
729
|
}
|
|
603
730
|
_handleUserTyping() {
|
|
604
731
|
// Don't send typing if not connected to Ably
|
|
@@ -625,7 +752,7 @@ class LazyLoadedChat {
|
|
|
625
752
|
try {
|
|
626
753
|
this._typingChannel.publish("typing", {
|
|
627
754
|
sender_type: "user",
|
|
628
|
-
sender_id: this.
|
|
755
|
+
sender_id: this._distinctId,
|
|
629
756
|
sender_name: null,
|
|
630
757
|
is_typing: isTyping,
|
|
631
758
|
});
|
|
@@ -668,8 +795,39 @@ class LazyLoadedChat {
|
|
|
668
795
|
this._state.unreadCount > 0 ? "flex" : "none";
|
|
669
796
|
badge.textContent = String(this._state.unreadCount);
|
|
670
797
|
}
|
|
671
|
-
//
|
|
672
|
-
this.
|
|
798
|
+
// Detect view change for animation
|
|
799
|
+
const viewChanged = this._previousView !== this._state.currentView;
|
|
800
|
+
const animationClass = viewChanged
|
|
801
|
+
? this._state.currentView === "conversation"
|
|
802
|
+
? "vtilt-view-enter-right"
|
|
803
|
+
: "vtilt-view-enter-left"
|
|
804
|
+
: "";
|
|
805
|
+
// Update content based on current view
|
|
806
|
+
const contentContainer = this._widget.querySelector(".vtilt-chat-content");
|
|
807
|
+
if (contentContainer) {
|
|
808
|
+
// Remove previous animation classes
|
|
809
|
+
contentContainer.classList.remove("vtilt-view-enter-right", "vtilt-view-enter-left");
|
|
810
|
+
if (this._state.currentView === "list") {
|
|
811
|
+
contentContainer.innerHTML = this._getChannelListHTML();
|
|
812
|
+
this._attachChannelListListeners();
|
|
813
|
+
}
|
|
814
|
+
else {
|
|
815
|
+
contentContainer.innerHTML = this._getConversationHTML();
|
|
816
|
+
this._attachConversationListeners();
|
|
817
|
+
// Render messages in conversation view
|
|
818
|
+
this._renderMessages();
|
|
819
|
+
}
|
|
820
|
+
// Add animation class if view changed
|
|
821
|
+
if (animationClass) {
|
|
822
|
+
// Force reflow to restart animation
|
|
823
|
+
void contentContainer.offsetWidth;
|
|
824
|
+
contentContainer.classList.add(animationClass);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
// Update previous view
|
|
828
|
+
this._previousView = this._state.currentView;
|
|
829
|
+
// Update header based on view
|
|
830
|
+
this._updateHeader();
|
|
673
831
|
// Update loading state
|
|
674
832
|
const loader = this._widget.querySelector(".vtilt-chat-loader");
|
|
675
833
|
if (loader) {
|
|
@@ -677,38 +835,475 @@ class LazyLoadedChat {
|
|
|
677
835
|
? "flex"
|
|
678
836
|
: "none";
|
|
679
837
|
}
|
|
680
|
-
// Update typing indicator
|
|
838
|
+
// Update typing indicator (only in conversation view)
|
|
681
839
|
const typing = this._widget.querySelector(".vtilt-chat-typing");
|
|
682
840
|
if (typing) {
|
|
683
|
-
typing.style.display =
|
|
684
|
-
|
|
685
|
-
|
|
841
|
+
typing.style.display =
|
|
842
|
+
this._state.isTyping && this._state.currentView === "conversation"
|
|
843
|
+
? "flex"
|
|
844
|
+
: "none";
|
|
686
845
|
const typingText = typing.querySelector("span");
|
|
687
846
|
if (typingText && this._state.typingSender) {
|
|
688
847
|
typingText.textContent = `${this._state.typingSender} is typing...`;
|
|
689
848
|
}
|
|
690
849
|
}
|
|
691
850
|
}
|
|
851
|
+
_updateHeader() {
|
|
852
|
+
var _a, _b;
|
|
853
|
+
const header = (_a = this._widget) === null || _a === void 0 ? void 0 : _a.querySelector(".vtilt-chat-header");
|
|
854
|
+
if (!header)
|
|
855
|
+
return;
|
|
856
|
+
const greeting = this._config.greeting || "Messages";
|
|
857
|
+
const primary = this._theme.primaryColor;
|
|
858
|
+
if (this._state.currentView === "list") {
|
|
859
|
+
header.style.cssText = `
|
|
860
|
+
background: #ffffff;
|
|
861
|
+
border-bottom: 1px solid #E5E5E5;
|
|
862
|
+
padding: 18px 16px;
|
|
863
|
+
padding-top: max(18px, env(safe-area-inset-top, 18px));
|
|
864
|
+
display: flex;
|
|
865
|
+
align-items: center;
|
|
866
|
+
justify-content: space-between;
|
|
867
|
+
min-height: 60px;
|
|
868
|
+
box-sizing: border-box;
|
|
869
|
+
flex-shrink: 0;
|
|
870
|
+
`;
|
|
871
|
+
header.innerHTML = `
|
|
872
|
+
<div style="font-weight: 600; font-size: 17px; color: #000000;">${greeting}</div>
|
|
873
|
+
<button class="vtilt-chat-close" style="
|
|
874
|
+
background: transparent;
|
|
875
|
+
border: none;
|
|
876
|
+
color: #666666;
|
|
877
|
+
cursor: pointer;
|
|
878
|
+
padding: 6px;
|
|
879
|
+
margin: -6px;
|
|
880
|
+
border-radius: 4px;
|
|
881
|
+
display: flex;
|
|
882
|
+
align-items: center;
|
|
883
|
+
justify-content: center;
|
|
884
|
+
-webkit-tap-highlight-color: transparent;
|
|
885
|
+
">
|
|
886
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
887
|
+
<path d="M18 6L6 18M6 6l12 12"></path>
|
|
888
|
+
</svg>
|
|
889
|
+
</button>
|
|
890
|
+
`;
|
|
891
|
+
}
|
|
892
|
+
else {
|
|
893
|
+
const isAiMode = (_b = this._state.channel) === null || _b === void 0 ? void 0 : _b.ai_mode;
|
|
894
|
+
header.style.cssText = `
|
|
895
|
+
background: #ffffff;
|
|
896
|
+
border-bottom: 1px solid #E5E5E5;
|
|
897
|
+
padding: 12px 16px;
|
|
898
|
+
padding-top: max(12px, env(safe-area-inset-top, 12px));
|
|
899
|
+
display: flex;
|
|
900
|
+
align-items: center;
|
|
901
|
+
gap: 12px;
|
|
902
|
+
min-height: 60px;
|
|
903
|
+
box-sizing: border-box;
|
|
904
|
+
flex-shrink: 0;
|
|
905
|
+
`;
|
|
906
|
+
header.innerHTML = `
|
|
907
|
+
<button class="vtilt-chat-back" style="
|
|
908
|
+
background: transparent;
|
|
909
|
+
border: none;
|
|
910
|
+
color: #666666;
|
|
911
|
+
cursor: pointer;
|
|
912
|
+
padding: 6px;
|
|
913
|
+
margin-left: -6px;
|
|
914
|
+
border-radius: 4px;
|
|
915
|
+
display: flex;
|
|
916
|
+
align-items: center;
|
|
917
|
+
justify-content: center;
|
|
918
|
+
-webkit-tap-highlight-color: transparent;
|
|
919
|
+
">
|
|
920
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
921
|
+
<path d="M15 18l-6-6 6-6"></path>
|
|
922
|
+
</svg>
|
|
923
|
+
</button>
|
|
924
|
+
<div style="
|
|
925
|
+
width: 44px;
|
|
926
|
+
height: 44px;
|
|
927
|
+
border-radius: 50%;
|
|
928
|
+
background: ${isAiMode ? primary : "#DEDEDE"};
|
|
929
|
+
display: flex;
|
|
930
|
+
align-items: center;
|
|
931
|
+
justify-content: center;
|
|
932
|
+
flex-shrink: 0;
|
|
933
|
+
">
|
|
934
|
+
${isAiMode
|
|
935
|
+
? `<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>`
|
|
936
|
+
: `<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>`}
|
|
937
|
+
</div>
|
|
938
|
+
<div style="flex: 1; min-width: 0;">
|
|
939
|
+
<div style="font-weight: 600; font-size: 16px; color: #000000;">${isAiMode ? "AI Assistant" : "Support"}</div>
|
|
940
|
+
<div style="font-size: 13px; color: #16A34A; display: flex; align-items: center; gap: 5px; margin-top: 1px;">
|
|
941
|
+
<span style="width: 7px; height: 7px; background: #16A34A; border-radius: 50%;"></span>
|
|
942
|
+
Online
|
|
943
|
+
</div>
|
|
944
|
+
</div>
|
|
945
|
+
<button class="vtilt-chat-close" style="
|
|
946
|
+
background: transparent;
|
|
947
|
+
border: none;
|
|
948
|
+
color: #666666;
|
|
949
|
+
cursor: pointer;
|
|
950
|
+
padding: 6px;
|
|
951
|
+
margin-right: -6px;
|
|
952
|
+
border-radius: 4px;
|
|
953
|
+
display: flex;
|
|
954
|
+
align-items: center;
|
|
955
|
+
justify-content: center;
|
|
956
|
+
-webkit-tap-highlight-color: transparent;
|
|
957
|
+
">
|
|
958
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
959
|
+
<path d="M18 6L6 18M6 6l12 12"></path>
|
|
960
|
+
</svg>
|
|
961
|
+
</button>
|
|
962
|
+
`;
|
|
963
|
+
}
|
|
964
|
+
// Re-attach header event listeners
|
|
965
|
+
const closeBtn = header.querySelector(".vtilt-chat-close");
|
|
966
|
+
closeBtn === null || closeBtn === void 0 ? void 0 : closeBtn.addEventListener("click", () => this.close());
|
|
967
|
+
const backBtn = header.querySelector(".vtilt-chat-back");
|
|
968
|
+
backBtn === null || backBtn === void 0 ? void 0 : backBtn.addEventListener("click", () => this.goToChannelList());
|
|
969
|
+
}
|
|
970
|
+
_getChannelListHTML() {
|
|
971
|
+
const primary = this._theme.primaryColor;
|
|
972
|
+
if (this._state.channels.length === 0 && !this._state.isLoading) {
|
|
973
|
+
return `
|
|
974
|
+
<div style="
|
|
975
|
+
flex: 1;
|
|
976
|
+
display: flex;
|
|
977
|
+
flex-direction: column;
|
|
978
|
+
align-items: center;
|
|
979
|
+
justify-content: center;
|
|
980
|
+
padding: 48px 24px;
|
|
981
|
+
text-align: center;
|
|
982
|
+
animation: vtilt-fadein 0.4s ease;
|
|
983
|
+
">
|
|
984
|
+
<div style="
|
|
985
|
+
width: 72px;
|
|
986
|
+
height: 72px;
|
|
987
|
+
margin-bottom: 24px;
|
|
988
|
+
background: ${primary};
|
|
989
|
+
border-radius: 50%;
|
|
990
|
+
display: flex;
|
|
991
|
+
align-items: center;
|
|
992
|
+
justify-content: center;
|
|
993
|
+
animation: vtilt-bubble-pop 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275) 0.1s both;
|
|
994
|
+
">
|
|
995
|
+
<svg width="36" height="36" viewBox="0 0 24 24" fill="white">
|
|
996
|
+
<path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z"/>
|
|
997
|
+
</svg>
|
|
998
|
+
</div>
|
|
999
|
+
<div style="font-size: 18px; font-weight: 600; color: #000000; margin-bottom: 8px; animation: vtilt-fadein 0.4s ease 0.15s both;">No conversations yet</div>
|
|
1000
|
+
<div style="font-size: 15px; color: #666666; margin-bottom: 28px; line-height: 1.5; max-width: 280px; animation: vtilt-fadein 0.4s ease 0.2s both;">Questions? We're here to help. Start a conversation with us.</div>
|
|
1001
|
+
<button class="vtilt-chat-new-channel" style="
|
|
1002
|
+
background: ${primary};
|
|
1003
|
+
color: white;
|
|
1004
|
+
border: none;
|
|
1005
|
+
border-radius: 100px;
|
|
1006
|
+
padding: 14px 28px;
|
|
1007
|
+
cursor: pointer;
|
|
1008
|
+
font-weight: 500;
|
|
1009
|
+
font-size: 15px;
|
|
1010
|
+
-webkit-tap-highlight-color: transparent;
|
|
1011
|
+
touch-action: manipulation;
|
|
1012
|
+
display: flex;
|
|
1013
|
+
align-items: center;
|
|
1014
|
+
gap: 10px;
|
|
1015
|
+
box-shadow: 0 2px 8px rgba(123, 104, 238, 0.3);
|
|
1016
|
+
animation: vtilt-fadein 0.4s ease 0.25s both;
|
|
1017
|
+
">
|
|
1018
|
+
Send us a message
|
|
1019
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="white">
|
|
1020
|
+
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
|
|
1021
|
+
</svg>
|
|
1022
|
+
</button>
|
|
1023
|
+
</div>
|
|
1024
|
+
`;
|
|
1025
|
+
}
|
|
1026
|
+
const channelsHtml = this._state.channels
|
|
1027
|
+
.map((ch, index) => this._getChannelItemHTML(ch, index))
|
|
1028
|
+
.join("");
|
|
1029
|
+
return `
|
|
1030
|
+
<div style="flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch;">
|
|
1031
|
+
${channelsHtml}
|
|
1032
|
+
</div>
|
|
1033
|
+
<div style="
|
|
1034
|
+
padding: 16px;
|
|
1035
|
+
padding-bottom: max(16px, env(safe-area-inset-bottom, 16px));
|
|
1036
|
+
border-top: 1px solid #E5E5E5;
|
|
1037
|
+
">
|
|
1038
|
+
<button class="vtilt-chat-new-channel" style="
|
|
1039
|
+
width: 100%;
|
|
1040
|
+
background: ${primary};
|
|
1041
|
+
color: white;
|
|
1042
|
+
border: none;
|
|
1043
|
+
border-radius: 100px;
|
|
1044
|
+
padding: 14px 24px;
|
|
1045
|
+
cursor: pointer;
|
|
1046
|
+
font-weight: 500;
|
|
1047
|
+
font-size: 15px;
|
|
1048
|
+
display: flex;
|
|
1049
|
+
align-items: center;
|
|
1050
|
+
justify-content: center;
|
|
1051
|
+
gap: 10px;
|
|
1052
|
+
-webkit-tap-highlight-color: transparent;
|
|
1053
|
+
touch-action: manipulation;
|
|
1054
|
+
box-shadow: 0 2px 8px rgba(123, 104, 238, 0.3);
|
|
1055
|
+
">
|
|
1056
|
+
Send us a message
|
|
1057
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="white">
|
|
1058
|
+
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
|
|
1059
|
+
</svg>
|
|
1060
|
+
</button>
|
|
1061
|
+
</div>
|
|
1062
|
+
`;
|
|
1063
|
+
}
|
|
1064
|
+
_getChannelItemHTML(channel, index = 0) {
|
|
1065
|
+
const hasUnread = channel.unread_count > 0;
|
|
1066
|
+
const timeStr = this._formatRelativeTime(channel.last_message_at || channel.created_at);
|
|
1067
|
+
const preview = channel.last_message_preview || "No messages yet";
|
|
1068
|
+
const primary = this._theme.primaryColor;
|
|
1069
|
+
const senderPrefix = channel.last_message_sender === "user" ? "You: " : "";
|
|
1070
|
+
const animationDelay = Math.min(index * 0.05, 0.3); // Max 300ms total delay
|
|
1071
|
+
return `
|
|
1072
|
+
<div class="vtilt-channel-item" data-channel-id="${channel.id}" style="
|
|
1073
|
+
padding: 14px 16px;
|
|
1074
|
+
cursor: pointer;
|
|
1075
|
+
-webkit-tap-highlight-color: transparent;
|
|
1076
|
+
touch-action: manipulation;
|
|
1077
|
+
display: flex;
|
|
1078
|
+
align-items: center;
|
|
1079
|
+
gap: 12px;
|
|
1080
|
+
background: white;
|
|
1081
|
+
border-bottom: 1px solid #EEEEEE;
|
|
1082
|
+
animation: vtilt-item-in 0.3s cubic-bezier(0.16, 1, 0.3, 1) ${animationDelay}s both;
|
|
1083
|
+
">
|
|
1084
|
+
<div style="
|
|
1085
|
+
width: 48px;
|
|
1086
|
+
height: 48px;
|
|
1087
|
+
border-radius: 50%;
|
|
1088
|
+
background: ${channel.ai_mode ? primary : "#DEDEDE"};
|
|
1089
|
+
display: flex;
|
|
1090
|
+
align-items: center;
|
|
1091
|
+
justify-content: center;
|
|
1092
|
+
flex-shrink: 0;
|
|
1093
|
+
">
|
|
1094
|
+
${channel.ai_mode
|
|
1095
|
+
? `<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>`
|
|
1096
|
+
: `<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>`}
|
|
1097
|
+
</div>
|
|
1098
|
+
<div style="flex: 1; min-width: 0;">
|
|
1099
|
+
<div style="display: flex; justify-content: space-between; align-items: center; gap: 8px; margin-bottom: 4px;">
|
|
1100
|
+
<div style="font-weight: ${hasUnread ? "600" : "500"}; font-size: 15px; color: #000000; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
|
1101
|
+
${channel.ai_mode ? "AI Assistant" : "Support"}
|
|
1102
|
+
</div>
|
|
1103
|
+
<div style="font-size: 13px; color: #888888; white-space: nowrap; flex-shrink: 0;">${timeStr}</div>
|
|
1104
|
+
</div>
|
|
1105
|
+
<div style="display: flex; justify-content: space-between; align-items: center; gap: 8px;">
|
|
1106
|
+
<div style="
|
|
1107
|
+
font-size: 14px;
|
|
1108
|
+
color: ${hasUnread ? "#333333" : "#888888"};
|
|
1109
|
+
font-weight: 400;
|
|
1110
|
+
white-space: nowrap;
|
|
1111
|
+
overflow: hidden;
|
|
1112
|
+
text-overflow: ellipsis;
|
|
1113
|
+
flex: 1;
|
|
1114
|
+
min-width: 0;
|
|
1115
|
+
line-height: 1.4;
|
|
1116
|
+
">${senderPrefix}${this._escapeHTML(preview)}${channel.status === "closed" ? ' · Closed' : ""}</div>
|
|
1117
|
+
${hasUnread ? `<div style="
|
|
1118
|
+
min-width: 10px;
|
|
1119
|
+
width: 10px;
|
|
1120
|
+
height: 10px;
|
|
1121
|
+
background: ${primary};
|
|
1122
|
+
border-radius: 50%;
|
|
1123
|
+
flex-shrink: 0;
|
|
1124
|
+
"></div>` : ""}
|
|
1125
|
+
</div>
|
|
1126
|
+
</div>
|
|
1127
|
+
</div>
|
|
1128
|
+
`;
|
|
1129
|
+
}
|
|
1130
|
+
_getConversationHTML() {
|
|
1131
|
+
const primary = this._theme.primaryColor;
|
|
1132
|
+
return `
|
|
1133
|
+
<div class="vtilt-chat-messages" style="
|
|
1134
|
+
flex: 1;
|
|
1135
|
+
overflow-y: auto;
|
|
1136
|
+
-webkit-overflow-scrolling: touch;
|
|
1137
|
+
padding: 20px 16px;
|
|
1138
|
+
display: flex;
|
|
1139
|
+
flex-direction: column;
|
|
1140
|
+
gap: 12px;
|
|
1141
|
+
min-height: 0;
|
|
1142
|
+
background: #FAFAFA;
|
|
1143
|
+
"></div>
|
|
1144
|
+
|
|
1145
|
+
<div class="vtilt-chat-typing" style="
|
|
1146
|
+
display: none;
|
|
1147
|
+
padding: 0 16px 12px;
|
|
1148
|
+
background: #FAFAFA;
|
|
1149
|
+
align-items: center;
|
|
1150
|
+
">
|
|
1151
|
+
<div style="
|
|
1152
|
+
display: inline-flex;
|
|
1153
|
+
align-items: center;
|
|
1154
|
+
gap: 4px;
|
|
1155
|
+
padding: 12px 16px;
|
|
1156
|
+
background: white;
|
|
1157
|
+
border-radius: 20px;
|
|
1158
|
+
">
|
|
1159
|
+
<span style="width: 7px; height: 7px; background: #999; border-radius: 50%; animation: vtilt-typing 1.2s infinite 0s;"></span>
|
|
1160
|
+
<span style="width: 7px; height: 7px; background: #999; border-radius: 50%; animation: vtilt-typing 1.2s infinite 0.2s;"></span>
|
|
1161
|
+
<span style="width: 7px; height: 7px; background: #999; border-radius: 50%; animation: vtilt-typing 1.2s infinite 0.4s;"></span>
|
|
1162
|
+
</div>
|
|
1163
|
+
</div>
|
|
1164
|
+
<style>
|
|
1165
|
+
@keyframes vtilt-typing { 0%, 60%, 100% { opacity: 0.35; transform: translateY(0); } 30% { opacity: 1; transform: translateY(-2px); } }
|
|
1166
|
+
</style>
|
|
1167
|
+
|
|
1168
|
+
<div class="vtilt-chat-input-container" style="
|
|
1169
|
+
padding: 12px 16px;
|
|
1170
|
+
padding-bottom: max(12px, env(safe-area-inset-bottom, 12px));
|
|
1171
|
+
border-top: 1px solid #E5E5E5;
|
|
1172
|
+
display: flex;
|
|
1173
|
+
align-items: center;
|
|
1174
|
+
gap: 12px;
|
|
1175
|
+
flex-shrink: 0;
|
|
1176
|
+
background: #ffffff;
|
|
1177
|
+
">
|
|
1178
|
+
<div style="flex: 1; min-width: 0;">
|
|
1179
|
+
<input
|
|
1180
|
+
type="text"
|
|
1181
|
+
class="vtilt-chat-input"
|
|
1182
|
+
placeholder="Message..."
|
|
1183
|
+
autocomplete="off"
|
|
1184
|
+
autocorrect="on"
|
|
1185
|
+
autocapitalize="sentences"
|
|
1186
|
+
style="
|
|
1187
|
+
width: 100%;
|
|
1188
|
+
box-sizing: border-box;
|
|
1189
|
+
border: 1px solid #DDDDDD;
|
|
1190
|
+
border-radius: 24px;
|
|
1191
|
+
padding: 12px 18px;
|
|
1192
|
+
font-size: 16px;
|
|
1193
|
+
line-height: 1.4;
|
|
1194
|
+
outline: none;
|
|
1195
|
+
background: #ffffff;
|
|
1196
|
+
-webkit-appearance: none;
|
|
1197
|
+
appearance: none;
|
|
1198
|
+
color: #000000;
|
|
1199
|
+
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
|
1200
|
+
"
|
|
1201
|
+
/>
|
|
1202
|
+
</div>
|
|
1203
|
+
<button class="vtilt-chat-send" style="
|
|
1204
|
+
background: ${primary};
|
|
1205
|
+
color: white;
|
|
1206
|
+
border: none;
|
|
1207
|
+
border-radius: 50%;
|
|
1208
|
+
padding: 0;
|
|
1209
|
+
width: 44px;
|
|
1210
|
+
height: 44px;
|
|
1211
|
+
cursor: pointer;
|
|
1212
|
+
display: flex;
|
|
1213
|
+
align-items: center;
|
|
1214
|
+
justify-content: center;
|
|
1215
|
+
-webkit-tap-highlight-color: transparent;
|
|
1216
|
+
touch-action: manipulation;
|
|
1217
|
+
flex-shrink: 0;
|
|
1218
|
+
">
|
|
1219
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="white">
|
|
1220
|
+
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
|
|
1221
|
+
</svg>
|
|
1222
|
+
</button>
|
|
1223
|
+
</div>
|
|
1224
|
+
`;
|
|
1225
|
+
}
|
|
1226
|
+
_attachChannelListListeners() {
|
|
1227
|
+
var _a, _b;
|
|
1228
|
+
// New channel button
|
|
1229
|
+
const newChannelBtn = (_a = this._widget) === null || _a === void 0 ? void 0 : _a.querySelector(".vtilt-chat-new-channel");
|
|
1230
|
+
newChannelBtn === null || newChannelBtn === void 0 ? void 0 : newChannelBtn.addEventListener("click", () => this.createChannel());
|
|
1231
|
+
// Channel items
|
|
1232
|
+
const channelItems = (_b = this._widget) === null || _b === void 0 ? void 0 : _b.querySelectorAll(".vtilt-channel-item");
|
|
1233
|
+
const isTouchDevice = globals_1.window && ("ontouchstart" in globals_1.window || navigator.maxTouchPoints > 0);
|
|
1234
|
+
channelItems === null || channelItems === void 0 ? void 0 : channelItems.forEach((item) => {
|
|
1235
|
+
item.addEventListener("click", () => {
|
|
1236
|
+
const channelId = item.getAttribute("data-channel-id");
|
|
1237
|
+
if (channelId)
|
|
1238
|
+
this.selectChannel(channelId);
|
|
1239
|
+
});
|
|
1240
|
+
// Hover effect (only on non-touch devices)
|
|
1241
|
+
if (!isTouchDevice) {
|
|
1242
|
+
item.addEventListener("mouseenter", () => {
|
|
1243
|
+
item.style.background = "#F5F5F5";
|
|
1244
|
+
});
|
|
1245
|
+
item.addEventListener("mouseleave", () => {
|
|
1246
|
+
item.style.background = "white";
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
_attachConversationListeners() {
|
|
1252
|
+
var _a, _b;
|
|
1253
|
+
// Send button
|
|
1254
|
+
const sendBtn = (_a = this._widget) === null || _a === void 0 ? void 0 : _a.querySelector(".vtilt-chat-send");
|
|
1255
|
+
sendBtn === null || sendBtn === void 0 ? void 0 : sendBtn.addEventListener("click", () => this._handleSend());
|
|
1256
|
+
// Input enter key and typing indicator
|
|
1257
|
+
const input = (_b = this._widget) === null || _b === void 0 ? void 0 : _b.querySelector(".vtilt-chat-input");
|
|
1258
|
+
input === null || input === void 0 ? void 0 : input.addEventListener("keypress", (e) => {
|
|
1259
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
1260
|
+
e.preventDefault();
|
|
1261
|
+
this._handleSend();
|
|
1262
|
+
}
|
|
1263
|
+
});
|
|
1264
|
+
// Send typing indicator on input
|
|
1265
|
+
input === null || input === void 0 ? void 0 : input.addEventListener("input", () => {
|
|
1266
|
+
this._handleUserTyping();
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
_formatRelativeTime(isoString) {
|
|
1270
|
+
const date = new Date(isoString);
|
|
1271
|
+
const now = new Date();
|
|
1272
|
+
const diffMs = now.getTime() - date.getTime();
|
|
1273
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
1274
|
+
const diffHours = Math.floor(diffMs / 3600000);
|
|
1275
|
+
const diffDays = Math.floor(diffMs / 86400000);
|
|
1276
|
+
if (diffMins < 1)
|
|
1277
|
+
return "Just now";
|
|
1278
|
+
if (diffMins < 60)
|
|
1279
|
+
return `${diffMins}m ago`;
|
|
1280
|
+
if (diffHours < 24)
|
|
1281
|
+
return `${diffHours}h ago`;
|
|
1282
|
+
if (diffDays < 7)
|
|
1283
|
+
return `${diffDays}d ago`;
|
|
1284
|
+
return date.toLocaleDateString();
|
|
1285
|
+
}
|
|
692
1286
|
_renderMessages() {
|
|
693
1287
|
var _a;
|
|
694
1288
|
const messagesContainer = (_a = this._widget) === null || _a === void 0 ? void 0 : _a.querySelector(".vtilt-chat-messages");
|
|
695
1289
|
if (!messagesContainer)
|
|
696
1290
|
return;
|
|
697
|
-
const
|
|
1291
|
+
const primary = this._theme.primaryColor;
|
|
698
1292
|
// Find first unread agent message index
|
|
699
|
-
const firstUnreadIndex = this._state.messages.findIndex((m) => (m.sender_type === "agent" || m.sender_type === "ai") &&
|
|
1293
|
+
const firstUnreadIndex = this._state.messages.findIndex((m) => (m.sender_type === "agent" || m.sender_type === "ai") &&
|
|
1294
|
+
!this._isMessageReadByUser(m.created_at));
|
|
700
1295
|
// Build HTML with unread divider
|
|
701
1296
|
const messagesHtml = this._state.messages
|
|
702
1297
|
.map((msg, index) => {
|
|
703
1298
|
let html = "";
|
|
704
1299
|
// Add unread divider before first unread message
|
|
705
1300
|
if (index === firstUnreadIndex && firstUnreadIndex > 0) {
|
|
706
|
-
html += `
|
|
707
|
-
<div style="display: flex; align-items: center; gap: 12px; margin:
|
|
708
|
-
<div style="flex: 1; height: 1px; background:
|
|
709
|
-
<span style="font-size:
|
|
710
|
-
<div style="flex: 1; height: 1px; background:
|
|
711
|
-
</div>
|
|
1301
|
+
html += `
|
|
1302
|
+
<div style="display: flex; align-items: center; gap: 12px; margin: 12px 0;">
|
|
1303
|
+
<div style="flex: 1; height: 1px; background: #DDDDDD;"></div>
|
|
1304
|
+
<span style="font-size: 12px; font-weight: 600; color: ${primary};">New</span>
|
|
1305
|
+
<div style="flex: 1; height: 1px; background: #DDDDDD;"></div>
|
|
1306
|
+
</div>
|
|
712
1307
|
`;
|
|
713
1308
|
}
|
|
714
1309
|
html += this._getMessageHTML(msg);
|
|
@@ -716,230 +1311,277 @@ class LazyLoadedChat {
|
|
|
716
1311
|
})
|
|
717
1312
|
.join("");
|
|
718
1313
|
messagesContainer.innerHTML = messagesHtml;
|
|
719
|
-
// Scroll to bottom
|
|
720
1314
|
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|
721
1315
|
}
|
|
722
1316
|
// ============================================================================
|
|
723
1317
|
// Private - Styles & HTML
|
|
724
1318
|
// ============================================================================
|
|
1319
|
+
_isMobile() {
|
|
1320
|
+
return globals_1.window ? globals_1.window.innerWidth < 480 : false;
|
|
1321
|
+
}
|
|
725
1322
|
_getContainerStyles() {
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
${
|
|
733
|
-
|
|
734
|
-
font-
|
|
1323
|
+
const isRight = (this._config.position || DEFAULT_POSITION) === "bottom-right";
|
|
1324
|
+
return `
|
|
1325
|
+
position: fixed;
|
|
1326
|
+
bottom: 20px;
|
|
1327
|
+
${isRight ? "right: 20px;" : "left: 20px;"}
|
|
1328
|
+
z-index: 999999;
|
|
1329
|
+
font-family: ${this._theme.fontFamily};
|
|
1330
|
+
-webkit-font-smoothing: antialiased;
|
|
1331
|
+
-moz-osx-font-smoothing: grayscale;
|
|
735
1332
|
`;
|
|
736
1333
|
}
|
|
737
1334
|
_getBubbleStyles() {
|
|
738
|
-
const
|
|
739
|
-
return `
|
|
740
|
-
width: 60px;
|
|
741
|
-
height: 60px;
|
|
742
|
-
border-radius: 50%;
|
|
743
|
-
background: ${
|
|
744
|
-
cursor: pointer;
|
|
745
|
-
display: flex;
|
|
746
|
-
align-items: center;
|
|
747
|
-
justify-content: center;
|
|
748
|
-
box-shadow: 0 4px
|
|
749
|
-
transition: transform 0.2s, box-shadow 0.2s;
|
|
750
|
-
position: relative;
|
|
1335
|
+
const primary = this._theme.primaryColor;
|
|
1336
|
+
return `
|
|
1337
|
+
width: 60px;
|
|
1338
|
+
height: 60px;
|
|
1339
|
+
border-radius: 50%;
|
|
1340
|
+
background: ${primary};
|
|
1341
|
+
cursor: pointer;
|
|
1342
|
+
display: flex;
|
|
1343
|
+
align-items: center;
|
|
1344
|
+
justify-content: center;
|
|
1345
|
+
box-shadow: 0 4px 16px rgba(123, 104, 238, 0.4);
|
|
1346
|
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
1347
|
+
position: relative;
|
|
1348
|
+
-webkit-tap-highlight-color: transparent;
|
|
1349
|
+
touch-action: manipulation;
|
|
751
1350
|
`;
|
|
752
1351
|
}
|
|
753
1352
|
_getBubbleHTML() {
|
|
754
|
-
return `
|
|
755
|
-
<svg width="28" height="28" viewBox="0 0
|
|
756
|
-
<path d="
|
|
757
|
-
</svg>
|
|
758
|
-
<div class="vtilt-chat-badge" style="
|
|
759
|
-
display: none;
|
|
760
|
-
position: absolute;
|
|
761
|
-
top: -
|
|
762
|
-
right: -
|
|
763
|
-
background: #
|
|
764
|
-
color: white;
|
|
765
|
-
font-size:
|
|
766
|
-
font-weight:
|
|
767
|
-
min-width: 20px;
|
|
768
|
-
height: 20px;
|
|
769
|
-
border-radius: 10px;
|
|
770
|
-
align-items: center;
|
|
771
|
-
justify-content: center;
|
|
772
|
-
|
|
1353
|
+
return `
|
|
1354
|
+
<svg width="28" height="28" viewBox="0 0 28 28" fill="white">
|
|
1355
|
+
<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"/>
|
|
1356
|
+
</svg>
|
|
1357
|
+
<div class="vtilt-chat-badge" style="
|
|
1358
|
+
display: none;
|
|
1359
|
+
position: absolute;
|
|
1360
|
+
top: -4px;
|
|
1361
|
+
right: -4px;
|
|
1362
|
+
background: #E53935;
|
|
1363
|
+
color: white;
|
|
1364
|
+
font-size: 11px;
|
|
1365
|
+
font-weight: 700;
|
|
1366
|
+
min-width: 20px;
|
|
1367
|
+
height: 20px;
|
|
1368
|
+
border-radius: 10px;
|
|
1369
|
+
align-items: center;
|
|
1370
|
+
justify-content: center;
|
|
1371
|
+
padding: 0 6px;
|
|
1372
|
+
box-sizing: border-box;
|
|
1373
|
+
border: 2px solid white;
|
|
1374
|
+
">0</div>
|
|
773
1375
|
`;
|
|
774
1376
|
}
|
|
775
1377
|
_getWidgetStyles() {
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
width:
|
|
784
|
-
height:
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
1378
|
+
return `
|
|
1379
|
+
display: none;
|
|
1380
|
+
flex-direction: column;
|
|
1381
|
+
position: absolute;
|
|
1382
|
+
bottom: 80px;
|
|
1383
|
+
right: 0;
|
|
1384
|
+
width: 380px;
|
|
1385
|
+
max-width: calc(100vw - 40px);
|
|
1386
|
+
height: 600px;
|
|
1387
|
+
max-height: calc(100vh - 120px);
|
|
1388
|
+
max-height: calc(100dvh - 120px);
|
|
1389
|
+
background: #ffffff;
|
|
1390
|
+
border-radius: 16px;
|
|
1391
|
+
box-shadow: 0 5px 40px rgba(0, 0, 0, 0.16);
|
|
1392
|
+
overflow: hidden;
|
|
789
1393
|
`;
|
|
790
1394
|
}
|
|
791
1395
|
_getWidgetHTML() {
|
|
792
|
-
const
|
|
793
|
-
const
|
|
794
|
-
return `
|
|
795
|
-
<div class="vtilt-chat-header" style="
|
|
796
|
-
background:
|
|
797
|
-
|
|
798
|
-
padding: 16px;
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1396
|
+
const greeting = this._config.greeting || "Messages";
|
|
1397
|
+
const primary = this._theme.primaryColor;
|
|
1398
|
+
return `
|
|
1399
|
+
<div class="vtilt-chat-header" style="
|
|
1400
|
+
background: #ffffff;
|
|
1401
|
+
border-bottom: 1px solid #E5E5E5;
|
|
1402
|
+
padding: 18px 16px;
|
|
1403
|
+
padding-top: max(18px, env(safe-area-inset-top, 18px));
|
|
1404
|
+
display: flex;
|
|
1405
|
+
align-items: center;
|
|
1406
|
+
justify-content: space-between;
|
|
1407
|
+
min-height: 60px;
|
|
1408
|
+
box-sizing: border-box;
|
|
1409
|
+
flex-shrink: 0;
|
|
1410
|
+
">
|
|
1411
|
+
<div style="font-weight: 600; font-size: 17px; color: #000000;">${greeting}</div>
|
|
1412
|
+
<button class="vtilt-chat-close" style="
|
|
1413
|
+
background: transparent;
|
|
1414
|
+
border: none;
|
|
1415
|
+
color: #666666;
|
|
1416
|
+
cursor: pointer;
|
|
1417
|
+
padding: 6px;
|
|
1418
|
+
margin: -6px;
|
|
1419
|
+
border-radius: 4px;
|
|
1420
|
+
display: flex;
|
|
1421
|
+
align-items: center;
|
|
1422
|
+
justify-content: center;
|
|
1423
|
+
-webkit-tap-highlight-color: transparent;
|
|
1424
|
+
touch-action: manipulation;
|
|
1425
|
+
">
|
|
1426
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
1427
|
+
<path d="M18 6L6 18M6 6l12 12"></path>
|
|
1428
|
+
</svg>
|
|
1429
|
+
</button>
|
|
1430
|
+
</div>
|
|
1431
|
+
|
|
1432
|
+
<div class="vtilt-chat-content" style="
|
|
1433
|
+
flex: 1;
|
|
1434
|
+
display: flex;
|
|
1435
|
+
flex-direction: column;
|
|
1436
|
+
overflow: hidden;
|
|
1437
|
+
min-height: 0;
|
|
1438
|
+
background: #ffffff;
|
|
1439
|
+
">
|
|
1440
|
+
</div>
|
|
1441
|
+
|
|
1442
|
+
<div class="vtilt-chat-loader" style="
|
|
1443
|
+
display: none;
|
|
1444
|
+
position: absolute;
|
|
1445
|
+
top: 0;
|
|
1446
|
+
left: 0;
|
|
1447
|
+
right: 0;
|
|
1448
|
+
bottom: 0;
|
|
1449
|
+
background: rgba(255, 255, 255, 0.95);
|
|
1450
|
+
align-items: center;
|
|
1451
|
+
justify-content: center;
|
|
1452
|
+
z-index: 10;
|
|
1453
|
+
">
|
|
1454
|
+
<div style="
|
|
1455
|
+
width: 32px;
|
|
1456
|
+
height: 32px;
|
|
1457
|
+
border: 3px solid #E5E5E5;
|
|
1458
|
+
border-top-color: ${primary};
|
|
1459
|
+
border-radius: 50%;
|
|
1460
|
+
animation: vtilt-spin 0.8s linear infinite;
|
|
1461
|
+
"></div>
|
|
1462
|
+
</div>
|
|
1463
|
+
|
|
1464
|
+
<style>
|
|
1465
|
+
@keyframes vtilt-spin { to { transform: rotate(360deg); } }
|
|
1466
|
+
@keyframes vtilt-fade { from { opacity: 0; } to { opacity: 1; } }
|
|
1467
|
+
|
|
1468
|
+
#vtilt-chat-bubble { transition: transform 0.15s ease, box-shadow 0.15s ease; }
|
|
1469
|
+
#vtilt-chat-bubble:hover { transform: scale(1.05); box-shadow: 0 6px 20px rgba(123, 104, 238, 0.45); }
|
|
1470
|
+
#vtilt-chat-bubble:active { transform: scale(0.95); }
|
|
1471
|
+
|
|
1472
|
+
#vtilt-chat-widget { transition: opacity 0.2s ease; }
|
|
1473
|
+
#vtilt-chat-widget.vtilt-opening { animation: vtilt-fade 0.2s ease forwards; }
|
|
1474
|
+
#vtilt-chat-widget.vtilt-closing { opacity: 0; }
|
|
1475
|
+
|
|
1476
|
+
.vtilt-chat-content { transition: opacity 0.15s ease; }
|
|
1477
|
+
|
|
1478
|
+
.vtilt-chat-close:hover { color: #000 !important; background: #F0F0F0 !important; }
|
|
1479
|
+
.vtilt-chat-back:hover { color: #000 !important; background: #F0F0F0 !important; }
|
|
1480
|
+
|
|
1481
|
+
.vtilt-chat-input { font-size: 16px !important; -webkit-text-size-adjust: 100%; transition: border-color 0.15s ease, box-shadow 0.15s ease; }
|
|
1482
|
+
.vtilt-chat-input:focus { border-color: ${primary} !important; box-shadow: 0 0 0 2px ${primary}20 !important; outline: none !important; }
|
|
1483
|
+
.vtilt-chat-input::placeholder { color: #999999; }
|
|
1484
|
+
|
|
1485
|
+
.vtilt-chat-send { transition: opacity 0.1s ease; }
|
|
1486
|
+
.vtilt-chat-send:hover { opacity: 0.85; }
|
|
1487
|
+
.vtilt-chat-send:active { opacity: 0.7; }
|
|
1488
|
+
|
|
1489
|
+
.vtilt-chat-new-channel { transition: opacity 0.1s ease; }
|
|
1490
|
+
.vtilt-chat-new-channel:hover { opacity: 0.9; }
|
|
1491
|
+
.vtilt-chat-new-channel:active { opacity: 0.8; }
|
|
1492
|
+
|
|
1493
|
+
.vtilt-channel-item { transition: background 0.1s ease; cursor: pointer; }
|
|
1494
|
+
.vtilt-channel-item:hover { background: #F5F5F5 !important; }
|
|
1495
|
+
.vtilt-channel-item:active { background: #EBEBEB !important; }
|
|
1496
|
+
|
|
1497
|
+
@media (max-width: 480px) {
|
|
1498
|
+
#vtilt-chat-container { bottom: 16px !important; right: 16px !important; }
|
|
1499
|
+
#vtilt-chat-bubble { width: 56px !important; height: 56px !important; }
|
|
1500
|
+
#vtilt-chat-bubble svg { width: 24px !important; height: 24px !important; }
|
|
1501
|
+
#vtilt-chat-widget {
|
|
1502
|
+
position: fixed !important;
|
|
1503
|
+
top: 0 !important; left: 0 !important; right: 0 !important; bottom: 0 !important;
|
|
1504
|
+
width: 100% !important; max-width: 100% !important;
|
|
1505
|
+
height: 100% !important; max-height: 100% !important;
|
|
1506
|
+
border-radius: 0 !important;
|
|
1507
|
+
z-index: 1000000 !important;
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1512
|
+
* { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
|
|
1513
|
+
}
|
|
1514
|
+
</style>
|
|
902
1515
|
`;
|
|
903
1516
|
}
|
|
904
1517
|
_getMessageHTML(message) {
|
|
905
|
-
const theme = this._config.theme || DEFAULT_THEME;
|
|
906
1518
|
const isUser = message.sender_type === "user";
|
|
907
|
-
const
|
|
908
|
-
// Check read status
|
|
1519
|
+
const isAi = message.sender_type === "ai";
|
|
909
1520
|
const isReadByAgent = isUser && this._isMessageReadByAgent(message.created_at);
|
|
910
|
-
const
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
:
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1521
|
+
const primary = this._theme.primaryColor;
|
|
1522
|
+
if (isUser) {
|
|
1523
|
+
return `
|
|
1524
|
+
<div class="vtilt-msg" style="
|
|
1525
|
+
display: flex;
|
|
1526
|
+
flex-direction: column;
|
|
1527
|
+
align-items: flex-end;
|
|
1528
|
+
">
|
|
1529
|
+
<div style="
|
|
1530
|
+
max-width: 80%;
|
|
1531
|
+
padding: 12px 16px;
|
|
1532
|
+
background: ${primary};
|
|
1533
|
+
color: white;
|
|
1534
|
+
border-radius: 20px 20px 4px 20px;
|
|
1535
|
+
font-size: 15px;
|
|
1536
|
+
line-height: 1.45;
|
|
1537
|
+
word-wrap: break-word;
|
|
1538
|
+
overflow-wrap: break-word;
|
|
1539
|
+
">${this._escapeHTML(message.content)}</div>
|
|
1540
|
+
<div style="font-size: 12px; color: #888888; margin-top: 6px; display: flex; align-items: center; gap: 4px;">
|
|
1541
|
+
${this._formatTime(message.created_at)}
|
|
1542
|
+
${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>` : ""}
|
|
1543
|
+
</div>
|
|
1544
|
+
</div>
|
|
1545
|
+
`;
|
|
1546
|
+
}
|
|
1547
|
+
const senderLabel = isAi ? "AI Assistant" : (message.sender_name || "Support");
|
|
1548
|
+
return `
|
|
1549
|
+
<div class="vtilt-msg" style="
|
|
1550
|
+
display: flex;
|
|
1551
|
+
gap: 10px;
|
|
1552
|
+
align-items: flex-end;
|
|
1553
|
+
">
|
|
1554
|
+
<div style="
|
|
1555
|
+
width: 32px;
|
|
1556
|
+
height: 32px;
|
|
1557
|
+
border-radius: 50%;
|
|
1558
|
+
background: ${isAi ? primary : "#DEDEDE"};
|
|
1559
|
+
display: flex;
|
|
1560
|
+
align-items: center;
|
|
1561
|
+
justify-content: center;
|
|
1562
|
+
flex-shrink: 0;
|
|
1563
|
+
">
|
|
1564
|
+
${isAi
|
|
1565
|
+
? `<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>`
|
|
1566
|
+
: `<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>`}
|
|
1567
|
+
</div>
|
|
1568
|
+
<div style="flex: 1; min-width: 0; display: flex; flex-direction: column; align-items: flex-start;">
|
|
1569
|
+
<div style="
|
|
1570
|
+
max-width: 85%;
|
|
1571
|
+
padding: 12px 16px;
|
|
1572
|
+
background: #ffffff;
|
|
1573
|
+
color: #000000;
|
|
1574
|
+
border-radius: 20px 20px 20px 4px;
|
|
1575
|
+
font-size: 15px;
|
|
1576
|
+
line-height: 1.45;
|
|
1577
|
+
word-wrap: break-word;
|
|
1578
|
+
overflow-wrap: break-word;
|
|
1579
|
+
">${this._escapeHTML(message.content)}</div>
|
|
1580
|
+
<div style="font-size: 12px; color: #888888; margin-top: 6px; margin-left: 4px;">
|
|
1581
|
+
${senderLabel} · ${this._formatTime(message.created_at)}
|
|
1582
|
+
</div>
|
|
1583
|
+
</div>
|
|
1584
|
+
</div>
|
|
943
1585
|
`;
|
|
944
1586
|
}
|
|
945
1587
|
/**
|
|
@@ -957,7 +1599,9 @@ class LazyLoadedChat {
|
|
|
957
1599
|
const config = this._instance.getConfig();
|
|
958
1600
|
const apiHost = config.api_host || "";
|
|
959
1601
|
const token = config.token || "";
|
|
960
|
-
|
|
1602
|
+
// Use & if endpoint already has query params, otherwise use ?
|
|
1603
|
+
const separator = endpoint.includes("?") ? "&" : "?";
|
|
1604
|
+
const url = `${apiHost}${endpoint}${separator}token=${encodeURIComponent(token)}`;
|
|
961
1605
|
try {
|
|
962
1606
|
const response = await fetch(url, {
|
|
963
1607
|
...options,
|