keystone-design-bootstrap 1.0.85 → 1.0.86

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.js CHANGED
@@ -19917,8 +19917,108 @@ async function submitLeadFormAction(formData) {
19917
19917
  }
19918
19918
 
19919
19919
  // src/design_system/components/ChatWidget.tsx
19920
- import React63, { useState as useState33, useEffect as useEffect13, useRef as useRef19, useCallback as useCallback11 } from "react";
19920
+ import React63, { useState as useState33, useEffect as useEffect14, useRef as useRef20, useCallback as useCallback12 } from "react";
19921
19921
  import { X, MessageChatSquare } from "@untitledui/icons";
19922
+
19923
+ // src/design_system/chat/useRealtimeReplyOrchestrator.ts
19924
+ import { useCallback as useCallback11, useEffect as useEffect13, useRef as useRef19 } from "react";
19925
+ import { createConsumer } from "@rails/actioncable";
19926
+ function useRealtimeReplyOrchestrator({
19927
+ debugLabel,
19928
+ fetchRealtimeData,
19929
+ loadLatestHasReply,
19930
+ onReplyResolved,
19931
+ onAgentThinking
19932
+ }) {
19933
+ const cableRef = useRef19(null);
19934
+ const subscriptionRef = useRef19(null);
19935
+ const subscribedContactRef = useRef19(null);
19936
+ const clearRealtime = useCallback11(() => {
19937
+ if (subscriptionRef.current) {
19938
+ subscriptionRef.current.unsubscribe();
19939
+ subscriptionRef.current = null;
19940
+ }
19941
+ if (cableRef.current) {
19942
+ cableRef.current.disconnect();
19943
+ cableRef.current = null;
19944
+ }
19945
+ subscribedContactRef.current = null;
19946
+ }, []);
19947
+ const resolveReply = useCallback11(() => {
19948
+ onReplyResolved();
19949
+ }, [onReplyResolved]);
19950
+ const subscribeRealtime = useCallback11((realtime) => {
19951
+ const token = realtime.token;
19952
+ const contactIdForStream = realtime.contact_id;
19953
+ const cableUrl = realtime.cable_url;
19954
+ if (!token || !contactIdForStream || !cableUrl) return;
19955
+ if (subscribedContactRef.current === contactIdForStream && subscriptionRef.current && cableRef.current) {
19956
+ return;
19957
+ }
19958
+ clearRealtime();
19959
+ const cable = createConsumer(`${cableUrl}?token=${encodeURIComponent(token)}`);
19960
+ cableRef.current = cable;
19961
+ subscribedContactRef.current = contactIdForStream;
19962
+ subscriptionRef.current = cable.subscriptions.create(
19963
+ { channel: "ContactCommunicationsChannel", contact_id: String(contactIdForStream) },
19964
+ {
19965
+ connected: () => {
19966
+ console.info(`[${debugLabel}] realtime connected contact_id=${contactIdForStream}`);
19967
+ },
19968
+ disconnected: () => {
19969
+ console.warn(`[${debugLabel}] realtime disconnected contact_id=${contactIdForStream}`);
19970
+ },
19971
+ rejected: () => {
19972
+ console.warn(`[${debugLabel}] realtime rejected contact_id=${contactIdForStream}`);
19973
+ },
19974
+ received: async (data) => {
19975
+ var _a;
19976
+ console.info(`[${debugLabel}] realtime received type=${(_a = data == null ? void 0 : data.type) != null ? _a : "unknown"} contact_id=${contactIdForStream}`);
19977
+ if ((data == null ? void 0 : data.type) === "agent_thinking") {
19978
+ onAgentThinking == null ? void 0 : onAgentThinking();
19979
+ return;
19980
+ }
19981
+ if ((data == null ? void 0 : data.type) !== "new_communication") return;
19982
+ try {
19983
+ const hasReply = await loadLatestHasReply();
19984
+ if (hasReply) {
19985
+ resolveReply();
19986
+ }
19987
+ } catch (error) {
19988
+ console.error(`[${debugLabel}] realtime receive handling error`, error);
19989
+ }
19990
+ }
19991
+ }
19992
+ );
19993
+ }, [clearRealtime, debugLabel, loadLatestHasReply, onAgentThinking, resolveReply]);
19994
+ const ensureRealtimeSubscription = useCallback11(async () => {
19995
+ try {
19996
+ const realtime = await fetchRealtimeData();
19997
+ if (!realtime) return;
19998
+ subscribeRealtime(realtime);
19999
+ } catch (e) {
20000
+ }
20001
+ }, [fetchRealtimeData, subscribeRealtime]);
20002
+ const beginReplyWait = useCallback11((options) => {
20003
+ if (options == null ? void 0 : options.realtimeData) {
20004
+ subscribeRealtime(options.realtimeData);
20005
+ } else {
20006
+ void ensureRealtimeSubscription();
20007
+ }
20008
+ }, [ensureRealtimeSubscription, subscribeRealtime]);
20009
+ useEffect13(() => {
20010
+ return () => {
20011
+ clearRealtime();
20012
+ };
20013
+ }, [clearRealtime]);
20014
+ return {
20015
+ ensureRealtimeSubscription,
20016
+ subscribeRealtime,
20017
+ beginReplyWait
20018
+ };
20019
+ }
20020
+
20021
+ // src/design_system/components/ChatWidget.tsx
19922
20022
  var formatTime = (isoString) => {
19923
20023
  const date = new Date(isoString);
19924
20024
  const now2 = /* @__PURE__ */ new Date();
@@ -19948,8 +20048,8 @@ function ChatWidget({
19948
20048
  const [isLoading, setIsLoading] = useState33(false);
19949
20049
  const [sessionId, setSessionId] = useState33("");
19950
20050
  const [waitingForReply, setWaitingForReply] = useState33(false);
19951
- const messagesEndRef = useRef19(null);
19952
- useEffect13(() => {
20051
+ const messagesEndRef = useRef20(null);
20052
+ useEffect14(() => {
19953
20053
  if (contactId) return;
19954
20054
  if (providedSessionId) {
19955
20055
  setSessionId(providedSessionId);
@@ -19964,7 +20064,7 @@ function ChatWidget({
19964
20064
  }
19965
20065
  }
19966
20066
  }, [contactId, providedSessionId]);
19967
- const loadMessages = useCallback11(async () => {
20067
+ const loadMessages = useCallback12(async () => {
19968
20068
  if (!contactId && !sessionId) return [];
19969
20069
  try {
19970
20070
  const query = contactId ? `contact_id=${encodeURIComponent(contactId)}` : `identifier=${encodeURIComponent(sessionId)}`;
@@ -19980,46 +20080,55 @@ function ChatWidget({
19980
20080
  }
19981
20081
  return [];
19982
20082
  }, [contactId, sessionId]);
19983
- useEffect13(() => {
20083
+ useEffect14(() => {
19984
20084
  if (isOpen && (contactId || sessionId)) {
19985
20085
  loadMessages();
19986
20086
  }
19987
20087
  }, [isOpen, contactId, sessionId, loadMessages]);
19988
- useEffect13(() => {
20088
+ useEffect14(() => {
19989
20089
  var _a;
19990
20090
  (_a = messagesEndRef.current) == null ? void 0 : _a.scrollIntoView({ behavior: "smooth" });
19991
20091
  }, [messages]);
19992
- const pollIntervalRef = useRef19(null);
19993
- useEffect13(() => {
19994
- return () => {
19995
- if (pollIntervalRef.current !== null) {
19996
- clearInterval(pollIntervalRef.current);
19997
- }
19998
- };
20092
+ const hasAgentReplyWithBody = useCallback12((list) => {
20093
+ const latest = list[list.length - 1];
20094
+ return (latest == null ? void 0 : latest.sender_type) === "agent" && (latest == null ? void 0 : latest.body) != null && String(latest.body).trim() !== "";
19999
20095
  }, []);
20000
- const pollForAgentReply = () => {
20001
- setWaitingForReply(true);
20002
- let attempts = 0;
20003
- const maxAttempts = 30;
20004
- pollIntervalRef.current = setInterval(async () => {
20005
- attempts++;
20006
- try {
20007
- const newMessages = await loadMessages();
20008
- const latest = newMessages[newMessages.length - 1];
20009
- const hasAgentReplyWithBody = (latest == null ? void 0 : latest.sender_type) === "agent" && (latest == null ? void 0 : latest.body) != null && String(latest.body).trim() !== "";
20010
- if (hasAgentReplyWithBody || attempts >= maxAttempts) {
20011
- clearInterval(pollIntervalRef.current);
20012
- pollIntervalRef.current = null;
20013
- setWaitingForReply(false);
20014
- setIsLoading(false);
20015
- }
20016
- } catch (error) {
20017
- console.error("[ChatWidget] Error polling for messages:", error);
20018
- }
20019
- }, 1e3);
20020
- };
20021
- const sendMessage = async () => {
20096
+ const fetchRealtimeData = useCallback12(async () => {
20022
20097
  var _a, _b, _c;
20098
+ if (!contactId && !sessionId) return null;
20099
+ const query = contactId ? `contact_id=${encodeURIComponent(contactId)}` : `identifier=${encodeURIComponent(sessionId)}`;
20100
+ const response = await fetch(`/api/chat/?action=realtime_token&${query}`);
20101
+ if (!response.ok) return null;
20102
+ const result = await response.json();
20103
+ const token = (_a = result == null ? void 0 : result.data) == null ? void 0 : _a.token;
20104
+ const contactIdForStream = (_b = result == null ? void 0 : result.data) == null ? void 0 : _b.contact_id;
20105
+ const cableUrl = (_c = result == null ? void 0 : result.data) == null ? void 0 : _c.cable_url;
20106
+ if (!token || !contactIdForStream || !cableUrl) return null;
20107
+ return { token, contact_id: contactIdForStream, cable_url: cableUrl };
20108
+ }, [contactId, sessionId]);
20109
+ const {
20110
+ beginReplyWait,
20111
+ ensureRealtimeSubscription
20112
+ } = useRealtimeReplyOrchestrator({
20113
+ debugLabel: "ChatWidget",
20114
+ fetchRealtimeData,
20115
+ loadLatestHasReply: async () => hasAgentReplyWithBody(await loadMessages()),
20116
+ onReplyResolved: () => {
20117
+ setWaitingForReply(false);
20118
+ setIsLoading(false);
20119
+ },
20120
+ onAgentThinking: () => {
20121
+ setWaitingForReply(true);
20122
+ }
20123
+ });
20124
+ const hasPersistedMessages = messages.some((message) => !String(message.id).startsWith("temp_"));
20125
+ useEffect14(() => {
20126
+ if (!isOpen || !contactId && !sessionId) return;
20127
+ if (!contactId && !hasPersistedMessages) return;
20128
+ ensureRealtimeSubscription();
20129
+ }, [contactId, ensureRealtimeSubscription, hasPersistedMessages, isOpen, sessionId]);
20130
+ const sendMessage = async () => {
20131
+ var _a, _b, _c, _d, _e, _f;
20023
20132
  if (!inputValue.trim() || !contactId && !sessionId) return;
20024
20133
  const messageText = inputValue.trim();
20025
20134
  setInputValue("");
@@ -20044,24 +20153,34 @@ function ChatWidget({
20044
20153
  const result = await response.json();
20045
20154
  captureEvent("chat_message_sent", { is_authenticated: Boolean(contactId) });
20046
20155
  if ((_a = result.data) == null ? void 0 : _a.job_id) {
20047
- pollForAgentReply();
20048
- } else if (((_b = result.data) == null ? void 0 : _b.status) === "agent_unavailable" || ((_c = result.data) == null ? void 0 : _c.status) === "no_auto_reply") {
20156
+ setWaitingForReply(true);
20157
+ const realtimeFromSend = ((_b = result.data) == null ? void 0 : _b.realtime_token) && ((_c = result.data) == null ? void 0 : _c.contact_id) && ((_d = result.data) == null ? void 0 : _d.cable_url) ? {
20158
+ token: result.data.realtime_token,
20159
+ contact_id: result.data.contact_id,
20160
+ cable_url: result.data.cable_url
20161
+ } : null;
20162
+ beginReplyWait({ realtimeData: realtimeFromSend });
20163
+ } else if (((_e = result.data) == null ? void 0 : _e.status) === "agent_unavailable" || ((_f = result.data) == null ? void 0 : _f.status) === "no_auto_reply") {
20049
20164
  setIsLoading(false);
20165
+ setWaitingForReply(false);
20050
20166
  } else {
20051
20167
  await loadMessages();
20052
20168
  setIsLoading(false);
20169
+ setWaitingForReply(false);
20053
20170
  }
20054
20171
  } else {
20055
20172
  setMessages((prev) => prev.filter((m) => m.id !== tempMessage.id));
20056
20173
  captureEvent("chat_message_failed", { error: "send_failed" });
20057
20174
  console.error("Failed to send message");
20058
20175
  setIsLoading(false);
20176
+ setWaitingForReply(false);
20059
20177
  }
20060
20178
  } catch (error) {
20061
20179
  setMessages((prev) => prev.filter((m) => m.id !== tempMessage.id));
20062
20180
  captureEvent("chat_message_failed", { error: "network_error" });
20063
20181
  console.error("Failed to send message:", error);
20064
20182
  setIsLoading(false);
20183
+ setWaitingForReply(false);
20065
20184
  }
20066
20185
  };
20067
20186
  const handleKeyDown = (e) => {