@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/dist/index.cjs ADDED
@@ -0,0 +1,718 @@
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
+ function ChatLauncher() {
498
+ return null;
499
+ }
500
+ function ChatPanel() {
501
+ return null;
502
+ }
503
+ function MessageComposer() {
504
+ return null;
505
+ }
506
+ function PreChatForm() {
507
+ return null;
508
+ }
509
+ function ConversationView() {
510
+ return null;
511
+ }
512
+ //#endregion
513
+ //#region src/transport.ts
514
+ function isServerEvent(value) {
515
+ if (typeof value !== "object" || value === null) return false;
516
+ return typeof value.type === "string";
517
+ }
518
+ function createWebSocketTransport({ createSocket = (url, protocols) => new WebSocket(url, protocols), getProtocols, resolveUrl }) {
519
+ return ({ handlers, session }) => {
520
+ const socket = createSocket(resolveUrl(session), getProtocols?.(session));
521
+ socket.addEventListener("open", () => {
522
+ handlers.onOpen?.();
523
+ });
524
+ socket.addEventListener("close", () => {
525
+ handlers.onClose();
526
+ });
527
+ socket.addEventListener("error", () => {
528
+ handlers.onError(/* @__PURE__ */ new Error("Chat socket error."));
529
+ });
530
+ socket.addEventListener("message", (event) => {
531
+ try {
532
+ const payload = JSON.parse(String(event.data));
533
+ if (!isServerEvent(payload)) throw new Error("Chat socket event had an invalid shape.");
534
+ handlers.onEvent(payload);
535
+ } catch (error) {
536
+ const resolved = error instanceof Error ? error : /* @__PURE__ */ new Error("Chat socket event could not be parsed.");
537
+ handlers.onError(resolved);
538
+ }
539
+ });
540
+ return {
541
+ close: () => {
542
+ socket.close();
543
+ },
544
+ sendMessage: (message) => {
545
+ if (socket.readyState !== WebSocket.OPEN) throw new Error("Chat socket is not connected.");
546
+ socket.send(JSON.stringify(message));
547
+ }
548
+ };
549
+ };
550
+ }
551
+ function createLoopbackTransport({ operatorName = "Clivly", operatorReply = (body) => `Thanks for your message: ${body}`, replyDelayMs = 30 } = {}) {
552
+ return ({ handlers, session }) => {
553
+ let closed = false;
554
+ const timers = /* @__PURE__ */ new Set();
555
+ const schedule = (callback, delayMs) => {
556
+ const timer = window.setTimeout(() => {
557
+ timers.delete(timer);
558
+ if (!closed) callback();
559
+ }, delayMs);
560
+ timers.add(timer);
561
+ };
562
+ schedule(() => {
563
+ handlers.onOpen?.();
564
+ handlers.onEvent({
565
+ type: "session.ready",
566
+ conversationId: session.conversation.id
567
+ });
568
+ }, 0);
569
+ return {
570
+ close: () => {
571
+ closed = true;
572
+ for (const timer of timers) window.clearTimeout(timer);
573
+ timers.clear();
574
+ },
575
+ sendMessage: (message) => {
576
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
577
+ schedule(() => {
578
+ handlers.onEvent({
579
+ type: "message.ack",
580
+ clientMessageId: message.id,
581
+ message: {
582
+ author: session.contact.name,
583
+ body: message.body,
584
+ createdAt,
585
+ id: `${message.id}:server`,
586
+ senderType: "visitor"
587
+ }
588
+ });
589
+ }, 10);
590
+ const reply = operatorReply(message.body);
591
+ if (!reply) return;
592
+ schedule(() => {
593
+ handlers.onEvent({
594
+ type: "message.created",
595
+ message: {
596
+ author: operatorName,
597
+ body: reply,
598
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
599
+ id: `${message.id}:reply`,
600
+ senderType: "operator"
601
+ }
602
+ });
603
+ }, replyDelayMs);
604
+ }
605
+ };
606
+ };
607
+ }
608
+ //#endregion
609
+ //#region src/clivly-transport.ts
610
+ /**
611
+ * The transport a host app should use against a real Clivly backend.
612
+ *
613
+ * The URL comes from the bootstrap response rather than host configuration, so
614
+ * no host page hardcodes Clivly's hostname and a misconfigured template cannot
615
+ * produce a silently dead socket.
616
+ */
617
+ function createClivlyTransport$1(options = {}) {
618
+ return createWebSocketTransport({
619
+ ...options,
620
+ resolveUrl: (session) => {
621
+ if (!session.socketUrl) throw new Error("Chat session response is missing socketUrl. The Clivly server is older than this widget.");
622
+ return session.socketUrl;
623
+ }
624
+ });
625
+ }
626
+ //#endregion
627
+ //#region src/session.ts
628
+ function isWidgetSessionPayload(value) {
629
+ return typeof value === "object" && value !== null;
630
+ }
631
+ function createSessionFetcher({ fetchImpl = fetch, sessionUrl }) {
632
+ return async ({ visitor, visitorToken, widgetId }) => {
633
+ const response = await fetchImpl(sessionUrl, {
634
+ method: "POST",
635
+ headers: { "Content-Type": "application/json" },
636
+ body: JSON.stringify({
637
+ visitor,
638
+ visitorToken,
639
+ widgetId
640
+ })
641
+ });
642
+ if (!response.ok) throw new Error(`Chat session request failed with ${response.status}.`);
643
+ const data = await response.json();
644
+ if (!isWidgetSessionPayload(data)) throw new Error("Chat session response was not an object.");
645
+ if ("session" in data) return data.session;
646
+ return data;
647
+ };
648
+ }
649
+ //#endregion
650
+ //#region src/browser-mount.tsx
651
+ const MOUNTED_DATA_ATTR = "clivlyMounted";
652
+ function resolveSocketUrl(socketUrlTemplate, conversationId, token) {
653
+ return socketUrlTemplate.replaceAll("{conversationId}", encodeURIComponent(conversationId)).replaceAll("{token}", encodeURIComponent(token));
654
+ }
655
+ function mountChatWidget$1({ container, createTransport, sessionUrl, socketUrlTemplate, widgetId, ...props }) {
656
+ const transport = createTransport ?? (socketUrlTemplate ? createWebSocketTransport({ resolveUrl: (session) => session.socketUrl ?? resolveSocketUrl(socketUrlTemplate, session.conversation.id, session.token) }) : createClivlyTransport$1());
657
+ const root = (0, react_dom_client.createRoot)(container);
658
+ root.render(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChatWidget, {
659
+ ...props,
660
+ createTransport: transport,
661
+ getSession: createSessionFetcher({ sessionUrl }),
662
+ widgetId
663
+ }));
664
+ return root;
665
+ }
666
+ function markScriptMounted(script) {
667
+ script.dataset[MOUNTED_DATA_ATTR] = "true";
668
+ }
669
+ function scriptAlreadyMounted(script) {
670
+ return script.dataset[MOUNTED_DATA_ATTR] === "true";
671
+ }
672
+ function mountChatWidgetScript$1(script = document.currentScript) {
673
+ if (scriptAlreadyMounted(script)) throw new Error("Widget script has already been mounted.");
674
+ const widgetId = script.dataset.widgetId;
675
+ const sessionUrl = script.dataset.sessionUrl;
676
+ if (!widgetId) throw new Error("Widget script is missing data-widget-id.");
677
+ if (!sessionUrl) throw new Error("Widget script is missing data-session-url.");
678
+ const host = document.createElement("div");
679
+ script.insertAdjacentElement("afterend", host);
680
+ const shadowRoot = host.attachShadow({ mode: "open" });
681
+ const container = document.createElement("div");
682
+ shadowRoot.append(container);
683
+ const root = mountChatWidget$1({
684
+ container,
685
+ sessionUrl,
686
+ socketUrlTemplate: script.dataset.socketUrlTemplate,
687
+ widgetId
688
+ });
689
+ markScriptMounted(script);
690
+ return {
691
+ root,
692
+ shadowRoot
693
+ };
694
+ }
695
+ function autoMountChatWidgetScript$1(script = document.currentScript) {
696
+ if (!script || scriptAlreadyMounted(script)) return null;
697
+ return mountChatWidgetScript$1(script);
698
+ }
699
+ //#endregion
700
+ //#region src/index.ts
701
+ const autoMountChatWidgetScript = autoMountChatWidgetScript$1;
702
+ const createClivlyTransport = createClivlyTransport$1;
703
+ const mountChatWidget = mountChatWidget$1;
704
+ const mountChatWidgetScript = mountChatWidgetScript$1;
705
+ //#endregion
706
+ exports.ChatLauncher = ChatLauncher;
707
+ exports.ChatPanel = ChatPanel;
708
+ exports.ChatWidget = ChatWidget;
709
+ exports.ConversationView = ConversationView;
710
+ exports.MessageComposer = MessageComposer;
711
+ exports.PreChatForm = PreChatForm;
712
+ exports.autoMountChatWidgetScript = autoMountChatWidgetScript;
713
+ exports.createClivlyTransport = createClivlyTransport;
714
+ exports.createLoopbackTransport = createLoopbackTransport;
715
+ exports.createSessionFetcher = createSessionFetcher;
716
+ exports.createWebSocketTransport = createWebSocketTransport;
717
+ exports.mountChatWidget = mountChatWidget;
718
+ exports.mountChatWidgetScript = mountChatWidgetScript;