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 +156 -37
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
- package/src/design_system/chat/useRealtimeReplyOrchestrator.ts +127 -0
- package/src/design_system/components/ChatWidget.tsx +58 -37
- package/src/design_system/portal/MessageComposer.tsx +53 -1
- package/src/next/routes/chat.ts +57 -1
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
|
|
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 =
|
|
19952
|
-
|
|
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 =
|
|
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
|
-
|
|
20083
|
+
useEffect14(() => {
|
|
19984
20084
|
if (isOpen && (contactId || sessionId)) {
|
|
19985
20085
|
loadMessages();
|
|
19986
20086
|
}
|
|
19987
20087
|
}, [isOpen, contactId, sessionId, loadMessages]);
|
|
19988
|
-
|
|
20088
|
+
useEffect14(() => {
|
|
19989
20089
|
var _a;
|
|
19990
20090
|
(_a = messagesEndRef.current) == null ? void 0 : _a.scrollIntoView({ behavior: "smooth" });
|
|
19991
20091
|
}, [messages]);
|
|
19992
|
-
const
|
|
19993
|
-
|
|
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
|
|
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
|
-
|
|
20048
|
-
|
|
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) => {
|