@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/dist/browser.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.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 {
|
package/dist/browser.mjs
CHANGED
|
@@ -162,6 +162,13 @@ const WIDGET_STYLES = `
|
|
|
162
162
|
.clivly-chat-widget textarea {
|
|
163
163
|
font: inherit;
|
|
164
164
|
}
|
|
165
|
+
/* The standalone embed does not load the app's shared stylesheet. */
|
|
166
|
+
@media (width < 48rem), (any-pointer: coarse) {
|
|
167
|
+
.clivly-chat-widget input,
|
|
168
|
+
.clivly-chat-widget textarea {
|
|
169
|
+
font-size: max(16px, 1em);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
165
172
|
.clivly-chat-widget__launcher {
|
|
166
173
|
align-items: center;
|
|
167
174
|
background: var(--clivly-chat-accent);
|
|
@@ -371,6 +378,47 @@ const WIDGET_STYLES = `
|
|
|
371
378
|
margin: 0;
|
|
372
379
|
padding: 0.625rem 0.75rem;
|
|
373
380
|
}
|
|
381
|
+
/* Automated and AI messages are marked as such, never styled as a teammate. */
|
|
382
|
+
.clivly-chat-widget__message[data-automation] {
|
|
383
|
+
background: transparent;
|
|
384
|
+
border: 1px dashed var(--clivly-chat-border);
|
|
385
|
+
}
|
|
386
|
+
.clivly-chat-widget__badge {
|
|
387
|
+
border: 1px solid var(--clivly-chat-border);
|
|
388
|
+
border-radius: 999px;
|
|
389
|
+
font-size: 0.6875rem;
|
|
390
|
+
padding: 0 0.4rem;
|
|
391
|
+
}
|
|
392
|
+
.clivly-chat-widget__topics {
|
|
393
|
+
display: flex;
|
|
394
|
+
flex-wrap: wrap;
|
|
395
|
+
gap: 0.5rem;
|
|
396
|
+
margin: 0;
|
|
397
|
+
padding: 0;
|
|
398
|
+
list-style: none;
|
|
399
|
+
}
|
|
400
|
+
.clivly-chat-widget__topic {
|
|
401
|
+
background: var(--clivly-chat-panel);
|
|
402
|
+
border: 1px solid var(--clivly-chat-accent);
|
|
403
|
+
border-radius: 999px;
|
|
404
|
+
color: var(--clivly-chat-text);
|
|
405
|
+
cursor: pointer;
|
|
406
|
+
font-size: 0.875rem;
|
|
407
|
+
min-height: 2.25rem;
|
|
408
|
+
padding: 0.4rem 0.85rem;
|
|
409
|
+
}
|
|
410
|
+
.clivly-chat-widget__topic:hover,
|
|
411
|
+
.clivly-chat-widget__topic:focus-visible {
|
|
412
|
+
background: var(--clivly-chat-visitor);
|
|
413
|
+
}
|
|
414
|
+
.clivly-chat-widget__topic:focus-visible {
|
|
415
|
+
outline: 2px solid var(--clivly-chat-accent);
|
|
416
|
+
outline-offset: 2px;
|
|
417
|
+
}
|
|
418
|
+
.clivly-chat-widget__topic[disabled] {
|
|
419
|
+
cursor: not-allowed;
|
|
420
|
+
opacity: 0.55;
|
|
421
|
+
}
|
|
374
422
|
.clivly-chat-widget__empty {
|
|
375
423
|
color: var(--clivly-chat-muted);
|
|
376
424
|
font-size: 0.875rem;
|
|
@@ -467,8 +515,15 @@ function useResolvedTheme(theme) {
|
|
|
467
515
|
* protocol reads as a single unit, and so `ChatWidget` stays inside the
|
|
468
516
|
* cognitive-complexity budget.
|
|
469
517
|
*/
|
|
470
|
-
function applyServerEvent(event,
|
|
518
|
+
function applyServerEvent(event, actions) {
|
|
519
|
+
const { setError, setIsReady, setMessages } = actions;
|
|
471
520
|
switch (event.type) {
|
|
521
|
+
case "assistant.status":
|
|
522
|
+
actions.setAssistantThinking(event.status === "thinking");
|
|
523
|
+
return;
|
|
524
|
+
case "support.state":
|
|
525
|
+
actions.setSupportState(event.state);
|
|
526
|
+
return;
|
|
472
527
|
case "conversation.snapshot":
|
|
473
528
|
setMessages(sortMessages(event.messages.map((message) => createLocalMessage(message))));
|
|
474
529
|
return;
|
|
@@ -489,14 +544,41 @@ function applyServerEvent(event, { setError, setIsReady, setMessages }) {
|
|
|
489
544
|
default: return;
|
|
490
545
|
}
|
|
491
546
|
}
|
|
547
|
+
/**
|
|
548
|
+
* What a message from the support assistant is called. Automated
|
|
549
|
+
* acknowledgements, AI answers and people are never presented alike, so a
|
|
550
|
+
* visitor is never led to think a person has read their message.
|
|
551
|
+
*/
|
|
552
|
+
const AUTOMATION_BADGES = {
|
|
553
|
+
ai: "AI assistant",
|
|
554
|
+
ai_clarify: "AI assistant",
|
|
555
|
+
greeting: "Automated",
|
|
556
|
+
handoff: "Automated",
|
|
557
|
+
topic: "Automated"
|
|
558
|
+
};
|
|
559
|
+
const AUTHOR_LABEL_SUFFIX = / · (?:Automated|AI assistant)$/;
|
|
560
|
+
function automationBadge(message) {
|
|
561
|
+
if (message.senderType !== "system" || !message.automation) return null;
|
|
562
|
+
return AUTOMATION_BADGES[message.automation] ?? "Automated";
|
|
563
|
+
}
|
|
492
564
|
function MessageBubble({ message }) {
|
|
565
|
+
const badge = automationBadge(message);
|
|
566
|
+
const author = badge ? (message.author ?? "").replace(AUTHOR_LABEL_SUFFIX, "") : message.author;
|
|
493
567
|
return /* @__PURE__ */ jsxs("article", {
|
|
494
568
|
className: "clivly-chat-widget__message",
|
|
569
|
+
"data-automation": badge ? message.automation : void 0,
|
|
495
570
|
"data-sender": message.senderType,
|
|
496
571
|
children: [
|
|
497
572
|
/* @__PURE__ */ jsxs("div", {
|
|
498
573
|
className: "clivly-chat-widget__message-meta",
|
|
499
|
-
children: [
|
|
574
|
+
children: [
|
|
575
|
+
/* @__PURE__ */ jsx("span", { children: author || "Clivly" }),
|
|
576
|
+
badge ? /* @__PURE__ */ jsx("span", {
|
|
577
|
+
className: "clivly-chat-widget__badge",
|
|
578
|
+
children: badge
|
|
579
|
+
}) : null,
|
|
580
|
+
/* @__PURE__ */ jsx("span", { children: formatTimestamp(message.createdAt) })
|
|
581
|
+
]
|
|
500
582
|
}),
|
|
501
583
|
/* @__PURE__ */ jsx("div", { children: message.body }),
|
|
502
584
|
message.delivery === "sent" ? null : /* @__PURE__ */ jsx("div", {
|
|
@@ -539,6 +621,7 @@ function awaitingReplySince(messages) {
|
|
|
539
621
|
for (let index = messages.length - 1; index >= 0; index--) {
|
|
540
622
|
const message = messages[index];
|
|
541
623
|
if (!message || message.delivery === "failed") continue;
|
|
624
|
+
if (message.senderType === "system" || message.automation === "topic_choice") continue;
|
|
542
625
|
if (message.senderType !== "visitor") return null;
|
|
543
626
|
return new Date(message.createdAt).getTime();
|
|
544
627
|
}
|
|
@@ -592,6 +675,70 @@ function composerStatusText(hasTransport, isReady) {
|
|
|
592
675
|
if (!hasTransport) return "Chat is unavailable";
|
|
593
676
|
return isReady ? "Conversation ready" : "Connecting…";
|
|
594
677
|
}
|
|
678
|
+
/**
|
|
679
|
+
* Guided options under the greeting. Native buttons in a labelled group, so
|
|
680
|
+
* they are reachable with Tab and operable with Enter/Space; free text stays
|
|
681
|
+
* available beside them the whole time.
|
|
682
|
+
*/
|
|
683
|
+
function SupportTopics({ disabled, onSelect, topics }) {
|
|
684
|
+
return /* @__PURE__ */ jsx("ul", {
|
|
685
|
+
"aria-label": "Suggested topics",
|
|
686
|
+
className: "clivly-chat-widget__topics",
|
|
687
|
+
children: topics.map((topic) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("button", {
|
|
688
|
+
className: "clivly-chat-widget__topic",
|
|
689
|
+
disabled,
|
|
690
|
+
onClick: () => {
|
|
691
|
+
onSelect(topic);
|
|
692
|
+
},
|
|
693
|
+
type: "button",
|
|
694
|
+
children: topic.label
|
|
695
|
+
}) }, topic.id))
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
/** Topics are offered only while automation is answering this conversation. */
|
|
699
|
+
function visibleTopics(session, state) {
|
|
700
|
+
return state === "automated" ? session?.support?.topics ?? [] : [];
|
|
701
|
+
}
|
|
702
|
+
function composerPlaceholder(hasTopics) {
|
|
703
|
+
return hasTopics ? "Or type your question" : "Write your message";
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Hand one visitor message to the transport: a topic click as `topic.select`
|
|
707
|
+
* when the transport supports it (the server resolves the answer), otherwise
|
|
708
|
+
* as the label, typed. Throws what the transport throws.
|
|
709
|
+
*/
|
|
710
|
+
function dispatchVisitorMessage(transport, clientMessageId, body, topic) {
|
|
711
|
+
if (topic && transport.selectTopic) {
|
|
712
|
+
transport.selectTopic({
|
|
713
|
+
id: clientMessageId,
|
|
714
|
+
topicId: topic.id,
|
|
715
|
+
type: "topic.select"
|
|
716
|
+
});
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
transport.sendMessage({
|
|
720
|
+
body,
|
|
721
|
+
id: clientMessageId,
|
|
722
|
+
type: "message.create"
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* One honest status line: the AI is working on an answer, or — once the
|
|
727
|
+
* visitor has waited — that the team has been told. Never both.
|
|
728
|
+
*/
|
|
729
|
+
function StatusNotice({ assistantThinking, email, showWaitingNotice }) {
|
|
730
|
+
if (assistantThinking) return /* @__PURE__ */ jsx("p", {
|
|
731
|
+
className: "clivly-chat-widget__notice",
|
|
732
|
+
role: "status",
|
|
733
|
+
children: "The AI assistant is looking for an answer…"
|
|
734
|
+
});
|
|
735
|
+
if (!showWaitingNotice) return null;
|
|
736
|
+
return /* @__PURE__ */ jsx("p", {
|
|
737
|
+
className: "clivly-chat-widget__notice",
|
|
738
|
+
role: "status",
|
|
739
|
+
children: waitingNoticeText(email)
|
|
740
|
+
});
|
|
741
|
+
}
|
|
595
742
|
function ChatWidget({ className, createTransport, defaultVisitor, getSession, initiallyOpen = false, launcherLabel = "Chat with us", storage = DEFAULT_STORAGE, style, theme = "auto", widgetId }) {
|
|
596
743
|
const [isOpen, setIsOpen] = useState(initiallyOpen);
|
|
597
744
|
const [error, setError] = useState(null);
|
|
@@ -600,6 +747,8 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
600
747
|
const [messageDraft, setMessageDraft] = useState("");
|
|
601
748
|
const [messages, setMessages] = useState([]);
|
|
602
749
|
const [session, setSession] = useState(null);
|
|
750
|
+
const [supportState, setSupportState] = useState(null);
|
|
751
|
+
const [assistantThinking, setAssistantThinking] = useState(false);
|
|
603
752
|
const [visitor, setVisitor] = useState({
|
|
604
753
|
email: defaultVisitor?.email ?? "",
|
|
605
754
|
name: defaultVisitor?.name ?? ""
|
|
@@ -630,15 +779,18 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
630
779
|
handlers: {
|
|
631
780
|
onClose: () => {
|
|
632
781
|
setIsReady(false);
|
|
782
|
+
setAssistantThinking(false);
|
|
633
783
|
},
|
|
634
784
|
onError: (transportError) => {
|
|
635
785
|
setError(transportError.message);
|
|
636
786
|
},
|
|
637
787
|
onEvent: (event) => {
|
|
638
788
|
applyServerEvent(event, {
|
|
789
|
+
setAssistantThinking,
|
|
639
790
|
setError,
|
|
640
791
|
setIsReady,
|
|
641
|
-
setMessages
|
|
792
|
+
setMessages,
|
|
793
|
+
setSupportState
|
|
642
794
|
});
|
|
643
795
|
},
|
|
644
796
|
onOpen: () => {
|
|
@@ -680,6 +832,7 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
680
832
|
widgetId
|
|
681
833
|
});
|
|
682
834
|
storage?.setItem(createStorageKey(widgetId), nextSession.visitorToken);
|
|
835
|
+
setSupportState(nextSession.support?.state ?? null);
|
|
683
836
|
setSession(nextSession);
|
|
684
837
|
setMessages(sortMessages(nextSession.messages.map((message) => createLocalMessage(message))));
|
|
685
838
|
} catch (sessionError) {
|
|
@@ -712,41 +865,48 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
712
865
|
session,
|
|
713
866
|
startSession
|
|
714
867
|
]);
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
868
|
+
/**
|
|
869
|
+
* Show the visitor's message at once and hand it to the transport. A topic
|
|
870
|
+
* click goes as `topic.select` when the transport supports it, so the
|
|
871
|
+
* server resolves the answer; otherwise as the label, typed.
|
|
872
|
+
*/
|
|
873
|
+
const sendVisitorMessage = (body, topic) => {
|
|
874
|
+
const transport = transportRef.current;
|
|
875
|
+
if (!(session && transport)) {
|
|
718
876
|
setError("Chat is not connected yet.");
|
|
719
877
|
return;
|
|
720
878
|
}
|
|
721
|
-
const body = messageDraft.trim();
|
|
722
|
-
if (!body) return;
|
|
723
879
|
const clientMessageId = generateId();
|
|
724
880
|
const optimisticMessage = {
|
|
725
881
|
author: sessionDisplayName(session),
|
|
882
|
+
automation: topic && transport.selectTopic ? "topic_choice" : null,
|
|
726
883
|
body,
|
|
727
884
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
728
885
|
id: clientMessageId,
|
|
729
886
|
senderType: "visitor"
|
|
730
887
|
};
|
|
731
888
|
setMessages((currentMessages) => sortMessages([...currentMessages, createLocalMessage(optimisticMessage, "pending")]));
|
|
732
|
-
setMessageDraft("");
|
|
733
889
|
setError(null);
|
|
734
890
|
try {
|
|
735
|
-
|
|
736
|
-
body,
|
|
737
|
-
id: clientMessageId,
|
|
738
|
-
type: "message.create"
|
|
739
|
-
});
|
|
891
|
+
dispatchVisitorMessage(transport, clientMessageId, body, topic);
|
|
740
892
|
} catch (transportError) {
|
|
741
893
|
const resolved = transportError instanceof Error ? transportError : /* @__PURE__ */ new Error("Failed to send your message.");
|
|
742
894
|
setMessages((currentMessages) => markMessageFailed(currentMessages, clientMessageId));
|
|
743
895
|
setError(resolved.message);
|
|
744
896
|
}
|
|
745
897
|
};
|
|
898
|
+
const submitMessage = (event) => {
|
|
899
|
+
event?.preventDefault();
|
|
900
|
+
const body = messageDraft.trim();
|
|
901
|
+
if (!body) return;
|
|
902
|
+
setMessageDraft("");
|
|
903
|
+
sendVisitorMessage(body);
|
|
904
|
+
};
|
|
746
905
|
const canSend = Boolean(createTransport) && isReady;
|
|
747
906
|
const greeting = session?.widget.config.greeting ?? "Tell us a little about yourself and we’ll pick up the conversation from there.";
|
|
748
907
|
const panelTitle = session?.widget.name ?? "Start a conversation";
|
|
749
908
|
const sessionSummary = sessionSummaryText(session, greeting);
|
|
909
|
+
const topics = visibleTopics(session, supportState);
|
|
750
910
|
return /* @__PURE__ */ jsxs("div", {
|
|
751
911
|
className: `clivly-chat-widget ${className ?? ""}`.trim(),
|
|
752
912
|
"data-clivly-theme": resolvedTheme,
|
|
@@ -784,8 +944,10 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
784
944
|
})]
|
|
785
945
|
}),
|
|
786
946
|
/* @__PURE__ */ jsxs("div", {
|
|
947
|
+
"aria-label": "Conversation",
|
|
787
948
|
className: "clivly-chat-widget__body",
|
|
788
949
|
ref: listRef,
|
|
950
|
+
role: "log",
|
|
789
951
|
children: [
|
|
790
952
|
error ? /* @__PURE__ */ jsx("div", {
|
|
791
953
|
className: "clivly-chat-widget__error",
|
|
@@ -796,10 +958,17 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
796
958
|
hasSession: Boolean(session),
|
|
797
959
|
messages
|
|
798
960
|
}),
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
961
|
+
topics.length > 0 ? /* @__PURE__ */ jsx(SupportTopics, {
|
|
962
|
+
disabled: !canSend,
|
|
963
|
+
onSelect: (topic) => {
|
|
964
|
+
sendVisitorMessage(topic.label, topic);
|
|
965
|
+
},
|
|
966
|
+
topics
|
|
967
|
+
}) : null,
|
|
968
|
+
session ? /* @__PURE__ */ jsx(StatusNotice, {
|
|
969
|
+
assistantThinking,
|
|
970
|
+
email: session.contact?.email,
|
|
971
|
+
showWaitingNotice
|
|
803
972
|
}) : null
|
|
804
973
|
]
|
|
805
974
|
}),
|
|
@@ -821,7 +990,7 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
821
990
|
event.preventDefault();
|
|
822
991
|
if (messageDraft.trim() && canSend) submitMessage();
|
|
823
992
|
},
|
|
824
|
-
placeholder:
|
|
993
|
+
placeholder: composerPlaceholder(topics.length > 0),
|
|
825
994
|
value: messageDraft
|
|
826
995
|
})]
|
|
827
996
|
}), /* @__PURE__ */ jsxs("div", {
|
|
@@ -947,6 +1116,10 @@ function createWebSocketTransport({ createSocket = (url, protocols) => new WebSo
|
|
|
947
1116
|
markRead: () => {
|
|
948
1117
|
if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify({ type: "conversation.read" }));
|
|
949
1118
|
},
|
|
1119
|
+
selectTopic: (event) => {
|
|
1120
|
+
if (socket.readyState !== WebSocket.OPEN) throw new Error("Chat socket is not connected.");
|
|
1121
|
+
socket.send(JSON.stringify(event));
|
|
1122
|
+
},
|
|
950
1123
|
sendMessage: (message) => {
|
|
951
1124
|
if (socket.readyState !== WebSocket.OPEN) throw new Error("Chat socket is not connected.");
|
|
952
1125
|
socket.send(JSON.stringify(message));
|