@v-tilt/browser 1.4.1 → 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.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/extensions/chat/chat-wrapper.d.ts +25 -1
- package/lib/extensions/chat/chat-wrapper.js +42 -0
- package/lib/extensions/chat/chat.d.ts +30 -2
- package/lib/extensions/chat/chat.js +881 -243
- package/lib/extensions/chat/types.d.ts +3 -1
- package/lib/utils/globals.d.ts +29 -0
- package/package.json +1 -1
|
@@ -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,33 +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
169
|
$messages_sent: this._state.messages.filter((m) => m.sender_type === "user").length,
|
|
140
170
|
});
|
|
171
|
+
// Disconnect Ably when widget closes to save connection minutes
|
|
172
|
+
this._disconnectRealtime();
|
|
141
173
|
}
|
|
142
174
|
toggle() {
|
|
143
175
|
if (this._state.isOpen) {
|
|
@@ -156,15 +188,165 @@ class LazyLoadedChat {
|
|
|
156
188
|
this._updateUI();
|
|
157
189
|
}
|
|
158
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
|
+
// ============================================================================
|
|
159
340
|
// Public API - Messaging
|
|
160
341
|
// ============================================================================
|
|
161
342
|
async sendMessage(content) {
|
|
162
343
|
var _a, _b, _c, _d;
|
|
163
344
|
if (!content.trim())
|
|
164
345
|
return;
|
|
165
|
-
// Ensure channel
|
|
166
|
-
if (!this._state.channel) {
|
|
167
|
-
|
|
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;
|
|
168
350
|
}
|
|
169
351
|
const channelId = (_a = this._state.channel) === null || _a === void 0 ? void 0 : _a.id;
|
|
170
352
|
if (!channelId) {
|
|
@@ -176,7 +358,7 @@ class LazyLoadedChat {
|
|
|
176
358
|
id: `temp-${Date.now()}`,
|
|
177
359
|
channel_id: channelId,
|
|
178
360
|
sender_type: "user",
|
|
179
|
-
sender_id: this.
|
|
361
|
+
sender_id: this._distinctId || null,
|
|
180
362
|
sender_name: null,
|
|
181
363
|
sender_avatar_url: null,
|
|
182
364
|
content,
|
|
@@ -188,11 +370,11 @@ class LazyLoadedChat {
|
|
|
188
370
|
this._updateUI();
|
|
189
371
|
try {
|
|
190
372
|
// Send to API
|
|
191
|
-
const response = await this._apiRequest(
|
|
373
|
+
const response = await this._apiRequest(API.messages, {
|
|
192
374
|
method: "POST",
|
|
193
375
|
body: JSON.stringify({
|
|
194
376
|
channel_id: channelId,
|
|
195
|
-
distinct_id: this.
|
|
377
|
+
distinct_id: this._distinctId,
|
|
196
378
|
content,
|
|
197
379
|
}),
|
|
198
380
|
});
|
|
@@ -246,11 +428,11 @@ class LazyLoadedChat {
|
|
|
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,53 +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 =
|
|
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
|
-
}
|
|
372
|
-
// ============================================================================
|
|
373
508
|
// Private - Ably Realtime Connection
|
|
374
509
|
// ============================================================================
|
|
375
510
|
async _connectRealtime() {
|
|
@@ -380,10 +515,10 @@ class LazyLoadedChat {
|
|
|
380
515
|
this._notifyConnectionChange(false);
|
|
381
516
|
try {
|
|
382
517
|
// Get Ably token from server
|
|
383
|
-
const tokenResponse = await this._apiRequest(
|
|
518
|
+
const tokenResponse = await this._apiRequest(API.ablyToken, {
|
|
384
519
|
method: "POST",
|
|
385
520
|
body: JSON.stringify({
|
|
386
|
-
distinct_id: this.
|
|
521
|
+
distinct_id: this._distinctId,
|
|
387
522
|
channel_id: this._state.channel.id,
|
|
388
523
|
}),
|
|
389
524
|
});
|
|
@@ -397,10 +532,10 @@ class LazyLoadedChat {
|
|
|
397
532
|
authCallback: async (_, callback) => {
|
|
398
533
|
var _a;
|
|
399
534
|
try {
|
|
400
|
-
const refreshResponse = await this._apiRequest(
|
|
535
|
+
const refreshResponse = await this._apiRequest(API.ablyToken, {
|
|
401
536
|
method: "POST",
|
|
402
537
|
body: JSON.stringify({
|
|
403
|
-
distinct_id: this.
|
|
538
|
+
distinct_id: this._distinctId,
|
|
404
539
|
channel_id: (_a = this._state.channel) === null || _a === void 0 ? void 0 : _a.id,
|
|
405
540
|
}),
|
|
406
541
|
});
|
|
@@ -495,7 +630,7 @@ class LazyLoadedChat {
|
|
|
495
630
|
// Skip user's own messages - we already have them from optimistic updates
|
|
496
631
|
// The sender_id for user messages is the distinct_id
|
|
497
632
|
if (message.sender_type === "user" &&
|
|
498
|
-
message.sender_id === this.
|
|
633
|
+
message.sender_id === this._distinctId) {
|
|
499
634
|
// But DO replace temp message with real one if present
|
|
500
635
|
const tempIndex = this._state.messages.findIndex((m) => m.id.startsWith("temp-") &&
|
|
501
636
|
m.content === message.content &&
|
|
@@ -583,27 +718,14 @@ class LazyLoadedChat {
|
|
|
583
718
|
globals_1.document.body.appendChild(this._container);
|
|
584
719
|
}
|
|
585
720
|
_attachEventListeners() {
|
|
586
|
-
var _a, _b
|
|
721
|
+
var _a, _b;
|
|
587
722
|
// Bubble click
|
|
588
723
|
(_a = this._bubble) === null || _a === void 0 ? void 0 : _a.addEventListener("click", () => this.toggle());
|
|
589
|
-
// Close button
|
|
724
|
+
// Close button (initial attachment - re-attached in _updateHeader)
|
|
590
725
|
const closeBtn = (_b = this._widget) === null || _b === void 0 ? void 0 : _b.querySelector(".vtilt-chat-close");
|
|
591
726
|
closeBtn === null || closeBtn === void 0 ? void 0 : closeBtn.addEventListener("click", () => this.close());
|
|
592
|
-
//
|
|
593
|
-
|
|
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
|
-
});
|
|
727
|
+
// Note: Channel list and conversation listeners are attached dynamically
|
|
728
|
+
// in _attachChannelListListeners() and _attachConversationListeners()
|
|
607
729
|
}
|
|
608
730
|
_handleUserTyping() {
|
|
609
731
|
// Don't send typing if not connected to Ably
|
|
@@ -630,7 +752,7 @@ class LazyLoadedChat {
|
|
|
630
752
|
try {
|
|
631
753
|
this._typingChannel.publish("typing", {
|
|
632
754
|
sender_type: "user",
|
|
633
|
-
sender_id: this.
|
|
755
|
+
sender_id: this._distinctId,
|
|
634
756
|
sender_name: null,
|
|
635
757
|
is_typing: isTyping,
|
|
636
758
|
});
|
|
@@ -673,8 +795,39 @@ class LazyLoadedChat {
|
|
|
673
795
|
this._state.unreadCount > 0 ? "flex" : "none";
|
|
674
796
|
badge.textContent = String(this._state.unreadCount);
|
|
675
797
|
}
|
|
676
|
-
//
|
|
677
|
-
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();
|
|
678
831
|
// Update loading state
|
|
679
832
|
const loader = this._widget.querySelector(".vtilt-chat-loader");
|
|
680
833
|
if (loader) {
|
|
@@ -682,24 +835,460 @@ class LazyLoadedChat {
|
|
|
682
835
|
? "flex"
|
|
683
836
|
: "none";
|
|
684
837
|
}
|
|
685
|
-
// Update typing indicator
|
|
838
|
+
// Update typing indicator (only in conversation view)
|
|
686
839
|
const typing = this._widget.querySelector(".vtilt-chat-typing");
|
|
687
840
|
if (typing) {
|
|
688
|
-
typing.style.display =
|
|
689
|
-
|
|
690
|
-
|
|
841
|
+
typing.style.display =
|
|
842
|
+
this._state.isTyping && this._state.currentView === "conversation"
|
|
843
|
+
? "flex"
|
|
844
|
+
: "none";
|
|
691
845
|
const typingText = typing.querySelector("span");
|
|
692
846
|
if (typingText && this._state.typingSender) {
|
|
693
847
|
typingText.textContent = `${this._state.typingSender} is typing...`;
|
|
694
848
|
}
|
|
695
849
|
}
|
|
696
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
|
+
}
|
|
697
1286
|
_renderMessages() {
|
|
698
1287
|
var _a;
|
|
699
1288
|
const messagesContainer = (_a = this._widget) === null || _a === void 0 ? void 0 : _a.querySelector(".vtilt-chat-messages");
|
|
700
1289
|
if (!messagesContainer)
|
|
701
1290
|
return;
|
|
702
|
-
const
|
|
1291
|
+
const primary = this._theme.primaryColor;
|
|
703
1292
|
// Find first unread agent message index
|
|
704
1293
|
const firstUnreadIndex = this._state.messages.findIndex((m) => (m.sender_type === "agent" || m.sender_type === "ai") &&
|
|
705
1294
|
!this._isMessageReadByUser(m.created_at));
|
|
@@ -710,10 +1299,10 @@ class LazyLoadedChat {
|
|
|
710
1299
|
// Add unread divider before first unread message
|
|
711
1300
|
if (index === firstUnreadIndex && firstUnreadIndex > 0) {
|
|
712
1301
|
html += `
|
|
713
|
-
<div style="display: flex; align-items: center; gap: 12px; margin:
|
|
714
|
-
<div style="flex: 1; height: 1px; background:
|
|
715
|
-
<span style="font-size:
|
|
716
|
-
<div style="flex: 1; height: 1px; background:
|
|
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>
|
|
717
1306
|
</div>
|
|
718
1307
|
`;
|
|
719
1308
|
}
|
|
@@ -722,64 +1311,70 @@ class LazyLoadedChat {
|
|
|
722
1311
|
})
|
|
723
1312
|
.join("");
|
|
724
1313
|
messagesContainer.innerHTML = messagesHtml;
|
|
725
|
-
// Scroll to bottom
|
|
726
1314
|
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|
727
1315
|
}
|
|
728
1316
|
// ============================================================================
|
|
729
1317
|
// Private - Styles & HTML
|
|
730
1318
|
// ============================================================================
|
|
1319
|
+
_isMobile() {
|
|
1320
|
+
return globals_1.window ? globals_1.window.innerWidth < 480 : false;
|
|
1321
|
+
}
|
|
731
1322
|
_getContainerStyles() {
|
|
732
|
-
|
|
733
|
-
const position = this._config.position || DEFAULT_POSITION;
|
|
734
|
-
const isRight = position === "bottom-right";
|
|
1323
|
+
const isRight = (this._config.position || DEFAULT_POSITION) === "bottom-right";
|
|
735
1324
|
return `
|
|
736
1325
|
position: fixed;
|
|
737
1326
|
bottom: 20px;
|
|
738
1327
|
${isRight ? "right: 20px;" : "left: 20px;"}
|
|
739
1328
|
z-index: 999999;
|
|
740
|
-
font-family: ${
|
|
1329
|
+
font-family: ${this._theme.fontFamily};
|
|
1330
|
+
-webkit-font-smoothing: antialiased;
|
|
1331
|
+
-moz-osx-font-smoothing: grayscale;
|
|
741
1332
|
`;
|
|
742
1333
|
}
|
|
743
1334
|
_getBubbleStyles() {
|
|
744
|
-
const
|
|
1335
|
+
const primary = this._theme.primaryColor;
|
|
745
1336
|
return `
|
|
746
1337
|
width: 60px;
|
|
747
1338
|
height: 60px;
|
|
748
1339
|
border-radius: 50%;
|
|
749
|
-
background: ${
|
|
1340
|
+
background: ${primary};
|
|
750
1341
|
cursor: pointer;
|
|
751
1342
|
display: flex;
|
|
752
1343
|
align-items: center;
|
|
753
1344
|
justify-content: center;
|
|
754
|
-
box-shadow: 0 4px
|
|
755
|
-
transition: transform 0.2s, box-shadow 0.2s;
|
|
1345
|
+
box-shadow: 0 4px 16px rgba(123, 104, 238, 0.4);
|
|
1346
|
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
756
1347
|
position: relative;
|
|
1348
|
+
-webkit-tap-highlight-color: transparent;
|
|
1349
|
+
touch-action: manipulation;
|
|
757
1350
|
`;
|
|
758
1351
|
}
|
|
759
1352
|
_getBubbleHTML() {
|
|
760
1353
|
return `
|
|
761
|
-
<svg width="28" height="28" viewBox="0 0
|
|
762
|
-
<path d="
|
|
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"/>
|
|
763
1356
|
</svg>
|
|
764
1357
|
<div class="vtilt-chat-badge" style="
|
|
765
1358
|
display: none;
|
|
766
1359
|
position: absolute;
|
|
767
|
-
top: -
|
|
768
|
-
right: -
|
|
769
|
-
background: #
|
|
1360
|
+
top: -4px;
|
|
1361
|
+
right: -4px;
|
|
1362
|
+
background: #E53935;
|
|
770
1363
|
color: white;
|
|
771
|
-
font-size:
|
|
772
|
-
font-weight:
|
|
1364
|
+
font-size: 11px;
|
|
1365
|
+
font-weight: 700;
|
|
773
1366
|
min-width: 20px;
|
|
774
1367
|
height: 20px;
|
|
775
1368
|
border-radius: 10px;
|
|
776
1369
|
align-items: center;
|
|
777
1370
|
justify-content: center;
|
|
1371
|
+
padding: 0 6px;
|
|
1372
|
+
box-sizing: border-box;
|
|
1373
|
+
border: 2px solid white;
|
|
778
1374
|
">0</div>
|
|
779
1375
|
`;
|
|
780
1376
|
}
|
|
781
1377
|
_getWidgetStyles() {
|
|
782
|
-
const theme = this._config.theme || DEFAULT_THEME;
|
|
783
1378
|
return `
|
|
784
1379
|
display: none;
|
|
785
1380
|
flex-direction: column;
|
|
@@ -787,163 +1382,204 @@ class LazyLoadedChat {
|
|
|
787
1382
|
bottom: 80px;
|
|
788
1383
|
right: 0;
|
|
789
1384
|
width: 380px;
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
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);
|
|
794
1392
|
overflow: hidden;
|
|
795
1393
|
`;
|
|
796
1394
|
}
|
|
797
1395
|
_getWidgetHTML() {
|
|
798
|
-
const
|
|
799
|
-
const
|
|
1396
|
+
const greeting = this._config.greeting || "Messages";
|
|
1397
|
+
const primary = this._theme.primaryColor;
|
|
800
1398
|
return `
|
|
801
1399
|
<div class="vtilt-chat-header" style="
|
|
802
|
-
background:
|
|
803
|
-
|
|
804
|
-
padding: 16px;
|
|
1400
|
+
background: #ffffff;
|
|
1401
|
+
border-bottom: 1px solid #E5E5E5;
|
|
1402
|
+
padding: 18px 16px;
|
|
1403
|
+
padding-top: max(18px, env(safe-area-inset-top, 18px));
|
|
805
1404
|
display: flex;
|
|
806
1405
|
align-items: center;
|
|
807
1406
|
justify-content: space-between;
|
|
1407
|
+
min-height: 60px;
|
|
1408
|
+
box-sizing: border-box;
|
|
1409
|
+
flex-shrink: 0;
|
|
808
1410
|
">
|
|
809
|
-
<div style="font-weight: 600; font-size:
|
|
1411
|
+
<div style="font-weight: 600; font-size: 17px; color: #000000;">${greeting}</div>
|
|
810
1412
|
<button class="vtilt-chat-close" style="
|
|
811
|
-
background:
|
|
1413
|
+
background: transparent;
|
|
812
1414
|
border: none;
|
|
813
|
-
color:
|
|
1415
|
+
color: #666666;
|
|
814
1416
|
cursor: pointer;
|
|
815
|
-
padding:
|
|
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;
|
|
816
1425
|
">
|
|
817
|
-
<svg width="
|
|
1426
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
818
1427
|
<path d="M18 6L6 18M6 6l12 12"></path>
|
|
819
1428
|
</svg>
|
|
820
1429
|
</button>
|
|
821
1430
|
</div>
|
|
822
1431
|
|
|
823
|
-
<div class="vtilt-chat-
|
|
1432
|
+
<div class="vtilt-chat-content" style="
|
|
824
1433
|
flex: 1;
|
|
825
|
-
overflow-y: auto;
|
|
826
|
-
padding: 16px;
|
|
827
1434
|
display: flex;
|
|
828
1435
|
flex-direction: column;
|
|
829
|
-
|
|
830
|
-
|
|
1436
|
+
overflow: hidden;
|
|
1437
|
+
min-height: 0;
|
|
1438
|
+
background: #ffffff;
|
|
1439
|
+
">
|
|
1440
|
+
</div>
|
|
831
1441
|
|
|
832
1442
|
<div class="vtilt-chat-loader" style="
|
|
833
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);
|
|
834
1450
|
align-items: center;
|
|
835
1451
|
justify-content: center;
|
|
836
|
-
|
|
1452
|
+
z-index: 10;
|
|
837
1453
|
">
|
|
838
1454
|
<div style="
|
|
839
|
-
width:
|
|
840
|
-
height:
|
|
841
|
-
border:
|
|
842
|
-
border-top-color: ${
|
|
1455
|
+
width: 32px;
|
|
1456
|
+
height: 32px;
|
|
1457
|
+
border: 3px solid #E5E5E5;
|
|
1458
|
+
border-top-color: ${primary};
|
|
843
1459
|
border-radius: 50%;
|
|
844
1460
|
animation: vtilt-spin 0.8s linear infinite;
|
|
845
1461
|
"></div>
|
|
846
1462
|
</div>
|
|
847
1463
|
|
|
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
1464
|
<style>
|
|
890
|
-
@keyframes vtilt-spin {
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
#vtilt-chat-
|
|
898
|
-
|
|
899
|
-
|
|
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
|
+
}
|
|
900
1509
|
}
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
.vtilt-chat-send:hover {
|
|
905
|
-
opacity: 0.9;
|
|
1510
|
+
|
|
1511
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1512
|
+
* { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
|
|
906
1513
|
}
|
|
907
1514
|
</style>
|
|
908
1515
|
`;
|
|
909
1516
|
}
|
|
910
1517
|
_getMessageHTML(message) {
|
|
911
|
-
const theme = this._config.theme || DEFAULT_THEME;
|
|
912
1518
|
const isUser = message.sender_type === "user";
|
|
913
|
-
const
|
|
914
|
-
// Check read status
|
|
1519
|
+
const isAi = message.sender_type === "ai";
|
|
915
1520
|
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
|
-
:
|
|
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");
|
|
932
1548
|
return `
|
|
933
|
-
<div
|
|
934
|
-
|
|
1549
|
+
<div class="vtilt-msg" style="
|
|
1550
|
+
display: flex;
|
|
1551
|
+
gap: 10px;
|
|
1552
|
+
align-items: flex-end;
|
|
1553
|
+
">
|
|
935
1554
|
<div style="
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
border-radius:
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
${
|
|
946
|
-
|
|
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>
|
|
947
1583
|
</div>
|
|
948
1584
|
</div>
|
|
949
1585
|
`;
|
|
@@ -963,7 +1599,9 @@ class LazyLoadedChat {
|
|
|
963
1599
|
const config = this._instance.getConfig();
|
|
964
1600
|
const apiHost = config.api_host || "";
|
|
965
1601
|
const token = config.token || "";
|
|
966
|
-
|
|
1602
|
+
// Use & if endpoint already has query params, otherwise use ?
|
|
1603
|
+
const separator = endpoint.includes("?") ? "&" : "?";
|
|
1604
|
+
const url = `${apiHost}${endpoint}${separator}token=${encodeURIComponent(token)}`;
|
|
967
1605
|
try {
|
|
968
1606
|
const response = await fetch(url, {
|
|
969
1607
|
...options,
|