oneskies-live-chat-widget 0.1.4 → 0.1.6
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/index.d.ts.map +1 -1
- package/dist/index.js +306 -29
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.tsx +331 -31
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAYA,MAAM,MAAM,kBAAkB,GAAG,UAAU,GAAG,YAAY,CAAC;AAE3D,MAAM,MAAM,mBAAmB,GAAG;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,kBAAkB,CAAC;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAYA,MAAM,MAAM,kBAAkB,GAAG,UAAU,GAAG,YAAY,CAAC;AAE3D,MAAM,MAAM,mBAAmB,GAAG;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,kBAAkB,CAAC;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAmFF,MAAM,MAAM,wBAAwB,GAAG;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,EAAE,IAAI,EAAE,IAAS,EAAE,SAAc,EAAE,EAAE,wBAAwB,2CA4EhG;AA8ID,wBAAgB,cAAc,CAAC,EAC7B,UAAU,EACV,MAAM,EACN,SAAS,EACT,UAAU,EACV,WAA4B,EAC5B,YAA+B,GAChC,EAAE,mBAAmB,2CAqwCrB"}
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ const TYPING_BROADCAST_INTERVAL_MS = 120;
|
|
|
9
9
|
const WS_RECONNECT_BASE_DELAY_MS = 650;
|
|
10
10
|
const WS_RECONNECT_MAX_DELAY_MS = 12000;
|
|
11
11
|
const WS_MAX_MESSAGE_CHARS = 128 * 1024;
|
|
12
|
+
const LIVE_CHAT_BLOCKED_STATUS = 423;
|
|
12
13
|
function toTitleCase(name) {
|
|
13
14
|
return name.replace(/\S+/g, (word) => word
|
|
14
15
|
.split(/(['-])/)
|
|
@@ -124,6 +125,37 @@ function apiHeaders(apiKey) {
|
|
|
124
125
|
headers.set("Content-Type", "application/json");
|
|
125
126
|
return headers;
|
|
126
127
|
}
|
|
128
|
+
function liveChatQueryParam(name) {
|
|
129
|
+
try {
|
|
130
|
+
return new URL(window.location.href).searchParams.get(name)?.trim() ?? "";
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return "";
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function liveChatCurrentUrl() {
|
|
137
|
+
try {
|
|
138
|
+
return window.location.href;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return "";
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function liveChatTrackedFieldKey(element) {
|
|
145
|
+
if (!(element instanceof HTMLInputElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement)) {
|
|
146
|
+
return "";
|
|
147
|
+
}
|
|
148
|
+
if (element.closest(".os-live-chat"))
|
|
149
|
+
return "";
|
|
150
|
+
if (element instanceof HTMLInputElement && ["hidden", "submit", "button", "reset", "image"].includes(element.type)) {
|
|
151
|
+
return "";
|
|
152
|
+
}
|
|
153
|
+
const form = element.closest("form");
|
|
154
|
+
const formKey = form?.getAttribute("id") || form?.getAttribute("name") || form?.getAttribute("aria-label") || "";
|
|
155
|
+
const placeholder = element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement ? element.placeholder : "";
|
|
156
|
+
const fieldKey = element.name || element.id || element.getAttribute("aria-label") || placeholder || element.type || element.tagName;
|
|
157
|
+
return `${formKey}:${fieldKey}`.slice(0, 220);
|
|
158
|
+
}
|
|
127
159
|
function messageTime(value) {
|
|
128
160
|
const date = new Date(value);
|
|
129
161
|
if (Number.isNaN(date.getTime()))
|
|
@@ -199,6 +231,7 @@ async function buildDeviceSignature(visitorId) {
|
|
|
199
231
|
return sha256(parts.join("|"));
|
|
200
232
|
}
|
|
201
233
|
export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, accentColor = DEFAULT_ACCENT, supportLabel = "Travel support", }) {
|
|
234
|
+
const initialBlockedKey = `${STORAGE_PREFIX}:${leadSource}:blocked`;
|
|
202
235
|
const [visible, setVisible] = useState(false);
|
|
203
236
|
const [open, setOpen] = useState(false);
|
|
204
237
|
const [session, setSession] = useState(null);
|
|
@@ -210,6 +243,15 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
|
|
|
210
243
|
const [loading, setLoading] = useState(false);
|
|
211
244
|
const [sending, setSending] = useState(false);
|
|
212
245
|
const [error, setError] = useState("");
|
|
246
|
+
const [blocked, setBlocked] = useState(() => {
|
|
247
|
+
try {
|
|
248
|
+
return window.localStorage.getItem(initialBlockedKey) === "1";
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
const [blockedRetryNonce, setBlockedRetryNonce] = useState(0);
|
|
213
255
|
const [staffTypingName, setStaffTypingName] = useState("");
|
|
214
256
|
const [unreadCount, setUnreadCount] = useState(0);
|
|
215
257
|
const [unreadPreview, setUnreadPreview] = useState("");
|
|
@@ -223,11 +265,75 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
|
|
|
223
265
|
const typingSentAtRef = useRef(0);
|
|
224
266
|
const typingTimeoutRef = useRef(null);
|
|
225
267
|
const unreadPreviewTimeoutRef = useRef(null);
|
|
268
|
+
const pageUrlsRef = useRef(new Set());
|
|
269
|
+
const touchedFieldsRef = useRef(new Set());
|
|
270
|
+
const stateBroadcastTimeoutRef = useRef(null);
|
|
271
|
+
const banReloadedRef = useRef(false);
|
|
226
272
|
const baseUrl = useMemo(() => normalizedBaseUrl(apiBaseUrl), [apiBaseUrl]);
|
|
227
273
|
const storageKey = `${STORAGE_PREFIX}:${leadSource}`;
|
|
228
274
|
const profileKey = `${storageKey}:profile`;
|
|
275
|
+
const blockedKey = `${storageKey}:blocked`;
|
|
229
276
|
const visitorName = formatGuestName(profile?.name);
|
|
230
277
|
const needsProfile = profileReady && !visitorName;
|
|
278
|
+
function buildStatePayload() {
|
|
279
|
+
const currentUrl = liveChatCurrentUrl();
|
|
280
|
+
if (currentUrl)
|
|
281
|
+
pageUrlsRef.current.add(currentUrl);
|
|
282
|
+
return {
|
|
283
|
+
type: "state",
|
|
284
|
+
open: openRef.current,
|
|
285
|
+
page_url: currentUrl,
|
|
286
|
+
referrer_url: document.referrer || "",
|
|
287
|
+
utm_source: liveChatQueryParam("utm_source"),
|
|
288
|
+
utm_medium: liveChatQueryParam("utm_medium"),
|
|
289
|
+
utm_campaign: liveChatQueryParam("utm_campaign"),
|
|
290
|
+
page_view_count: Math.max(1, pageUrlsRef.current.size),
|
|
291
|
+
fields_touched_count: touchedFieldsRef.current.size,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
function sendStateNow() {
|
|
295
|
+
const socket = wsRef.current;
|
|
296
|
+
if (socket?.readyState !== WebSocket.OPEN)
|
|
297
|
+
return;
|
|
298
|
+
socket.send(JSON.stringify(buildStatePayload()));
|
|
299
|
+
}
|
|
300
|
+
function scheduleStateBroadcast(delay = 250) {
|
|
301
|
+
if (stateBroadcastTimeoutRef.current)
|
|
302
|
+
return;
|
|
303
|
+
stateBroadcastTimeoutRef.current = window.setTimeout(() => {
|
|
304
|
+
stateBroadcastTimeoutRef.current = null;
|
|
305
|
+
sendStateNow();
|
|
306
|
+
}, delay);
|
|
307
|
+
}
|
|
308
|
+
function enterBlockedState({ reload = false } = {}) {
|
|
309
|
+
try {
|
|
310
|
+
window.localStorage.setItem(blockedKey, "1");
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
// Ignore storage failures; the in-memory state still blocks the current page.
|
|
314
|
+
}
|
|
315
|
+
setBlocked(true);
|
|
316
|
+
setVisible(true);
|
|
317
|
+
setOpen(false);
|
|
318
|
+
setSession(null);
|
|
319
|
+
setMessages([]);
|
|
320
|
+
setDraft("");
|
|
321
|
+
setError("");
|
|
322
|
+
setLoading(true);
|
|
323
|
+
if (reload && !banReloadedRef.current) {
|
|
324
|
+
banReloadedRef.current = true;
|
|
325
|
+
window.setTimeout(() => window.location.reload(), 120);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function leaveBlockedState() {
|
|
329
|
+
try {
|
|
330
|
+
window.localStorage.removeItem(blockedKey);
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
// Ignore storage failures; clearing React state is enough for this tab.
|
|
334
|
+
}
|
|
335
|
+
setBlocked(false);
|
|
336
|
+
}
|
|
231
337
|
useEffect(() => {
|
|
232
338
|
openRef.current = open;
|
|
233
339
|
if (open) {
|
|
@@ -239,7 +345,7 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
|
|
|
239
345
|
}
|
|
240
346
|
const socket = wsRef.current;
|
|
241
347
|
if (socket?.readyState === WebSocket.OPEN) {
|
|
242
|
-
|
|
348
|
+
sendStateNow();
|
|
243
349
|
}
|
|
244
350
|
}, [open]);
|
|
245
351
|
useEffect(() => {
|
|
@@ -261,9 +367,9 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
|
|
|
261
367
|
return () => window.removeEventListener("keydown", onKeyDown);
|
|
262
368
|
}, []);
|
|
263
369
|
useEffect(() => {
|
|
264
|
-
setTidioTemporarilyHidden(visible);
|
|
370
|
+
setTidioTemporarilyHidden(visible || blocked);
|
|
265
371
|
return () => setTidioTemporarilyHidden(false);
|
|
266
|
-
}, [visible]);
|
|
372
|
+
}, [blocked, visible]);
|
|
267
373
|
useEffect(() => {
|
|
268
374
|
if (profileReady)
|
|
269
375
|
return;
|
|
@@ -310,6 +416,63 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
|
|
|
310
416
|
return;
|
|
311
417
|
messagesEndRef.current?.scrollIntoView({ block: "end" });
|
|
312
418
|
}, [messages.length, staffTypingName, open, visible]);
|
|
419
|
+
useEffect(() => {
|
|
420
|
+
if (!session)
|
|
421
|
+
return;
|
|
422
|
+
pageUrlsRef.current.add(liveChatCurrentUrl());
|
|
423
|
+
let lastUrl = liveChatCurrentUrl();
|
|
424
|
+
function recordPageView() {
|
|
425
|
+
const currentUrl = liveChatCurrentUrl();
|
|
426
|
+
if (!currentUrl || currentUrl === lastUrl)
|
|
427
|
+
return;
|
|
428
|
+
lastUrl = currentUrl;
|
|
429
|
+
pageUrlsRef.current.add(currentUrl);
|
|
430
|
+
scheduleStateBroadcast(120);
|
|
431
|
+
}
|
|
432
|
+
function recordFieldTouch(event) {
|
|
433
|
+
const target = event.target;
|
|
434
|
+
if (!(target instanceof Element))
|
|
435
|
+
return;
|
|
436
|
+
const key = liveChatTrackedFieldKey(target);
|
|
437
|
+
if (!key || touchedFieldsRef.current.has(key))
|
|
438
|
+
return;
|
|
439
|
+
touchedFieldsRef.current.add(key);
|
|
440
|
+
scheduleStateBroadcast(220);
|
|
441
|
+
}
|
|
442
|
+
const originalPushState = window.history.pushState;
|
|
443
|
+
const originalReplaceState = window.history.replaceState;
|
|
444
|
+
window.history.pushState = function patchedPushState(...args) {
|
|
445
|
+
const result = originalPushState.apply(this, args);
|
|
446
|
+
window.setTimeout(recordPageView, 0);
|
|
447
|
+
return result;
|
|
448
|
+
};
|
|
449
|
+
window.history.replaceState = function patchedReplaceState(...args) {
|
|
450
|
+
const result = originalReplaceState.apply(this, args);
|
|
451
|
+
window.setTimeout(recordPageView, 0);
|
|
452
|
+
return result;
|
|
453
|
+
};
|
|
454
|
+
const interval = window.setInterval(recordPageView, 1000);
|
|
455
|
+
window.addEventListener("popstate", recordPageView);
|
|
456
|
+
window.addEventListener("hashchange", recordPageView);
|
|
457
|
+
document.addEventListener("focusin", recordFieldTouch, true);
|
|
458
|
+
document.addEventListener("input", recordFieldTouch, true);
|
|
459
|
+
document.addEventListener("change", recordFieldTouch, true);
|
|
460
|
+
scheduleStateBroadcast(350);
|
|
461
|
+
return () => {
|
|
462
|
+
window.history.pushState = originalPushState;
|
|
463
|
+
window.history.replaceState = originalReplaceState;
|
|
464
|
+
window.clearInterval(interval);
|
|
465
|
+
window.removeEventListener("popstate", recordPageView);
|
|
466
|
+
window.removeEventListener("hashchange", recordPageView);
|
|
467
|
+
document.removeEventListener("focusin", recordFieldTouch, true);
|
|
468
|
+
document.removeEventListener("input", recordFieldTouch, true);
|
|
469
|
+
document.removeEventListener("change", recordFieldTouch, true);
|
|
470
|
+
if (stateBroadcastTimeoutRef.current) {
|
|
471
|
+
window.clearTimeout(stateBroadcastTimeoutRef.current);
|
|
472
|
+
stateBroadcastTimeoutRef.current = null;
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
}, [session]);
|
|
313
476
|
useEffect(() => {
|
|
314
477
|
if (!profileReady || session || !profile)
|
|
315
478
|
return;
|
|
@@ -318,6 +481,7 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
|
|
|
318
481
|
const currentProfile = profile;
|
|
319
482
|
let cancelled = false;
|
|
320
483
|
async function startSession() {
|
|
484
|
+
let keepLoading = false;
|
|
321
485
|
if (visible && open)
|
|
322
486
|
setLoading(true);
|
|
323
487
|
setError("");
|
|
@@ -331,12 +495,24 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
|
|
|
331
495
|
lead_source: leadSource,
|
|
332
496
|
guest_name: formatGuestName(currentProfile.name),
|
|
333
497
|
page_url: window.location.href,
|
|
498
|
+
referrer_url: document.referrer || "",
|
|
499
|
+
utm_source: liveChatQueryParam("utm_source"),
|
|
500
|
+
utm_medium: liveChatQueryParam("utm_medium"),
|
|
501
|
+
utm_campaign: liveChatQueryParam("utm_campaign"),
|
|
334
502
|
device_signature: signature,
|
|
335
503
|
}),
|
|
336
504
|
});
|
|
505
|
+
if (response.status === LIVE_CHAT_BLOCKED_STATUS) {
|
|
506
|
+
keepLoading = true;
|
|
507
|
+
if (!cancelled) {
|
|
508
|
+
enterBlockedState();
|
|
509
|
+
}
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
337
512
|
const envelope = await readJson(response);
|
|
338
513
|
if (cancelled)
|
|
339
514
|
return;
|
|
515
|
+
leaveBlockedState();
|
|
340
516
|
window.localStorage.setItem(storageKey, JSON.stringify(envelope.data));
|
|
341
517
|
setSession(envelope.data);
|
|
342
518
|
}
|
|
@@ -345,7 +521,7 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
|
|
|
345
521
|
setError(err instanceof Error ? err.message : "Chat is unavailable.");
|
|
346
522
|
}
|
|
347
523
|
finally {
|
|
348
|
-
if (!cancelled)
|
|
524
|
+
if (!cancelled && !keepLoading)
|
|
349
525
|
setLoading(false);
|
|
350
526
|
if (startSessionRef.current === startPromise)
|
|
351
527
|
startSessionRef.current = null;
|
|
@@ -356,7 +532,15 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
|
|
|
356
532
|
return () => {
|
|
357
533
|
cancelled = true;
|
|
358
534
|
};
|
|
359
|
-
}, [apiKey, baseUrl, leadSource, open, profile, profileReady, session, storageKey, visible]);
|
|
535
|
+
}, [apiKey, baseUrl, blockedRetryNonce, leadSource, open, profile, profileReady, session, storageKey, visible]);
|
|
536
|
+
useEffect(() => {
|
|
537
|
+
if (!blocked || session || !profileReady || !profile)
|
|
538
|
+
return;
|
|
539
|
+
const timer = window.setTimeout(() => {
|
|
540
|
+
setBlockedRetryNonce((value) => value + 1);
|
|
541
|
+
}, 15000);
|
|
542
|
+
return () => window.clearTimeout(timer);
|
|
543
|
+
}, [blocked, profile, profileReady, session]);
|
|
360
544
|
useEffect(() => {
|
|
361
545
|
if (!visible || !open || !session)
|
|
362
546
|
return;
|
|
@@ -401,7 +585,7 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
|
|
|
401
585
|
if (cancelled || activeSocket !== socket)
|
|
402
586
|
return;
|
|
403
587
|
reconnectAttempts = 0;
|
|
404
|
-
|
|
588
|
+
sendStateNow();
|
|
405
589
|
};
|
|
406
590
|
socket.onmessage = (event) => {
|
|
407
591
|
if (cancelled || activeSocket !== socket)
|
|
@@ -419,19 +603,26 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
|
|
|
419
603
|
typingTimeoutRef.current = setTimeout(() => setStaffTypingName(""), 2200);
|
|
420
604
|
return;
|
|
421
605
|
}
|
|
422
|
-
if (payload.action === "session_removed") {
|
|
606
|
+
if (payload.action === "session_removed" || payload.action === "session_banned") {
|
|
423
607
|
window.localStorage.removeItem(storageKey);
|
|
424
|
-
|
|
608
|
+
if (payload.action === "session_removed" && payload.clear_profile !== false) {
|
|
609
|
+
window.localStorage.removeItem(profileKey);
|
|
610
|
+
}
|
|
425
611
|
setSession(null);
|
|
426
612
|
setMessages([]);
|
|
427
613
|
setDraft("");
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
614
|
+
if (payload.action === "session_removed" && payload.clear_profile !== false) {
|
|
615
|
+
setNameDraft("");
|
|
616
|
+
setProfile(null);
|
|
617
|
+
setProfileReady(false);
|
|
618
|
+
}
|
|
431
619
|
setStaffTypingName("");
|
|
432
620
|
setUnreadCount(0);
|
|
433
621
|
setUnreadPreview("");
|
|
434
622
|
setPreviewVisible(false);
|
|
623
|
+
if (payload.action === "session_banned") {
|
|
624
|
+
enterBlockedState({ reload: true });
|
|
625
|
+
}
|
|
435
626
|
socket.close();
|
|
436
627
|
return;
|
|
437
628
|
}
|
|
@@ -498,7 +689,8 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
|
|
|
498
689
|
if (reconnectTimer)
|
|
499
690
|
clearTimeout(reconnectTimer);
|
|
500
691
|
if (activeSocket?.readyState === WebSocket.OPEN) {
|
|
501
|
-
|
|
692
|
+
openRef.current = false;
|
|
693
|
+
activeSocket.send(JSON.stringify(buildStatePayload()));
|
|
502
694
|
}
|
|
503
695
|
endPresence();
|
|
504
696
|
activeSocket?.close();
|
|
@@ -565,7 +757,7 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
|
|
|
565
757
|
const rootStyle = {
|
|
566
758
|
"--os-chat-accent": accentColor,
|
|
567
759
|
};
|
|
568
|
-
return (_jsxs("div", { className: `os-live-chat ${visible ? "is-visible" : "is-hidden"}`, style: rootStyle, children: [open ? (_jsxs("section", { className: "os-live-chat-panel", "aria-label": `${brandName} live chat`, children: [_jsxs("header", { className: "os-live-chat-header", children: [_jsx("span", { className: "os-live-chat-mark", children: _jsx("span", { className: "os-live-chat-mark-icon", children: _jsx(Headphones, { size: 22, "aria-hidden": true }) }) }), _jsxs("div", { children: [_jsx("p", { children: supportLabel }), _jsx("span", { children: visitorName ? `${visitorName}, ${brandName} team is here` : `${brandName} team` })] }), _jsx("button", { type: "button", "aria-label": "Close chat", onClick: () => setOpen(false), children: _jsx(X, { size: 20, "aria-hidden": true }) })] }), needsProfile ? (_jsxs("form", { className: "os-live-chat-intro", onSubmit: saveName, children: [_jsx("strong", { children: "Welcome. What should we call you?" }), onlineAgents.length > 0 ? (_jsxs("div", { className: "os-live-chat-online", "aria-label": `${onlineAgents.length} support agents online`, children: [_jsx("div", { className: "os-live-chat-online-avatars", children: onlineAgents.map((agent, index) => (_jsx("span", { className: "os-live-chat-online-avatar", style: { "--os-agent-color": agent.color || accentColor }, children: agent.avatar_url ? _jsx("img", { src: agent.avatar_url, alt: "" }) : null }, `${agent.avatar_url || agent.color || "agent"}-${index}`))) }), _jsx("span", { children: "Support is online now" })] })) : null, _jsx("input", { autoFocus: true, value: nameDraft, onChange: (event) => setNameDraft(event.target.value), placeholder: "Your name", maxLength: 80 }), _jsx("button", { type: "submit", disabled: !nameDraft.trim(), children: "Start chat" })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "os-live-chat-body", children: [loading ? (_jsxs("div", { className: "os-live-chat-empty", children: [_jsx(Loader2, { size: 18, className: "os-live-chat-spin", "aria-hidden": true }), "Connecting..."] })) : messages.length === 0 ? (_jsxs("div", { className: "os-live-chat-empty", children: [_jsxs("strong", { children: ["Hi there ", _jsx("span", { "aria-hidden": true, children: "\uD83D\uDC4B" })] }), onlineAgents.length > 0 ? (_jsxs("div", { className: "os-live-chat-online", "aria-label": `${onlineAgents.length} support agents online`, children: [_jsx("div", { className: "os-live-chat-online-avatars", children: onlineAgents.map((agent, index) => (_jsx("span", { className: "os-live-chat-online-avatar", style: { "--os-agent-color": agent.color || accentColor }, children: agent.avatar_url ? _jsx("img", { src: agent.avatar_url, alt: "" }) : null }, `${agent.avatar_url || agent.color || "agent"}-${index}`))) }), _jsx("span", { children: "Support is online now" })] })) : null, _jsx("span", { children: "How may we help you today?" })] })) : (messages.map((message) => (_jsxs("article", { className: `os-live-chat-message ${message.sender === "guest" ? "is-guest" : "is-staff"}`, children: [_jsx("p", { children: message.body }), _jsxs("span", { children: [message.sender === "guest" ? "You" : brandName, " \u00B7 ", messageTime(message.created_at)] })] }, message.id)))), staffTypingName ? (_jsxs("div", { className: "os-live-chat-typing", "aria-live": "polite", children: [_jsx("span", { children: "Support is typing" }), _jsx("i", {}), _jsx("i", {}), _jsx("i", {})] })) : null, _jsx("div", { ref: messagesEndRef })] }), error ? _jsx("p", { className: "os-live-chat-error", children: error }) : null, _jsxs("form", { className: "os-live-chat-form", onSubmit: sendMessage, children: [_jsx("textarea", { value: draft, onChange: (event) => {
|
|
760
|
+
return (_jsxs("div", { className: `os-live-chat ${blocked || visible ? "is-visible" : "is-hidden"} ${blocked ? "is-blocked" : ""}`, style: rootStyle, children: [blocked ? (_jsx("div", { className: "os-live-chat-blocker", "aria-live": "polite", "aria-busy": "true", children: _jsx("div", { className: "os-live-chat-blocker-card", "aria-label": "Loading", children: _jsx(Loader2, { size: 32, className: "os-live-chat-spin", "aria-hidden": true }) }) })) : null, !blocked && open ? (_jsxs("section", { className: "os-live-chat-panel", "aria-label": `${brandName} live chat`, children: [_jsxs("header", { className: "os-live-chat-header", children: [_jsx("span", { className: "os-live-chat-mark", children: _jsx("span", { className: "os-live-chat-mark-icon", children: _jsx(Headphones, { size: 22, "aria-hidden": true }) }) }), _jsxs("div", { children: [_jsx("p", { children: supportLabel }), _jsx("span", { children: visitorName ? `${visitorName}, ${brandName} team is here` : `${brandName} team` })] }), _jsx("button", { type: "button", "aria-label": "Close chat", onClick: () => setOpen(false), children: _jsx(X, { size: 20, "aria-hidden": true }) })] }), needsProfile ? (_jsxs("form", { className: "os-live-chat-intro", onSubmit: saveName, children: [_jsx("strong", { children: "Welcome. What should we call you?" }), onlineAgents.length > 0 ? (_jsxs("div", { className: "os-live-chat-online", "aria-label": `${onlineAgents.length} support agents online`, children: [_jsx("div", { className: "os-live-chat-online-avatars", children: onlineAgents.map((agent, index) => (_jsx("span", { className: "os-live-chat-online-avatar", style: { "--os-agent-color": agent.color || accentColor }, children: agent.avatar_url ? _jsx("img", { src: agent.avatar_url, alt: "" }) : null }, `${agent.avatar_url || agent.color || "agent"}-${index}`))) }), _jsx("span", { children: "Support is online now" })] })) : null, _jsx("input", { autoFocus: true, value: nameDraft, onChange: (event) => setNameDraft(event.target.value), placeholder: "Your name", maxLength: 80 }), _jsx("button", { type: "submit", disabled: !nameDraft.trim(), children: "Start chat" })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "os-live-chat-body", children: [loading ? (_jsxs("div", { className: "os-live-chat-empty", children: [_jsx(Loader2, { size: 18, className: "os-live-chat-spin", "aria-hidden": true }), "Connecting..."] })) : messages.length === 0 ? (_jsxs("div", { className: "os-live-chat-empty", children: [_jsxs("strong", { children: ["Hi there ", _jsx("span", { "aria-hidden": true, children: "\uD83D\uDC4B" })] }), onlineAgents.length > 0 ? (_jsxs("div", { className: "os-live-chat-online", "aria-label": `${onlineAgents.length} support agents online`, children: [_jsx("div", { className: "os-live-chat-online-avatars", children: onlineAgents.map((agent, index) => (_jsx("span", { className: "os-live-chat-online-avatar", style: { "--os-agent-color": agent.color || accentColor }, children: agent.avatar_url ? _jsx("img", { src: agent.avatar_url, alt: "" }) : null }, `${agent.avatar_url || agent.color || "agent"}-${index}`))) }), _jsx("span", { children: "Support is online now" })] })) : null, _jsx("span", { children: "How may we help you today?" })] })) : (messages.map((message) => (_jsxs("article", { className: `os-live-chat-message ${message.sender === "guest" ? "is-guest" : "is-staff"}`, children: [_jsx("p", { children: message.body }), _jsxs("span", { children: [message.sender === "guest" ? "You" : brandName, " \u00B7 ", messageTime(message.created_at)] })] }, message.id)))), staffTypingName ? (_jsxs("div", { className: "os-live-chat-typing", "aria-live": "polite", children: [_jsx("span", { children: "Support is typing" }), _jsx("i", {}), _jsx("i", {}), _jsx("i", {})] })) : null, _jsx("div", { ref: messagesEndRef })] }), error ? _jsx("p", { className: "os-live-chat-error", children: error }) : null, _jsxs("form", { className: "os-live-chat-form", onSubmit: sendMessage, children: [_jsx("textarea", { value: draft, onChange: (event) => {
|
|
569
761
|
const nextDraft = event.target.value;
|
|
570
762
|
setDraft(nextDraft);
|
|
571
763
|
announceTyping(nextDraft);
|
|
@@ -574,7 +766,7 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
|
|
|
574
766
|
event.preventDefault();
|
|
575
767
|
event.currentTarget.form?.requestSubmit();
|
|
576
768
|
}
|
|
577
|
-
} }), _jsx("button", { type: "submit", disabled: !draft.trim() || !session || sending, "aria-label": "Send message", children: sending ? (_jsx(Loader2, { size: 19, className: "os-live-chat-spin", "aria-hidden": true })) : (_jsx(SendHorizontal, { size: 20, "aria-hidden": true })) })] })] }))] })) : null, visible ? (_jsxs("button", { className: "os-live-chat-bubble", type: "button", onClick: () => setOpen((value) => !value), "aria-label": "Open live chat", children: [_jsx(MessageCircle, { size: 25, "aria-hidden": true }), unreadCount > 0 ? _jsx("strong", { className: "os-live-chat-badge", children: unreadCount }) : null, !open ? (_jsx("span", { className: unreadCount > 0 ? `has-unread ${previewVisible ? "is-preview-visible" : "is-preview-hidden"}` : "", children: unreadCount > 0 && previewVisible ? (_jsxs(_Fragment, { children: [_jsx("b", { children: "New message" }), _jsx("small", { children: unreadPreview })] })) : (_jsxs(_Fragment, { children: ["Need live assistance? ", _jsx("small", { className: "os-live-chat-wave", "aria-hidden": true, children: "\uD83D\uDC4B" })] })) })) : null] })) : null, _jsx("style", { children: `
|
|
769
|
+
} }), _jsx("button", { type: "submit", disabled: !draft.trim() || !session || sending, "aria-label": "Send message", children: sending ? (_jsx(Loader2, { size: 19, className: "os-live-chat-spin", "aria-hidden": true })) : (_jsx(SendHorizontal, { size: 20, "aria-hidden": true })) })] })] }))] })) : null, !blocked && visible ? (_jsxs("button", { className: "os-live-chat-bubble", type: "button", onClick: () => setOpen((value) => !value), "aria-label": "Open live chat", children: [_jsx(MessageCircle, { size: 25, "aria-hidden": true }), unreadCount > 0 ? _jsx("strong", { className: "os-live-chat-badge", children: unreadCount }) : null, !open ? (_jsx("span", { className: unreadCount > 0 ? `has-unread ${previewVisible ? "is-preview-visible" : "is-preview-hidden"}` : "", "aria-live": unreadCount > 0 ? "polite" : undefined, children: unreadCount > 0 && previewVisible ? (_jsxs(_Fragment, { children: [_jsx("b", { children: "New message" }), _jsx("small", { children: unreadPreview })] })) : (_jsxs(_Fragment, { children: ["Need live assistance? ", _jsx("small", { className: "os-live-chat-wave", "aria-hidden": true, children: "\uD83D\uDC4B" })] })) }, unreadCount > 0 ? `preview-${unreadPreview}` : "assist-prompt")) : null] })) : null, _jsx("style", { children: `
|
|
578
770
|
.os-live-chat {
|
|
579
771
|
position: fixed;
|
|
580
772
|
right: max(18px, env(safe-area-inset-right));
|
|
@@ -585,6 +777,34 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
|
|
|
585
777
|
.os-live-chat.is-hidden {
|
|
586
778
|
pointer-events: none;
|
|
587
779
|
}
|
|
780
|
+
.os-live-chat.is-blocked {
|
|
781
|
+
pointer-events: auto;
|
|
782
|
+
}
|
|
783
|
+
.os-live-chat-blocker {
|
|
784
|
+
position: fixed;
|
|
785
|
+
inset: 0;
|
|
786
|
+
z-index: 2147483000;
|
|
787
|
+
display: grid;
|
|
788
|
+
place-items: center;
|
|
789
|
+
padding: 24px;
|
|
790
|
+
background:
|
|
791
|
+
radial-gradient(circle at 50% 42%, color-mix(in srgb, var(--os-chat-accent) 14%, transparent), transparent 34%),
|
|
792
|
+
rgba(248, 250, 252, 0.98);
|
|
793
|
+
backdrop-filter: blur(18px);
|
|
794
|
+
-webkit-backdrop-filter: blur(18px);
|
|
795
|
+
animation: os-chat-blocker-in 320ms cubic-bezier(.16,1,.3,1) both;
|
|
796
|
+
}
|
|
797
|
+
.os-live-chat-blocker-card {
|
|
798
|
+
display: grid;
|
|
799
|
+
width: 84px;
|
|
800
|
+
height: 84px;
|
|
801
|
+
place-items: center;
|
|
802
|
+
border: 1px solid rgba(148, 163, 184, 0.24);
|
|
803
|
+
border-radius: 28px;
|
|
804
|
+
color: var(--os-chat-accent);
|
|
805
|
+
background: rgba(255, 255, 255, 0.86);
|
|
806
|
+
box-shadow: 0 24px 80px rgba(15, 23, 42, 0.16);
|
|
807
|
+
}
|
|
588
808
|
.os-live-chat-bubble {
|
|
589
809
|
position: relative;
|
|
590
810
|
display: grid;
|
|
@@ -602,8 +822,8 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
|
|
|
602
822
|
}
|
|
603
823
|
.os-live-chat-bubble span {
|
|
604
824
|
position: absolute;
|
|
605
|
-
right:
|
|
606
|
-
bottom:
|
|
825
|
+
right: 68px;
|
|
826
|
+
bottom: 11px;
|
|
607
827
|
width: max-content;
|
|
608
828
|
max-width: 230px;
|
|
609
829
|
border: 1px solid rgba(148, 163, 184, 0.28);
|
|
@@ -615,25 +835,27 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
|
|
|
615
835
|
font-size: 14px;
|
|
616
836
|
font-weight: 800;
|
|
617
837
|
pointer-events: none;
|
|
618
|
-
|
|
838
|
+
transform-origin: bottom right;
|
|
839
|
+
animation: os-chat-nudge 520ms cubic-bezier(.16,1,.3,1) 350ms both;
|
|
619
840
|
}
|
|
620
841
|
.os-live-chat-bubble span.has-unread {
|
|
621
842
|
display: grid;
|
|
622
843
|
gap: 2px;
|
|
623
|
-
width: min(260px, calc(100vw -
|
|
844
|
+
width: min(260px, calc(100vw - 130px));
|
|
624
845
|
max-width: 260px;
|
|
625
|
-
border-radius:
|
|
626
|
-
padding:
|
|
846
|
+
border-radius: 20px;
|
|
847
|
+
padding: 11px 14px;
|
|
627
848
|
text-align: left;
|
|
849
|
+
box-shadow: 0 18px 44px rgba(15, 23, 42, 0.16);
|
|
628
850
|
}
|
|
629
851
|
.os-live-chat-bubble span.has-unread.is-preview-visible {
|
|
630
|
-
animation: os-chat-preview-in
|
|
852
|
+
animation: os-chat-preview-in 420ms cubic-bezier(.16,1,.3,1) both;
|
|
631
853
|
}
|
|
632
854
|
.os-live-chat-bubble span.has-unread.is-preview-hidden {
|
|
633
855
|
opacity: 0;
|
|
634
|
-
transform: translateX(10px) scale(0.98);
|
|
635
856
|
visibility: hidden;
|
|
636
|
-
|
|
857
|
+
animation: os-chat-preview-out 360ms cubic-bezier(.4,0,.2,1) both;
|
|
858
|
+
transition: visibility 0s linear 360ms;
|
|
637
859
|
}
|
|
638
860
|
.os-live-chat-bubble span.has-unread b,
|
|
639
861
|
.os-live-chat-bubble span.has-unread small {
|
|
@@ -697,7 +919,7 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
|
|
|
697
919
|
border-radius: 28px;
|
|
698
920
|
background: rgba(255, 255, 255, 0.98);
|
|
699
921
|
box-shadow: 0 28px 80px rgba(15, 23, 42, 0.24);
|
|
700
|
-
animation: os-chat-in
|
|
922
|
+
animation: os-chat-in 360ms cubic-bezier(.16,1,.3,1);
|
|
701
923
|
transform-origin: bottom right;
|
|
702
924
|
}
|
|
703
925
|
.os-live-chat-header {
|
|
@@ -883,6 +1105,7 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
|
|
|
883
1105
|
gap: 10px;
|
|
884
1106
|
overflow-y: auto;
|
|
885
1107
|
padding: 16px;
|
|
1108
|
+
scroll-behavior: smooth;
|
|
886
1109
|
background:
|
|
887
1110
|
radial-gradient(circle at 22% 0%, color-mix(in srgb, var(--os-chat-accent) 8%, transparent), transparent 32%),
|
|
888
1111
|
#f8fafc;
|
|
@@ -908,7 +1131,13 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
|
|
|
908
1131
|
font-size: 14px;
|
|
909
1132
|
line-height: 1.45;
|
|
910
1133
|
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.07);
|
|
911
|
-
|
|
1134
|
+
will-change: transform, opacity;
|
|
1135
|
+
animation: os-message-in 340ms cubic-bezier(.16,1,.3,1) both;
|
|
1136
|
+
transition: transform 260ms cubic-bezier(.16,1,.3,1), box-shadow 260ms ease, opacity 260ms ease;
|
|
1137
|
+
}
|
|
1138
|
+
.os-live-chat-message:hover {
|
|
1139
|
+
transform: translateY(-1px);
|
|
1140
|
+
box-shadow: 0 11px 26px rgba(15, 23, 42, 0.09);
|
|
912
1141
|
}
|
|
913
1142
|
.os-live-chat-message p {
|
|
914
1143
|
margin: 0;
|
|
@@ -947,7 +1176,7 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
|
|
|
947
1176
|
background: white;
|
|
948
1177
|
font-size: 12px;
|
|
949
1178
|
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.06);
|
|
950
|
-
animation: os-message-in
|
|
1179
|
+
animation: os-message-in 340ms cubic-bezier(.16,1,.3,1) both;
|
|
951
1180
|
}
|
|
952
1181
|
.os-live-chat-typing i {
|
|
953
1182
|
width: 5px;
|
|
@@ -1018,11 +1247,13 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
|
|
|
1018
1247
|
@keyframes os-chat-in {
|
|
1019
1248
|
from {
|
|
1020
1249
|
opacity: 0;
|
|
1021
|
-
transform: translateY(
|
|
1250
|
+
transform: translateY(18px) scale(0.965);
|
|
1251
|
+
filter: blur(5px);
|
|
1022
1252
|
}
|
|
1023
1253
|
to {
|
|
1024
1254
|
opacity: 1;
|
|
1025
1255
|
transform: translateY(0) scale(1);
|
|
1256
|
+
filter: blur(0);
|
|
1026
1257
|
}
|
|
1027
1258
|
}
|
|
1028
1259
|
@keyframes os-chat-bubble-in {
|
|
@@ -1035,6 +1266,16 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
|
|
|
1035
1266
|
transform: translateY(0) scale(1);
|
|
1036
1267
|
}
|
|
1037
1268
|
}
|
|
1269
|
+
@keyframes os-chat-blocker-in {
|
|
1270
|
+
from {
|
|
1271
|
+
opacity: 0;
|
|
1272
|
+
filter: blur(6px);
|
|
1273
|
+
}
|
|
1274
|
+
to {
|
|
1275
|
+
opacity: 1;
|
|
1276
|
+
filter: blur(0);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1038
1279
|
@keyframes os-chat-nudge {
|
|
1039
1280
|
from {
|
|
1040
1281
|
opacity: 0;
|
|
@@ -1059,21 +1300,37 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
|
|
|
1059
1300
|
@keyframes os-chat-preview-in {
|
|
1060
1301
|
from {
|
|
1061
1302
|
opacity: 0;
|
|
1062
|
-
transform: translateX(
|
|
1303
|
+
transform: translateX(14px) translateY(4px) scale(0.96);
|
|
1304
|
+
filter: blur(4px);
|
|
1063
1305
|
}
|
|
1064
1306
|
to {
|
|
1065
1307
|
opacity: 1;
|
|
1066
1308
|
transform: translateX(0) scale(1);
|
|
1309
|
+
filter: blur(0);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
@keyframes os-chat-preview-out {
|
|
1313
|
+
from {
|
|
1314
|
+
opacity: 1;
|
|
1315
|
+
transform: translateX(0) scale(1);
|
|
1316
|
+
filter: blur(0);
|
|
1317
|
+
}
|
|
1318
|
+
to {
|
|
1319
|
+
opacity: 0;
|
|
1320
|
+
transform: translateX(10px) translateY(3px) scale(0.96);
|
|
1321
|
+
filter: blur(4px);
|
|
1067
1322
|
}
|
|
1068
1323
|
}
|
|
1069
1324
|
@keyframes os-message-in {
|
|
1070
1325
|
from {
|
|
1071
1326
|
opacity: 0;
|
|
1072
|
-
transform: translateY(
|
|
1327
|
+
transform: translateY(10px) scale(0.975);
|
|
1328
|
+
filter: blur(3px);
|
|
1073
1329
|
}
|
|
1074
1330
|
to {
|
|
1075
1331
|
opacity: 1;
|
|
1076
1332
|
transform: translateY(0) scale(1);
|
|
1333
|
+
filter: blur(0);
|
|
1077
1334
|
}
|
|
1078
1335
|
}
|
|
1079
1336
|
@keyframes os-typing-dot {
|
|
@@ -1105,6 +1362,26 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
|
|
|
1105
1362
|
.os-live-chat-bubble span:not(.has-unread) {
|
|
1106
1363
|
display: none;
|
|
1107
1364
|
}
|
|
1365
|
+
.os-live-chat-bubble span.has-unread {
|
|
1366
|
+
right: 68px;
|
|
1367
|
+
width: min(242px, calc(100vw - 116px));
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1371
|
+
.os-live-chat-bubble,
|
|
1372
|
+
.os-live-chat-bubble span,
|
|
1373
|
+
.os-live-chat-bubble span.has-unread.is-preview-visible,
|
|
1374
|
+
.os-live-chat-bubble span.has-unread.is-preview-hidden,
|
|
1375
|
+
.os-live-chat-panel,
|
|
1376
|
+
.os-live-chat-message,
|
|
1377
|
+
.os-live-chat-typing,
|
|
1378
|
+
.os-live-chat-wave {
|
|
1379
|
+
animation: none !important;
|
|
1380
|
+
transition: none !important;
|
|
1381
|
+
}
|
|
1382
|
+
.os-live-chat-body {
|
|
1383
|
+
scroll-behavior: auto;
|
|
1384
|
+
}
|
|
1108
1385
|
}
|
|
1109
1386
|
` })] }));
|
|
1110
1387
|
}
|