@clivly/chat-widget 0.4.0 → 0.7.0
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/README.md +81 -4
- package/dist/browser-mount.cjs +297 -71
- package/dist/browser-mount.d.cts +40 -11
- package/dist/browser-mount.d.mts +40 -11
- package/dist/browser-mount.mjs +298 -72
- package/dist/browser.cjs +297 -71
- package/dist/browser.d.cts +41 -11
- package/dist/browser.d.mts +41 -11
- package/dist/browser.mjs +298 -72
- package/dist/index.cjs +299 -72
- package/dist/index.d.cts +59 -36
- package/dist/index.d.mts +59 -36
- package/dist/index.mjs +300 -74
- package/dist/widget.global.js +228 -84
- package/package.json +7 -3
package/dist/index.mjs
CHANGED
|
@@ -1,21 +1,162 @@
|
|
|
1
1
|
import { createRoot } from "react-dom/client";
|
|
2
2
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
//#region src/theme.ts
|
|
5
|
+
const DARK_MEDIA_QUERY = "(prefers-color-scheme: dark)";
|
|
6
|
+
/** The attribute set by next-themes, daisyUI, and most `data-*` theme setups. */
|
|
7
|
+
const THEME_ATTRIBUTE = "data-theme";
|
|
8
|
+
/** Attributes worth re-resolving on. Anything else on `<html>` is noise. */
|
|
9
|
+
const OBSERVED_ATTRIBUTES = ["class", THEME_ATTRIBUTE];
|
|
10
|
+
function isResolvedTheme(value) {
|
|
11
|
+
return value === "dark" || value === "light";
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The host's explicit signals, in precedence order: a `data-theme` attribute
|
|
15
|
+
* beats a class, because a host that sets both is using the attribute as the
|
|
16
|
+
* source of truth and the class as a styling convenience.
|
|
17
|
+
*
|
|
18
|
+
* Both conventions are checked for `light` as well as `dark`. A host that says
|
|
19
|
+
* `class="light"` on an OS-dark machine means it, and the explicit signal
|
|
20
|
+
* should win over the OS preference below it.
|
|
21
|
+
*/
|
|
22
|
+
function readHostTheme(root) {
|
|
23
|
+
const attribute = root.getAttribute(THEME_ATTRIBUTE);
|
|
24
|
+
if (isResolvedTheme(attribute)) return attribute;
|
|
25
|
+
if (root.classList.contains("dark")) return "dark";
|
|
26
|
+
if (root.classList.contains("light")) return "light";
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
function prefersDark(view) {
|
|
30
|
+
return view.matchMedia?.(DARK_MEDIA_QUERY).matches ?? false;
|
|
31
|
+
}
|
|
32
|
+
function defaultRoot() {
|
|
33
|
+
return typeof document === "undefined" ? null : document.documentElement;
|
|
34
|
+
}
|
|
35
|
+
function defaultView() {
|
|
36
|
+
return typeof window === "undefined" ? null : window;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Resolve a requested theme to the one the widget should actually paint.
|
|
40
|
+
*
|
|
41
|
+
* Falls back to `light` when there is nothing to read — during SSR there is no
|
|
42
|
+
* document, and guessing dark would flash the wrong palette on hydration for
|
|
43
|
+
* the majority of pages.
|
|
44
|
+
*/
|
|
45
|
+
function resolveTheme(theme, { root = defaultRoot(), view = defaultView() } = {}) {
|
|
46
|
+
if (theme !== "auto") return theme;
|
|
47
|
+
const hostTheme = root ? readHostTheme(root) : null;
|
|
48
|
+
if (hostTheme) return hostTheme;
|
|
49
|
+
if (view && prefersDark(view)) return "dark";
|
|
50
|
+
return "light";
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Watch both host signals and call `onChange` whenever the resolved theme
|
|
54
|
+
* changes. Returns an unsubscribe function.
|
|
55
|
+
*
|
|
56
|
+
* An explicit `light`/`dark` subscribes to nothing — there is no signal that
|
|
57
|
+
* could change the answer — so callers can pass any theme without branching.
|
|
58
|
+
*/
|
|
59
|
+
function observeTheme(theme, onChange, { root = defaultRoot(), view = defaultView() } = {}) {
|
|
60
|
+
if (theme !== "auto") return () => {};
|
|
61
|
+
let current = resolveTheme(theme, {
|
|
62
|
+
root,
|
|
63
|
+
view
|
|
64
|
+
});
|
|
65
|
+
const publish = () => {
|
|
66
|
+
const next = resolveTheme(theme, {
|
|
67
|
+
root,
|
|
68
|
+
view
|
|
69
|
+
});
|
|
70
|
+
if (next !== current) {
|
|
71
|
+
current = next;
|
|
72
|
+
onChange(next);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const teardowns = [];
|
|
76
|
+
if (root && typeof MutationObserver !== "undefined") {
|
|
77
|
+
const observer = new MutationObserver(publish);
|
|
78
|
+
observer.observe(root, {
|
|
79
|
+
attributeFilter: OBSERVED_ATTRIBUTES,
|
|
80
|
+
attributes: true
|
|
81
|
+
});
|
|
82
|
+
teardowns.push(() => {
|
|
83
|
+
observer.disconnect();
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
const media = view?.matchMedia?.(DARK_MEDIA_QUERY);
|
|
87
|
+
if (media) {
|
|
88
|
+
if (media.addEventListener) {
|
|
89
|
+
media.addEventListener("change", publish);
|
|
90
|
+
teardowns.push(() => {
|
|
91
|
+
media.removeEventListener("change", publish);
|
|
92
|
+
});
|
|
93
|
+
} else if (media.addListener) {
|
|
94
|
+
media.addListener(publish);
|
|
95
|
+
teardowns.push(() => {
|
|
96
|
+
media.removeListener(publish);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return () => {
|
|
101
|
+
for (const teardown of teardowns) teardown();
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Narrow an untrusted string — a `data-theme` script attribute, a `setTheme`
|
|
106
|
+
* call from plain JS — to a theme, falling back to `auto`.
|
|
107
|
+
*/
|
|
108
|
+
function parseTheme(value) {
|
|
109
|
+
if (value === "auto" || value === "dark" || value === "light") return value;
|
|
110
|
+
return "auto";
|
|
111
|
+
}
|
|
112
|
+
//#endregion
|
|
4
113
|
//#region src/chat-widget.tsx
|
|
114
|
+
const DELIVERY_LABELS = {
|
|
115
|
+
failed: "Failed to send",
|
|
116
|
+
pending: "Sending…",
|
|
117
|
+
sent: null
|
|
118
|
+
};
|
|
5
119
|
const WIDGET_STYLES = `
|
|
6
120
|
.clivly-chat-widget, .clivly-chat-widget * { box-sizing: border-box; }
|
|
7
121
|
.clivly-chat-widget {
|
|
8
122
|
--clivly-chat-accent: #185adb;
|
|
123
|
+
--clivly-chat-accent-contrast: #ffffff;
|
|
9
124
|
--clivly-chat-panel: #ffffff;
|
|
125
|
+
--clivly-chat-input: transparent;
|
|
10
126
|
--clivly-chat-border: #d6dde8;
|
|
11
127
|
--clivly-chat-text: #132238;
|
|
12
128
|
--clivly-chat-muted: #60738a;
|
|
13
129
|
--clivly-chat-visitor: #ecf3ff;
|
|
14
130
|
--clivly-chat-operator: #f5f7fa;
|
|
131
|
+
--clivly-chat-shadow: rgba(19, 34, 56, 0.18);
|
|
132
|
+
--clivly-chat-error-bg: #fff1f1;
|
|
133
|
+
--clivly-chat-error-border: #f2c2c2;
|
|
134
|
+
--clivly-chat-error-text: #9f2d2d;
|
|
15
135
|
color: var(--clivly-chat-text);
|
|
136
|
+
color-scheme: light;
|
|
16
137
|
font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
17
138
|
position: relative;
|
|
18
139
|
}
|
|
140
|
+
/* Dark is a pure token override — every rule below reads through a variable, so
|
|
141
|
+
the palette is the only thing that changes between themes. The color-scheme
|
|
142
|
+
declaration carries the theme to the parts of the UI that CSS cannot reach:
|
|
143
|
+
native scrollbars, autofill backgrounds, and the caret. */
|
|
144
|
+
.clivly-chat-widget[data-clivly-theme="dark"] {
|
|
145
|
+
--clivly-chat-accent: #4f8cff;
|
|
146
|
+
--clivly-chat-accent-contrast: #0b1220;
|
|
147
|
+
--clivly-chat-panel: #111827;
|
|
148
|
+
--clivly-chat-input: #0b1220;
|
|
149
|
+
--clivly-chat-border: #2a3546;
|
|
150
|
+
--clivly-chat-text: #e8edf5;
|
|
151
|
+
--clivly-chat-muted: #93a2b8;
|
|
152
|
+
--clivly-chat-visitor: #1e3a6b;
|
|
153
|
+
--clivly-chat-operator: #1b2534;
|
|
154
|
+
--clivly-chat-shadow: rgba(0, 0, 0, 0.5);
|
|
155
|
+
--clivly-chat-error-bg: #2c1618;
|
|
156
|
+
--clivly-chat-error-border: #5c2a2e;
|
|
157
|
+
--clivly-chat-error-text: #ffb4b4;
|
|
158
|
+
color-scheme: dark;
|
|
159
|
+
}
|
|
19
160
|
.clivly-chat-widget button,
|
|
20
161
|
.clivly-chat-widget input,
|
|
21
162
|
.clivly-chat-widget textarea {
|
|
@@ -26,7 +167,7 @@ const WIDGET_STYLES = `
|
|
|
26
167
|
background: var(--clivly-chat-accent);
|
|
27
168
|
border: 0;
|
|
28
169
|
border-radius: 999px;
|
|
29
|
-
color:
|
|
170
|
+
color: var(--clivly-chat-accent-contrast);
|
|
30
171
|
cursor: pointer;
|
|
31
172
|
display: inline-flex;
|
|
32
173
|
gap: 0.5rem;
|
|
@@ -36,7 +177,7 @@ const WIDGET_STYLES = `
|
|
|
36
177
|
background: var(--clivly-chat-panel);
|
|
37
178
|
border: 1px solid var(--clivly-chat-border);
|
|
38
179
|
border-radius: 1rem;
|
|
39
|
-
box-shadow: 0 24px 80px
|
|
180
|
+
box-shadow: 0 24px 80px var(--clivly-chat-shadow);
|
|
40
181
|
display: grid;
|
|
41
182
|
grid-template-rows: auto 1fr auto;
|
|
42
183
|
height: min(44rem, 75vh);
|
|
@@ -139,37 +280,54 @@ const WIDGET_STYLES = `
|
|
|
139
280
|
}
|
|
140
281
|
.clivly-chat-widget__input,
|
|
141
282
|
.clivly-chat-widget__textarea {
|
|
283
|
+
background: var(--clivly-chat-input);
|
|
142
284
|
border: 1px solid var(--clivly-chat-border);
|
|
143
285
|
border-radius: 0.8rem;
|
|
286
|
+
color: inherit;
|
|
144
287
|
min-width: 0;
|
|
145
288
|
padding: 0.75rem 0.85rem;
|
|
146
289
|
}
|
|
290
|
+
.clivly-chat-widget__input::placeholder,
|
|
291
|
+
.clivly-chat-widget__textarea::placeholder {
|
|
292
|
+
color: var(--clivly-chat-muted);
|
|
293
|
+
}
|
|
147
294
|
.clivly-chat-widget__textarea {
|
|
148
295
|
min-height: 5.5rem;
|
|
149
296
|
resize: vertical;
|
|
150
297
|
}
|
|
151
298
|
.clivly-chat-widget__actions {
|
|
299
|
+
align-items: center;
|
|
152
300
|
display: flex;
|
|
153
301
|
gap: 0.75rem;
|
|
154
302
|
justify-content: space-between;
|
|
155
303
|
}
|
|
304
|
+
/* The subtitle is the only part that may reflow; without min-width the flex
|
|
305
|
+
default (min-content) lets it push the button below its intrinsic width. */
|
|
306
|
+
.clivly-chat-widget__actions .clivly-chat-widget__subtitle {
|
|
307
|
+
min-width: 0;
|
|
308
|
+
}
|
|
156
309
|
.clivly-chat-widget__button {
|
|
157
310
|
background: var(--clivly-chat-accent);
|
|
158
311
|
border: 0;
|
|
159
312
|
border-radius: 0.8rem;
|
|
160
|
-
color:
|
|
313
|
+
color: var(--clivly-chat-accent-contrast);
|
|
161
314
|
cursor: pointer;
|
|
315
|
+
flex: 0 0 auto;
|
|
316
|
+
font: inherit;
|
|
317
|
+
font-weight: 600;
|
|
318
|
+
line-height: 1.2;
|
|
162
319
|
padding: 0.75rem 0.95rem;
|
|
320
|
+
white-space: nowrap;
|
|
163
321
|
}
|
|
164
322
|
.clivly-chat-widget__button[disabled] {
|
|
165
323
|
cursor: not-allowed;
|
|
166
324
|
opacity: 0.55;
|
|
167
325
|
}
|
|
168
326
|
.clivly-chat-widget__error {
|
|
169
|
-
background:
|
|
170
|
-
border: 1px solid
|
|
327
|
+
background: var(--clivly-chat-error-bg);
|
|
328
|
+
border: 1px solid var(--clivly-chat-error-border);
|
|
171
329
|
border-radius: 0.8rem;
|
|
172
|
-
color:
|
|
330
|
+
color: var(--clivly-chat-error-text);
|
|
173
331
|
font-size: 0.875rem;
|
|
174
332
|
padding: 0.75rem;
|
|
175
333
|
}
|
|
@@ -181,6 +339,13 @@ const WIDGET_STYLES = `
|
|
|
181
339
|
}
|
|
182
340
|
`;
|
|
183
341
|
const DEFAULT_STORAGE = typeof window === "undefined" ? void 0 : window.localStorage;
|
|
342
|
+
function sessionDisplayName(session) {
|
|
343
|
+
return session.contact?.name ?? "You";
|
|
344
|
+
}
|
|
345
|
+
function sessionSummaryText(session, fallback) {
|
|
346
|
+
if (!session) return fallback;
|
|
347
|
+
return session.contact ? `${session.contact.name} • ${session.contact.email}` : "Verified visitor";
|
|
348
|
+
}
|
|
184
349
|
function createLocalMessage(message, delivery = "sent") {
|
|
185
350
|
return {
|
|
186
351
|
...message,
|
|
@@ -231,14 +396,93 @@ function markMessageFailed(messages, clientMessageId) {
|
|
|
231
396
|
function sortMessages(messages) {
|
|
232
397
|
return [...messages].sort((left, right) => left.createdAt.localeCompare(right.createdAt));
|
|
233
398
|
}
|
|
234
|
-
function useThemeStyle(session) {
|
|
399
|
+
function useThemeStyle(session, style) {
|
|
235
400
|
return useMemo(() => {
|
|
236
401
|
const brandColor = session?.widget.config.brandColor ?? null;
|
|
237
|
-
if (!brandColor) return;
|
|
238
|
-
return {
|
|
239
|
-
|
|
402
|
+
if (!brandColor) return style;
|
|
403
|
+
return {
|
|
404
|
+
["--clivly-chat-accent"]: brandColor,
|
|
405
|
+
...style
|
|
406
|
+
};
|
|
407
|
+
}, [session?.widget.config.brandColor, style]);
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Track the theme the widget should paint.
|
|
411
|
+
*
|
|
412
|
+
* The initial value is resolved synchronously so the first paint is already
|
|
413
|
+
* correct — deferring it to an effect would flash light on a dark page.
|
|
414
|
+
*/
|
|
415
|
+
function useResolvedTheme(theme) {
|
|
416
|
+
const [resolved, setResolved] = useState(() => resolveTheme(theme));
|
|
417
|
+
useEffect(() => {
|
|
418
|
+
setResolved(resolveTheme(theme));
|
|
419
|
+
return observeTheme(theme, setResolved);
|
|
420
|
+
}, [theme]);
|
|
421
|
+
return resolved;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Reduce one socket event into widget state.
|
|
425
|
+
*
|
|
426
|
+
* Lives at module scope rather than inside the connect effect so the wire
|
|
427
|
+
* protocol reads as a single unit, and so `ChatWidget` stays inside the
|
|
428
|
+
* cognitive-complexity budget.
|
|
429
|
+
*/
|
|
430
|
+
function applyServerEvent(event, { setError, setIsReady, setMessages }) {
|
|
431
|
+
switch (event.type) {
|
|
432
|
+
case "conversation.snapshot":
|
|
433
|
+
setMessages(sortMessages(event.messages.map((message) => createLocalMessage(message))));
|
|
434
|
+
return;
|
|
435
|
+
case "error":
|
|
436
|
+
setError(event.message);
|
|
437
|
+
return;
|
|
438
|
+
case "message.ack":
|
|
439
|
+
setMessages((currentMessages) => sortMessages(replacePendingMessage(currentMessages, event.clientMessageId, event.message)));
|
|
440
|
+
return;
|
|
441
|
+
case "message.created": {
|
|
442
|
+
const clientMessageId = event.clientMessageId;
|
|
443
|
+
setMessages((currentMessages) => sortMessages(clientMessageId ? replacePendingMessage(currentMessages, clientMessageId, event.message) : mergeMessage(currentMessages, event.message)));
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
case "session.ready":
|
|
447
|
+
setIsReady(true);
|
|
448
|
+
return;
|
|
449
|
+
default: return;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
function MessageBubble({ message }) {
|
|
453
|
+
return /* @__PURE__ */ jsxs("article", {
|
|
454
|
+
className: "clivly-chat-widget__message",
|
|
455
|
+
"data-sender": message.senderType,
|
|
456
|
+
children: [
|
|
457
|
+
/* @__PURE__ */ jsxs("div", {
|
|
458
|
+
className: "clivly-chat-widget__message-meta",
|
|
459
|
+
children: [/* @__PURE__ */ jsx("span", { children: message.author ?? "Clivly" }), /* @__PURE__ */ jsx("span", { children: formatTimestamp(message.createdAt) })]
|
|
460
|
+
}),
|
|
461
|
+
/* @__PURE__ */ jsx("div", { children: message.body }),
|
|
462
|
+
message.delivery === "sent" ? null : /* @__PURE__ */ jsx("div", {
|
|
463
|
+
className: "clivly-chat-widget__status",
|
|
464
|
+
children: DELIVERY_LABELS[message.delivery]
|
|
465
|
+
})
|
|
466
|
+
]
|
|
467
|
+
});
|
|
240
468
|
}
|
|
241
|
-
|
|
469
|
+
/**
|
|
470
|
+
* The scrollable transcript. Split out of `ChatWidget` because the three states
|
|
471
|
+
* (no session, empty thread, messages) were a nested ternary in the render tree
|
|
472
|
+
* — early returns make them readable and keep the parent's complexity down.
|
|
473
|
+
*/
|
|
474
|
+
function MessageList({ greeting, hasSession, messages }) {
|
|
475
|
+
if (!hasSession) return /* @__PURE__ */ jsx("p", {
|
|
476
|
+
className: "clivly-chat-widget__empty",
|
|
477
|
+
children: greeting
|
|
478
|
+
});
|
|
479
|
+
if (messages.length === 0) return /* @__PURE__ */ jsx("p", {
|
|
480
|
+
className: "clivly-chat-widget__empty",
|
|
481
|
+
children: "No messages yet. Say hello to get the conversation started."
|
|
482
|
+
});
|
|
483
|
+
return /* @__PURE__ */ jsx(Fragment, { children: messages.map((message) => /* @__PURE__ */ jsx(MessageBubble, { message }, message.id)) });
|
|
484
|
+
}
|
|
485
|
+
function ChatWidget({ className, createTransport, getSession, initiallyOpen = false, launcherLabel = "Chat with us", storage = DEFAULT_STORAGE, style, theme = "auto", widgetId }) {
|
|
242
486
|
const [isOpen, setIsOpen] = useState(initiallyOpen);
|
|
243
487
|
const [error, setError] = useState(null);
|
|
244
488
|
const [isConnecting, setIsConnecting] = useState(false);
|
|
@@ -252,7 +496,8 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
|
|
|
252
496
|
});
|
|
253
497
|
const transportRef = useRef(null);
|
|
254
498
|
const listRef = useRef(null);
|
|
255
|
-
const themeStyle = useThemeStyle(session);
|
|
499
|
+
const themeStyle = useThemeStyle(session, style);
|
|
500
|
+
const resolvedTheme = useResolvedTheme(theme);
|
|
256
501
|
useEffect(() => () => {
|
|
257
502
|
transportRef.current?.close();
|
|
258
503
|
transportRef.current = null;
|
|
@@ -266,31 +511,6 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
|
|
|
266
511
|
transportRef.current = null;
|
|
267
512
|
setIsReady(false);
|
|
268
513
|
if (!(session && createTransport)) return;
|
|
269
|
-
const handleEvent = (event) => {
|
|
270
|
-
switch (event.type) {
|
|
271
|
-
case "conversation.snapshot":
|
|
272
|
-
setMessages(sortMessages(event.messages.map((message) => createLocalMessage(message))));
|
|
273
|
-
return;
|
|
274
|
-
case "error":
|
|
275
|
-
setError(event.message);
|
|
276
|
-
return;
|
|
277
|
-
case "message.ack":
|
|
278
|
-
setMessages((currentMessages) => sortMessages(replacePendingMessage(currentMessages, event.clientMessageId, event.message)));
|
|
279
|
-
return;
|
|
280
|
-
case "message.created": {
|
|
281
|
-
const clientMessageId = event.clientMessageId;
|
|
282
|
-
if (clientMessageId) {
|
|
283
|
-
setMessages((currentMessages) => sortMessages(replacePendingMessage(currentMessages, clientMessageId, event.message)));
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
setMessages((currentMessages) => sortMessages(mergeMessage(currentMessages, event.message)));
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
case "session.ready":
|
|
290
|
-
setIsReady(true);
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
};
|
|
294
514
|
const connection = createTransport({
|
|
295
515
|
handlers: {
|
|
296
516
|
onClose: () => {
|
|
@@ -299,7 +519,13 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
|
|
|
299
519
|
onError: (transportError) => {
|
|
300
520
|
setError(transportError.message);
|
|
301
521
|
},
|
|
302
|
-
onEvent:
|
|
522
|
+
onEvent: (event) => {
|
|
523
|
+
applyServerEvent(event, {
|
|
524
|
+
setError,
|
|
525
|
+
setIsReady,
|
|
526
|
+
setMessages
|
|
527
|
+
});
|
|
528
|
+
},
|
|
303
529
|
onOpen: () => {
|
|
304
530
|
setIsReady(true);
|
|
305
531
|
}
|
|
@@ -317,16 +543,18 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
|
|
|
317
543
|
setError(null);
|
|
318
544
|
setIsConnecting(true);
|
|
319
545
|
try {
|
|
546
|
+
const existingVisitorToken = storage?.getItem(createStorageKey(widgetId)) ?? null;
|
|
320
547
|
const nextSession = await getSession({
|
|
321
548
|
visitor,
|
|
322
|
-
visitorToken:
|
|
549
|
+
visitorToken: existingVisitorToken,
|
|
323
550
|
widgetId
|
|
324
551
|
});
|
|
325
552
|
storage?.setItem(createStorageKey(widgetId), nextSession.visitorToken);
|
|
326
553
|
setSession(nextSession);
|
|
327
554
|
setMessages(sortMessages(nextSession.messages.map((message) => createLocalMessage(message))));
|
|
328
555
|
} catch (sessionError) {
|
|
329
|
-
|
|
556
|
+
const resolved = sessionError instanceof Error ? sessionError : /* @__PURE__ */ new Error("Failed to start the conversation.");
|
|
557
|
+
setError(resolved.message);
|
|
330
558
|
} finally {
|
|
331
559
|
setIsConnecting(false);
|
|
332
560
|
}
|
|
@@ -341,7 +569,7 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
|
|
|
341
569
|
if (!body) return;
|
|
342
570
|
const clientMessageId = generateId();
|
|
343
571
|
const optimisticMessage = {
|
|
344
|
-
author: session
|
|
572
|
+
author: sessionDisplayName(session),
|
|
345
573
|
body,
|
|
346
574
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
347
575
|
id: clientMessageId,
|
|
@@ -364,9 +592,10 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
|
|
|
364
592
|
};
|
|
365
593
|
const greeting = session?.widget.config.greeting ?? "Tell us a little about yourself and we’ll pick up the conversation from there.";
|
|
366
594
|
const panelTitle = session?.widget.name ?? "Start a conversation";
|
|
367
|
-
const sessionSummary = session
|
|
595
|
+
const sessionSummary = sessionSummaryText(session, greeting);
|
|
368
596
|
return /* @__PURE__ */ jsxs("div", {
|
|
369
597
|
className: `clivly-chat-widget ${className ?? ""}`.trim(),
|
|
598
|
+
"data-clivly-theme": resolvedTheme,
|
|
370
599
|
style: themeStyle,
|
|
371
600
|
children: [
|
|
372
601
|
/* @__PURE__ */ jsx("style", { children: WIDGET_STYLES }),
|
|
@@ -406,26 +635,10 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
|
|
|
406
635
|
children: [error ? /* @__PURE__ */ jsx("div", {
|
|
407
636
|
className: "clivly-chat-widget__error",
|
|
408
637
|
children: error
|
|
409
|
-
}) : null,
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
/* @__PURE__ */ jsxs("div", {
|
|
414
|
-
className: "clivly-chat-widget__message-meta",
|
|
415
|
-
children: [/* @__PURE__ */ jsx("span", { children: message.author ?? "Clivly" }), /* @__PURE__ */ jsx("span", { children: formatTimestamp(message.createdAt) })]
|
|
416
|
-
}),
|
|
417
|
-
/* @__PURE__ */ jsx("div", { children: message.body }),
|
|
418
|
-
message.delivery === "sent" ? null : /* @__PURE__ */ jsx("div", {
|
|
419
|
-
className: "clivly-chat-widget__status",
|
|
420
|
-
children: message.delivery === "pending" ? "Sending…" : "Failed to send"
|
|
421
|
-
})
|
|
422
|
-
]
|
|
423
|
-
}, message.id)) : /* @__PURE__ */ jsx("p", {
|
|
424
|
-
className: "clivly-chat-widget__empty",
|
|
425
|
-
children: "No messages yet. Say hello to get the conversation started."
|
|
426
|
-
}) : /* @__PURE__ */ jsx("p", {
|
|
427
|
-
className: "clivly-chat-widget__empty",
|
|
428
|
-
children: greeting
|
|
638
|
+
}) : null, /* @__PURE__ */ jsx(MessageList, {
|
|
639
|
+
greeting,
|
|
640
|
+
hasSession: Boolean(session),
|
|
641
|
+
messages
|
|
429
642
|
})]
|
|
430
643
|
}),
|
|
431
644
|
session ? /* @__PURE__ */ jsxs("form", {
|
|
@@ -615,7 +828,7 @@ function createLoopbackTransport({ operatorName = "Clivly", operatorReply = (bod
|
|
|
615
828
|
type: "message.ack",
|
|
616
829
|
clientMessageId: message.id,
|
|
617
830
|
message: {
|
|
618
|
-
author: session.contact
|
|
831
|
+
author: session.contact?.name ?? "You",
|
|
619
832
|
body: message.body,
|
|
620
833
|
createdAt,
|
|
621
834
|
id: `${message.id}:server`,
|
|
@@ -688,16 +901,27 @@ const MOUNTED_DATA_ATTR = "clivlyMounted";
|
|
|
688
901
|
function resolveSocketUrl(socketUrlTemplate, conversationId, token) {
|
|
689
902
|
return socketUrlTemplate.replaceAll("{conversationId}", encodeURIComponent(conversationId)).replaceAll("{token}", encodeURIComponent(token));
|
|
690
903
|
}
|
|
691
|
-
function
|
|
904
|
+
function createWidgetMount({ container, createTransport, sessionUrl, socketUrlTemplate, theme = "auto", widgetId, ...props }) {
|
|
692
905
|
const transport = createTransport ?? (socketUrlTemplate ? createWebSocketTransport({ resolveUrl: (session) => session.socketUrl ?? resolveSocketUrl(socketUrlTemplate, session.conversation.id, session.token) }) : createClivlyTransport$1());
|
|
906
|
+
const getSession = createSessionFetcher({ sessionUrl });
|
|
693
907
|
const root = createRoot(container);
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
908
|
+
const render = (nextTheme) => {
|
|
909
|
+
root.render(/* @__PURE__ */ jsx(ChatWidget, {
|
|
910
|
+
...props,
|
|
911
|
+
createTransport: transport,
|
|
912
|
+
getSession,
|
|
913
|
+
theme: nextTheme,
|
|
914
|
+
widgetId
|
|
915
|
+
}));
|
|
916
|
+
};
|
|
917
|
+
render(theme);
|
|
918
|
+
return {
|
|
919
|
+
root,
|
|
920
|
+
setTheme: render
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
function mountChatWidget$1(options) {
|
|
924
|
+
return createWidgetMount(options).root;
|
|
701
925
|
}
|
|
702
926
|
function markScriptMounted(script) {
|
|
703
927
|
script.dataset[MOUNTED_DATA_ATTR] = "true";
|
|
@@ -716,15 +940,17 @@ function mountChatWidgetScript$1(script = document.currentScript) {
|
|
|
716
940
|
const shadowRoot = host.attachShadow({ mode: "open" });
|
|
717
941
|
const container = document.createElement("div");
|
|
718
942
|
shadowRoot.append(container);
|
|
719
|
-
const root =
|
|
943
|
+
const { root, setTheme } = createWidgetMount({
|
|
720
944
|
container,
|
|
721
945
|
sessionUrl,
|
|
722
946
|
socketUrlTemplate: script.dataset.socketUrlTemplate,
|
|
947
|
+
theme: parseTheme(script.dataset.theme),
|
|
723
948
|
widgetId
|
|
724
949
|
});
|
|
725
950
|
markScriptMounted(script);
|
|
726
951
|
return {
|
|
727
952
|
root,
|
|
953
|
+
setTheme,
|
|
728
954
|
shadowRoot
|
|
729
955
|
};
|
|
730
956
|
}
|
|
@@ -739,4 +965,4 @@ const createClivlyTransport = createClivlyTransport$1;
|
|
|
739
965
|
const mountChatWidget = mountChatWidget$1;
|
|
740
966
|
const mountChatWidgetScript = mountChatWidgetScript$1;
|
|
741
967
|
//#endregion
|
|
742
|
-
export { ChatLauncher, ChatPanel, ChatWidget, ConversationView, MessageComposer, PreChatForm, autoMountChatWidgetScript, createClivlyTransport, createLoopbackTransport, createSessionFetcher, createWebSocketTransport, mountChatWidget, mountChatWidgetScript };
|
|
968
|
+
export { ChatLauncher, ChatPanel, ChatWidget, ConversationView, MessageComposer, PreChatForm, autoMountChatWidgetScript, createClivlyTransport, createLoopbackTransport, createSessionFetcher, createWebSocketTransport, mountChatWidget, mountChatWidgetScript, resolveTheme };
|