@clivly/chat-widget 0.13.0 → 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 +41 -1
- package/dist/browser-mount.cjs +283 -23
- package/dist/browser-mount.d.cts +48 -1
- package/dist/browser-mount.d.mts +48 -1
- package/dist/browser-mount.mjs +283 -23
- package/dist/browser.cjs +283 -23
- package/dist/browser.d.cts +48 -1
- package/dist/browser.d.mts +48 -1
- package/dist/browser.mjs +283 -23
- package/dist/index.cjs +283 -23
- package/dist/index.d.cts +54 -3
- package/dist/index.d.mts +54 -3
- package/dist/index.mjs +283 -23
- package/dist/widget.global.js +66 -298
- package/package.json +1 -1
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);
|
|
@@ -363,6 +370,55 @@ const WIDGET_STYLES = `
|
|
|
363
370
|
font-size: 0.875rem;
|
|
364
371
|
padding: 0.75rem;
|
|
365
372
|
}
|
|
373
|
+
.clivly-chat-widget__notice {
|
|
374
|
+
background: var(--clivly-chat-operator);
|
|
375
|
+
border-radius: 0.8rem;
|
|
376
|
+
color: var(--clivly-chat-muted);
|
|
377
|
+
font-size: 0.8125rem;
|
|
378
|
+
margin: 0;
|
|
379
|
+
padding: 0.625rem 0.75rem;
|
|
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
|
+
}
|
|
366
422
|
.clivly-chat-widget__empty {
|
|
367
423
|
color: var(--clivly-chat-muted);
|
|
368
424
|
font-size: 0.875rem;
|
|
@@ -459,8 +515,15 @@ function useResolvedTheme(theme) {
|
|
|
459
515
|
* protocol reads as a single unit, and so `ChatWidget` stays inside the
|
|
460
516
|
* cognitive-complexity budget.
|
|
461
517
|
*/
|
|
462
|
-
function applyServerEvent(event,
|
|
518
|
+
function applyServerEvent(event, actions) {
|
|
519
|
+
const { setError, setIsReady, setMessages } = actions;
|
|
463
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;
|
|
464
527
|
case "conversation.snapshot":
|
|
465
528
|
setMessages(sortMessages(event.messages.map((message) => createLocalMessage(message))));
|
|
466
529
|
return;
|
|
@@ -481,14 +544,41 @@ function applyServerEvent(event, { setError, setIsReady, setMessages }) {
|
|
|
481
544
|
default: return;
|
|
482
545
|
}
|
|
483
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
|
+
}
|
|
484
564
|
function MessageBubble({ message }) {
|
|
565
|
+
const badge = automationBadge(message);
|
|
566
|
+
const author = badge ? (message.author ?? "").replace(AUTHOR_LABEL_SUFFIX, "") : message.author;
|
|
485
567
|
return /* @__PURE__ */ jsxs("article", {
|
|
486
568
|
className: "clivly-chat-widget__message",
|
|
569
|
+
"data-automation": badge ? message.automation : void 0,
|
|
487
570
|
"data-sender": message.senderType,
|
|
488
571
|
children: [
|
|
489
572
|
/* @__PURE__ */ jsxs("div", {
|
|
490
573
|
className: "clivly-chat-widget__message-meta",
|
|
491
|
-
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
|
+
]
|
|
492
582
|
}),
|
|
493
583
|
/* @__PURE__ */ jsx("div", { children: message.body }),
|
|
494
584
|
message.delivery === "sent" ? null : /* @__PURE__ */ jsx("div", {
|
|
@@ -515,6 +605,57 @@ function MessageList({ greeting, hasSession, messages }) {
|
|
|
515
605
|
return /* @__PURE__ */ jsx(Fragment, { children: messages.map((message) => /* @__PURE__ */ jsx(MessageBubble, { message }, message.id)) });
|
|
516
606
|
}
|
|
517
607
|
/**
|
|
608
|
+
* How often an open, visible widget re-sends its read receipt. The room drops
|
|
609
|
+
* a receipt that arrives within 5 s of the last one, so a reply read in that
|
|
610
|
+
* window would otherwise stay "unread" and be emailed to the visitor.
|
|
611
|
+
*/
|
|
612
|
+
const READ_RECEIPT_REFRESH_MS = 15e3;
|
|
613
|
+
/** How long a visitor waits unanswered before the widget reassures them (D20a). */
|
|
614
|
+
const WAITING_NOTICE_AFTER_MS = 12e4;
|
|
615
|
+
/**
|
|
616
|
+
* When the visitor's latest message was sent, if nobody has answered it yet;
|
|
617
|
+
* null when the latest message is a reply or there is nothing waiting. Failed
|
|
618
|
+
* sends don't count: nobody was notified about them.
|
|
619
|
+
*/
|
|
620
|
+
function awaitingReplySince(messages) {
|
|
621
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
622
|
+
const message = messages[index];
|
|
623
|
+
if (!message || message.delivery === "failed") continue;
|
|
624
|
+
if (message.senderType === "system" || message.automation === "topic_choice") continue;
|
|
625
|
+
if (message.senderType !== "visitor") return null;
|
|
626
|
+
return new Date(message.createdAt).getTime();
|
|
627
|
+
}
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
630
|
+
/** True once the visitor has waited `WAITING_NOTICE_AFTER_MS` unanswered. */
|
|
631
|
+
function useWaitingNotice(messages) {
|
|
632
|
+
const [due, setDue] = useState(false);
|
|
633
|
+
useEffect(() => {
|
|
634
|
+
const since = awaitingReplySince(messages);
|
|
635
|
+
if (since === null) {
|
|
636
|
+
setDue(false);
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
const remaining = since + WAITING_NOTICE_AFTER_MS - Date.now();
|
|
640
|
+
if (remaining <= 0) {
|
|
641
|
+
setDue(true);
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
setDue(false);
|
|
645
|
+
const timer = setTimeout(() => {
|
|
646
|
+
setDue(true);
|
|
647
|
+
}, remaining);
|
|
648
|
+
return () => {
|
|
649
|
+
clearTimeout(timer);
|
|
650
|
+
};
|
|
651
|
+
}, [messages]);
|
|
652
|
+
return due;
|
|
653
|
+
}
|
|
654
|
+
function waitingNoticeText(email) {
|
|
655
|
+
const address = email?.trim();
|
|
656
|
+
return address ? `We've notified the team — we'll reply here or by email at ${address}.` : "We've notified the team — we'll reply here as soon as we can.";
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
518
659
|
* Whether a seeded visitor is complete enough to skip the form.
|
|
519
660
|
*
|
|
520
661
|
* Both fields have to be present and non-blank: the server requires each, so
|
|
@@ -534,6 +675,70 @@ function composerStatusText(hasTransport, isReady) {
|
|
|
534
675
|
if (!hasTransport) return "Chat is unavailable";
|
|
535
676
|
return isReady ? "Conversation ready" : "Connecting…";
|
|
536
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
|
+
}
|
|
537
742
|
function ChatWidget({ className, createTransport, defaultVisitor, getSession, initiallyOpen = false, launcherLabel = "Chat with us", storage = DEFAULT_STORAGE, style, theme = "auto", widgetId }) {
|
|
538
743
|
const [isOpen, setIsOpen] = useState(initiallyOpen);
|
|
539
744
|
const [error, setError] = useState(null);
|
|
@@ -542,6 +747,8 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
542
747
|
const [messageDraft, setMessageDraft] = useState("");
|
|
543
748
|
const [messages, setMessages] = useState([]);
|
|
544
749
|
const [session, setSession] = useState(null);
|
|
750
|
+
const [supportState, setSupportState] = useState(null);
|
|
751
|
+
const [assistantThinking, setAssistantThinking] = useState(false);
|
|
545
752
|
const [visitor, setVisitor] = useState({
|
|
546
753
|
email: defaultVisitor?.email ?? "",
|
|
547
754
|
name: defaultVisitor?.name ?? ""
|
|
@@ -552,6 +759,7 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
552
759
|
if (createTransport) return;
|
|
553
760
|
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");
|
|
554
761
|
}, []);
|
|
762
|
+
const showWaitingNotice = useWaitingNotice(messages);
|
|
555
763
|
const themeStyle = useThemeStyle(session, style);
|
|
556
764
|
const resolvedTheme = useResolvedTheme(theme);
|
|
557
765
|
useEffect(() => () => {
|
|
@@ -571,15 +779,18 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
571
779
|
handlers: {
|
|
572
780
|
onClose: () => {
|
|
573
781
|
setIsReady(false);
|
|
782
|
+
setAssistantThinking(false);
|
|
574
783
|
},
|
|
575
784
|
onError: (transportError) => {
|
|
576
785
|
setError(transportError.message);
|
|
577
786
|
},
|
|
578
787
|
onEvent: (event) => {
|
|
579
788
|
applyServerEvent(event, {
|
|
789
|
+
setAssistantThinking,
|
|
580
790
|
setError,
|
|
581
791
|
setIsReady,
|
|
582
|
-
setMessages
|
|
792
|
+
setMessages,
|
|
793
|
+
setSupportState
|
|
583
794
|
});
|
|
584
795
|
},
|
|
585
796
|
onOpen: () => {
|
|
@@ -594,6 +805,23 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
594
805
|
if (transportRef.current === connection) transportRef.current = null;
|
|
595
806
|
};
|
|
596
807
|
}, [createTransport, session]);
|
|
808
|
+
useEffect(() => {
|
|
809
|
+
if (!(isOpen && isReady)) return;
|
|
810
|
+
const markReadIfVisible = () => {
|
|
811
|
+
if (document.visibilityState === "visible") transportRef.current?.markRead?.();
|
|
812
|
+
};
|
|
813
|
+
markReadIfVisible();
|
|
814
|
+
const interval = window.setInterval(markReadIfVisible, READ_RECEIPT_REFRESH_MS);
|
|
815
|
+
document.addEventListener("visibilitychange", markReadIfVisible);
|
|
816
|
+
return () => {
|
|
817
|
+
window.clearInterval(interval);
|
|
818
|
+
document.removeEventListener("visibilitychange", markReadIfVisible);
|
|
819
|
+
};
|
|
820
|
+
}, [
|
|
821
|
+
isOpen,
|
|
822
|
+
isReady,
|
|
823
|
+
messages
|
|
824
|
+
]);
|
|
597
825
|
const startSession = useCallback(async (identity) => {
|
|
598
826
|
setError(null);
|
|
599
827
|
setIsConnecting(true);
|
|
@@ -604,6 +832,7 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
604
832
|
widgetId
|
|
605
833
|
});
|
|
606
834
|
storage?.setItem(createStorageKey(widgetId), nextSession.visitorToken);
|
|
835
|
+
setSupportState(nextSession.support?.state ?? null);
|
|
607
836
|
setSession(nextSession);
|
|
608
837
|
setMessages(sortMessages(nextSession.messages.map((message) => createLocalMessage(message))));
|
|
609
838
|
} catch (sessionError) {
|
|
@@ -636,41 +865,48 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
636
865
|
session,
|
|
637
866
|
startSession
|
|
638
867
|
]);
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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)) {
|
|
642
876
|
setError("Chat is not connected yet.");
|
|
643
877
|
return;
|
|
644
878
|
}
|
|
645
|
-
const body = messageDraft.trim();
|
|
646
|
-
if (!body) return;
|
|
647
879
|
const clientMessageId = generateId();
|
|
648
880
|
const optimisticMessage = {
|
|
649
881
|
author: sessionDisplayName(session),
|
|
882
|
+
automation: topic && transport.selectTopic ? "topic_choice" : null,
|
|
650
883
|
body,
|
|
651
884
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
652
885
|
id: clientMessageId,
|
|
653
886
|
senderType: "visitor"
|
|
654
887
|
};
|
|
655
888
|
setMessages((currentMessages) => sortMessages([...currentMessages, createLocalMessage(optimisticMessage, "pending")]));
|
|
656
|
-
setMessageDraft("");
|
|
657
889
|
setError(null);
|
|
658
890
|
try {
|
|
659
|
-
|
|
660
|
-
body,
|
|
661
|
-
id: clientMessageId,
|
|
662
|
-
type: "message.create"
|
|
663
|
-
});
|
|
891
|
+
dispatchVisitorMessage(transport, clientMessageId, body, topic);
|
|
664
892
|
} catch (transportError) {
|
|
665
893
|
const resolved = transportError instanceof Error ? transportError : /* @__PURE__ */ new Error("Failed to send your message.");
|
|
666
894
|
setMessages((currentMessages) => markMessageFailed(currentMessages, clientMessageId));
|
|
667
895
|
setError(resolved.message);
|
|
668
896
|
}
|
|
669
897
|
};
|
|
898
|
+
const submitMessage = (event) => {
|
|
899
|
+
event?.preventDefault();
|
|
900
|
+
const body = messageDraft.trim();
|
|
901
|
+
if (!body) return;
|
|
902
|
+
setMessageDraft("");
|
|
903
|
+
sendVisitorMessage(body);
|
|
904
|
+
};
|
|
670
905
|
const canSend = Boolean(createTransport) && isReady;
|
|
671
906
|
const greeting = session?.widget.config.greeting ?? "Tell us a little about yourself and we’ll pick up the conversation from there.";
|
|
672
907
|
const panelTitle = session?.widget.name ?? "Start a conversation";
|
|
673
908
|
const sessionSummary = sessionSummaryText(session, greeting);
|
|
909
|
+
const topics = visibleTopics(session, supportState);
|
|
674
910
|
return /* @__PURE__ */ jsxs("div", {
|
|
675
911
|
className: `clivly-chat-widget ${className ?? ""}`.trim(),
|
|
676
912
|
"data-clivly-theme": resolvedTheme,
|
|
@@ -708,16 +944,33 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
708
944
|
})]
|
|
709
945
|
}),
|
|
710
946
|
/* @__PURE__ */ jsxs("div", {
|
|
947
|
+
"aria-label": "Conversation",
|
|
711
948
|
className: "clivly-chat-widget__body",
|
|
712
949
|
ref: listRef,
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
950
|
+
role: "log",
|
|
951
|
+
children: [
|
|
952
|
+
error ? /* @__PURE__ */ jsx("div", {
|
|
953
|
+
className: "clivly-chat-widget__error",
|
|
954
|
+
children: error
|
|
955
|
+
}) : null,
|
|
956
|
+
/* @__PURE__ */ jsx(MessageList, {
|
|
957
|
+
greeting,
|
|
958
|
+
hasSession: Boolean(session),
|
|
959
|
+
messages
|
|
960
|
+
}),
|
|
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
|
|
972
|
+
}) : null
|
|
973
|
+
]
|
|
721
974
|
}),
|
|
722
975
|
session ? /* @__PURE__ */ jsxs("form", {
|
|
723
976
|
className: "clivly-chat-widget__composer",
|
|
@@ -737,7 +990,7 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
|
|
|
737
990
|
event.preventDefault();
|
|
738
991
|
if (messageDraft.trim() && canSend) submitMessage();
|
|
739
992
|
},
|
|
740
|
-
placeholder:
|
|
993
|
+
placeholder: composerPlaceholder(topics.length > 0),
|
|
741
994
|
value: messageDraft
|
|
742
995
|
})]
|
|
743
996
|
}), /* @__PURE__ */ jsxs("div", {
|
|
@@ -860,6 +1113,13 @@ function createWebSocketTransport({ createSocket = (url, protocols) => new WebSo
|
|
|
860
1113
|
close: () => {
|
|
861
1114
|
socket.close();
|
|
862
1115
|
},
|
|
1116
|
+
markRead: () => {
|
|
1117
|
+
if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify({ type: "conversation.read" }));
|
|
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
|
+
},
|
|
863
1123
|
sendMessage: (message) => {
|
|
864
1124
|
if (socket.readyState !== WebSocket.OPEN) throw new Error("Chat socket is not connected.");
|
|
865
1125
|
socket.send(JSON.stringify(message));
|