@clivly/chat-widget 0.14.1 → 0.15.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 +22 -1
- package/dist/browser-mount.cjs +192 -19
- package/dist/browser-mount.d.cts +42 -1
- package/dist/browser-mount.d.mts +42 -1
- package/dist/browser-mount.mjs +192 -19
- package/dist/browser.cjs +192 -19
- package/dist/browser.d.cts +42 -1
- package/dist/browser.d.mts +42 -1
- package/dist/browser.mjs +192 -19
- package/dist/index.cjs +192 -19
- package/dist/index.d.cts +44 -3
- package/dist/index.d.mts +44 -3
- package/dist/index.mjs +192 -19
- package/dist/widget.global.js +58 -298
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -289,6 +289,7 @@ Important fields include:
|
|
|
289
289
|
- `contact`
|
|
290
290
|
- `messages`
|
|
291
291
|
- optional `socketUrl`
|
|
292
|
+
- optional `support` (see [Support assistant](#support-assistant))
|
|
292
293
|
|
|
293
294
|
When `socketUrl` is present, the widget prefers it over reconstructing the
|
|
294
295
|
socket URL from a client-side template.
|
|
@@ -326,6 +327,26 @@ shows a short note that the team has been
|
|
|
326
327
|
notified, and that the reply will also go to the visitor's email when one is
|
|
327
328
|
known. A failed send doesn't start the clock, because nobody was notified about it.
|
|
328
329
|
|
|
330
|
+
## Support assistant
|
|
331
|
+
|
|
332
|
+
When the widget has guided support turned on in Clivly (**Settings → Chat
|
|
333
|
+
widget → Support**), the session payload carries `support` — the mode, the
|
|
334
|
+
conversation's state, and the topics to offer (ids and labels only; answers stay
|
|
335
|
+
on the server). The widget then:
|
|
336
|
+
|
|
337
|
+
- shows the server-written greeting, and every automated or AI message, with an
|
|
338
|
+
**Automated** / **AI assistant** badge (`message.automation` says which);
|
|
339
|
+
- offers the topics as buttons under the transcript while `support.state` is
|
|
340
|
+
`"automated"`, alongside the message box, and hides them after a handoff or a
|
|
341
|
+
teammate's reply (`support.state` server event);
|
|
342
|
+
- shows *The AI assistant is looking for an answer…* while an
|
|
343
|
+
`assistant.status` event says `thinking`.
|
|
344
|
+
|
|
345
|
+
A topic click is sent as `{ type: "topic.select", id, topicId }`. A **custom
|
|
346
|
+
transport** opts in with the optional `ChatTransportConnection.selectTopic()`;
|
|
347
|
+
without it, a click is sent as the topic's label in an ordinary message. A
|
|
348
|
+
payload without `support` renders exactly as before.
|
|
349
|
+
|
|
329
350
|
## Build outputs
|
|
330
351
|
|
|
331
352
|
The package currently produces:
|
|
@@ -373,7 +394,7 @@ unreachable Clivly into a JSON `502` instead of a thrown `fetch`.
|
|
|
373
394
|
| --- | --- | --- |
|
|
374
395
|
| Widget mounts but every session is `403` | The origin is not in the widget record's allowlist. It is **exact-match**: `https://example.com` does not cover `https://www.example.com` | Add every embedding origin, with scheme, under **Settings → Chat widget** |
|
|
375
396
|
| A brand-new widget refuses everything | An empty origin list permits nothing | Add at least one origin |
|
|
376
|
-
| `503 widget_not_configured` | The Clivly server
|
|
397
|
+
| `503 widget_not_configured` | The Clivly server cannot sign widget sessions | A Clivly-side configuration problem, not something in your app — contact Clivly support |
|
|
377
398
|
| Session request returns an opaque `400` | The route was hand-written and omits `origin` from the **body**, or rejects `visitorToken: null` | Use `createChatSessionHandler` from `clivly/sdk` |
|
|
378
399
|
| `404` on the session request | `sessionUrl` does not match where you mounted the route | `clivly add widget` writes `/api/clivly/widget/session` — match it, or set `sessionUrl` to your path |
|
|
379
400
|
| Bundle throws on `node:fs` / `node:child_process` | Something imported the `clivly` package root | Import the handler from `clivly/sdk` |
|
package/dist/browser-mount.cjs
CHANGED
|
@@ -163,6 +163,13 @@ const WIDGET_STYLES = `
|
|
|
163
163
|
.clivly-chat-widget textarea {
|
|
164
164
|
font: inherit;
|
|
165
165
|
}
|
|
166
|
+
/* The standalone embed does not load the app's shared stylesheet. */
|
|
167
|
+
@media (width < 48rem), (any-pointer: coarse) {
|
|
168
|
+
.clivly-chat-widget input,
|
|
169
|
+
.clivly-chat-widget textarea {
|
|
170
|
+
font-size: max(16px, 1em);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
166
173
|
.clivly-chat-widget__launcher {
|
|
167
174
|
align-items: center;
|
|
168
175
|
background: var(--clivly-chat-accent);
|
|
@@ -372,6 +379,47 @@ const WIDGET_STYLES = `
|
|
|
372
379
|
margin: 0;
|
|
373
380
|
padding: 0.625rem 0.75rem;
|
|
374
381
|
}
|
|
382
|
+
/* Automated and AI messages are marked as such, never styled as a teammate. */
|
|
383
|
+
.clivly-chat-widget__message[data-automation] {
|
|
384
|
+
background: transparent;
|
|
385
|
+
border: 1px dashed var(--clivly-chat-border);
|
|
386
|
+
}
|
|
387
|
+
.clivly-chat-widget__badge {
|
|
388
|
+
border: 1px solid var(--clivly-chat-border);
|
|
389
|
+
border-radius: 999px;
|
|
390
|
+
font-size: 0.6875rem;
|
|
391
|
+
padding: 0 0.4rem;
|
|
392
|
+
}
|
|
393
|
+
.clivly-chat-widget__topics {
|
|
394
|
+
display: flex;
|
|
395
|
+
flex-wrap: wrap;
|
|
396
|
+
gap: 0.5rem;
|
|
397
|
+
margin: 0;
|
|
398
|
+
padding: 0;
|
|
399
|
+
list-style: none;
|
|
400
|
+
}
|
|
401
|
+
.clivly-chat-widget__topic {
|
|
402
|
+
background: var(--clivly-chat-panel);
|
|
403
|
+
border: 1px solid var(--clivly-chat-accent);
|
|
404
|
+
border-radius: 999px;
|
|
405
|
+
color: var(--clivly-chat-text);
|
|
406
|
+
cursor: pointer;
|
|
407
|
+
font-size: 0.875rem;
|
|
408
|
+
min-height: 2.25rem;
|
|
409
|
+
padding: 0.4rem 0.85rem;
|
|
410
|
+
}
|
|
411
|
+
.clivly-chat-widget__topic:hover,
|
|
412
|
+
.clivly-chat-widget__topic:focus-visible {
|
|
413
|
+
background: var(--clivly-chat-visitor);
|
|
414
|
+
}
|
|
415
|
+
.clivly-chat-widget__topic:focus-visible {
|
|
416
|
+
outline: 2px solid var(--clivly-chat-accent);
|
|
417
|
+
outline-offset: 2px;
|
|
418
|
+
}
|
|
419
|
+
.clivly-chat-widget__topic[disabled] {
|
|
420
|
+
cursor: not-allowed;
|
|
421
|
+
opacity: 0.55;
|
|
422
|
+
}
|
|
375
423
|
.clivly-chat-widget__empty {
|
|
376
424
|
color: var(--clivly-chat-muted);
|
|
377
425
|
font-size: 0.875rem;
|
|
@@ -468,8 +516,15 @@ function useResolvedTheme(theme) {
|
|
|
468
516
|
* protocol reads as a single unit, and so `ChatWidget` stays inside the
|
|
469
517
|
* cognitive-complexity budget.
|
|
470
518
|
*/
|
|
471
|
-
function applyServerEvent(event,
|
|
519
|
+
function applyServerEvent(event, actions) {
|
|
520
|
+
const { setError, setIsReady, setMessages } = actions;
|
|
472
521
|
switch (event.type) {
|
|
522
|
+
case "assistant.status":
|
|
523
|
+
actions.setAssistantThinking(event.status === "thinking");
|
|
524
|
+
return;
|
|
525
|
+
case "support.state":
|
|
526
|
+
actions.setSupportState(event.state);
|
|
527
|
+
return;
|
|
473
528
|
case "conversation.snapshot":
|
|
474
529
|
setMessages(sortMessages(event.messages.map((message) => createLocalMessage(message))));
|
|
475
530
|
return;
|
|
@@ -490,14 +545,41 @@ function applyServerEvent(event, { setError, setIsReady, setMessages }) {
|
|
|
490
545
|
default: return;
|
|
491
546
|
}
|
|
492
547
|
}
|
|
548
|
+
/**
|
|
549
|
+
* What a message from the support assistant is called. Automated
|
|
550
|
+
* acknowledgements, AI answers and people are never presented alike, so a
|
|
551
|
+
* visitor is never led to think a person has read their message.
|
|
552
|
+
*/
|
|
553
|
+
const AUTOMATION_BADGES = {
|
|
554
|
+
ai: "AI assistant",
|
|
555
|
+
ai_clarify: "AI assistant",
|
|
556
|
+
greeting: "Automated",
|
|
557
|
+
handoff: "Automated",
|
|
558
|
+
topic: "Automated"
|
|
559
|
+
};
|
|
560
|
+
const AUTHOR_LABEL_SUFFIX = / · (?:Automated|AI assistant)$/;
|
|
561
|
+
function automationBadge(message) {
|
|
562
|
+
if (message.senderType !== "system" || !message.automation) return null;
|
|
563
|
+
return AUTOMATION_BADGES[message.automation] ?? "Automated";
|
|
564
|
+
}
|
|
493
565
|
function MessageBubble({ message }) {
|
|
566
|
+
const badge = automationBadge(message);
|
|
567
|
+
const author = badge ? (message.author ?? "").replace(AUTHOR_LABEL_SUFFIX, "") : message.author;
|
|
494
568
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
|
|
495
569
|
className: "clivly-chat-widget__message",
|
|
570
|
+
"data-automation": badge ? message.automation : void 0,
|
|
496
571
|
"data-sender": message.senderType,
|
|
497
572
|
children: [
|
|
498
573
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
499
574
|
className: "clivly-chat-widget__message-meta",
|
|
500
|
-
children: [
|
|
575
|
+
children: [
|
|
576
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: author || "Clivly" }),
|
|
577
|
+
badge ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
578
|
+
className: "clivly-chat-widget__badge",
|
|
579
|
+
children: badge
|
|
580
|
+
}) : null,
|
|
581
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatTimestamp(message.createdAt) })
|
|
582
|
+
]
|
|
501
583
|
}),
|
|
502
584
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: message.body }),
|
|
503
585
|
message.delivery === "sent" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -540,6 +622,7 @@ function awaitingReplySince(messages) {
|
|
|
540
622
|
for (let index = messages.length - 1; index >= 0; index--) {
|
|
541
623
|
const message = messages[index];
|
|
542
624
|
if (!message || message.delivery === "failed") continue;
|
|
625
|
+
if (message.senderType === "system" || message.automation === "topic_choice") continue;
|
|
543
626
|
if (message.senderType !== "visitor") return null;
|
|
544
627
|
return new Date(message.createdAt).getTime();
|
|
545
628
|
}
|
|
@@ -593,6 +676,70 @@ function composerStatusText(hasTransport, isReady) {
|
|
|
593
676
|
if (!hasTransport) return "Chat is unavailable";
|
|
594
677
|
return isReady ? "Conversation ready" : "Connecting…";
|
|
595
678
|
}
|
|
679
|
+
/**
|
|
680
|
+
* Guided options under the greeting. Native buttons in a labelled group, so
|
|
681
|
+
* they are reachable with Tab and operable with Enter/Space; free text stays
|
|
682
|
+
* available beside them the whole time.
|
|
683
|
+
*/
|
|
684
|
+
function SupportTopics({ disabled, onSelect, topics }) {
|
|
685
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
686
|
+
"aria-label": "Suggested topics",
|
|
687
|
+
className: "clivly-chat-widget__topics",
|
|
688
|
+
children: topics.map((topic) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
689
|
+
className: "clivly-chat-widget__topic",
|
|
690
|
+
disabled,
|
|
691
|
+
onClick: () => {
|
|
692
|
+
onSelect(topic);
|
|
693
|
+
},
|
|
694
|
+
type: "button",
|
|
695
|
+
children: topic.label
|
|
696
|
+
}) }, topic.id))
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
/** Topics are offered only while automation is answering this conversation. */
|
|
700
|
+
function visibleTopics(session, state) {
|
|
701
|
+
return state === "automated" ? session?.support?.topics ?? [] : [];
|
|
702
|
+
}
|
|
703
|
+
function composerPlaceholder(hasTopics) {
|
|
704
|
+
return hasTopics ? "Or type your question" : "Write your message";
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Hand one visitor message to the transport: a topic click as `topic.select`
|
|
708
|
+
* when the transport supports it (the server resolves the answer), otherwise
|
|
709
|
+
* as the label, typed. Throws what the transport throws.
|
|
710
|
+
*/
|
|
711
|
+
function dispatchVisitorMessage(transport, clientMessageId, body, topic) {
|
|
712
|
+
if (topic && transport.selectTopic) {
|
|
713
|
+
transport.selectTopic({
|
|
714
|
+
id: clientMessageId,
|
|
715
|
+
topicId: topic.id,
|
|
716
|
+
type: "topic.select"
|
|
717
|
+
});
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
transport.sendMessage({
|
|
721
|
+
body,
|
|
722
|
+
id: clientMessageId,
|
|
723
|
+
type: "message.create"
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* One honest status line: the AI is working on an answer, or — once the
|
|
728
|
+
* visitor has waited — that the team has been told. Never both.
|
|
729
|
+
*/
|
|
730
|
+
function StatusNotice({ assistantThinking, email, showWaitingNotice }) {
|
|
731
|
+
if (assistantThinking) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
732
|
+
className: "clivly-chat-widget__notice",
|
|
733
|
+
role: "status",
|
|
734
|
+
children: "The AI assistant is looking for an answer…"
|
|
735
|
+
});
|
|
736
|
+
if (!showWaitingNotice) return null;
|
|
737
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
738
|
+
className: "clivly-chat-widget__notice",
|
|
739
|
+
role: "status",
|
|
740
|
+
children: waitingNoticeText(email)
|
|
741
|
+
});
|
|
742
|
+
}
|
|
596
743
|
function ChatWidget({ className, createTransport, defaultVisitor, getSession, initiallyOpen = false, launcherLabel = "Chat with us", storage = DEFAULT_STORAGE, style, theme = "auto", widgetId }) {
|
|
597
744
|
const [isOpen, setIsOpen] = (0, react.useState)(initiallyOpen);
|
|
598
745
|
const [error, setError] = (0, react.useState)(null);
|
|
@@ -601,6 +748,8 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
601
748
|
const [messageDraft, setMessageDraft] = (0, react.useState)("");
|
|
602
749
|
const [messages, setMessages] = (0, react.useState)([]);
|
|
603
750
|
const [session, setSession] = (0, react.useState)(null);
|
|
751
|
+
const [supportState, setSupportState] = (0, react.useState)(null);
|
|
752
|
+
const [assistantThinking, setAssistantThinking] = (0, react.useState)(false);
|
|
604
753
|
const [visitor, setVisitor] = (0, react.useState)({
|
|
605
754
|
email: defaultVisitor?.email ?? "",
|
|
606
755
|
name: defaultVisitor?.name ?? ""
|
|
@@ -631,15 +780,18 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
631
780
|
handlers: {
|
|
632
781
|
onClose: () => {
|
|
633
782
|
setIsReady(false);
|
|
783
|
+
setAssistantThinking(false);
|
|
634
784
|
},
|
|
635
785
|
onError: (transportError) => {
|
|
636
786
|
setError(transportError.message);
|
|
637
787
|
},
|
|
638
788
|
onEvent: (event) => {
|
|
639
789
|
applyServerEvent(event, {
|
|
790
|
+
setAssistantThinking,
|
|
640
791
|
setError,
|
|
641
792
|
setIsReady,
|
|
642
|
-
setMessages
|
|
793
|
+
setMessages,
|
|
794
|
+
setSupportState
|
|
643
795
|
});
|
|
644
796
|
},
|
|
645
797
|
onOpen: () => {
|
|
@@ -681,6 +833,7 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
681
833
|
widgetId
|
|
682
834
|
});
|
|
683
835
|
storage?.setItem(createStorageKey(widgetId), nextSession.visitorToken);
|
|
836
|
+
setSupportState(nextSession.support?.state ?? null);
|
|
684
837
|
setSession(nextSession);
|
|
685
838
|
setMessages(sortMessages(nextSession.messages.map((message) => createLocalMessage(message))));
|
|
686
839
|
} catch (sessionError) {
|
|
@@ -713,41 +866,48 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
713
866
|
session,
|
|
714
867
|
startSession
|
|
715
868
|
]);
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
869
|
+
/**
|
|
870
|
+
* Show the visitor's message at once and hand it to the transport. A topic
|
|
871
|
+
* click goes as `topic.select` when the transport supports it, so the
|
|
872
|
+
* server resolves the answer; otherwise as the label, typed.
|
|
873
|
+
*/
|
|
874
|
+
const sendVisitorMessage = (body, topic) => {
|
|
875
|
+
const transport = transportRef.current;
|
|
876
|
+
if (!(session && transport)) {
|
|
719
877
|
setError("Chat is not connected yet.");
|
|
720
878
|
return;
|
|
721
879
|
}
|
|
722
|
-
const body = messageDraft.trim();
|
|
723
|
-
if (!body) return;
|
|
724
880
|
const clientMessageId = generateId();
|
|
725
881
|
const optimisticMessage = {
|
|
726
882
|
author: sessionDisplayName(session),
|
|
883
|
+
automation: topic && transport.selectTopic ? "topic_choice" : null,
|
|
727
884
|
body,
|
|
728
885
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
729
886
|
id: clientMessageId,
|
|
730
887
|
senderType: "visitor"
|
|
731
888
|
};
|
|
732
889
|
setMessages((currentMessages) => sortMessages([...currentMessages, createLocalMessage(optimisticMessage, "pending")]));
|
|
733
|
-
setMessageDraft("");
|
|
734
890
|
setError(null);
|
|
735
891
|
try {
|
|
736
|
-
|
|
737
|
-
body,
|
|
738
|
-
id: clientMessageId,
|
|
739
|
-
type: "message.create"
|
|
740
|
-
});
|
|
892
|
+
dispatchVisitorMessage(transport, clientMessageId, body, topic);
|
|
741
893
|
} catch (transportError) {
|
|
742
894
|
const resolved = transportError instanceof Error ? transportError : /* @__PURE__ */ new Error("Failed to send your message.");
|
|
743
895
|
setMessages((currentMessages) => markMessageFailed(currentMessages, clientMessageId));
|
|
744
896
|
setError(resolved.message);
|
|
745
897
|
}
|
|
746
898
|
};
|
|
899
|
+
const submitMessage = (event) => {
|
|
900
|
+
event?.preventDefault();
|
|
901
|
+
const body = messageDraft.trim();
|
|
902
|
+
if (!body) return;
|
|
903
|
+
setMessageDraft("");
|
|
904
|
+
sendVisitorMessage(body);
|
|
905
|
+
};
|
|
747
906
|
const canSend = Boolean(createTransport) && isReady;
|
|
748
907
|
const greeting = session?.widget.config.greeting ?? "Tell us a little about yourself and we’ll pick up the conversation from there.";
|
|
749
908
|
const panelTitle = session?.widget.name ?? "Start a conversation";
|
|
750
909
|
const sessionSummary = sessionSummaryText(session, greeting);
|
|
910
|
+
const topics = visibleTopics(session, supportState);
|
|
751
911
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
752
912
|
className: `clivly-chat-widget ${className ?? ""}`.trim(),
|
|
753
913
|
"data-clivly-theme": resolvedTheme,
|
|
@@ -785,8 +945,10 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
785
945
|
})]
|
|
786
946
|
}),
|
|
787
947
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
948
|
+
"aria-label": "Conversation",
|
|
788
949
|
className: "clivly-chat-widget__body",
|
|
789
950
|
ref: listRef,
|
|
951
|
+
role: "log",
|
|
790
952
|
children: [
|
|
791
953
|
error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
792
954
|
className: "clivly-chat-widget__error",
|
|
@@ -797,10 +959,17 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
797
959
|
hasSession: Boolean(session),
|
|
798
960
|
messages
|
|
799
961
|
}),
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
962
|
+
topics.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SupportTopics, {
|
|
963
|
+
disabled: !canSend,
|
|
964
|
+
onSelect: (topic) => {
|
|
965
|
+
sendVisitorMessage(topic.label, topic);
|
|
966
|
+
},
|
|
967
|
+
topics
|
|
968
|
+
}) : null,
|
|
969
|
+
session ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusNotice, {
|
|
970
|
+
assistantThinking,
|
|
971
|
+
email: session.contact?.email,
|
|
972
|
+
showWaitingNotice
|
|
804
973
|
}) : null
|
|
805
974
|
]
|
|
806
975
|
}),
|
|
@@ -822,7 +991,7 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
822
991
|
event.preventDefault();
|
|
823
992
|
if (messageDraft.trim() && canSend) submitMessage();
|
|
824
993
|
},
|
|
825
|
-
placeholder:
|
|
994
|
+
placeholder: composerPlaceholder(topics.length > 0),
|
|
826
995
|
value: messageDraft
|
|
827
996
|
})]
|
|
828
997
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -948,6 +1117,10 @@ function createWebSocketTransport({ createSocket = (url, protocols) => new WebSo
|
|
|
948
1117
|
markRead: () => {
|
|
949
1118
|
if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify({ type: "conversation.read" }));
|
|
950
1119
|
},
|
|
1120
|
+
selectTopic: (event) => {
|
|
1121
|
+
if (socket.readyState !== WebSocket.OPEN) throw new Error("Chat socket is not connected.");
|
|
1122
|
+
socket.send(JSON.stringify(event));
|
|
1123
|
+
},
|
|
951
1124
|
sendMessage: (message) => {
|
|
952
1125
|
if (socket.readyState !== WebSocket.OPEN) throw new Error("Chat socket is not connected.");
|
|
953
1126
|
socket.send(JSON.stringify(message));
|
package/dist/browser-mount.d.cts
CHANGED
|
@@ -34,11 +34,30 @@ interface WidgetSummary {
|
|
|
34
34
|
}
|
|
35
35
|
interface WidgetMessage {
|
|
36
36
|
author: string | null;
|
|
37
|
+
/**
|
|
38
|
+
* Set on messages the support assistant wrote (`greeting`, `topic`,
|
|
39
|
+
* `handoff`, `ai`, `ai_clarify`) and on a visitor's topic click
|
|
40
|
+
* (`topic_choice`). Absent or null on everything a person typed, and on
|
|
41
|
+
* every message from a server that predates the assistant.
|
|
42
|
+
*/
|
|
43
|
+
automation?: string | null;
|
|
37
44
|
body: string;
|
|
38
45
|
createdAt: string;
|
|
39
46
|
id: string;
|
|
40
47
|
senderType: WidgetSenderType;
|
|
41
48
|
}
|
|
49
|
+
type WidgetSupportState = "automated" | "handoff" | "human";
|
|
50
|
+
interface WidgetSupportTopic {
|
|
51
|
+
action: "answer" | "ask" | "link" | "human";
|
|
52
|
+
id: string;
|
|
53
|
+
label: string;
|
|
54
|
+
}
|
|
55
|
+
/** Guided support for one conversation. Answers stay on the server. */
|
|
56
|
+
interface WidgetSupport {
|
|
57
|
+
mode: "human" | "guided" | "ai";
|
|
58
|
+
state: WidgetSupportState | null;
|
|
59
|
+
topics: WidgetSupportTopic[];
|
|
60
|
+
}
|
|
42
61
|
interface WidgetSessionPayload {
|
|
43
62
|
contact: WidgetContact | null;
|
|
44
63
|
conversation: WidgetConversation;
|
|
@@ -46,6 +65,8 @@ interface WidgetSessionPayload {
|
|
|
46
65
|
messages: WidgetMessage[];
|
|
47
66
|
/** Absolute socket URL returned by Clivly's bootstrap. */
|
|
48
67
|
socketUrl?: string;
|
|
68
|
+
/** Absent or null when the widget has no guided support configured. */
|
|
69
|
+
support?: WidgetSupport | null;
|
|
49
70
|
token: string;
|
|
50
71
|
visitorToken: string;
|
|
51
72
|
widget: WidgetSummary;
|
|
@@ -65,6 +86,12 @@ interface WidgetClientCreateMessageEvent {
|
|
|
65
86
|
id: string;
|
|
66
87
|
type: "message.create";
|
|
67
88
|
}
|
|
89
|
+
/** The visitor picked a guided topic (see `ChatTransportConnection.selectTopic`). */
|
|
90
|
+
interface WidgetClientTopicEvent {
|
|
91
|
+
id: string;
|
|
92
|
+
topicId: string;
|
|
93
|
+
type: "topic.select";
|
|
94
|
+
}
|
|
68
95
|
interface WidgetSessionReadyEvent {
|
|
69
96
|
conversationId: string;
|
|
70
97
|
type: "session.ready";
|
|
@@ -83,11 +110,20 @@ interface WidgetMessageAckEvent {
|
|
|
83
110
|
message: WidgetMessage;
|
|
84
111
|
type: "message.ack";
|
|
85
112
|
}
|
|
113
|
+
interface WidgetSupportStateEvent {
|
|
114
|
+
state: WidgetSupportState;
|
|
115
|
+
type: "support.state";
|
|
116
|
+
}
|
|
117
|
+
/** The assistant is preparing an answer (`thinking`) or has finished (`idle`). */
|
|
118
|
+
interface WidgetAssistantStatusEvent {
|
|
119
|
+
status: "thinking" | "idle";
|
|
120
|
+
type: "assistant.status";
|
|
121
|
+
}
|
|
86
122
|
interface WidgetErrorEvent {
|
|
87
123
|
message: string;
|
|
88
124
|
type: "error";
|
|
89
125
|
}
|
|
90
|
-
type WidgetServerEvent = WidgetConversationSnapshotEvent | WidgetErrorEvent | WidgetMessageAckEvent | WidgetMessageCreatedEvent | WidgetSessionReadyEvent;
|
|
126
|
+
type WidgetServerEvent = WidgetAssistantStatusEvent | WidgetConversationSnapshotEvent | WidgetErrorEvent | WidgetMessageAckEvent | WidgetMessageCreatedEvent | WidgetSessionReadyEvent | WidgetSupportStateEvent;
|
|
91
127
|
//#endregion
|
|
92
128
|
//#region src/transport.d.ts
|
|
93
129
|
interface ChatTransportHandlers {
|
|
@@ -104,6 +140,11 @@ interface ChatTransportConnection {
|
|
|
104
140
|
* without it still works, its visitors are just emailed replies they saw.
|
|
105
141
|
*/
|
|
106
142
|
markRead?: () => void;
|
|
143
|
+
/**
|
|
144
|
+
* Send a guided-topic click. Optional: without it the widget sends the
|
|
145
|
+
* topic's label as an ordinary message, so a custom transport still works.
|
|
146
|
+
*/
|
|
147
|
+
selectTopic?: (event: WidgetClientTopicEvent) => void;
|
|
107
148
|
sendMessage: (event: WidgetClientCreateMessageEvent) => void;
|
|
108
149
|
}
|
|
109
150
|
interface CreateChatTransportOptions {
|
package/dist/browser-mount.d.mts
CHANGED
|
@@ -34,11 +34,30 @@ interface WidgetSummary {
|
|
|
34
34
|
}
|
|
35
35
|
interface WidgetMessage {
|
|
36
36
|
author: string | null;
|
|
37
|
+
/**
|
|
38
|
+
* Set on messages the support assistant wrote (`greeting`, `topic`,
|
|
39
|
+
* `handoff`, `ai`, `ai_clarify`) and on a visitor's topic click
|
|
40
|
+
* (`topic_choice`). Absent or null on everything a person typed, and on
|
|
41
|
+
* every message from a server that predates the assistant.
|
|
42
|
+
*/
|
|
43
|
+
automation?: string | null;
|
|
37
44
|
body: string;
|
|
38
45
|
createdAt: string;
|
|
39
46
|
id: string;
|
|
40
47
|
senderType: WidgetSenderType;
|
|
41
48
|
}
|
|
49
|
+
type WidgetSupportState = "automated" | "handoff" | "human";
|
|
50
|
+
interface WidgetSupportTopic {
|
|
51
|
+
action: "answer" | "ask" | "link" | "human";
|
|
52
|
+
id: string;
|
|
53
|
+
label: string;
|
|
54
|
+
}
|
|
55
|
+
/** Guided support for one conversation. Answers stay on the server. */
|
|
56
|
+
interface WidgetSupport {
|
|
57
|
+
mode: "human" | "guided" | "ai";
|
|
58
|
+
state: WidgetSupportState | null;
|
|
59
|
+
topics: WidgetSupportTopic[];
|
|
60
|
+
}
|
|
42
61
|
interface WidgetSessionPayload {
|
|
43
62
|
contact: WidgetContact | null;
|
|
44
63
|
conversation: WidgetConversation;
|
|
@@ -46,6 +65,8 @@ interface WidgetSessionPayload {
|
|
|
46
65
|
messages: WidgetMessage[];
|
|
47
66
|
/** Absolute socket URL returned by Clivly's bootstrap. */
|
|
48
67
|
socketUrl?: string;
|
|
68
|
+
/** Absent or null when the widget has no guided support configured. */
|
|
69
|
+
support?: WidgetSupport | null;
|
|
49
70
|
token: string;
|
|
50
71
|
visitorToken: string;
|
|
51
72
|
widget: WidgetSummary;
|
|
@@ -65,6 +86,12 @@ interface WidgetClientCreateMessageEvent {
|
|
|
65
86
|
id: string;
|
|
66
87
|
type: "message.create";
|
|
67
88
|
}
|
|
89
|
+
/** The visitor picked a guided topic (see `ChatTransportConnection.selectTopic`). */
|
|
90
|
+
interface WidgetClientTopicEvent {
|
|
91
|
+
id: string;
|
|
92
|
+
topicId: string;
|
|
93
|
+
type: "topic.select";
|
|
94
|
+
}
|
|
68
95
|
interface WidgetSessionReadyEvent {
|
|
69
96
|
conversationId: string;
|
|
70
97
|
type: "session.ready";
|
|
@@ -83,11 +110,20 @@ interface WidgetMessageAckEvent {
|
|
|
83
110
|
message: WidgetMessage;
|
|
84
111
|
type: "message.ack";
|
|
85
112
|
}
|
|
113
|
+
interface WidgetSupportStateEvent {
|
|
114
|
+
state: WidgetSupportState;
|
|
115
|
+
type: "support.state";
|
|
116
|
+
}
|
|
117
|
+
/** The assistant is preparing an answer (`thinking`) or has finished (`idle`). */
|
|
118
|
+
interface WidgetAssistantStatusEvent {
|
|
119
|
+
status: "thinking" | "idle";
|
|
120
|
+
type: "assistant.status";
|
|
121
|
+
}
|
|
86
122
|
interface WidgetErrorEvent {
|
|
87
123
|
message: string;
|
|
88
124
|
type: "error";
|
|
89
125
|
}
|
|
90
|
-
type WidgetServerEvent = WidgetConversationSnapshotEvent | WidgetErrorEvent | WidgetMessageAckEvent | WidgetMessageCreatedEvent | WidgetSessionReadyEvent;
|
|
126
|
+
type WidgetServerEvent = WidgetAssistantStatusEvent | WidgetConversationSnapshotEvent | WidgetErrorEvent | WidgetMessageAckEvent | WidgetMessageCreatedEvent | WidgetSessionReadyEvent | WidgetSupportStateEvent;
|
|
91
127
|
//#endregion
|
|
92
128
|
//#region src/transport.d.ts
|
|
93
129
|
interface ChatTransportHandlers {
|
|
@@ -104,6 +140,11 @@ interface ChatTransportConnection {
|
|
|
104
140
|
* without it still works, its visitors are just emailed replies they saw.
|
|
105
141
|
*/
|
|
106
142
|
markRead?: () => void;
|
|
143
|
+
/**
|
|
144
|
+
* Send a guided-topic click. Optional: without it the widget sends the
|
|
145
|
+
* topic's label as an ordinary message, so a custom transport still works.
|
|
146
|
+
*/
|
|
147
|
+
selectTopic?: (event: WidgetClientTopicEvent) => void;
|
|
107
148
|
sendMessage: (event: WidgetClientCreateMessageEvent) => void;
|
|
108
149
|
}
|
|
109
150
|
interface CreateChatTransportOptions {
|