@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/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);
@@ -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, { setError, setIsReady, setMessages }) {
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: [/* @__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
+ ]
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
- const submitMessage = (event) => {
717
- event?.preventDefault();
718
- 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)) {
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
- transportRef.current.sendMessage({
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
- session && showWaitingNotice ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
801
- className: "clivly-chat-widget__notice",
802
- role: "status",
803
- children: waitingNoticeText(session.contact?.email)
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: "Write your message",
994
+ placeholder: composerPlaceholder(topics.length > 0),
826
995
  value: messageDraft
827
996
  })]
828
997
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -963,6 +1132,10 @@ function createWebSocketTransport({ createSocket = (url, protocols) => new WebSo
963
1132
  markRead: () => {
964
1133
  if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify({ type: "conversation.read" }));
965
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
+ },
966
1139
  sendMessage: (message) => {
967
1140
  if (socket.readyState !== WebSocket.OPEN) throw new Error("Chat socket is not connected.");
968
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;
@@ -83,7 +104,13 @@ interface WidgetClientCreateMessageEvent {
83
104
  interface WidgetClientReadEvent {
84
105
  type: "conversation.read";
85
106
  }
86
- type WidgetClientEvent = WidgetClientCreateMessageEvent | WidgetClientReadEvent;
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;
87
114
  interface WidgetSessionReadyEvent {
88
115
  conversationId: string;
89
116
  type: "session.ready";
@@ -102,11 +129,20 @@ interface WidgetMessageAckEvent {
102
129
  message: WidgetMessage;
103
130
  type: "message.ack";
104
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
+ }
105
141
  interface WidgetErrorEvent {
106
142
  message: string;
107
143
  type: "error";
108
144
  }
109
- type WidgetServerEvent = WidgetConversationSnapshotEvent | WidgetErrorEvent | WidgetMessageAckEvent | WidgetMessageCreatedEvent | WidgetSessionReadyEvent;
145
+ type WidgetServerEvent = WidgetAssistantStatusEvent | WidgetConversationSnapshotEvent | WidgetErrorEvent | WidgetMessageAckEvent | WidgetMessageCreatedEvent | WidgetSessionReadyEvent | WidgetSupportStateEvent;
110
146
  //#endregion
111
147
  //#region src/transport.d.ts
112
148
  interface ChatTransportHandlers {
@@ -123,6 +159,11 @@ interface ChatTransportConnection {
123
159
  * without it still works, its visitors are just emailed replies they saw.
124
160
  */
125
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;
126
167
  sendMessage: (event: WidgetClientCreateMessageEvent) => void;
127
168
  }
128
169
  interface CreateChatTransportOptions {
@@ -246,4 +287,4 @@ export declare const createClivlyTransport: typeof createClivlyTransport$1;
246
287
  export declare const mountChatWidget: typeof mountChatWidget$1;
247
288
  export declare const mountChatWidgetScript: typeof mountChatWidgetScript$1;
248
289
  //#endregion
249
- export type { ChatTransportConnection, ChatTransportHandlers, ChatWidgetProps, ChatWidgetTheme, ChatWidgetVisitor, CreateChatTransport, CreateChatTransportOptions, CreateLoopbackTransportOptions, CreateWebSocketTransportOptions, GetSession, GetSessionOptions, ResolvedTheme, WidgetClientCreateMessageEvent, WidgetClientEvent, WidgetClientReadEvent, 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;
@@ -83,7 +104,13 @@ interface WidgetClientCreateMessageEvent {
83
104
  interface WidgetClientReadEvent {
84
105
  type: "conversation.read";
85
106
  }
86
- type WidgetClientEvent = WidgetClientCreateMessageEvent | WidgetClientReadEvent;
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;
87
114
  interface WidgetSessionReadyEvent {
88
115
  conversationId: string;
89
116
  type: "session.ready";
@@ -102,11 +129,20 @@ interface WidgetMessageAckEvent {
102
129
  message: WidgetMessage;
103
130
  type: "message.ack";
104
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
+ }
105
141
  interface WidgetErrorEvent {
106
142
  message: string;
107
143
  type: "error";
108
144
  }
109
- type WidgetServerEvent = WidgetConversationSnapshotEvent | WidgetErrorEvent | WidgetMessageAckEvent | WidgetMessageCreatedEvent | WidgetSessionReadyEvent;
145
+ type WidgetServerEvent = WidgetAssistantStatusEvent | WidgetConversationSnapshotEvent | WidgetErrorEvent | WidgetMessageAckEvent | WidgetMessageCreatedEvent | WidgetSessionReadyEvent | WidgetSupportStateEvent;
110
146
  //#endregion
111
147
  //#region src/transport.d.ts
112
148
  interface ChatTransportHandlers {
@@ -123,6 +159,11 @@ interface ChatTransportConnection {
123
159
  * without it still works, its visitors are just emailed replies they saw.
124
160
  */
125
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;
126
167
  sendMessage: (event: WidgetClientCreateMessageEvent) => void;
127
168
  }
128
169
  interface CreateChatTransportOptions {
@@ -246,4 +287,4 @@ export declare const createClivlyTransport: typeof createClivlyTransport$1;
246
287
  export declare const mountChatWidget: typeof mountChatWidget$1;
247
288
  export declare const mountChatWidgetScript: typeof mountChatWidgetScript$1;
248
289
  //#endregion
249
- export type { ChatTransportConnection, ChatTransportHandlers, ChatWidgetProps, ChatWidgetTheme, ChatWidgetVisitor, CreateChatTransport, CreateChatTransportOptions, CreateLoopbackTransportOptions, CreateWebSocketTransportOptions, GetSession, GetSessionOptions, ResolvedTheme, WidgetClientCreateMessageEvent, WidgetClientEvent, WidgetClientReadEvent, 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 };