@yext/chat-ui-react 0.11.3 → 0.12.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/THIRD-PARTY-NOTICES +386 -5155
- package/lib/commonjs/package.json.js +1 -1
- package/lib/commonjs/src/components/ChatPanel.d.ts +13 -0
- package/lib/commonjs/src/components/ChatPanel.d.ts.map +1 -1
- package/lib/commonjs/src/components/ChatPanel.js +78 -12
- package/lib/commonjs/src/components/ChatPanel.js.map +1 -1
- package/lib/commonjs/src/components/ChatPopUp.d.ts +6 -0
- package/lib/commonjs/src/components/ChatPopUp.d.ts.map +1 -1
- package/lib/commonjs/src/components/ChatPopUp.js +53 -26
- package/lib/commonjs/src/components/ChatPopUp.js.map +1 -1
- package/lib/commonjs/src/components/Markdown.d.ts.map +1 -1
- package/lib/commonjs/src/components/Markdown.js.map +1 -1
- package/lib/esm/index.d.ts +6 -0
- package/lib/esm/package.json.mjs +1 -1
- package/lib/esm/src/components/ChatPanel.d.ts +13 -0
- package/lib/esm/src/components/ChatPanel.d.ts.map +1 -1
- package/lib/esm/src/components/ChatPanel.mjs +77 -14
- package/lib/esm/src/components/ChatPanel.mjs.map +1 -1
- package/lib/esm/src/components/ChatPopUp.d.ts +6 -0
- package/lib/esm/src/components/ChatPopUp.d.ts.map +1 -1
- package/lib/esm/src/components/ChatPopUp.mjs +53 -26
- package/lib/esm/src/components/ChatPopUp.mjs.map +1 -1
- package/lib/esm/src/components/Markdown.d.ts.map +1 -1
- package/lib/esm/src/components/Markdown.mjs.map +1 -1
- package/package.json +4 -4
- package/src/components/ChatPanel.tsx +109 -14
- package/src/components/ChatPopUp.tsx +80 -32
- package/src/components/Markdown.tsx +6 -3
|
@@ -5,6 +5,7 @@ import React, {
|
|
|
5
5
|
useMemo,
|
|
6
6
|
useRef,
|
|
7
7
|
useState,
|
|
8
|
+
useLayoutEffect,
|
|
8
9
|
} from "react";
|
|
9
10
|
import { useChatState } from "@yext/chat-headless-react";
|
|
10
11
|
import {
|
|
@@ -45,7 +46,8 @@ const builtInCssClasses: ChatPanelCssClasses = withStylelessCssClasses(
|
|
|
45
46
|
{
|
|
46
47
|
container: "h-full w-full flex flex-col relative shadow-2xl bg-white",
|
|
47
48
|
messagesScrollContainer: "flex flex-col mt-auto overflow-hidden",
|
|
48
|
-
messagesContainer:
|
|
49
|
+
messagesContainer:
|
|
50
|
+
"flex flex-col gap-y-1 px-4 overflow-auto [&>*:first-child]:mt-3",
|
|
49
51
|
inputContainer: "w-full p-4",
|
|
50
52
|
messageBubbleCssClasses: {
|
|
51
53
|
topContainer: "mt-1",
|
|
@@ -116,6 +118,9 @@ export function ChatPanel(props: ChatPanelProps) {
|
|
|
116
118
|
const suggestedReplies = useChatState(
|
|
117
119
|
(state) => state.conversation.notes?.suggestedReplies
|
|
118
120
|
);
|
|
121
|
+
const conversationId = useChatState(
|
|
122
|
+
(state) => state.conversation.conversationId
|
|
123
|
+
);
|
|
119
124
|
const cssClasses = useComposedCssClasses(builtInCssClasses, customCssClasses);
|
|
120
125
|
const reportAnalyticsEvent = useReportAnalyticsEvent();
|
|
121
126
|
useFetchInitialMessage(handleError, stream);
|
|
@@ -156,25 +161,40 @@ export function ChatPanel(props: ChatPanelProps) {
|
|
|
156
161
|
const messagesRef = useRef<Array<HTMLDivElement | null>>([]);
|
|
157
162
|
const messagesContainer = useRef<HTMLDivElement>(null);
|
|
158
163
|
|
|
164
|
+
// State to help detect initial messages rendering
|
|
165
|
+
const [initialMessagesLength] = useState(messages.length);
|
|
166
|
+
|
|
167
|
+
const savedPanelState = useMemo(() => {
|
|
168
|
+
if (!conversationId) {
|
|
169
|
+
return {};
|
|
170
|
+
}
|
|
171
|
+
return loadSessionState(conversationId);
|
|
172
|
+
}, [conversationId]);
|
|
173
|
+
|
|
159
174
|
// Handle scrolling when messages change
|
|
160
175
|
useEffect(() => {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
176
|
+
const isInitialRender = messages.length === initialMessagesLength;
|
|
177
|
+
let scrollPos = 0;
|
|
178
|
+
if (isInitialRender && savedPanelState.scrollPosition !== undefined) {
|
|
179
|
+
// memorized position
|
|
180
|
+
scrollPos = savedPanelState?.scrollPosition;
|
|
181
|
+
} else {
|
|
182
|
+
messagesRef.current = messagesRef.current.slice(0, messages.length);
|
|
183
|
+
// Sums up scroll heights of all messages except the last one
|
|
184
|
+
if (messagesRef?.current.length > 1) {
|
|
185
|
+
// position of the top of the last message
|
|
186
|
+
scrollPos = messagesRef.current
|
|
187
|
+
.slice(0, -1)
|
|
188
|
+
.map((elem, _) => elem?.scrollHeight ?? 0)
|
|
189
|
+
.reduce((total, height) => total + height);
|
|
190
|
+
}
|
|
170
191
|
}
|
|
171
192
|
|
|
172
|
-
// Scroll to the top of the last message
|
|
173
193
|
messagesContainer.current?.scroll({
|
|
174
|
-
top:
|
|
194
|
+
top: scrollPos,
|
|
175
195
|
behavior: "smooth",
|
|
176
196
|
});
|
|
177
|
-
}, [messages]);
|
|
197
|
+
}, [messages, initialMessagesLength, savedPanelState.scrollPosition]);
|
|
178
198
|
|
|
179
199
|
const setMessagesRef = useCallback((index) => {
|
|
180
200
|
if (!messagesRef?.current) return null;
|
|
@@ -189,12 +209,32 @@ export function ChatPanel(props: ChatPanelProps) {
|
|
|
189
209
|
[cssClasses]
|
|
190
210
|
);
|
|
191
211
|
|
|
212
|
+
useLayoutEffect(() => {
|
|
213
|
+
const curr = messagesContainer.current;
|
|
214
|
+
const onScroll = () => {
|
|
215
|
+
if (!conversationId) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
saveSessionState(conversationId, {
|
|
219
|
+
scrollPosition: curr?.scrollTop,
|
|
220
|
+
});
|
|
221
|
+
};
|
|
222
|
+
curr?.addEventListener("scroll", onScroll);
|
|
223
|
+
return () => {
|
|
224
|
+
curr?.removeEventListener("scroll", onScroll);
|
|
225
|
+
};
|
|
226
|
+
}, [messagesContainer, conversationId]);
|
|
227
|
+
|
|
192
228
|
return (
|
|
193
229
|
<div className="yext-chat w-full h-full">
|
|
194
230
|
<div className={cssClasses.container}>
|
|
195
231
|
{header}
|
|
196
232
|
<div className={cssClasses.messagesScrollContainer}>
|
|
197
|
-
<div
|
|
233
|
+
<div
|
|
234
|
+
ref={messagesContainer}
|
|
235
|
+
className={cssClasses.messagesContainer}
|
|
236
|
+
aria-label="Chat Panel Messages Container"
|
|
237
|
+
>
|
|
198
238
|
{messages.map((message, index) => (
|
|
199
239
|
<div key={index} ref={setMessagesRef(index)}>
|
|
200
240
|
<MessageBubble
|
|
@@ -249,3 +289,58 @@ export function ChatPanel(props: ChatPanelProps) {
|
|
|
249
289
|
</div>
|
|
250
290
|
);
|
|
251
291
|
}
|
|
292
|
+
|
|
293
|
+
const BASE_STATE_LOCAL_STORAGE_KEY = "yext_chat_panel_state";
|
|
294
|
+
|
|
295
|
+
export function getStateLocalStorageKey(
|
|
296
|
+
hostname: string,
|
|
297
|
+
conversationId: string
|
|
298
|
+
): string {
|
|
299
|
+
return `${BASE_STATE_LOCAL_STORAGE_KEY}__${hostname}__${conversationId}`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Maintains the panel state of the session.
|
|
304
|
+
*/
|
|
305
|
+
export interface PanelState {
|
|
306
|
+
/** The scroll position of the panel. */
|
|
307
|
+
scrollPosition?: number;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Loads the {@link PanelState} from local storage.
|
|
312
|
+
*/
|
|
313
|
+
export const loadSessionState = (conversationId: string): PanelState => {
|
|
314
|
+
const hostname = window?.location?.hostname;
|
|
315
|
+
if (!localStorage || !hostname) {
|
|
316
|
+
return {};
|
|
317
|
+
}
|
|
318
|
+
const savedState = localStorage.getItem(
|
|
319
|
+
getStateLocalStorageKey(hostname, conversationId)
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
if (savedState) {
|
|
323
|
+
try {
|
|
324
|
+
const parsedState: PanelState = JSON.parse(savedState);
|
|
325
|
+
return parsedState;
|
|
326
|
+
} catch (e) {
|
|
327
|
+
console.warn("Unabled to load saved panel state: error parsing state.");
|
|
328
|
+
localStorage.removeItem(
|
|
329
|
+
getStateLocalStorageKey(hostname, conversationId)
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return {};
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
export const saveSessionState = (conversationId: string, state: PanelState) => {
|
|
338
|
+
const hostname = window?.location?.hostname;
|
|
339
|
+
if (!localStorage || !hostname) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
localStorage.setItem(
|
|
343
|
+
getStateLocalStorageKey(hostname, conversationId),
|
|
344
|
+
JSON.stringify(state)
|
|
345
|
+
);
|
|
346
|
+
};
|
|
@@ -110,6 +110,12 @@ export interface ChatPopUpProps
|
|
|
110
110
|
* This prop will override the "showInitialMessagePopUp" prop, if specified.
|
|
111
111
|
*/
|
|
112
112
|
ctaLabel?: string;
|
|
113
|
+
/**
|
|
114
|
+
* A controlled prop to open or close the panel. If provided, the prop
|
|
115
|
+
* will override the openOnLoad prop and the panel will be controlled
|
|
116
|
+
* by the parent component.
|
|
117
|
+
*/
|
|
118
|
+
isOpen?: boolean;
|
|
113
119
|
}
|
|
114
120
|
|
|
115
121
|
/**
|
|
@@ -134,6 +140,7 @@ export function ChatPopUp(props: ChatPopUpProps) {
|
|
|
134
140
|
ctaLabel,
|
|
135
141
|
title,
|
|
136
142
|
footer,
|
|
143
|
+
isOpen,
|
|
137
144
|
} = props;
|
|
138
145
|
|
|
139
146
|
const reportAnalyticsEvent = useReportAnalyticsEvent();
|
|
@@ -147,24 +154,7 @@ export function ChatPopUp(props: ChatPopUpProps) {
|
|
|
147
154
|
const [numReadMessages, setNumReadMessagesLength] = useState<number>(0);
|
|
148
155
|
const [numUnreadMessages, setNumUnreadMessagesLength] = useState<number>(0);
|
|
149
156
|
|
|
150
|
-
const [showInitialMessage, setshowInitialMessage] = useState(
|
|
151
|
-
//only show initial message popup (if specified) when CTA label is not provided
|
|
152
|
-
!ctaLabel && showInitialMessagePopUp
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
const onCloseInitialMessage = useCallback(() => {
|
|
156
|
-
setshowInitialMessage(false);
|
|
157
|
-
}, []);
|
|
158
|
-
|
|
159
|
-
// control CSS behavior (fade-in/out animation) on open/close state of the panel.
|
|
160
|
-
const [showChat, setShowChat] = useState(false);
|
|
161
|
-
|
|
162
|
-
// control the actual DOM rendering of the panel. Start rendering on first open state
|
|
163
|
-
// to avoid message requests immediately on load while the popup is still "hidden"
|
|
164
|
-
const [renderChat, setRenderChat] = useState(false);
|
|
165
|
-
|
|
166
157
|
// Set the initial value of the local storage flag for opening on load only if it doesn't already exist
|
|
167
|
-
|
|
168
158
|
if (window.localStorage.getItem(popupLocalStorageKey) === null) {
|
|
169
159
|
window.localStorage.setItem(
|
|
170
160
|
popupLocalStorageKey,
|
|
@@ -179,6 +169,15 @@ export function ChatPopUp(props: ChatPopUpProps) {
|
|
|
179
169
|
const isOpenOnLoad =
|
|
180
170
|
(messages.length > 1 && openOnLoadLocalStorage === "true") || openOnLoad;
|
|
181
171
|
|
|
172
|
+
const {
|
|
173
|
+
renderChat,
|
|
174
|
+
showChat,
|
|
175
|
+
showInitialMessage,
|
|
176
|
+
toggleChat,
|
|
177
|
+
closeChat,
|
|
178
|
+
closeInitialMessage,
|
|
179
|
+
} = usePanelState(isOpen, isOpenOnLoad, !ctaLabel && showInitialMessagePopUp);
|
|
180
|
+
|
|
182
181
|
// only fetch initial message when ChatPanel is closed on load (otherwise, it will be fetched in ChatPanel)
|
|
183
182
|
useFetchInitialMessage(
|
|
184
183
|
showInitialMessagePopUp ? console.error : handleError,
|
|
@@ -188,28 +187,18 @@ export function ChatPopUp(props: ChatPopUpProps) {
|
|
|
188
187
|
!isOpenOnLoad
|
|
189
188
|
);
|
|
190
189
|
|
|
191
|
-
useEffect(() => {
|
|
192
|
-
if (!renderChat && isOpenOnLoad) {
|
|
193
|
-
setShowChat(true);
|
|
194
|
-
setRenderChat(true);
|
|
195
|
-
setshowInitialMessage(false);
|
|
196
|
-
}
|
|
197
|
-
}, [renderChat, messages.length, isOpenOnLoad]);
|
|
198
|
-
|
|
199
190
|
const onClick = useCallback(() => {
|
|
200
|
-
|
|
201
|
-
setRenderChat(true);
|
|
202
|
-
setshowInitialMessage(false);
|
|
191
|
+
toggleChat();
|
|
203
192
|
window.localStorage.setItem(popupLocalStorageKey, "true");
|
|
204
|
-
}, []);
|
|
193
|
+
}, [toggleChat]);
|
|
205
194
|
|
|
206
195
|
const onClose = useCallback(() => {
|
|
207
|
-
|
|
196
|
+
closeChat();
|
|
208
197
|
customOnClose?.();
|
|
209
198
|
// consider all the messages are read while the panel was open
|
|
210
199
|
setNumReadMessagesLength(messages.length);
|
|
211
200
|
window.localStorage.setItem(popupLocalStorageKey, "false");
|
|
212
|
-
}, [customOnClose, messages.length]);
|
|
201
|
+
}, [closeChat, customOnClose, messages.length]);
|
|
213
202
|
|
|
214
203
|
useEffect(() => {
|
|
215
204
|
// update number of unread messages if there are new messages added while the panel is closed
|
|
@@ -255,7 +244,7 @@ export function ChatPopUp(props: ChatPopUpProps) {
|
|
|
255
244
|
>
|
|
256
245
|
{showInitialMessage && (
|
|
257
246
|
<InitialMessagePopUp
|
|
258
|
-
onClose={
|
|
247
|
+
onClose={closeInitialMessage}
|
|
259
248
|
customCssClasses={cssClasses.initialMessagePopUpCssClasses}
|
|
260
249
|
/>
|
|
261
250
|
)}
|
|
@@ -298,3 +287,62 @@ export function ChatPopUp(props: ChatPopUpProps) {
|
|
|
298
287
|
</div>
|
|
299
288
|
);
|
|
300
289
|
}
|
|
290
|
+
|
|
291
|
+
function usePanelState(
|
|
292
|
+
isOpen: boolean | undefined,
|
|
293
|
+
isOpenOnLoad: boolean | undefined,
|
|
294
|
+
initialMessageVisible: boolean | undefined
|
|
295
|
+
) {
|
|
296
|
+
// control CSS behavior (fade-in/out animation) on open/close state of the panel.
|
|
297
|
+
const [showChat, setShowChat] = useState(false);
|
|
298
|
+
// control the actual DOM rendering of the panel. Start rendering on first open state
|
|
299
|
+
// to avoid message requests immediately on load while the popup is still "hidden"
|
|
300
|
+
const [renderChat, setRenderChat] = useState(false);
|
|
301
|
+
const [showInitialMessage, setshowInitialMessage] = useState(
|
|
302
|
+
initialMessageVisible
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
useEffect(() => {
|
|
306
|
+
if (isOpen !== undefined) {
|
|
307
|
+
setShowChat(isOpen);
|
|
308
|
+
setRenderChat(isOpen);
|
|
309
|
+
}
|
|
310
|
+
}, [isOpen]);
|
|
311
|
+
|
|
312
|
+
useEffect(() => {
|
|
313
|
+
if (!renderChat && isOpenOnLoad && isOpen === undefined) {
|
|
314
|
+
setShowChat(true);
|
|
315
|
+
setRenderChat(true);
|
|
316
|
+
setshowInitialMessage(false);
|
|
317
|
+
}
|
|
318
|
+
}, [renderChat, isOpen, isOpenOnLoad]);
|
|
319
|
+
|
|
320
|
+
const toggleChat = useCallback(() => {
|
|
321
|
+
if (isOpen !== undefined) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
setShowChat((prev) => !prev);
|
|
325
|
+
setRenderChat(true);
|
|
326
|
+
setshowInitialMessage(false);
|
|
327
|
+
}, [isOpen]);
|
|
328
|
+
|
|
329
|
+
const closeChat = useCallback(() => {
|
|
330
|
+
if (isOpen !== undefined) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
setShowChat(false);
|
|
334
|
+
}, [isOpen]);
|
|
335
|
+
|
|
336
|
+
const closeInitialMessage = useCallback(() => {
|
|
337
|
+
setshowInitialMessage(false);
|
|
338
|
+
}, []);
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
showChat,
|
|
342
|
+
renderChat,
|
|
343
|
+
showInitialMessage,
|
|
344
|
+
toggleChat,
|
|
345
|
+
closeChat,
|
|
346
|
+
closeInitialMessage,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import ReactMarkdown, {
|
|
1
|
+
import ReactMarkdown, {
|
|
2
|
+
PluggableList,
|
|
3
|
+
ReactMarkdownOptions,
|
|
4
|
+
} from "react-markdown";
|
|
2
5
|
import remarkGfm from "remark-gfm";
|
|
3
6
|
import rehypeRaw from "rehype-raw";
|
|
4
7
|
import rehypeSanitize from "rehype-sanitize";
|
|
@@ -7,7 +10,7 @@ import { useReportAnalyticsEvent } from "../hooks/useReportAnalyticsEvent";
|
|
|
7
10
|
import { useComposedCssClasses } from "../hooks/useComposedCssClasses";
|
|
8
11
|
|
|
9
12
|
// The Remark and Rehype plugins to use in conjunction with ReactMarkdown.
|
|
10
|
-
const unifiedPlugins: { remark?:
|
|
13
|
+
const unifiedPlugins: { remark?: PluggableList; rehype: PluggableList } = {
|
|
11
14
|
remark: [
|
|
12
15
|
remarkGfm, //renders Github-Flavored Markdown
|
|
13
16
|
],
|
|
@@ -69,7 +72,7 @@ export function Markdown({
|
|
|
69
72
|
const reportAnalyticsEvent = useReportAnalyticsEvent();
|
|
70
73
|
const cssClasses = useComposedCssClasses(builtInCssClasses, customCssClasses);
|
|
71
74
|
|
|
72
|
-
const components:
|
|
75
|
+
const components: ReactMarkdownOptions["components"] = useMemo(() => {
|
|
73
76
|
const createClickHandlerFn = (href?: string) => () => {
|
|
74
77
|
reportAnalyticsEvent({
|
|
75
78
|
action: linkClickEvent,
|