@clivly/chat-widget 0.3.0-next.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/README.md +123 -0
- package/dist/browser-mount.cjs +630 -0
- package/dist/browser-mount.d.cts +129 -0
- package/dist/browser-mount.d.mts +129 -0
- package/dist/browser-mount.mjs +627 -0
- package/dist/browser.cjs +636 -0
- package/dist/browser.d.cts +136 -0
- package/dist/browser.d.mts +136 -0
- package/dist/browser.mjs +633 -0
- package/dist/index.cjs +718 -0
- package/dist/index.d.cts +198 -0
- package/dist/index.d.mts +198 -0
- package/dist/index.mjs +705 -0
- package/dist/widget.global.js +407 -0
- package/package.json +75 -0
package/dist/browser.cjs
ADDED
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let react_dom_client = require("react-dom/client");
|
|
3
|
+
let react = require("react");
|
|
4
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
5
|
+
//#region src/chat-widget.tsx
|
|
6
|
+
const WIDGET_STYLES = `
|
|
7
|
+
.clivly-chat-widget, .clivly-chat-widget * { box-sizing: border-box; }
|
|
8
|
+
.clivly-chat-widget {
|
|
9
|
+
--clivly-chat-accent: #185adb;
|
|
10
|
+
--clivly-chat-panel: #ffffff;
|
|
11
|
+
--clivly-chat-border: #d6dde8;
|
|
12
|
+
--clivly-chat-text: #132238;
|
|
13
|
+
--clivly-chat-muted: #60738a;
|
|
14
|
+
--clivly-chat-visitor: #ecf3ff;
|
|
15
|
+
--clivly-chat-operator: #f5f7fa;
|
|
16
|
+
color: var(--clivly-chat-text);
|
|
17
|
+
font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
18
|
+
position: relative;
|
|
19
|
+
}
|
|
20
|
+
.clivly-chat-widget button,
|
|
21
|
+
.clivly-chat-widget input,
|
|
22
|
+
.clivly-chat-widget textarea {
|
|
23
|
+
font: inherit;
|
|
24
|
+
}
|
|
25
|
+
.clivly-chat-widget__launcher {
|
|
26
|
+
align-items: center;
|
|
27
|
+
background: var(--clivly-chat-accent);
|
|
28
|
+
border: 0;
|
|
29
|
+
border-radius: 999px;
|
|
30
|
+
color: white;
|
|
31
|
+
cursor: pointer;
|
|
32
|
+
display: inline-flex;
|
|
33
|
+
gap: 0.5rem;
|
|
34
|
+
padding: 0.85rem 1.1rem;
|
|
35
|
+
}
|
|
36
|
+
.clivly-chat-widget__panel {
|
|
37
|
+
background: var(--clivly-chat-panel);
|
|
38
|
+
border: 1px solid var(--clivly-chat-border);
|
|
39
|
+
border-radius: 1rem;
|
|
40
|
+
box-shadow: 0 24px 80px rgba(19, 34, 56, 0.18);
|
|
41
|
+
display: grid;
|
|
42
|
+
grid-template-rows: auto 1fr auto;
|
|
43
|
+
height: min(44rem, 75vh);
|
|
44
|
+
overflow: hidden;
|
|
45
|
+
position: absolute;
|
|
46
|
+
right: 0;
|
|
47
|
+
bottom: 4.5rem;
|
|
48
|
+
width: min(24rem, calc(100vw - 2rem));
|
|
49
|
+
}
|
|
50
|
+
.clivly-chat-widget__header {
|
|
51
|
+
border-bottom: 1px solid var(--clivly-chat-border);
|
|
52
|
+
display: flex;
|
|
53
|
+
justify-content: space-between;
|
|
54
|
+
gap: 1rem;
|
|
55
|
+
padding: 1rem;
|
|
56
|
+
}
|
|
57
|
+
.clivly-chat-widget__title {
|
|
58
|
+
font-size: 1rem;
|
|
59
|
+
font-weight: 600;
|
|
60
|
+
margin: 0;
|
|
61
|
+
}
|
|
62
|
+
.clivly-chat-widget__subtitle {
|
|
63
|
+
color: var(--clivly-chat-muted);
|
|
64
|
+
font-size: 0.875rem;
|
|
65
|
+
margin: 0.25rem 0 0;
|
|
66
|
+
}
|
|
67
|
+
.clivly-chat-widget__close {
|
|
68
|
+
background: transparent;
|
|
69
|
+
border: 0;
|
|
70
|
+
color: var(--clivly-chat-muted);
|
|
71
|
+
cursor: pointer;
|
|
72
|
+
padding: 0;
|
|
73
|
+
}
|
|
74
|
+
.clivly-chat-widget__body {
|
|
75
|
+
display: flex;
|
|
76
|
+
flex-direction: column;
|
|
77
|
+
gap: 0.75rem;
|
|
78
|
+
overflow-y: auto;
|
|
79
|
+
padding: 1rem;
|
|
80
|
+
}
|
|
81
|
+
.clivly-chat-widget__message {
|
|
82
|
+
border-radius: 1rem;
|
|
83
|
+
max-width: 85%;
|
|
84
|
+
padding: 0.8rem 0.9rem;
|
|
85
|
+
white-space: pre-wrap;
|
|
86
|
+
word-break: break-word;
|
|
87
|
+
}
|
|
88
|
+
.clivly-chat-widget__message[data-sender="visitor"] {
|
|
89
|
+
align-self: flex-end;
|
|
90
|
+
background: var(--clivly-chat-visitor);
|
|
91
|
+
}
|
|
92
|
+
.clivly-chat-widget__message[data-sender="operator"],
|
|
93
|
+
.clivly-chat-widget__message[data-sender="system"] {
|
|
94
|
+
align-self: flex-start;
|
|
95
|
+
background: var(--clivly-chat-operator);
|
|
96
|
+
}
|
|
97
|
+
.clivly-chat-widget__message-meta {
|
|
98
|
+
color: var(--clivly-chat-muted);
|
|
99
|
+
display: flex;
|
|
100
|
+
font-size: 0.75rem;
|
|
101
|
+
gap: 0.5rem;
|
|
102
|
+
margin-bottom: 0.35rem;
|
|
103
|
+
}
|
|
104
|
+
.clivly-chat-widget__status {
|
|
105
|
+
color: var(--clivly-chat-muted);
|
|
106
|
+
font-size: 0.75rem;
|
|
107
|
+
margin-top: 0.35rem;
|
|
108
|
+
}
|
|
109
|
+
.clivly-chat-widget__composer,
|
|
110
|
+
.clivly-chat-widget__form {
|
|
111
|
+
border-top: 1px solid var(--clivly-chat-border);
|
|
112
|
+
display: grid;
|
|
113
|
+
gap: 0.75rem;
|
|
114
|
+
padding: 1rem;
|
|
115
|
+
}
|
|
116
|
+
.clivly-chat-widget__field {
|
|
117
|
+
display: grid;
|
|
118
|
+
gap: 0.35rem;
|
|
119
|
+
}
|
|
120
|
+
.clivly-chat-widget__label {
|
|
121
|
+
font-size: 0.875rem;
|
|
122
|
+
font-weight: 600;
|
|
123
|
+
}
|
|
124
|
+
.clivly-chat-widget__input,
|
|
125
|
+
.clivly-chat-widget__textarea {
|
|
126
|
+
border: 1px solid var(--clivly-chat-border);
|
|
127
|
+
border-radius: 0.8rem;
|
|
128
|
+
min-width: 0;
|
|
129
|
+
padding: 0.75rem 0.85rem;
|
|
130
|
+
}
|
|
131
|
+
.clivly-chat-widget__textarea {
|
|
132
|
+
min-height: 5.5rem;
|
|
133
|
+
resize: vertical;
|
|
134
|
+
}
|
|
135
|
+
.clivly-chat-widget__actions {
|
|
136
|
+
display: flex;
|
|
137
|
+
gap: 0.75rem;
|
|
138
|
+
justify-content: space-between;
|
|
139
|
+
}
|
|
140
|
+
.clivly-chat-widget__button {
|
|
141
|
+
background: var(--clivly-chat-accent);
|
|
142
|
+
border: 0;
|
|
143
|
+
border-radius: 0.8rem;
|
|
144
|
+
color: white;
|
|
145
|
+
cursor: pointer;
|
|
146
|
+
padding: 0.75rem 0.95rem;
|
|
147
|
+
}
|
|
148
|
+
.clivly-chat-widget__button[disabled] {
|
|
149
|
+
cursor: not-allowed;
|
|
150
|
+
opacity: 0.55;
|
|
151
|
+
}
|
|
152
|
+
.clivly-chat-widget__error {
|
|
153
|
+
background: #fff1f1;
|
|
154
|
+
border: 1px solid #f2c2c2;
|
|
155
|
+
border-radius: 0.8rem;
|
|
156
|
+
color: #9f2d2d;
|
|
157
|
+
font-size: 0.875rem;
|
|
158
|
+
padding: 0.75rem;
|
|
159
|
+
}
|
|
160
|
+
.clivly-chat-widget__empty {
|
|
161
|
+
color: var(--clivly-chat-muted);
|
|
162
|
+
font-size: 0.875rem;
|
|
163
|
+
margin: auto 0;
|
|
164
|
+
text-align: center;
|
|
165
|
+
}
|
|
166
|
+
`;
|
|
167
|
+
const DEFAULT_STORAGE = typeof window !== "undefined" ? window.localStorage : void 0;
|
|
168
|
+
function createLocalMessage(message, delivery = "sent") {
|
|
169
|
+
return {
|
|
170
|
+
...message,
|
|
171
|
+
delivery
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function createStorageKey(widgetId) {
|
|
175
|
+
return `clivly-chat-widget:${widgetId}:visitor-token`;
|
|
176
|
+
}
|
|
177
|
+
function formatTimestamp(value) {
|
|
178
|
+
const date = new Date(value);
|
|
179
|
+
if (Number.isNaN(date.getTime())) return "";
|
|
180
|
+
return new Intl.DateTimeFormat(void 0, {
|
|
181
|
+
hour: "numeric",
|
|
182
|
+
minute: "2-digit",
|
|
183
|
+
month: "short",
|
|
184
|
+
day: "numeric"
|
|
185
|
+
}).format(date);
|
|
186
|
+
}
|
|
187
|
+
function generateId() {
|
|
188
|
+
if (typeof crypto !== "undefined" && "randomUUID" in crypto) return crypto.randomUUID();
|
|
189
|
+
return `msg_${Math.random().toString(36).slice(2, 10)}`;
|
|
190
|
+
}
|
|
191
|
+
function mergeMessage(messages, nextMessage) {
|
|
192
|
+
const existingIndex = messages.findIndex((message) => message.id === nextMessage.id);
|
|
193
|
+
if (existingIndex === -1) return [...messages, createLocalMessage(nextMessage)];
|
|
194
|
+
const copy = [...messages];
|
|
195
|
+
copy[existingIndex] = createLocalMessage(nextMessage);
|
|
196
|
+
return copy;
|
|
197
|
+
}
|
|
198
|
+
function replacePendingMessage(messages, clientMessageId, nextMessage) {
|
|
199
|
+
return messages.map((message) => message.id === clientMessageId ? createLocalMessage(nextMessage) : message);
|
|
200
|
+
}
|
|
201
|
+
function markMessageFailed(messages, clientMessageId) {
|
|
202
|
+
return messages.map((message) => message.id === clientMessageId ? {
|
|
203
|
+
...message,
|
|
204
|
+
delivery: "failed"
|
|
205
|
+
} : message);
|
|
206
|
+
}
|
|
207
|
+
function sortMessages(messages) {
|
|
208
|
+
return [...messages].sort((left, right) => left.createdAt.localeCompare(right.createdAt));
|
|
209
|
+
}
|
|
210
|
+
function useThemeStyle(session) {
|
|
211
|
+
return (0, react.useMemo)(() => {
|
|
212
|
+
const brandColor = session?.widget.config.brandColor ?? null;
|
|
213
|
+
if (!brandColor) return;
|
|
214
|
+
return { ["--clivly-chat-accent"]: brandColor };
|
|
215
|
+
}, [session?.widget.config.brandColor]);
|
|
216
|
+
}
|
|
217
|
+
function ChatWidget({ className, createTransport, getSession, initiallyOpen = false, launcherLabel = "Chat with us", storage = DEFAULT_STORAGE, widgetId }) {
|
|
218
|
+
const [isOpen, setIsOpen] = (0, react.useState)(initiallyOpen);
|
|
219
|
+
const [error, setError] = (0, react.useState)(null);
|
|
220
|
+
const [isConnecting, setIsConnecting] = (0, react.useState)(false);
|
|
221
|
+
const [isReady, setIsReady] = (0, react.useState)(false);
|
|
222
|
+
const [messageDraft, setMessageDraft] = (0, react.useState)("");
|
|
223
|
+
const [messages, setMessages] = (0, react.useState)([]);
|
|
224
|
+
const [session, setSession] = (0, react.useState)(null);
|
|
225
|
+
const [visitor, setVisitor] = (0, react.useState)({
|
|
226
|
+
email: "",
|
|
227
|
+
name: ""
|
|
228
|
+
});
|
|
229
|
+
const transportRef = (0, react.useRef)(null);
|
|
230
|
+
const listRef = (0, react.useRef)(null);
|
|
231
|
+
const themeStyle = useThemeStyle(session);
|
|
232
|
+
(0, react.useEffect)(() => {
|
|
233
|
+
return () => {
|
|
234
|
+
transportRef.current?.close();
|
|
235
|
+
transportRef.current = null;
|
|
236
|
+
};
|
|
237
|
+
}, []);
|
|
238
|
+
(0, react.useEffect)(() => {
|
|
239
|
+
if (!listRef.current) return;
|
|
240
|
+
listRef.current.scrollTop = listRef.current.scrollHeight;
|
|
241
|
+
}, [messages]);
|
|
242
|
+
(0, react.useEffect)(() => {
|
|
243
|
+
transportRef.current?.close();
|
|
244
|
+
transportRef.current = null;
|
|
245
|
+
setIsReady(false);
|
|
246
|
+
if (!session || !createTransport) return;
|
|
247
|
+
const handleEvent = (event) => {
|
|
248
|
+
switch (event.type) {
|
|
249
|
+
case "conversation.snapshot":
|
|
250
|
+
setMessages(sortMessages(event.messages.map((message) => createLocalMessage(message))));
|
|
251
|
+
return;
|
|
252
|
+
case "error":
|
|
253
|
+
setError(event.message);
|
|
254
|
+
return;
|
|
255
|
+
case "message.ack":
|
|
256
|
+
setMessages((currentMessages) => sortMessages(replacePendingMessage(currentMessages, event.clientMessageId, event.message)));
|
|
257
|
+
return;
|
|
258
|
+
case "message.created": {
|
|
259
|
+
const clientMessageId = event.clientMessageId;
|
|
260
|
+
if (clientMessageId) {
|
|
261
|
+
setMessages((currentMessages) => sortMessages(replacePendingMessage(currentMessages, clientMessageId, event.message)));
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
setMessages((currentMessages) => sortMessages(mergeMessage(currentMessages, event.message)));
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
case "session.ready":
|
|
268
|
+
setIsReady(true);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
const connection = createTransport({
|
|
273
|
+
handlers: {
|
|
274
|
+
onClose: () => {
|
|
275
|
+
setIsReady(false);
|
|
276
|
+
},
|
|
277
|
+
onError: (transportError) => {
|
|
278
|
+
setError(transportError.message);
|
|
279
|
+
},
|
|
280
|
+
onEvent: handleEvent,
|
|
281
|
+
onOpen: () => {
|
|
282
|
+
setIsReady(true);
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
session
|
|
286
|
+
});
|
|
287
|
+
transportRef.current = connection;
|
|
288
|
+
return () => {
|
|
289
|
+
connection.close();
|
|
290
|
+
if (transportRef.current === connection) transportRef.current = null;
|
|
291
|
+
};
|
|
292
|
+
}, [createTransport, session]);
|
|
293
|
+
const submitVisitor = async (event) => {
|
|
294
|
+
event.preventDefault();
|
|
295
|
+
setError(null);
|
|
296
|
+
setIsConnecting(true);
|
|
297
|
+
try {
|
|
298
|
+
const nextSession = await getSession({
|
|
299
|
+
visitor,
|
|
300
|
+
visitorToken: storage?.getItem(createStorageKey(widgetId)) ?? null,
|
|
301
|
+
widgetId
|
|
302
|
+
});
|
|
303
|
+
storage?.setItem(createStorageKey(widgetId), nextSession.visitorToken);
|
|
304
|
+
setSession(nextSession);
|
|
305
|
+
setMessages(sortMessages(nextSession.messages.map((message) => createLocalMessage(message))));
|
|
306
|
+
} catch (sessionError) {
|
|
307
|
+
setError((sessionError instanceof Error ? sessionError : /* @__PURE__ */ new Error("Failed to start the conversation.")).message);
|
|
308
|
+
} finally {
|
|
309
|
+
setIsConnecting(false);
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
const submitMessage = (event) => {
|
|
313
|
+
event.preventDefault();
|
|
314
|
+
if (!session || !transportRef.current) {
|
|
315
|
+
setError("Chat is not connected yet.");
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const body = messageDraft.trim();
|
|
319
|
+
if (!body) return;
|
|
320
|
+
const clientMessageId = generateId();
|
|
321
|
+
const optimisticMessage = {
|
|
322
|
+
author: session.contact.name,
|
|
323
|
+
body,
|
|
324
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
325
|
+
id: clientMessageId,
|
|
326
|
+
senderType: "visitor"
|
|
327
|
+
};
|
|
328
|
+
setMessages((currentMessages) => sortMessages([...currentMessages, createLocalMessage(optimisticMessage, "pending")]));
|
|
329
|
+
setMessageDraft("");
|
|
330
|
+
setError(null);
|
|
331
|
+
try {
|
|
332
|
+
transportRef.current.sendMessage({
|
|
333
|
+
body,
|
|
334
|
+
id: clientMessageId,
|
|
335
|
+
type: "message.create"
|
|
336
|
+
});
|
|
337
|
+
} catch (transportError) {
|
|
338
|
+
const resolved = transportError instanceof Error ? transportError : /* @__PURE__ */ new Error("Failed to send your message.");
|
|
339
|
+
setMessages((currentMessages) => markMessageFailed(currentMessages, clientMessageId));
|
|
340
|
+
setError(resolved.message);
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
const greeting = session?.widget.config.greeting ?? "Tell us a little about yourself and we’ll pick up the conversation from there.";
|
|
344
|
+
const panelTitle = session?.widget.name ?? "Start a conversation";
|
|
345
|
+
const sessionSummary = session ? `${session.contact.name} • ${session.contact.email}` : greeting;
|
|
346
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
347
|
+
className: `clivly-chat-widget ${className ?? ""}`.trim(),
|
|
348
|
+
style: themeStyle,
|
|
349
|
+
children: [
|
|
350
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: WIDGET_STYLES }),
|
|
351
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
352
|
+
className: "clivly-chat-widget__launcher",
|
|
353
|
+
onClick: () => {
|
|
354
|
+
setIsOpen((open) => !open);
|
|
355
|
+
},
|
|
356
|
+
type: "button",
|
|
357
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: launcherLabel })
|
|
358
|
+
}),
|
|
359
|
+
isOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
360
|
+
"aria-label": panelTitle,
|
|
361
|
+
className: "clivly-chat-widget__panel",
|
|
362
|
+
children: [
|
|
363
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
364
|
+
className: "clivly-chat-widget__header",
|
|
365
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
366
|
+
className: "clivly-chat-widget__title",
|
|
367
|
+
children: panelTitle
|
|
368
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
369
|
+
className: "clivly-chat-widget__subtitle",
|
|
370
|
+
children: sessionSummary
|
|
371
|
+
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
372
|
+
"aria-label": "Close chat",
|
|
373
|
+
className: "clivly-chat-widget__close",
|
|
374
|
+
onClick: () => {
|
|
375
|
+
setIsOpen(false);
|
|
376
|
+
},
|
|
377
|
+
type: "button",
|
|
378
|
+
children: "Close"
|
|
379
|
+
})]
|
|
380
|
+
}),
|
|
381
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
382
|
+
className: "clivly-chat-widget__body",
|
|
383
|
+
ref: listRef,
|
|
384
|
+
children: [error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
385
|
+
className: "clivly-chat-widget__error",
|
|
386
|
+
children: error
|
|
387
|
+
}) : null, session ? messages.length > 0 ? messages.map((message) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
|
|
388
|
+
className: "clivly-chat-widget__message",
|
|
389
|
+
"data-sender": message.senderType,
|
|
390
|
+
children: [
|
|
391
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
392
|
+
className: "clivly-chat-widget__message-meta",
|
|
393
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: message.author ?? "Clivly" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatTimestamp(message.createdAt) })]
|
|
394
|
+
}),
|
|
395
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: message.body }),
|
|
396
|
+
message.delivery !== "sent" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
397
|
+
className: "clivly-chat-widget__status",
|
|
398
|
+
children: message.delivery === "pending" ? "Sending…" : "Failed to send"
|
|
399
|
+
}) : null
|
|
400
|
+
]
|
|
401
|
+
}, message.id)) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
402
|
+
className: "clivly-chat-widget__empty",
|
|
403
|
+
children: "No messages yet. Say hello to get the conversation started."
|
|
404
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
405
|
+
className: "clivly-chat-widget__empty",
|
|
406
|
+
children: greeting
|
|
407
|
+
})]
|
|
408
|
+
}),
|
|
409
|
+
session ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
|
|
410
|
+
className: "clivly-chat-widget__composer",
|
|
411
|
+
onSubmit: submitMessage,
|
|
412
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
413
|
+
className: "clivly-chat-widget__field",
|
|
414
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
415
|
+
className: "clivly-chat-widget__label",
|
|
416
|
+
children: "Message"
|
|
417
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
418
|
+
className: "clivly-chat-widget__textarea",
|
|
419
|
+
onChange: (event) => {
|
|
420
|
+
setMessageDraft(event.target.value);
|
|
421
|
+
},
|
|
422
|
+
placeholder: "Write your message",
|
|
423
|
+
value: messageDraft
|
|
424
|
+
})]
|
|
425
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
426
|
+
className: "clivly-chat-widget__actions",
|
|
427
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
428
|
+
className: "clivly-chat-widget__subtitle",
|
|
429
|
+
children: isReady || !createTransport ? "Conversation ready" : "Connecting…"
|
|
430
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
431
|
+
className: "clivly-chat-widget__button",
|
|
432
|
+
disabled: !messageDraft.trim() || !isReady,
|
|
433
|
+
type: "submit",
|
|
434
|
+
children: "Send"
|
|
435
|
+
})]
|
|
436
|
+
})]
|
|
437
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
|
|
438
|
+
className: "clivly-chat-widget__form",
|
|
439
|
+
onSubmit: submitVisitor,
|
|
440
|
+
children: [
|
|
441
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
442
|
+
className: "clivly-chat-widget__field",
|
|
443
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
444
|
+
className: "clivly-chat-widget__label",
|
|
445
|
+
children: "Name"
|
|
446
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
447
|
+
autoComplete: "name",
|
|
448
|
+
className: "clivly-chat-widget__input",
|
|
449
|
+
onChange: (event) => {
|
|
450
|
+
setVisitor((currentVisitor) => ({
|
|
451
|
+
...currentVisitor,
|
|
452
|
+
name: event.target.value
|
|
453
|
+
}));
|
|
454
|
+
},
|
|
455
|
+
required: true,
|
|
456
|
+
value: visitor.name
|
|
457
|
+
})]
|
|
458
|
+
}),
|
|
459
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
460
|
+
className: "clivly-chat-widget__field",
|
|
461
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
462
|
+
className: "clivly-chat-widget__label",
|
|
463
|
+
children: "Email"
|
|
464
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
465
|
+
autoComplete: "email",
|
|
466
|
+
className: "clivly-chat-widget__input",
|
|
467
|
+
onChange: (event) => {
|
|
468
|
+
setVisitor((currentVisitor) => ({
|
|
469
|
+
...currentVisitor,
|
|
470
|
+
email: event.target.value
|
|
471
|
+
}));
|
|
472
|
+
},
|
|
473
|
+
required: true,
|
|
474
|
+
type: "email",
|
|
475
|
+
value: visitor.email
|
|
476
|
+
})]
|
|
477
|
+
}),
|
|
478
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
479
|
+
className: "clivly-chat-widget__actions",
|
|
480
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
481
|
+
className: "clivly-chat-widget__subtitle",
|
|
482
|
+
children: "We use this to start the conversation."
|
|
483
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
484
|
+
className: "clivly-chat-widget__button",
|
|
485
|
+
disabled: isConnecting || !visitor.name.trim() || !visitor.email.trim(),
|
|
486
|
+
type: "submit",
|
|
487
|
+
children: isConnecting ? "Starting…" : "Start chat"
|
|
488
|
+
})]
|
|
489
|
+
})
|
|
490
|
+
]
|
|
491
|
+
})
|
|
492
|
+
]
|
|
493
|
+
}) : null
|
|
494
|
+
]
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
//#endregion
|
|
498
|
+
//#region src/transport.ts
|
|
499
|
+
function isServerEvent(value) {
|
|
500
|
+
if (typeof value !== "object" || value === null) return false;
|
|
501
|
+
return typeof value.type === "string";
|
|
502
|
+
}
|
|
503
|
+
function createWebSocketTransport({ createSocket = (url, protocols) => new WebSocket(url, protocols), getProtocols, resolveUrl }) {
|
|
504
|
+
return ({ handlers, session }) => {
|
|
505
|
+
const socket = createSocket(resolveUrl(session), getProtocols?.(session));
|
|
506
|
+
socket.addEventListener("open", () => {
|
|
507
|
+
handlers.onOpen?.();
|
|
508
|
+
});
|
|
509
|
+
socket.addEventListener("close", () => {
|
|
510
|
+
handlers.onClose();
|
|
511
|
+
});
|
|
512
|
+
socket.addEventListener("error", () => {
|
|
513
|
+
handlers.onError(/* @__PURE__ */ new Error("Chat socket error."));
|
|
514
|
+
});
|
|
515
|
+
socket.addEventListener("message", (event) => {
|
|
516
|
+
try {
|
|
517
|
+
const payload = JSON.parse(String(event.data));
|
|
518
|
+
if (!isServerEvent(payload)) throw new Error("Chat socket event had an invalid shape.");
|
|
519
|
+
handlers.onEvent(payload);
|
|
520
|
+
} catch (error) {
|
|
521
|
+
const resolved = error instanceof Error ? error : /* @__PURE__ */ new Error("Chat socket event could not be parsed.");
|
|
522
|
+
handlers.onError(resolved);
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
return {
|
|
526
|
+
close: () => {
|
|
527
|
+
socket.close();
|
|
528
|
+
},
|
|
529
|
+
sendMessage: (message) => {
|
|
530
|
+
if (socket.readyState !== WebSocket.OPEN) throw new Error("Chat socket is not connected.");
|
|
531
|
+
socket.send(JSON.stringify(message));
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
//#endregion
|
|
537
|
+
//#region src/clivly-transport.ts
|
|
538
|
+
/**
|
|
539
|
+
* The transport a host app should use against a real Clivly backend.
|
|
540
|
+
*
|
|
541
|
+
* The URL comes from the bootstrap response rather than host configuration, so
|
|
542
|
+
* no host page hardcodes Clivly's hostname and a misconfigured template cannot
|
|
543
|
+
* produce a silently dead socket.
|
|
544
|
+
*/
|
|
545
|
+
function createClivlyTransport(options = {}) {
|
|
546
|
+
return createWebSocketTransport({
|
|
547
|
+
...options,
|
|
548
|
+
resolveUrl: (session) => {
|
|
549
|
+
if (!session.socketUrl) throw new Error("Chat session response is missing socketUrl. The Clivly server is older than this widget.");
|
|
550
|
+
return session.socketUrl;
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
//#endregion
|
|
555
|
+
//#region src/session.ts
|
|
556
|
+
function isWidgetSessionPayload(value) {
|
|
557
|
+
return typeof value === "object" && value !== null;
|
|
558
|
+
}
|
|
559
|
+
function createSessionFetcher({ fetchImpl = fetch, sessionUrl }) {
|
|
560
|
+
return async ({ visitor, visitorToken, widgetId }) => {
|
|
561
|
+
const response = await fetchImpl(sessionUrl, {
|
|
562
|
+
method: "POST",
|
|
563
|
+
headers: { "Content-Type": "application/json" },
|
|
564
|
+
body: JSON.stringify({
|
|
565
|
+
visitor,
|
|
566
|
+
visitorToken,
|
|
567
|
+
widgetId
|
|
568
|
+
})
|
|
569
|
+
});
|
|
570
|
+
if (!response.ok) throw new Error(`Chat session request failed with ${response.status}.`);
|
|
571
|
+
const data = await response.json();
|
|
572
|
+
if (!isWidgetSessionPayload(data)) throw new Error("Chat session response was not an object.");
|
|
573
|
+
if ("session" in data) return data.session;
|
|
574
|
+
return data;
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
//#endregion
|
|
578
|
+
//#region src/browser-mount.tsx
|
|
579
|
+
const MOUNTED_DATA_ATTR = "clivlyMounted";
|
|
580
|
+
function resolveSocketUrl(socketUrlTemplate, conversationId, token) {
|
|
581
|
+
return socketUrlTemplate.replaceAll("{conversationId}", encodeURIComponent(conversationId)).replaceAll("{token}", encodeURIComponent(token));
|
|
582
|
+
}
|
|
583
|
+
function mountChatWidget$1({ container, createTransport, sessionUrl, socketUrlTemplate, widgetId, ...props }) {
|
|
584
|
+
const transport = createTransport ?? (socketUrlTemplate ? createWebSocketTransport({ resolveUrl: (session) => session.socketUrl ?? resolveSocketUrl(socketUrlTemplate, session.conversation.id, session.token) }) : createClivlyTransport());
|
|
585
|
+
const root = (0, react_dom_client.createRoot)(container);
|
|
586
|
+
root.render(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChatWidget, {
|
|
587
|
+
...props,
|
|
588
|
+
createTransport: transport,
|
|
589
|
+
getSession: createSessionFetcher({ sessionUrl }),
|
|
590
|
+
widgetId
|
|
591
|
+
}));
|
|
592
|
+
return root;
|
|
593
|
+
}
|
|
594
|
+
function markScriptMounted(script) {
|
|
595
|
+
script.dataset[MOUNTED_DATA_ATTR] = "true";
|
|
596
|
+
}
|
|
597
|
+
function scriptAlreadyMounted(script) {
|
|
598
|
+
return script.dataset[MOUNTED_DATA_ATTR] === "true";
|
|
599
|
+
}
|
|
600
|
+
function mountChatWidgetScript$1(script = document.currentScript) {
|
|
601
|
+
if (scriptAlreadyMounted(script)) throw new Error("Widget script has already been mounted.");
|
|
602
|
+
const widgetId = script.dataset.widgetId;
|
|
603
|
+
const sessionUrl = script.dataset.sessionUrl;
|
|
604
|
+
if (!widgetId) throw new Error("Widget script is missing data-widget-id.");
|
|
605
|
+
if (!sessionUrl) throw new Error("Widget script is missing data-session-url.");
|
|
606
|
+
const host = document.createElement("div");
|
|
607
|
+
script.insertAdjacentElement("afterend", host);
|
|
608
|
+
const shadowRoot = host.attachShadow({ mode: "open" });
|
|
609
|
+
const container = document.createElement("div");
|
|
610
|
+
shadowRoot.append(container);
|
|
611
|
+
const root = mountChatWidget$1({
|
|
612
|
+
container,
|
|
613
|
+
sessionUrl,
|
|
614
|
+
socketUrlTemplate: script.dataset.socketUrlTemplate,
|
|
615
|
+
widgetId
|
|
616
|
+
});
|
|
617
|
+
markScriptMounted(script);
|
|
618
|
+
return {
|
|
619
|
+
root,
|
|
620
|
+
shadowRoot
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
function autoMountChatWidgetScript$1(script = document.currentScript) {
|
|
624
|
+
if (!script || scriptAlreadyMounted(script)) return null;
|
|
625
|
+
return mountChatWidgetScript$1(script);
|
|
626
|
+
}
|
|
627
|
+
//#endregion
|
|
628
|
+
//#region src/browser.tsx
|
|
629
|
+
const mountChatWidget = mountChatWidget$1;
|
|
630
|
+
const mountChatWidgetScript = mountChatWidgetScript$1;
|
|
631
|
+
const autoMountChatWidgetScript = autoMountChatWidgetScript$1;
|
|
632
|
+
if (typeof document !== "undefined") autoMountChatWidgetScript();
|
|
633
|
+
//#endregion
|
|
634
|
+
exports.autoMountChatWidgetScript = autoMountChatWidgetScript;
|
|
635
|
+
exports.mountChatWidget = mountChatWidget;
|
|
636
|
+
exports.mountChatWidgetScript = mountChatWidgetScript;
|