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