@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/dist/index.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);
@@ -364,6 +371,55 @@ const WIDGET_STYLES = `
364
371
  font-size: 0.875rem;
365
372
  padding: 0.75rem;
366
373
  }
374
+ .clivly-chat-widget__notice {
375
+ background: var(--clivly-chat-operator);
376
+ border-radius: 0.8rem;
377
+ color: var(--clivly-chat-muted);
378
+ font-size: 0.8125rem;
379
+ margin: 0;
380
+ padding: 0.625rem 0.75rem;
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
+ }
367
423
  .clivly-chat-widget__empty {
368
424
  color: var(--clivly-chat-muted);
369
425
  font-size: 0.875rem;
@@ -460,8 +516,15 @@ function useResolvedTheme(theme) {
460
516
  * protocol reads as a single unit, and so `ChatWidget` stays inside the
461
517
  * cognitive-complexity budget.
462
518
  */
463
- function applyServerEvent(event, { setError, setIsReady, setMessages }) {
519
+ function applyServerEvent(event, actions) {
520
+ const { setError, setIsReady, setMessages } = actions;
464
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;
465
528
  case "conversation.snapshot":
466
529
  setMessages(sortMessages(event.messages.map((message) => createLocalMessage(message))));
467
530
  return;
@@ -482,14 +545,41 @@ function applyServerEvent(event, { setError, setIsReady, setMessages }) {
482
545
  default: return;
483
546
  }
484
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
+ }
485
565
  function MessageBubble({ message }) {
566
+ const badge = automationBadge(message);
567
+ const author = badge ? (message.author ?? "").replace(AUTHOR_LABEL_SUFFIX, "") : message.author;
486
568
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
487
569
  className: "clivly-chat-widget__message",
570
+ "data-automation": badge ? message.automation : void 0,
488
571
  "data-sender": message.senderType,
489
572
  children: [
490
573
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
491
574
  className: "clivly-chat-widget__message-meta",
492
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: message.author ?? "Clivly" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatTimestamp(message.createdAt) })]
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
+ ]
493
583
  }),
494
584
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: message.body }),
495
585
  message.delivery === "sent" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -516,6 +606,57 @@ function MessageList({ greeting, hasSession, messages }) {
516
606
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: messages.map((message) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageBubble, { message }, message.id)) });
517
607
  }
518
608
  /**
609
+ * How often an open, visible widget re-sends its read receipt. The room drops
610
+ * a receipt that arrives within 5 s of the last one, so a reply read in that
611
+ * window would otherwise stay "unread" and be emailed to the visitor.
612
+ */
613
+ const READ_RECEIPT_REFRESH_MS = 15e3;
614
+ /** How long a visitor waits unanswered before the widget reassures them (D20a). */
615
+ const WAITING_NOTICE_AFTER_MS = 12e4;
616
+ /**
617
+ * When the visitor's latest message was sent, if nobody has answered it yet;
618
+ * null when the latest message is a reply or there is nothing waiting. Failed
619
+ * sends don't count: nobody was notified about them.
620
+ */
621
+ function awaitingReplySince(messages) {
622
+ for (let index = messages.length - 1; index >= 0; index--) {
623
+ const message = messages[index];
624
+ if (!message || message.delivery === "failed") continue;
625
+ if (message.senderType === "system" || message.automation === "topic_choice") continue;
626
+ if (message.senderType !== "visitor") return null;
627
+ return new Date(message.createdAt).getTime();
628
+ }
629
+ return null;
630
+ }
631
+ /** True once the visitor has waited `WAITING_NOTICE_AFTER_MS` unanswered. */
632
+ function useWaitingNotice(messages) {
633
+ const [due, setDue] = (0, react.useState)(false);
634
+ (0, react.useEffect)(() => {
635
+ const since = awaitingReplySince(messages);
636
+ if (since === null) {
637
+ setDue(false);
638
+ return;
639
+ }
640
+ const remaining = since + WAITING_NOTICE_AFTER_MS - Date.now();
641
+ if (remaining <= 0) {
642
+ setDue(true);
643
+ return;
644
+ }
645
+ setDue(false);
646
+ const timer = setTimeout(() => {
647
+ setDue(true);
648
+ }, remaining);
649
+ return () => {
650
+ clearTimeout(timer);
651
+ };
652
+ }, [messages]);
653
+ return due;
654
+ }
655
+ function waitingNoticeText(email) {
656
+ const address = email?.trim();
657
+ 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.";
658
+ }
659
+ /**
519
660
  * Whether a seeded visitor is complete enough to skip the form.
520
661
  *
521
662
  * Both fields have to be present and non-blank: the server requires each, so
@@ -535,6 +676,70 @@ function composerStatusText(hasTransport, isReady) {
535
676
  if (!hasTransport) return "Chat is unavailable";
536
677
  return isReady ? "Conversation ready" : "Connecting…";
537
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
+ }
538
743
  function ChatWidget({ className, createTransport, defaultVisitor, getSession, initiallyOpen = false, launcherLabel = "Chat with us", storage = DEFAULT_STORAGE, style, theme = "auto", widgetId }) {
539
744
  const [isOpen, setIsOpen] = (0, react.useState)(initiallyOpen);
540
745
  const [error, setError] = (0, react.useState)(null);
@@ -543,6 +748,8 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
543
748
  const [messageDraft, setMessageDraft] = (0, react.useState)("");
544
749
  const [messages, setMessages] = (0, react.useState)([]);
545
750
  const [session, setSession] = (0, react.useState)(null);
751
+ const [supportState, setSupportState] = (0, react.useState)(null);
752
+ const [assistantThinking, setAssistantThinking] = (0, react.useState)(false);
546
753
  const [visitor, setVisitor] = (0, react.useState)({
547
754
  email: defaultVisitor?.email ?? "",
548
755
  name: defaultVisitor?.name ?? ""
@@ -553,6 +760,7 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
553
760
  if (createTransport) return;
554
761
  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");
555
762
  }, []);
763
+ const showWaitingNotice = useWaitingNotice(messages);
556
764
  const themeStyle = useThemeStyle(session, style);
557
765
  const resolvedTheme = useResolvedTheme(theme);
558
766
  (0, react.useEffect)(() => () => {
@@ -572,15 +780,18 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
572
780
  handlers: {
573
781
  onClose: () => {
574
782
  setIsReady(false);
783
+ setAssistantThinking(false);
575
784
  },
576
785
  onError: (transportError) => {
577
786
  setError(transportError.message);
578
787
  },
579
788
  onEvent: (event) => {
580
789
  applyServerEvent(event, {
790
+ setAssistantThinking,
581
791
  setError,
582
792
  setIsReady,
583
- setMessages
793
+ setMessages,
794
+ setSupportState
584
795
  });
585
796
  },
586
797
  onOpen: () => {
@@ -595,6 +806,23 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
595
806
  if (transportRef.current === connection) transportRef.current = null;
596
807
  };
597
808
  }, [createTransport, session]);
809
+ (0, react.useEffect)(() => {
810
+ if (!(isOpen && isReady)) return;
811
+ const markReadIfVisible = () => {
812
+ if (document.visibilityState === "visible") transportRef.current?.markRead?.();
813
+ };
814
+ markReadIfVisible();
815
+ const interval = window.setInterval(markReadIfVisible, READ_RECEIPT_REFRESH_MS);
816
+ document.addEventListener("visibilitychange", markReadIfVisible);
817
+ return () => {
818
+ window.clearInterval(interval);
819
+ document.removeEventListener("visibilitychange", markReadIfVisible);
820
+ };
821
+ }, [
822
+ isOpen,
823
+ isReady,
824
+ messages
825
+ ]);
598
826
  const startSession = (0, react.useCallback)(async (identity) => {
599
827
  setError(null);
600
828
  setIsConnecting(true);
@@ -605,6 +833,7 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
605
833
  widgetId
606
834
  });
607
835
  storage?.setItem(createStorageKey(widgetId), nextSession.visitorToken);
836
+ setSupportState(nextSession.support?.state ?? null);
608
837
  setSession(nextSession);
609
838
  setMessages(sortMessages(nextSession.messages.map((message) => createLocalMessage(message))));
610
839
  } catch (sessionError) {
@@ -637,41 +866,48 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
637
866
  session,
638
867
  startSession
639
868
  ]);
640
- const submitMessage = (event) => {
641
- event?.preventDefault();
642
- if (!(session && transportRef.current)) {
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)) {
643
877
  setError("Chat is not connected yet.");
644
878
  return;
645
879
  }
646
- const body = messageDraft.trim();
647
- if (!body) return;
648
880
  const clientMessageId = generateId();
649
881
  const optimisticMessage = {
650
882
  author: sessionDisplayName(session),
883
+ automation: topic && transport.selectTopic ? "topic_choice" : null,
651
884
  body,
652
885
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
653
886
  id: clientMessageId,
654
887
  senderType: "visitor"
655
888
  };
656
889
  setMessages((currentMessages) => sortMessages([...currentMessages, createLocalMessage(optimisticMessage, "pending")]));
657
- setMessageDraft("");
658
890
  setError(null);
659
891
  try {
660
- transportRef.current.sendMessage({
661
- body,
662
- id: clientMessageId,
663
- type: "message.create"
664
- });
892
+ dispatchVisitorMessage(transport, clientMessageId, body, topic);
665
893
  } catch (transportError) {
666
894
  const resolved = transportError instanceof Error ? transportError : /* @__PURE__ */ new Error("Failed to send your message.");
667
895
  setMessages((currentMessages) => markMessageFailed(currentMessages, clientMessageId));
668
896
  setError(resolved.message);
669
897
  }
670
898
  };
899
+ const submitMessage = (event) => {
900
+ event?.preventDefault();
901
+ const body = messageDraft.trim();
902
+ if (!body) return;
903
+ setMessageDraft("");
904
+ sendVisitorMessage(body);
905
+ };
671
906
  const canSend = Boolean(createTransport) && isReady;
672
907
  const greeting = session?.widget.config.greeting ?? "Tell us a little about yourself and we’ll pick up the conversation from there.";
673
908
  const panelTitle = session?.widget.name ?? "Start a conversation";
674
909
  const sessionSummary = sessionSummaryText(session, greeting);
910
+ const topics = visibleTopics(session, supportState);
675
911
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
676
912
  className: `clivly-chat-widget ${className ?? ""}`.trim(),
677
913
  "data-clivly-theme": resolvedTheme,
@@ -709,16 +945,33 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
709
945
  })]
710
946
  }),
711
947
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
948
+ "aria-label": "Conversation",
712
949
  className: "clivly-chat-widget__body",
713
950
  ref: listRef,
714
- children: [error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
715
- className: "clivly-chat-widget__error",
716
- children: error
717
- }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageList, {
718
- greeting,
719
- hasSession: Boolean(session),
720
- messages
721
- })]
951
+ role: "log",
952
+ children: [
953
+ error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
954
+ className: "clivly-chat-widget__error",
955
+ children: error
956
+ }) : null,
957
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageList, {
958
+ greeting,
959
+ hasSession: Boolean(session),
960
+ messages
961
+ }),
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
973
+ }) : null
974
+ ]
722
975
  }),
723
976
  session ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
724
977
  className: "clivly-chat-widget__composer",
@@ -738,7 +991,7 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
738
991
  event.preventDefault();
739
992
  if (messageDraft.trim() && canSend) submitMessage();
740
993
  },
741
- placeholder: "Write your message",
994
+ placeholder: composerPlaceholder(topics.length > 0),
742
995
  value: messageDraft
743
996
  })]
744
997
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -876,6 +1129,13 @@ function createWebSocketTransport({ createSocket = (url, protocols) => new WebSo
876
1129
  close: () => {
877
1130
  socket.close();
878
1131
  },
1132
+ markRead: () => {
1133
+ if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify({ type: "conversation.read" }));
1134
+ },
1135
+ selectTopic: (event) => {
1136
+ if (socket.readyState !== WebSocket.OPEN) throw new Error("Chat socket is not connected.");
1137
+ socket.send(JSON.stringify(event));
1138
+ },
879
1139
  sendMessage: (message) => {
880
1140
  if (socket.readyState !== WebSocket.OPEN) throw new Error("Chat socket is not connected.");
881
1141
  socket.send(JSON.stringify(message));
package/dist/index.d.cts CHANGED
@@ -48,11 +48,30 @@ interface WidgetSummary {
48
48
  }
49
49
  interface WidgetMessage {
50
50
  author: string | null;
51
+ /**
52
+ * Set on messages the support assistant wrote (`greeting`, `topic`,
53
+ * `handoff`, `ai`, `ai_clarify`) and on a visitor's topic click
54
+ * (`topic_choice`). Absent or null on everything a person typed, and on
55
+ * every message from a server that predates the assistant.
56
+ */
57
+ automation?: string | null;
51
58
  body: string;
52
59
  createdAt: string;
53
60
  id: string;
54
61
  senderType: WidgetSenderType;
55
62
  }
63
+ type WidgetSupportState = "automated" | "handoff" | "human";
64
+ interface WidgetSupportTopic {
65
+ action: "answer" | "ask" | "link" | "human";
66
+ id: string;
67
+ label: string;
68
+ }
69
+ /** Guided support for one conversation. Answers stay on the server. */
70
+ interface WidgetSupport {
71
+ mode: "human" | "guided" | "ai";
72
+ state: WidgetSupportState | null;
73
+ topics: WidgetSupportTopic[];
74
+ }
56
75
  interface WidgetSessionPayload {
57
76
  contact: WidgetContact | null;
58
77
  conversation: WidgetConversation;
@@ -60,6 +79,8 @@ interface WidgetSessionPayload {
60
79
  messages: WidgetMessage[];
61
80
  /** Absolute socket URL returned by Clivly's bootstrap. */
62
81
  socketUrl?: string;
82
+ /** Absent or null when the widget has no guided support configured. */
83
+ support?: WidgetSupport | null;
63
84
  token: string;
64
85
  visitorToken: string;
65
86
  widget: WidgetSummary;
@@ -79,7 +100,17 @@ interface WidgetClientCreateMessageEvent {
79
100
  id: string;
80
101
  type: "message.create";
81
102
  }
82
- type WidgetClientEvent = WidgetClientCreateMessageEvent;
103
+ /** The visitor has seen the thread (see `ChatTransportConnection.markRead`). */
104
+ interface WidgetClientReadEvent {
105
+ type: "conversation.read";
106
+ }
107
+ /** The visitor picked a guided topic (see `ChatTransportConnection.selectTopic`). */
108
+ interface WidgetClientTopicEvent {
109
+ id: string;
110
+ topicId: string;
111
+ type: "topic.select";
112
+ }
113
+ type WidgetClientEvent = WidgetClientCreateMessageEvent | WidgetClientReadEvent | WidgetClientTopicEvent;
83
114
  interface WidgetSessionReadyEvent {
84
115
  conversationId: string;
85
116
  type: "session.ready";
@@ -98,11 +129,20 @@ interface WidgetMessageAckEvent {
98
129
  message: WidgetMessage;
99
130
  type: "message.ack";
100
131
  }
132
+ interface WidgetSupportStateEvent {
133
+ state: WidgetSupportState;
134
+ type: "support.state";
135
+ }
136
+ /** The assistant is preparing an answer (`thinking`) or has finished (`idle`). */
137
+ interface WidgetAssistantStatusEvent {
138
+ status: "thinking" | "idle";
139
+ type: "assistant.status";
140
+ }
101
141
  interface WidgetErrorEvent {
102
142
  message: string;
103
143
  type: "error";
104
144
  }
105
- type WidgetServerEvent = WidgetConversationSnapshotEvent | WidgetErrorEvent | WidgetMessageAckEvent | WidgetMessageCreatedEvent | WidgetSessionReadyEvent;
145
+ type WidgetServerEvent = WidgetAssistantStatusEvent | WidgetConversationSnapshotEvent | WidgetErrorEvent | WidgetMessageAckEvent | WidgetMessageCreatedEvent | WidgetSessionReadyEvent | WidgetSupportStateEvent;
106
146
  //#endregion
107
147
  //#region src/transport.d.ts
108
148
  interface ChatTransportHandlers {
@@ -113,6 +153,17 @@ interface ChatTransportHandlers {
113
153
  }
114
154
  interface ChatTransportConnection {
115
155
  close: () => void;
156
+ /**
157
+ * Tell the server the visitor has seen the thread, so a teammate's reply
158
+ * they've read isn't also emailed to them. Optional: a custom transport
159
+ * without it still works, its visitors are just emailed replies they saw.
160
+ */
161
+ markRead?: () => void;
162
+ /**
163
+ * Send a guided-topic click. Optional: without it the widget sends the
164
+ * topic's label as an ordinary message, so a custom transport still works.
165
+ */
166
+ selectTopic?: (event: WidgetClientTopicEvent) => void;
116
167
  sendMessage: (event: WidgetClientCreateMessageEvent) => void;
117
168
  }
118
169
  interface CreateChatTransportOptions {
@@ -236,4 +287,4 @@ export declare const createClivlyTransport: typeof createClivlyTransport$1;
236
287
  export declare const mountChatWidget: typeof mountChatWidget$1;
237
288
  export declare const mountChatWidgetScript: typeof mountChatWidgetScript$1;
238
289
  //#endregion
239
- export type { ChatTransportConnection, ChatTransportHandlers, ChatWidgetProps, ChatWidgetTheme, ChatWidgetVisitor, CreateChatTransport, CreateChatTransportOptions, CreateLoopbackTransportOptions, CreateWebSocketTransportOptions, GetSession, GetSessionOptions, ResolvedTheme, WidgetClientCreateMessageEvent, WidgetClientEvent, WidgetConfig, WidgetContact, WidgetConversation, WidgetConversationSnapshotEvent, WidgetErrorEvent, WidgetMessage, WidgetMessageAckEvent, WidgetMessageCreatedEvent, WidgetSenderType, WidgetServerEvent, WidgetSessionPayload, WidgetSessionReadyEvent, WidgetSummary };
290
+ export type { ChatTransportConnection, ChatTransportHandlers, ChatWidgetProps, ChatWidgetTheme, ChatWidgetVisitor, CreateChatTransport, CreateChatTransportOptions, CreateLoopbackTransportOptions, CreateWebSocketTransportOptions, GetSession, GetSessionOptions, ResolvedTheme, WidgetAssistantStatusEvent, WidgetClientCreateMessageEvent, WidgetClientEvent, WidgetClientReadEvent, WidgetClientTopicEvent, WidgetConfig, WidgetContact, WidgetConversation, WidgetConversationSnapshotEvent, WidgetErrorEvent, WidgetMessage, WidgetMessageAckEvent, WidgetMessageCreatedEvent, WidgetSenderType, WidgetServerEvent, WidgetSessionPayload, WidgetSessionReadyEvent, WidgetSummary, WidgetSupport, WidgetSupportState, WidgetSupportStateEvent, WidgetSupportTopic };
package/dist/index.d.mts CHANGED
@@ -48,11 +48,30 @@ interface WidgetSummary {
48
48
  }
49
49
  interface WidgetMessage {
50
50
  author: string | null;
51
+ /**
52
+ * Set on messages the support assistant wrote (`greeting`, `topic`,
53
+ * `handoff`, `ai`, `ai_clarify`) and on a visitor's topic click
54
+ * (`topic_choice`). Absent or null on everything a person typed, and on
55
+ * every message from a server that predates the assistant.
56
+ */
57
+ automation?: string | null;
51
58
  body: string;
52
59
  createdAt: string;
53
60
  id: string;
54
61
  senderType: WidgetSenderType;
55
62
  }
63
+ type WidgetSupportState = "automated" | "handoff" | "human";
64
+ interface WidgetSupportTopic {
65
+ action: "answer" | "ask" | "link" | "human";
66
+ id: string;
67
+ label: string;
68
+ }
69
+ /** Guided support for one conversation. Answers stay on the server. */
70
+ interface WidgetSupport {
71
+ mode: "human" | "guided" | "ai";
72
+ state: WidgetSupportState | null;
73
+ topics: WidgetSupportTopic[];
74
+ }
56
75
  interface WidgetSessionPayload {
57
76
  contact: WidgetContact | null;
58
77
  conversation: WidgetConversation;
@@ -60,6 +79,8 @@ interface WidgetSessionPayload {
60
79
  messages: WidgetMessage[];
61
80
  /** Absolute socket URL returned by Clivly's bootstrap. */
62
81
  socketUrl?: string;
82
+ /** Absent or null when the widget has no guided support configured. */
83
+ support?: WidgetSupport | null;
63
84
  token: string;
64
85
  visitorToken: string;
65
86
  widget: WidgetSummary;
@@ -79,7 +100,17 @@ interface WidgetClientCreateMessageEvent {
79
100
  id: string;
80
101
  type: "message.create";
81
102
  }
82
- type WidgetClientEvent = WidgetClientCreateMessageEvent;
103
+ /** The visitor has seen the thread (see `ChatTransportConnection.markRead`). */
104
+ interface WidgetClientReadEvent {
105
+ type: "conversation.read";
106
+ }
107
+ /** The visitor picked a guided topic (see `ChatTransportConnection.selectTopic`). */
108
+ interface WidgetClientTopicEvent {
109
+ id: string;
110
+ topicId: string;
111
+ type: "topic.select";
112
+ }
113
+ type WidgetClientEvent = WidgetClientCreateMessageEvent | WidgetClientReadEvent | WidgetClientTopicEvent;
83
114
  interface WidgetSessionReadyEvent {
84
115
  conversationId: string;
85
116
  type: "session.ready";
@@ -98,11 +129,20 @@ interface WidgetMessageAckEvent {
98
129
  message: WidgetMessage;
99
130
  type: "message.ack";
100
131
  }
132
+ interface WidgetSupportStateEvent {
133
+ state: WidgetSupportState;
134
+ type: "support.state";
135
+ }
136
+ /** The assistant is preparing an answer (`thinking`) or has finished (`idle`). */
137
+ interface WidgetAssistantStatusEvent {
138
+ status: "thinking" | "idle";
139
+ type: "assistant.status";
140
+ }
101
141
  interface WidgetErrorEvent {
102
142
  message: string;
103
143
  type: "error";
104
144
  }
105
- type WidgetServerEvent = WidgetConversationSnapshotEvent | WidgetErrorEvent | WidgetMessageAckEvent | WidgetMessageCreatedEvent | WidgetSessionReadyEvent;
145
+ type WidgetServerEvent = WidgetAssistantStatusEvent | WidgetConversationSnapshotEvent | WidgetErrorEvent | WidgetMessageAckEvent | WidgetMessageCreatedEvent | WidgetSessionReadyEvent | WidgetSupportStateEvent;
106
146
  //#endregion
107
147
  //#region src/transport.d.ts
108
148
  interface ChatTransportHandlers {
@@ -113,6 +153,17 @@ interface ChatTransportHandlers {
113
153
  }
114
154
  interface ChatTransportConnection {
115
155
  close: () => void;
156
+ /**
157
+ * Tell the server the visitor has seen the thread, so a teammate's reply
158
+ * they've read isn't also emailed to them. Optional: a custom transport
159
+ * without it still works, its visitors are just emailed replies they saw.
160
+ */
161
+ markRead?: () => void;
162
+ /**
163
+ * Send a guided-topic click. Optional: without it the widget sends the
164
+ * topic's label as an ordinary message, so a custom transport still works.
165
+ */
166
+ selectTopic?: (event: WidgetClientTopicEvent) => void;
116
167
  sendMessage: (event: WidgetClientCreateMessageEvent) => void;
117
168
  }
118
169
  interface CreateChatTransportOptions {
@@ -236,4 +287,4 @@ export declare const createClivlyTransport: typeof createClivlyTransport$1;
236
287
  export declare const mountChatWidget: typeof mountChatWidget$1;
237
288
  export declare const mountChatWidgetScript: typeof mountChatWidgetScript$1;
238
289
  //#endregion
239
- export type { ChatTransportConnection, ChatTransportHandlers, ChatWidgetProps, ChatWidgetTheme, ChatWidgetVisitor, CreateChatTransport, CreateChatTransportOptions, CreateLoopbackTransportOptions, CreateWebSocketTransportOptions, GetSession, GetSessionOptions, ResolvedTheme, WidgetClientCreateMessageEvent, WidgetClientEvent, WidgetConfig, WidgetContact, WidgetConversation, WidgetConversationSnapshotEvent, WidgetErrorEvent, WidgetMessage, WidgetMessageAckEvent, WidgetMessageCreatedEvent, WidgetSenderType, WidgetServerEvent, WidgetSessionPayload, WidgetSessionReadyEvent, WidgetSummary };
290
+ export type { ChatTransportConnection, ChatTransportHandlers, ChatWidgetProps, ChatWidgetTheme, ChatWidgetVisitor, CreateChatTransport, CreateChatTransportOptions, CreateLoopbackTransportOptions, CreateWebSocketTransportOptions, GetSession, GetSessionOptions, ResolvedTheme, WidgetAssistantStatusEvent, WidgetClientCreateMessageEvent, WidgetClientEvent, WidgetClientReadEvent, WidgetClientTopicEvent, WidgetConfig, WidgetContact, WidgetConversation, WidgetConversationSnapshotEvent, WidgetErrorEvent, WidgetMessage, WidgetMessageAckEvent, WidgetMessageCreatedEvent, WidgetSenderType, WidgetServerEvent, WidgetSessionPayload, WidgetSessionReadyEvent, WidgetSummary, WidgetSupport, WidgetSupportState, WidgetSupportStateEvent, WidgetSupportTopic };