@clivly/chat-widget 0.15.0 → 0.16.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 +39 -0
- package/dist/browser-mount.cjs +284 -45
- package/dist/browser-mount.d.cts +31 -0
- package/dist/browser-mount.d.mts +31 -0
- package/dist/browser-mount.mjs +285 -46
- package/dist/browser.cjs +284 -45
- package/dist/browser.d.cts +31 -0
- package/dist/browser.d.mts +31 -0
- package/dist/browser.mjs +285 -46
- package/dist/index.cjs +284 -45
- package/dist/index.d.cts +33 -2
- package/dist/index.d.mts +33 -2
- package/dist/index.mjs +285 -46
- package/dist/widget.global.js +101 -9
- package/package.json +1 -1
package/dist/browser-mount.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRoot } from "react-dom/client";
|
|
2
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
4
4
|
//#region src/theme.ts
|
|
5
5
|
const DARK_MEDIA_QUERY = "(prefers-color-scheme: dark)";
|
|
@@ -169,16 +169,95 @@ const WIDGET_STYLES = `
|
|
|
169
169
|
font-size: max(16px, 1em);
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
|
+
/* Floating placements. The insets keep the launcher off a phone's home
|
|
173
|
+
indicator and rounded corners; --clivly-chat-z-index lets a host slot it
|
|
174
|
+
beneath its own dialogs. */
|
|
175
|
+
.clivly-chat-widget[data-clivly-placement="bottom-right"],
|
|
176
|
+
.clivly-chat-widget[data-clivly-placement="bottom-left"] {
|
|
177
|
+
bottom: max(1.25rem, env(safe-area-inset-bottom));
|
|
178
|
+
position: fixed;
|
|
179
|
+
z-index: var(--clivly-chat-z-index, 2147483000);
|
|
180
|
+
}
|
|
181
|
+
.clivly-chat-widget[data-clivly-placement="bottom-right"] {
|
|
182
|
+
right: max(1.25rem, env(safe-area-inset-right));
|
|
183
|
+
}
|
|
184
|
+
.clivly-chat-widget[data-clivly-placement="bottom-left"] {
|
|
185
|
+
left: max(1.25rem, env(safe-area-inset-left));
|
|
186
|
+
}
|
|
172
187
|
.clivly-chat-widget__launcher {
|
|
173
188
|
align-items: center;
|
|
174
189
|
background: var(--clivly-chat-accent);
|
|
175
190
|
border: 0;
|
|
176
191
|
border-radius: 999px;
|
|
192
|
+
box-shadow:
|
|
193
|
+
0 2px 6px rgba(0, 0, 0, 0.18),
|
|
194
|
+
0 10px 28px -10px var(--clivly-chat-accent);
|
|
177
195
|
color: var(--clivly-chat-accent-contrast);
|
|
178
196
|
cursor: pointer;
|
|
179
197
|
display: inline-flex;
|
|
198
|
+
font-weight: 500;
|
|
180
199
|
gap: 0.5rem;
|
|
181
|
-
|
|
200
|
+
justify-content: center;
|
|
201
|
+
min-height: 3rem;
|
|
202
|
+
padding: 0 1.15rem;
|
|
203
|
+
transition: transform 150ms ease-out, filter 150ms ease-out;
|
|
204
|
+
}
|
|
205
|
+
.clivly-chat-widget__launcher:hover {
|
|
206
|
+
filter: brightness(1.06);
|
|
207
|
+
transform: translateY(-1px);
|
|
208
|
+
}
|
|
209
|
+
.clivly-chat-widget__launcher:active {
|
|
210
|
+
transform: scale(0.96);
|
|
211
|
+
}
|
|
212
|
+
.clivly-chat-widget__launcher:focus-visible {
|
|
213
|
+
outline: 2px solid var(--clivly-chat-accent);
|
|
214
|
+
outline-offset: 3px;
|
|
215
|
+
}
|
|
216
|
+
.clivly-chat-widget__launcher-icon {
|
|
217
|
+
flex-shrink: 0;
|
|
218
|
+
height: 1.25rem;
|
|
219
|
+
width: 1.25rem;
|
|
220
|
+
}
|
|
221
|
+
/* Icon-only: a 48px circle — a comfortable touch target that does not cover
|
|
222
|
+
the host's pagination, toolbars or form controls the way a labelled pill
|
|
223
|
+
does on a phone. The label is clipped, not removed, so it stays the
|
|
224
|
+
button's accessible name. */
|
|
225
|
+
.clivly-chat-widget[data-clivly-launcher="icon"] .clivly-chat-widget__launcher {
|
|
226
|
+
height: 3rem;
|
|
227
|
+
padding: 0;
|
|
228
|
+
width: 3rem;
|
|
229
|
+
}
|
|
230
|
+
.clivly-chat-widget[data-clivly-launcher="icon"] .clivly-chat-widget__launcher-label {
|
|
231
|
+
clip-path: inset(50%);
|
|
232
|
+
height: 1px;
|
|
233
|
+
overflow: hidden;
|
|
234
|
+
position: absolute;
|
|
235
|
+
white-space: nowrap;
|
|
236
|
+
width: 1px;
|
|
237
|
+
}
|
|
238
|
+
@media (width < 48rem) {
|
|
239
|
+
.clivly-chat-widget[data-clivly-launcher="auto"] .clivly-chat-widget__launcher {
|
|
240
|
+
height: 3rem;
|
|
241
|
+
padding: 0;
|
|
242
|
+
width: 3rem;
|
|
243
|
+
}
|
|
244
|
+
.clivly-chat-widget[data-clivly-launcher="auto"] .clivly-chat-widget__launcher-label {
|
|
245
|
+
clip-path: inset(50%);
|
|
246
|
+
height: 1px;
|
|
247
|
+
overflow: hidden;
|
|
248
|
+
position: absolute;
|
|
249
|
+
white-space: nowrap;
|
|
250
|
+
width: 1px;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
@media (prefers-reduced-motion: reduce) {
|
|
254
|
+
.clivly-chat-widget__launcher {
|
|
255
|
+
transition: none;
|
|
256
|
+
}
|
|
257
|
+
.clivly-chat-widget__launcher:hover,
|
|
258
|
+
.clivly-chat-widget__launcher:active {
|
|
259
|
+
transform: none;
|
|
260
|
+
}
|
|
182
261
|
}
|
|
183
262
|
.clivly-chat-widget__panel {
|
|
184
263
|
background: var(--clivly-chat-panel);
|
|
@@ -194,6 +273,19 @@ const WIDGET_STYLES = `
|
|
|
194
273
|
bottom: 4.5rem;
|
|
195
274
|
width: min(24rem, calc(100vw - 2rem));
|
|
196
275
|
}
|
|
276
|
+
/* Opens toward the page from whichever corner the launcher is in. */
|
|
277
|
+
.clivly-chat-widget[data-clivly-placement="bottom-left"] .clivly-chat-widget__panel {
|
|
278
|
+
left: 0;
|
|
279
|
+
right: auto;
|
|
280
|
+
}
|
|
281
|
+
/* dvh: on a phone vh includes the collapsed URL bar, so a 75vh panel's top
|
|
282
|
+
edge could slide under it. The 48px launcher sits lower than the pill. */
|
|
283
|
+
@media (width < 48rem) {
|
|
284
|
+
.clivly-chat-widget__panel {
|
|
285
|
+
bottom: 4rem;
|
|
286
|
+
height: min(44rem, calc(100dvh - 6.5rem));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
197
289
|
.clivly-chat-widget__header {
|
|
198
290
|
border-bottom: 1px solid var(--clivly-chat-border);
|
|
199
291
|
display: flex;
|
|
@@ -561,6 +653,45 @@ function automationBadge(message) {
|
|
|
561
653
|
if (message.senderType !== "system" || !message.automation) return null;
|
|
562
654
|
return AUTOMATION_BADGES[message.automation] ?? "Automated";
|
|
563
655
|
}
|
|
656
|
+
/**
|
|
657
|
+
* The button that opens and closes the panel. Its size and shape come from the
|
|
658
|
+
* root's `data-clivly-launcher` (see WIDGET_STYLES); the label is always its
|
|
659
|
+
* accessible name, and `aria-controls` names the panel only while it exists.
|
|
660
|
+
*/
|
|
661
|
+
function Launcher({ label, onToggle, open, panelId }) {
|
|
662
|
+
return /* @__PURE__ */ jsxs("button", {
|
|
663
|
+
"aria-controls": open ? panelId : void 0,
|
|
664
|
+
"aria-expanded": open,
|
|
665
|
+
className: "clivly-chat-widget__launcher",
|
|
666
|
+
onClick: onToggle,
|
|
667
|
+
type: "button",
|
|
668
|
+
children: [/* @__PURE__ */ jsx(LauncherIcon, { open }), /* @__PURE__ */ jsx("span", {
|
|
669
|
+
className: "clivly-chat-widget__launcher-label",
|
|
670
|
+
children: label
|
|
671
|
+
})]
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* A chat bubble while closed, a cross while open — so the icon-only launcher
|
|
676
|
+
* still tells a sighted visitor what tapping it will do. Decorative: the
|
|
677
|
+
* label and `aria-expanded` carry the meaning for assistive technology.
|
|
678
|
+
* Inline SVG because the script-tag build renders inside a shadow root with no
|
|
679
|
+
* icon font or stylesheet of the host's to borrow.
|
|
680
|
+
*/
|
|
681
|
+
function LauncherIcon({ open }) {
|
|
682
|
+
return /* @__PURE__ */ jsx("svg", {
|
|
683
|
+
"aria-hidden": "true",
|
|
684
|
+
className: "clivly-chat-widget__launcher-icon",
|
|
685
|
+
fill: "none",
|
|
686
|
+
focusable: "false",
|
|
687
|
+
stroke: "currentColor",
|
|
688
|
+
strokeLinecap: "round",
|
|
689
|
+
strokeLinejoin: "round",
|
|
690
|
+
strokeWidth: 2,
|
|
691
|
+
viewBox: "0 0 24 24",
|
|
692
|
+
children: open ? /* @__PURE__ */ jsx("path", { d: "M18 6 6 18M6 6l12 12" }) : /* @__PURE__ */ jsx("path", { d: "M7.9 20A9 9 0 1 0 4 16.1L2 22Z" })
|
|
693
|
+
});
|
|
694
|
+
}
|
|
564
695
|
function MessageBubble({ message }) {
|
|
565
696
|
const badge = automationBadge(message);
|
|
566
697
|
const author = badge ? (message.author ?? "").replace(AUTHOR_LABEL_SUFFIX, "") : message.author;
|
|
@@ -671,9 +802,10 @@ function isCompleteVisitor(visitor) {
|
|
|
671
802
|
* the visitor is told to wait for something that will never arrive. The third
|
|
672
803
|
* string keeps the status honest in all three states.
|
|
673
804
|
*/
|
|
674
|
-
function composerStatusText(hasTransport, isReady) {
|
|
805
|
+
function composerStatusText(hasTransport, isReady, reconnecting) {
|
|
675
806
|
if (!hasTransport) return "Chat is unavailable";
|
|
676
|
-
|
|
807
|
+
if (isReady) return "Conversation ready";
|
|
808
|
+
return reconnecting ? "Reconnecting…" : "Connecting…";
|
|
677
809
|
}
|
|
678
810
|
/**
|
|
679
811
|
* Guided options under the greeting. Native buttons in a labelled group, so
|
|
@@ -739,8 +871,98 @@ function StatusNotice({ assistantThinking, email, showWaitingNotice }) {
|
|
|
739
871
|
children: waitingNoticeText(email)
|
|
740
872
|
});
|
|
741
873
|
}
|
|
742
|
-
|
|
874
|
+
const RECONNECT_BASE_MS = 1e3;
|
|
875
|
+
const RECONNECT_MAX_MS = 3e4;
|
|
876
|
+
/**
|
|
877
|
+
* How long to wait before reconnect attempt `attempt` (0-based): doubling from
|
|
878
|
+
* one second to a 30-second ceiling, ±25% jitter so a fleet of widgets dropped
|
|
879
|
+
* by the same server restart doesn't reconnect in lockstep.
|
|
880
|
+
*/
|
|
881
|
+
function reconnectDelayMs(attempt, random = Math.random) {
|
|
882
|
+
const base = Math.min(RECONNECT_MAX_MS, RECONNECT_BASE_MS * 2 ** attempt);
|
|
883
|
+
return Math.round(base * (.75 + random() * .5));
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Keep the socket for `session` open, and reconnect when it drops — a laptop
|
|
887
|
+
* waking, a network change, a server deploy — instead of leaving the visitor at
|
|
888
|
+
* "Connecting…" until they reload. A reconnect refreshes the session (same
|
|
889
|
+
* conversation, fresh token) and the server's snapshot on connect brings back
|
|
890
|
+
* anything that arrived meanwhile. Returns whether a reconnect is pending.
|
|
891
|
+
*/
|
|
892
|
+
function useLiveConnection({ actions, createTransport, refreshSession, session, setIsReady, transportRef }) {
|
|
893
|
+
const [reconnecting, setReconnecting] = useState(false);
|
|
894
|
+
const attemptRef = useRef(0);
|
|
895
|
+
const refreshRef = useRef(refreshSession);
|
|
896
|
+
const actionsRef = useRef(actions);
|
|
897
|
+
useEffect(() => {
|
|
898
|
+
refreshRef.current = refreshSession;
|
|
899
|
+
actionsRef.current = actions;
|
|
900
|
+
});
|
|
901
|
+
useEffect(() => {
|
|
902
|
+
transportRef.current?.close();
|
|
903
|
+
transportRef.current = null;
|
|
904
|
+
setIsReady(false);
|
|
905
|
+
if (!(session && createTransport)) return;
|
|
906
|
+
let disposed = false;
|
|
907
|
+
let timer;
|
|
908
|
+
const retry = () => {
|
|
909
|
+
timer = void 0;
|
|
910
|
+
refreshRef.current(session).catch(() => {
|
|
911
|
+
scheduleReconnect();
|
|
912
|
+
});
|
|
913
|
+
};
|
|
914
|
+
const scheduleReconnect = () => {
|
|
915
|
+
if (disposed || timer !== void 0) return;
|
|
916
|
+
setReconnecting(true);
|
|
917
|
+
timer = setTimeout(retry, reconnectDelayMs(attemptRef.current));
|
|
918
|
+
attemptRef.current += 1;
|
|
919
|
+
};
|
|
920
|
+
const retryNow = () => {
|
|
921
|
+
if (timer !== void 0) {
|
|
922
|
+
clearTimeout(timer);
|
|
923
|
+
retry();
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
window.addEventListener("online", retryNow);
|
|
927
|
+
const connection = createTransport({
|
|
928
|
+
handlers: {
|
|
929
|
+
onClose: () => {
|
|
930
|
+
setIsReady(false);
|
|
931
|
+
actionsRef.current.setAssistantThinking(false);
|
|
932
|
+
scheduleReconnect();
|
|
933
|
+
},
|
|
934
|
+
onError: (transportError) => {
|
|
935
|
+
if (attemptRef.current === 0) actionsRef.current.setError(transportError.message);
|
|
936
|
+
},
|
|
937
|
+
onEvent: (event) => {
|
|
938
|
+
applyServerEvent(event, {
|
|
939
|
+
...actionsRef.current,
|
|
940
|
+
setIsReady
|
|
941
|
+
});
|
|
942
|
+
},
|
|
943
|
+
onOpen: () => {
|
|
944
|
+
if (attemptRef.current > 0) actionsRef.current.setError(null);
|
|
945
|
+
attemptRef.current = 0;
|
|
946
|
+
setReconnecting(false);
|
|
947
|
+
setIsReady(true);
|
|
948
|
+
}
|
|
949
|
+
},
|
|
950
|
+
session
|
|
951
|
+
});
|
|
952
|
+
transportRef.current = connection;
|
|
953
|
+
return () => {
|
|
954
|
+
disposed = true;
|
|
955
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
956
|
+
window.removeEventListener("online", retryNow);
|
|
957
|
+
connection.close();
|
|
958
|
+
if (transportRef.current === connection) transportRef.current = null;
|
|
959
|
+
};
|
|
960
|
+
}, [createTransport, session]);
|
|
961
|
+
return reconnecting;
|
|
962
|
+
}
|
|
963
|
+
function ChatWidget({ className, createTransport, defaultVisitor, getSession, initiallyOpen = false, launcher = "auto", launcherLabel = "Chat with us", placement = "bottom-right", storage = DEFAULT_STORAGE, style, theme = "auto", widgetId }) {
|
|
743
964
|
const [isOpen, setIsOpen] = useState(initiallyOpen);
|
|
965
|
+
const panelId = useId();
|
|
744
966
|
const [error, setError] = useState(null);
|
|
745
967
|
const [isConnecting, setIsConnecting] = useState(false);
|
|
746
968
|
const [isReady, setIsReady] = useState(false);
|
|
@@ -755,6 +977,7 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
755
977
|
});
|
|
756
978
|
const transportRef = useRef(null);
|
|
757
979
|
const listRef = useRef(null);
|
|
980
|
+
const identityRef = useRef(null);
|
|
758
981
|
useEffect(() => {
|
|
759
982
|
if (createTransport) return;
|
|
760
983
|
console.warn("[clivly] <ChatWidget> was given no createTransport, so messages cannot be sent. Pass createTransport={createClivlyTransport()} — see https://www.npmjs.com/package/@clivly/chat-widget#five-minute-quick-start");
|
|
@@ -770,41 +993,30 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
770
993
|
if (!listRef.current) return;
|
|
771
994
|
listRef.current.scrollTop = listRef.current.scrollHeight;
|
|
772
995
|
}, [messages]);
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
setIsReady(true);
|
|
798
|
-
}
|
|
799
|
-
},
|
|
800
|
-
session
|
|
801
|
-
});
|
|
802
|
-
transportRef.current = connection;
|
|
803
|
-
return () => {
|
|
804
|
-
connection.close();
|
|
805
|
-
if (transportRef.current === connection) transportRef.current = null;
|
|
806
|
-
};
|
|
807
|
-
}, [createTransport, session]);
|
|
996
|
+
const reconnecting = useLiveConnection({
|
|
997
|
+
actions: {
|
|
998
|
+
setAssistantThinking,
|
|
999
|
+
setError,
|
|
1000
|
+
setMessages,
|
|
1001
|
+
setSupportState
|
|
1002
|
+
},
|
|
1003
|
+
createTransport,
|
|
1004
|
+
refreshSession: async (current) => {
|
|
1005
|
+
const identity = identityRef.current;
|
|
1006
|
+
if (!identity) throw new Error("No visitor to reconnect as.");
|
|
1007
|
+
const next = await getSession({
|
|
1008
|
+
visitor: identity,
|
|
1009
|
+
visitorToken: current.visitorToken,
|
|
1010
|
+
widgetId
|
|
1011
|
+
});
|
|
1012
|
+
storage?.setItem(createStorageKey(widgetId), next.visitorToken);
|
|
1013
|
+
setSupportState(next.support?.state ?? null);
|
|
1014
|
+
setSession(next);
|
|
1015
|
+
},
|
|
1016
|
+
session,
|
|
1017
|
+
setIsReady,
|
|
1018
|
+
transportRef
|
|
1019
|
+
});
|
|
808
1020
|
useEffect(() => {
|
|
809
1021
|
if (!(isOpen && isReady)) return;
|
|
810
1022
|
const markReadIfVisible = () => {
|
|
@@ -832,6 +1044,7 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
832
1044
|
widgetId
|
|
833
1045
|
});
|
|
834
1046
|
storage?.setItem(createStorageKey(widgetId), nextSession.visitorToken);
|
|
1047
|
+
identityRef.current = identity;
|
|
835
1048
|
setSupportState(nextSession.support?.state ?? null);
|
|
836
1049
|
setSession(nextSession);
|
|
837
1050
|
setMessages(sortMessages(nextSession.messages.map((message) => createLocalMessage(message))));
|
|
@@ -909,21 +1122,24 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
909
1122
|
const topics = visibleTopics(session, supportState);
|
|
910
1123
|
return /* @__PURE__ */ jsxs("div", {
|
|
911
1124
|
className: `clivly-chat-widget ${className ?? ""}`.trim(),
|
|
1125
|
+
"data-clivly-launcher": launcher,
|
|
1126
|
+
"data-clivly-placement": placement,
|
|
912
1127
|
"data-clivly-theme": resolvedTheme,
|
|
913
1128
|
style: themeStyle,
|
|
914
1129
|
children: [
|
|
915
1130
|
/* @__PURE__ */ jsx("style", { children: WIDGET_STYLES }),
|
|
916
|
-
/* @__PURE__ */ jsx(
|
|
917
|
-
|
|
918
|
-
|
|
1131
|
+
/* @__PURE__ */ jsx(Launcher, {
|
|
1132
|
+
label: launcherLabel,
|
|
1133
|
+
onToggle: () => {
|
|
919
1134
|
setIsOpen((open) => !open);
|
|
920
1135
|
},
|
|
921
|
-
|
|
922
|
-
|
|
1136
|
+
open: isOpen,
|
|
1137
|
+
panelId
|
|
923
1138
|
}),
|
|
924
1139
|
isOpen ? /* @__PURE__ */ jsxs("section", {
|
|
925
1140
|
"aria-label": panelTitle,
|
|
926
1141
|
className: "clivly-chat-widget__panel",
|
|
1142
|
+
id: panelId,
|
|
927
1143
|
children: [
|
|
928
1144
|
/* @__PURE__ */ jsxs("header", {
|
|
929
1145
|
className: "clivly-chat-widget__header",
|
|
@@ -996,8 +1212,9 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
996
1212
|
}), /* @__PURE__ */ jsxs("div", {
|
|
997
1213
|
className: "clivly-chat-widget__actions",
|
|
998
1214
|
children: [/* @__PURE__ */ jsx("span", {
|
|
1215
|
+
"aria-live": "polite",
|
|
999
1216
|
className: "clivly-chat-widget__subtitle",
|
|
1000
|
-
children: composerStatusText(Boolean(createTransport), isReady)
|
|
1217
|
+
children: composerStatusText(Boolean(createTransport), isReady, reconnecting)
|
|
1001
1218
|
}), /* @__PURE__ */ jsx("button", {
|
|
1002
1219
|
className: "clivly-chat-widget__button",
|
|
1003
1220
|
disabled: !(messageDraft.trim() && canSend),
|
|
@@ -1146,6 +1363,26 @@ function createClivlyTransport(options = {}) {
|
|
|
1146
1363
|
});
|
|
1147
1364
|
}
|
|
1148
1365
|
//#endregion
|
|
1366
|
+
//#region src/layout.ts
|
|
1367
|
+
const PLACEMENTS = [
|
|
1368
|
+
"bottom-left",
|
|
1369
|
+
"bottom-right",
|
|
1370
|
+
"inline"
|
|
1371
|
+
];
|
|
1372
|
+
const LAUNCHERS = [
|
|
1373
|
+
"auto",
|
|
1374
|
+
"icon",
|
|
1375
|
+
"pill"
|
|
1376
|
+
];
|
|
1377
|
+
/** A script tag's `data-placement`; anything unrecognised floats bottom-right. */
|
|
1378
|
+
function parsePlacement(value) {
|
|
1379
|
+
return PLACEMENTS.find((placement) => placement === value) ?? "bottom-right";
|
|
1380
|
+
}
|
|
1381
|
+
/** A script tag's `data-launcher`; anything unrecognised is `"auto"`. */
|
|
1382
|
+
function parseLauncher(value) {
|
|
1383
|
+
return LAUNCHERS.find((launcher) => launcher === value) ?? "auto";
|
|
1384
|
+
}
|
|
1385
|
+
//#endregion
|
|
1149
1386
|
//#region src/session.ts
|
|
1150
1387
|
function isWidgetSessionPayload(value) {
|
|
1151
1388
|
return typeof value === "object" && value !== null;
|
|
@@ -1243,6 +1480,8 @@ function mountChatWidgetScript(script = document.currentScript) {
|
|
|
1243
1480
|
const { root, setTheme } = createWidgetMount({
|
|
1244
1481
|
container,
|
|
1245
1482
|
defaultVisitor: readScriptVisitor(script.dataset),
|
|
1483
|
+
launcher: parseLauncher(script.dataset.launcher),
|
|
1484
|
+
placement: parsePlacement(script.dataset.placement),
|
|
1246
1485
|
sessionUrl,
|
|
1247
1486
|
socketUrlTemplate: script.dataset.socketUrlTemplate,
|
|
1248
1487
|
theme: parseTheme(script.dataset.theme),
|