@yext/chat-ui-react 0.11.4 → 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/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/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/package.json +3 -3
- package/src/components/ChatPanel.tsx +107 -13
|
@@ -58,4 +58,17 @@ export interface ChatPanelProps extends Omit<MessageBubbleProps, "customCssClass
|
|
|
58
58
|
* @param props - {@link ChatPanelProps}
|
|
59
59
|
*/
|
|
60
60
|
export declare function ChatPanel(props: ChatPanelProps): React.JSX.Element;
|
|
61
|
+
export declare function getStateLocalStorageKey(hostname: string, conversationId: string): string;
|
|
62
|
+
/**
|
|
63
|
+
* Maintains the panel state of the session.
|
|
64
|
+
*/
|
|
65
|
+
export interface PanelState {
|
|
66
|
+
/** The scroll position of the panel. */
|
|
67
|
+
scrollPosition?: number;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Loads the {@link PanelState} from local storage.
|
|
71
|
+
*/
|
|
72
|
+
export declare const loadSessionState: (conversationId: string) => PanelState;
|
|
73
|
+
export declare const saveSessionState: (conversationId: string, state: PanelState) => void;
|
|
61
74
|
//# sourceMappingURL=ChatPanel.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatPanel.d.ts","sourceRoot":"","sources":["../../../../src/components/ChatPanel.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EACZ,SAAS,
|
|
1
|
+
{"version":3,"file":"ChatPanel.d.ts","sourceRoot":"","sources":["../../../../src/components/ChatPanel.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EACZ,SAAS,EAOV,MAAM,OAAO,CAAC;AAEf,OAAO,EAEL,uBAAuB,EACvB,kBAAkB,EACnB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAa,mBAAmB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAM7E,OAAO,EACL,2BAA2B,EAE5B,MAAM,sBAAsB,CAAC;AAG9B;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,mBAAmB,CAAC;IACtC,uBAAuB,CAAC,EAAE,uBAAuB,CAAC;IAClD,wBAAwB,CAAC,EAAE,2BAA2B,CAAC;IACvD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAiBD;;;;GAIG;AACH,MAAM,WAAW,cACf,SAAQ,IAAI,CAAC,kBAAkB,EAAE,kBAAkB,GAAG,SAAS,CAAC,EAC9D,IAAI,CAAC,cAAc,EAAE,kBAAkB,CAAC;IAC1C,kDAAkD;IAClD,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;OAEG;IACH,gBAAgB,CAAC,EAAE,mBAAmB,CAAC;IACvC;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC9B;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0DAA0D;IAC1D,WAAW,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,qBA6L9C;AAID,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,MAAM,GACrB,MAAM,CAER;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,wCAAwC;IACxC,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;GAEG;AACH,eAAO,MAAM,gBAAgB,mBAAoB,MAAM,KAAG,UAsBzD,CAAC;AAEF,eAAO,MAAM,gBAAgB,mBAAoB,MAAM,SAAS,UAAU,SASzE,CAAC"}
|
|
@@ -40,6 +40,7 @@ function ChatPanel(props) {
|
|
|
40
40
|
const messages = chatHeadlessReact.useChatState((state) => state.conversation.messages);
|
|
41
41
|
const loading = chatHeadlessReact.useChatState((state) => state.conversation.isLoading);
|
|
42
42
|
const suggestedReplies = chatHeadlessReact.useChatState((state) => state.conversation.notes?.suggestedReplies);
|
|
43
|
+
const conversationId = chatHeadlessReact.useChatState((state) => state.conversation.conversationId);
|
|
43
44
|
const cssClasses = useComposedCssClasses.useComposedCssClasses(builtInCssClasses, customCssClasses);
|
|
44
45
|
const reportAnalyticsEvent = useReportAnalyticsEvent.useReportAnalyticsEvent();
|
|
45
46
|
useFetchInitialMessage.useFetchInitialMessage(handleError, stream);
|
|
@@ -66,23 +67,38 @@ function ChatPanel(props) {
|
|
|
66
67
|
}, [messages, suggestedReplies, messageSuggestions]);
|
|
67
68
|
const messagesRef = React.useRef([]);
|
|
68
69
|
const messagesContainer = React.useRef(null);
|
|
70
|
+
// State to help detect initial messages rendering
|
|
71
|
+
const [initialMessagesLength] = React.useState(messages.length);
|
|
72
|
+
const savedPanelState = React.useMemo(() => {
|
|
73
|
+
if (!conversationId) {
|
|
74
|
+
return {};
|
|
75
|
+
}
|
|
76
|
+
return loadSessionState(conversationId);
|
|
77
|
+
}, [conversationId]);
|
|
69
78
|
// Handle scrolling when messages change
|
|
70
79
|
React.useEffect(() => {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
80
|
+
const isInitialRender = messages.length === initialMessagesLength;
|
|
81
|
+
let scrollPos = 0;
|
|
82
|
+
if (isInitialRender && savedPanelState.scrollPosition !== undefined) {
|
|
83
|
+
// memorized position
|
|
84
|
+
scrollPos = savedPanelState?.scrollPosition;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
messagesRef.current = messagesRef.current.slice(0, messages.length);
|
|
88
|
+
// Sums up scroll heights of all messages except the last one
|
|
89
|
+
if (messagesRef?.current.length > 1) {
|
|
90
|
+
// position of the top of the last message
|
|
91
|
+
scrollPos = messagesRef.current
|
|
92
|
+
.slice(0, -1)
|
|
93
|
+
.map((elem, _) => elem?.scrollHeight ?? 0)
|
|
94
|
+
.reduce((total, height) => total + height);
|
|
95
|
+
}
|
|
79
96
|
}
|
|
80
|
-
// Scroll to the top of the last message
|
|
81
97
|
messagesContainer.current?.scroll({
|
|
82
|
-
top:
|
|
98
|
+
top: scrollPos,
|
|
83
99
|
behavior: "smooth",
|
|
84
100
|
});
|
|
85
|
-
}, [messages]);
|
|
101
|
+
}, [messages, initialMessagesLength, savedPanelState.scrollPosition]);
|
|
86
102
|
const setMessagesRef = React.useCallback((index) => {
|
|
87
103
|
if (!messagesRef?.current)
|
|
88
104
|
return null;
|
|
@@ -92,11 +108,26 @@ function ChatPanel(props) {
|
|
|
92
108
|
container: cssClasses.footer,
|
|
93
109
|
link: "cursor-pointer hover:underline text-blue-600",
|
|
94
110
|
}), [cssClasses]);
|
|
111
|
+
React.useLayoutEffect(() => {
|
|
112
|
+
const curr = messagesContainer.current;
|
|
113
|
+
const onScroll = () => {
|
|
114
|
+
if (!conversationId) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
saveSessionState(conversationId, {
|
|
118
|
+
scrollPosition: curr?.scrollTop,
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
curr?.addEventListener("scroll", onScroll);
|
|
122
|
+
return () => {
|
|
123
|
+
curr?.removeEventListener("scroll", onScroll);
|
|
124
|
+
};
|
|
125
|
+
}, [messagesContainer, conversationId]);
|
|
95
126
|
return (React__default.default.createElement("div", { className: "yext-chat w-full h-full" },
|
|
96
127
|
React__default.default.createElement("div", { className: cssClasses.container },
|
|
97
128
|
header,
|
|
98
129
|
React__default.default.createElement("div", { className: cssClasses.messagesScrollContainer },
|
|
99
|
-
React__default.default.createElement("div", { ref: messagesContainer, className: cssClasses.messagesContainer },
|
|
130
|
+
React__default.default.createElement("div", { ref: messagesContainer, className: cssClasses.messagesContainer, "aria-label": "Chat Panel Messages Container" },
|
|
100
131
|
messages.map((message, index) => (React__default.default.createElement("div", { key: index, ref: setMessagesRef(index) },
|
|
101
132
|
React__default.default.createElement(MessageBubble.MessageBubble, { ...props, customCssClasses: cssClasses.messageBubbleCssClasses, message: message, linkTarget: linkTarget, onLinkClick: onLinkClick })))),
|
|
102
133
|
loading && (React__default.default.createElement("div", { className: "flex" },
|
|
@@ -107,6 +138,41 @@ function ChatPanel(props) {
|
|
|
107
138
|
React__default.default.createElement(ChatInput.ChatInput, { ...props, onSend: onSend, onRetry: onRetry, customCssClasses: cssClasses.inputCssClasses })),
|
|
108
139
|
footer && (React__default.default.createElement(Markdown.Markdown, { content: footer, linkClickEvent: "WEBSITE", linkTarget: linkTarget, onLinkClick: onLinkClick, customCssClasses: footerCssClasses })))));
|
|
109
140
|
}
|
|
141
|
+
const BASE_STATE_LOCAL_STORAGE_KEY = "yext_chat_panel_state";
|
|
142
|
+
function getStateLocalStorageKey(hostname, conversationId) {
|
|
143
|
+
return `${BASE_STATE_LOCAL_STORAGE_KEY}__${hostname}__${conversationId}`;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Loads the {@link PanelState} from local storage.
|
|
147
|
+
*/
|
|
148
|
+
const loadSessionState = (conversationId) => {
|
|
149
|
+
const hostname = window?.location?.hostname;
|
|
150
|
+
if (!localStorage || !hostname) {
|
|
151
|
+
return {};
|
|
152
|
+
}
|
|
153
|
+
const savedState = localStorage.getItem(getStateLocalStorageKey(hostname, conversationId));
|
|
154
|
+
if (savedState) {
|
|
155
|
+
try {
|
|
156
|
+
const parsedState = JSON.parse(savedState);
|
|
157
|
+
return parsedState;
|
|
158
|
+
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
console.warn("Unabled to load saved panel state: error parsing state.");
|
|
161
|
+
localStorage.removeItem(getStateLocalStorageKey(hostname, conversationId));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return {};
|
|
165
|
+
};
|
|
166
|
+
const saveSessionState = (conversationId, state) => {
|
|
167
|
+
const hostname = window?.location?.hostname;
|
|
168
|
+
if (!localStorage || !hostname) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
localStorage.setItem(getStateLocalStorageKey(hostname, conversationId), JSON.stringify(state));
|
|
172
|
+
};
|
|
110
173
|
|
|
111
174
|
exports.ChatPanel = ChatPanel;
|
|
175
|
+
exports.getStateLocalStorageKey = getStateLocalStorageKey;
|
|
176
|
+
exports.loadSessionState = loadSessionState;
|
|
177
|
+
exports.saveSessionState = saveSessionState;
|
|
112
178
|
//# sourceMappingURL=ChatPanel.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatPanel.js","sources":["../../../../src/components/ChatPanel.tsx"],"sourcesContent":["import React, {\n ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { useChatState } from \"@yext/chat-headless-react\";\nimport {\n MessageBubble,\n MessageBubbleCssClasses,\n MessageBubbleProps,\n} from \"./MessageBubble\";\nimport { ChatInput, ChatInputCssClasses, ChatInputProps } from \"./ChatInput\";\nimport { LoadingDots } from \"./LoadingDots\";\nimport { useComposedCssClasses } from \"../hooks\";\nimport { withStylelessCssClasses } from \"../utils/withStylelessCssClasses\";\nimport { useReportAnalyticsEvent } from \"../hooks/useReportAnalyticsEvent\";\nimport { useFetchInitialMessage } from \"../hooks/useFetchInitialMessage\";\nimport {\n MessageSuggestionCssClasses,\n MessageSuggestions,\n} from \"./MessageSuggestions\";\nimport { Markdown, MarkdownCssClasses } from \"./Markdown\";\n\n/**\n * The CSS class interface for the {@link ChatPanel} component.\n *\n * @public\n */\nexport interface ChatPanelCssClasses {\n container?: string;\n messagesContainer?: string;\n messagesScrollContainer?: string;\n inputContainer?: string;\n inputCssClasses?: ChatInputCssClasses;\n messageBubbleCssClasses?: MessageBubbleCssClasses;\n messageSuggestionClasses?: MessageSuggestionCssClasses;\n footer?: string;\n}\n\nconst builtInCssClasses: ChatPanelCssClasses = withStylelessCssClasses(\n \"Panel\",\n {\n container: \"h-full w-full flex flex-col relative shadow-2xl bg-white\",\n messagesScrollContainer: \"flex flex-col mt-auto overflow-hidden\",\n messagesContainer:\n \"flex flex-col gap-y-1 px-4 overflow-auto [&>*:first-child]:mt-3\",\n inputContainer: \"w-full p-4\",\n messageBubbleCssClasses: {\n topContainer: \"mt-1\",\n },\n footer: \"text-center text-slate-400 rounded-b-3xl px-4 pb-4 text-[12px]\",\n }\n);\n\n/**\n * The props for the {@link ChatPanel} component.\n *\n * @public\n */\nexport interface ChatPanelProps\n extends Omit<MessageBubbleProps, \"customCssClasses\" | \"message\">,\n Omit<ChatInputProps, \"customCssClasses\"> {\n /** A header to render at the top of the panel. */\n header?: ReactNode;\n /** A footer markdown string to render at the bottom of the panel. */\n footer?: string;\n /**\n * CSS classes for customizing the component styling.\n */\n customCssClasses?: ChatPanelCssClasses;\n /**\n * A set of pre-written initial messages that the user\n * can click on instead of typing their own.\n */\n messageSuggestions?: string[];\n /** Link target open behavior on click.\n * Defaults to \"_blank\".\n */\n linkTarget?: string;\n /** A callback which is called when user clicks a link. */\n onLinkClick?: (href?: string) => void;\n /**\n * Text to display when retrying.\n * Defaults to \"Error occurred. Retrying\".\n */\n retryText?: string;\n}\n\n/**\n * A component that renders a full panel for chat bot interactions. This includes\n * the message bubbles for the conversation, input box with send button, and header\n * (if provided).\n *\n * @public\n *\n * @param props - {@link ChatPanelProps}\n */\nexport function ChatPanel(props: ChatPanelProps) {\n const {\n header,\n footer,\n customCssClasses,\n stream,\n handleError,\n messageSuggestions,\n linkTarget = \"_blank\",\n onLinkClick,\n onSend: onSendProp,\n onRetry: onRetryProp,\n retryText = \"Error occurred. Retrying\",\n } = props;\n const messages = useChatState((state) => state.conversation.messages);\n const loading = useChatState((state) => state.conversation.isLoading);\n const suggestedReplies = useChatState(\n (state) => state.conversation.notes?.suggestedReplies\n );\n const cssClasses = useComposedCssClasses(builtInCssClasses, customCssClasses);\n const reportAnalyticsEvent = useReportAnalyticsEvent();\n useFetchInitialMessage(handleError, stream);\n\n const [retry, setRetry] = useState(false);\n const onSend = useCallback(\n (message: string) => {\n onSendProp?.(message);\n setRetry(false);\n },\n [onSendProp]\n );\n\n const onRetry = useCallback(\n (e: unknown) => {\n onRetryProp?.(e);\n setRetry(true);\n },\n [onRetryProp]\n );\n\n useEffect(() => {\n reportAnalyticsEvent({\n action: \"CHAT_IMPRESSION\",\n });\n }, [reportAnalyticsEvent]);\n\n const suggestions = useMemo(() => {\n if (\n messages.length === 0 ||\n (messages.length === 1 && messages[0].source === \"BOT\")\n ) {\n return messageSuggestions;\n }\n return suggestedReplies;\n }, [messages, suggestedReplies, messageSuggestions]);\n\n const messagesRef = useRef<Array<HTMLDivElement | null>>([]);\n const messagesContainer = useRef<HTMLDivElement>(null);\n\n // Handle scrolling when messages change\n useEffect(() => {\n let scrollTop = 0;\n messagesRef.current = messagesRef.current.slice(0, messages.length);\n\n // Sums up scroll heights of all messages except the last one\n if (messagesRef?.current.length > 1) {\n scrollTop = messagesRef.current\n .slice(0, -1)\n .map((elem, _) => elem?.scrollHeight ?? 0)\n .reduce((total, height) => total + height);\n }\n\n // Scroll to the top of the last message\n messagesContainer.current?.scroll({\n top: scrollTop,\n behavior: \"smooth\",\n });\n }, [messages]);\n\n const setMessagesRef = useCallback((index) => {\n if (!messagesRef?.current) return null;\n return (message) => (messagesRef.current[index] = message);\n }, []);\n\n const footerCssClasses: MarkdownCssClasses = useMemo(\n () => ({\n container: cssClasses.footer,\n link: \"cursor-pointer hover:underline text-blue-600\",\n }),\n [cssClasses]\n );\n\n return (\n <div className=\"yext-chat w-full h-full\">\n <div className={cssClasses.container}>\n {header}\n <div className={cssClasses.messagesScrollContainer}>\n <div ref={messagesContainer} className={cssClasses.messagesContainer}>\n {messages.map((message, index) => (\n <div key={index} ref={setMessagesRef(index)}>\n <MessageBubble\n {...props}\n customCssClasses={cssClasses.messageBubbleCssClasses}\n message={message}\n linkTarget={linkTarget}\n onLinkClick={onLinkClick}\n />\n </div>\n ))}\n {loading && (\n <div className=\"flex\">\n <LoadingDots />\n {retry && (\n <p className=\"text-slate-500 text-[13px] font-bold\">\n {retryText}\n </p>\n )}\n </div>\n )}\n </div>\n </div>\n <div className={cssClasses.inputContainer}>\n {suggestions && (\n <MessageSuggestions\n stream={stream}\n onSend={onSend}\n onRetry={onRetry}\n handleError={handleError}\n suggestions={suggestions}\n customCssClasses={cssClasses.messageSuggestionClasses}\n />\n )}\n <ChatInput\n {...props}\n onSend={onSend}\n onRetry={onRetry}\n customCssClasses={cssClasses.inputCssClasses}\n />\n </div>\n {footer && (\n <Markdown\n content={footer}\n linkClickEvent=\"WEBSITE\"\n linkTarget={linkTarget}\n onLinkClick={onLinkClick}\n customCssClasses={footerCssClasses}\n />\n )}\n </div>\n </div>\n );\n}\n"],"names":["withStylelessCssClasses","useChatState","useComposedCssClasses","useReportAnalyticsEvent","useFetchInitialMessage","useState","useCallback","useEffect","useMemo","useRef","React","MessageBubble","LoadingDots","MessageSuggestions","ChatInput","Markdown"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ChatPanel.js","sources":["../../../../src/components/ChatPanel.tsx"],"sourcesContent":["import React, {\n ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n useLayoutEffect,\n} from \"react\";\nimport { useChatState } from \"@yext/chat-headless-react\";\nimport {\n MessageBubble,\n MessageBubbleCssClasses,\n MessageBubbleProps,\n} from \"./MessageBubble\";\nimport { ChatInput, ChatInputCssClasses, ChatInputProps } from \"./ChatInput\";\nimport { LoadingDots } from \"./LoadingDots\";\nimport { useComposedCssClasses } from \"../hooks\";\nimport { withStylelessCssClasses } from \"../utils/withStylelessCssClasses\";\nimport { useReportAnalyticsEvent } from \"../hooks/useReportAnalyticsEvent\";\nimport { useFetchInitialMessage } from \"../hooks/useFetchInitialMessage\";\nimport {\n MessageSuggestionCssClasses,\n MessageSuggestions,\n} from \"./MessageSuggestions\";\nimport { Markdown, MarkdownCssClasses } from \"./Markdown\";\n\n/**\n * The CSS class interface for the {@link ChatPanel} component.\n *\n * @public\n */\nexport interface ChatPanelCssClasses {\n container?: string;\n messagesContainer?: string;\n messagesScrollContainer?: string;\n inputContainer?: string;\n inputCssClasses?: ChatInputCssClasses;\n messageBubbleCssClasses?: MessageBubbleCssClasses;\n messageSuggestionClasses?: MessageSuggestionCssClasses;\n footer?: string;\n}\n\nconst builtInCssClasses: ChatPanelCssClasses = withStylelessCssClasses(\n \"Panel\",\n {\n container: \"h-full w-full flex flex-col relative shadow-2xl bg-white\",\n messagesScrollContainer: \"flex flex-col mt-auto overflow-hidden\",\n messagesContainer:\n \"flex flex-col gap-y-1 px-4 overflow-auto [&>*:first-child]:mt-3\",\n inputContainer: \"w-full p-4\",\n messageBubbleCssClasses: {\n topContainer: \"mt-1\",\n },\n footer: \"text-center text-slate-400 rounded-b-3xl px-4 pb-4 text-[12px]\",\n }\n);\n\n/**\n * The props for the {@link ChatPanel} component.\n *\n * @public\n */\nexport interface ChatPanelProps\n extends Omit<MessageBubbleProps, \"customCssClasses\" | \"message\">,\n Omit<ChatInputProps, \"customCssClasses\"> {\n /** A header to render at the top of the panel. */\n header?: ReactNode;\n /** A footer markdown string to render at the bottom of the panel. */\n footer?: string;\n /**\n * CSS classes for customizing the component styling.\n */\n customCssClasses?: ChatPanelCssClasses;\n /**\n * A set of pre-written initial messages that the user\n * can click on instead of typing their own.\n */\n messageSuggestions?: string[];\n /** Link target open behavior on click.\n * Defaults to \"_blank\".\n */\n linkTarget?: string;\n /** A callback which is called when user clicks a link. */\n onLinkClick?: (href?: string) => void;\n /**\n * Text to display when retrying.\n * Defaults to \"Error occurred. Retrying\".\n */\n retryText?: string;\n}\n\n/**\n * A component that renders a full panel for chat bot interactions. This includes\n * the message bubbles for the conversation, input box with send button, and header\n * (if provided).\n *\n * @public\n *\n * @param props - {@link ChatPanelProps}\n */\nexport function ChatPanel(props: ChatPanelProps) {\n const {\n header,\n footer,\n customCssClasses,\n stream,\n handleError,\n messageSuggestions,\n linkTarget = \"_blank\",\n onLinkClick,\n onSend: onSendProp,\n onRetry: onRetryProp,\n retryText = \"Error occurred. Retrying\",\n } = props;\n const messages = useChatState((state) => state.conversation.messages);\n const loading = useChatState((state) => state.conversation.isLoading);\n const suggestedReplies = useChatState(\n (state) => state.conversation.notes?.suggestedReplies\n );\n const conversationId = useChatState(\n (state) => state.conversation.conversationId\n );\n const cssClasses = useComposedCssClasses(builtInCssClasses, customCssClasses);\n const reportAnalyticsEvent = useReportAnalyticsEvent();\n useFetchInitialMessage(handleError, stream);\n\n const [retry, setRetry] = useState(false);\n const onSend = useCallback(\n (message: string) => {\n onSendProp?.(message);\n setRetry(false);\n },\n [onSendProp]\n );\n\n const onRetry = useCallback(\n (e: unknown) => {\n onRetryProp?.(e);\n setRetry(true);\n },\n [onRetryProp]\n );\n\n useEffect(() => {\n reportAnalyticsEvent({\n action: \"CHAT_IMPRESSION\",\n });\n }, [reportAnalyticsEvent]);\n\n const suggestions = useMemo(() => {\n if (\n messages.length === 0 ||\n (messages.length === 1 && messages[0].source === \"BOT\")\n ) {\n return messageSuggestions;\n }\n return suggestedReplies;\n }, [messages, suggestedReplies, messageSuggestions]);\n\n const messagesRef = useRef<Array<HTMLDivElement | null>>([]);\n const messagesContainer = useRef<HTMLDivElement>(null);\n\n // State to help detect initial messages rendering\n const [initialMessagesLength] = useState(messages.length);\n\n const savedPanelState = useMemo(() => {\n if (!conversationId) {\n return {};\n }\n return loadSessionState(conversationId);\n }, [conversationId]);\n\n // Handle scrolling when messages change\n useEffect(() => {\n const isInitialRender = messages.length === initialMessagesLength;\n let scrollPos = 0;\n if (isInitialRender && savedPanelState.scrollPosition !== undefined) {\n // memorized position\n scrollPos = savedPanelState?.scrollPosition;\n } else {\n messagesRef.current = messagesRef.current.slice(0, messages.length);\n // Sums up scroll heights of all messages except the last one\n if (messagesRef?.current.length > 1) {\n // position of the top of the last message\n scrollPos = messagesRef.current\n .slice(0, -1)\n .map((elem, _) => elem?.scrollHeight ?? 0)\n .reduce((total, height) => total + height);\n }\n }\n\n messagesContainer.current?.scroll({\n top: scrollPos,\n behavior: \"smooth\",\n });\n }, [messages, initialMessagesLength, savedPanelState.scrollPosition]);\n\n const setMessagesRef = useCallback((index) => {\n if (!messagesRef?.current) return null;\n return (message) => (messagesRef.current[index] = message);\n }, []);\n\n const footerCssClasses: MarkdownCssClasses = useMemo(\n () => ({\n container: cssClasses.footer,\n link: \"cursor-pointer hover:underline text-blue-600\",\n }),\n [cssClasses]\n );\n\n useLayoutEffect(() => {\n const curr = messagesContainer.current;\n const onScroll = () => {\n if (!conversationId) {\n return;\n }\n saveSessionState(conversationId, {\n scrollPosition: curr?.scrollTop,\n });\n };\n curr?.addEventListener(\"scroll\", onScroll);\n return () => {\n curr?.removeEventListener(\"scroll\", onScroll);\n };\n }, [messagesContainer, conversationId]);\n\n return (\n <div className=\"yext-chat w-full h-full\">\n <div className={cssClasses.container}>\n {header}\n <div className={cssClasses.messagesScrollContainer}>\n <div\n ref={messagesContainer}\n className={cssClasses.messagesContainer}\n aria-label=\"Chat Panel Messages Container\"\n >\n {messages.map((message, index) => (\n <div key={index} ref={setMessagesRef(index)}>\n <MessageBubble\n {...props}\n customCssClasses={cssClasses.messageBubbleCssClasses}\n message={message}\n linkTarget={linkTarget}\n onLinkClick={onLinkClick}\n />\n </div>\n ))}\n {loading && (\n <div className=\"flex\">\n <LoadingDots />\n {retry && (\n <p className=\"text-slate-500 text-[13px] font-bold\">\n {retryText}\n </p>\n )}\n </div>\n )}\n </div>\n </div>\n <div className={cssClasses.inputContainer}>\n {suggestions && (\n <MessageSuggestions\n stream={stream}\n onSend={onSend}\n onRetry={onRetry}\n handleError={handleError}\n suggestions={suggestions}\n customCssClasses={cssClasses.messageSuggestionClasses}\n />\n )}\n <ChatInput\n {...props}\n onSend={onSend}\n onRetry={onRetry}\n customCssClasses={cssClasses.inputCssClasses}\n />\n </div>\n {footer && (\n <Markdown\n content={footer}\n linkClickEvent=\"WEBSITE\"\n linkTarget={linkTarget}\n onLinkClick={onLinkClick}\n customCssClasses={footerCssClasses}\n />\n )}\n </div>\n </div>\n );\n}\n\nconst BASE_STATE_LOCAL_STORAGE_KEY = \"yext_chat_panel_state\";\n\nexport function getStateLocalStorageKey(\n hostname: string,\n conversationId: string\n): string {\n return `${BASE_STATE_LOCAL_STORAGE_KEY}__${hostname}__${conversationId}`;\n}\n\n/**\n * Maintains the panel state of the session.\n */\nexport interface PanelState {\n /** The scroll position of the panel. */\n scrollPosition?: number;\n}\n\n/**\n * Loads the {@link PanelState} from local storage.\n */\nexport const loadSessionState = (conversationId: string): PanelState => {\n const hostname = window?.location?.hostname;\n if (!localStorage || !hostname) {\n return {};\n }\n const savedState = localStorage.getItem(\n getStateLocalStorageKey(hostname, conversationId)\n );\n\n if (savedState) {\n try {\n const parsedState: PanelState = JSON.parse(savedState);\n return parsedState;\n } catch (e) {\n console.warn(\"Unabled to load saved panel state: error parsing state.\");\n localStorage.removeItem(\n getStateLocalStorageKey(hostname, conversationId)\n );\n }\n }\n\n return {};\n};\n\nexport const saveSessionState = (conversationId: string, state: PanelState) => {\n const hostname = window?.location?.hostname;\n if (!localStorage || !hostname) {\n return;\n }\n localStorage.setItem(\n getStateLocalStorageKey(hostname, conversationId),\n JSON.stringify(state)\n );\n};\n"],"names":["withStylelessCssClasses","useChatState","useComposedCssClasses","useReportAnalyticsEvent","useFetchInitialMessage","useState","useCallback","useEffect","useMemo","useRef","useLayoutEffect","React","MessageBubble","LoadingDots","MessageSuggestions","ChatInput","Markdown"],"mappings":"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"}
|
package/lib/esm/package.json.mjs
CHANGED
|
@@ -58,4 +58,17 @@ export interface ChatPanelProps extends Omit<MessageBubbleProps, "customCssClass
|
|
|
58
58
|
* @param props - {@link ChatPanelProps}
|
|
59
59
|
*/
|
|
60
60
|
export declare function ChatPanel(props: ChatPanelProps): React.JSX.Element;
|
|
61
|
+
export declare function getStateLocalStorageKey(hostname: string, conversationId: string): string;
|
|
62
|
+
/**
|
|
63
|
+
* Maintains the panel state of the session.
|
|
64
|
+
*/
|
|
65
|
+
export interface PanelState {
|
|
66
|
+
/** The scroll position of the panel. */
|
|
67
|
+
scrollPosition?: number;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Loads the {@link PanelState} from local storage.
|
|
71
|
+
*/
|
|
72
|
+
export declare const loadSessionState: (conversationId: string) => PanelState;
|
|
73
|
+
export declare const saveSessionState: (conversationId: string, state: PanelState) => void;
|
|
61
74
|
//# sourceMappingURL=ChatPanel.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatPanel.d.ts","sourceRoot":"","sources":["../../../../src/components/ChatPanel.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EACZ,SAAS,
|
|
1
|
+
{"version":3,"file":"ChatPanel.d.ts","sourceRoot":"","sources":["../../../../src/components/ChatPanel.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EACZ,SAAS,EAOV,MAAM,OAAO,CAAC;AAEf,OAAO,EAEL,uBAAuB,EACvB,kBAAkB,EACnB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAa,mBAAmB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAM7E,OAAO,EACL,2BAA2B,EAE5B,MAAM,sBAAsB,CAAC;AAG9B;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,mBAAmB,CAAC;IACtC,uBAAuB,CAAC,EAAE,uBAAuB,CAAC;IAClD,wBAAwB,CAAC,EAAE,2BAA2B,CAAC;IACvD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAiBD;;;;GAIG;AACH,MAAM,WAAW,cACf,SAAQ,IAAI,CAAC,kBAAkB,EAAE,kBAAkB,GAAG,SAAS,CAAC,EAC9D,IAAI,CAAC,cAAc,EAAE,kBAAkB,CAAC;IAC1C,kDAAkD;IAClD,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;OAEG;IACH,gBAAgB,CAAC,EAAE,mBAAmB,CAAC;IACvC;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC9B;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0DAA0D;IAC1D,WAAW,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,qBA6L9C;AAID,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,MAAM,GACrB,MAAM,CAER;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,wCAAwC;IACxC,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;GAEG;AACH,eAAO,MAAM,gBAAgB,mBAAoB,MAAM,KAAG,UAsBzD,CAAC;AAEF,eAAO,MAAM,gBAAgB,mBAAoB,MAAM,SAAS,UAAU,SASzE,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react';
|
|
1
|
+
import React, { useState, useCallback, useEffect, useMemo, useRef, useLayoutEffect } from 'react';
|
|
2
2
|
import { useChatState } from '@yext/chat-headless-react';
|
|
3
3
|
import { MessageBubble } from './MessageBubble.mjs';
|
|
4
4
|
import { ChatInput } from './ChatInput.mjs';
|
|
@@ -34,6 +34,7 @@ function ChatPanel(props) {
|
|
|
34
34
|
const messages = useChatState((state) => state.conversation.messages);
|
|
35
35
|
const loading = useChatState((state) => state.conversation.isLoading);
|
|
36
36
|
const suggestedReplies = useChatState((state) => state.conversation.notes?.suggestedReplies);
|
|
37
|
+
const conversationId = useChatState((state) => state.conversation.conversationId);
|
|
37
38
|
const cssClasses = useComposedCssClasses(builtInCssClasses, customCssClasses);
|
|
38
39
|
const reportAnalyticsEvent = useReportAnalyticsEvent();
|
|
39
40
|
useFetchInitialMessage(handleError, stream);
|
|
@@ -60,23 +61,38 @@ function ChatPanel(props) {
|
|
|
60
61
|
}, [messages, suggestedReplies, messageSuggestions]);
|
|
61
62
|
const messagesRef = useRef([]);
|
|
62
63
|
const messagesContainer = useRef(null);
|
|
64
|
+
// State to help detect initial messages rendering
|
|
65
|
+
const [initialMessagesLength] = useState(messages.length);
|
|
66
|
+
const savedPanelState = useMemo(() => {
|
|
67
|
+
if (!conversationId) {
|
|
68
|
+
return {};
|
|
69
|
+
}
|
|
70
|
+
return loadSessionState(conversationId);
|
|
71
|
+
}, [conversationId]);
|
|
63
72
|
// Handle scrolling when messages change
|
|
64
73
|
useEffect(() => {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
74
|
+
const isInitialRender = messages.length === initialMessagesLength;
|
|
75
|
+
let scrollPos = 0;
|
|
76
|
+
if (isInitialRender && savedPanelState.scrollPosition !== undefined) {
|
|
77
|
+
// memorized position
|
|
78
|
+
scrollPos = savedPanelState?.scrollPosition;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
messagesRef.current = messagesRef.current.slice(0, messages.length);
|
|
82
|
+
// Sums up scroll heights of all messages except the last one
|
|
83
|
+
if (messagesRef?.current.length > 1) {
|
|
84
|
+
// position of the top of the last message
|
|
85
|
+
scrollPos = messagesRef.current
|
|
86
|
+
.slice(0, -1)
|
|
87
|
+
.map((elem, _) => elem?.scrollHeight ?? 0)
|
|
88
|
+
.reduce((total, height) => total + height);
|
|
89
|
+
}
|
|
73
90
|
}
|
|
74
|
-
// Scroll to the top of the last message
|
|
75
91
|
messagesContainer.current?.scroll({
|
|
76
|
-
top:
|
|
92
|
+
top: scrollPos,
|
|
77
93
|
behavior: "smooth",
|
|
78
94
|
});
|
|
79
|
-
}, [messages]);
|
|
95
|
+
}, [messages, initialMessagesLength, savedPanelState.scrollPosition]);
|
|
80
96
|
const setMessagesRef = useCallback((index) => {
|
|
81
97
|
if (!messagesRef?.current)
|
|
82
98
|
return null;
|
|
@@ -86,11 +102,26 @@ function ChatPanel(props) {
|
|
|
86
102
|
container: cssClasses.footer,
|
|
87
103
|
link: "cursor-pointer hover:underline text-blue-600",
|
|
88
104
|
}), [cssClasses]);
|
|
105
|
+
useLayoutEffect(() => {
|
|
106
|
+
const curr = messagesContainer.current;
|
|
107
|
+
const onScroll = () => {
|
|
108
|
+
if (!conversationId) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
saveSessionState(conversationId, {
|
|
112
|
+
scrollPosition: curr?.scrollTop,
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
curr?.addEventListener("scroll", onScroll);
|
|
116
|
+
return () => {
|
|
117
|
+
curr?.removeEventListener("scroll", onScroll);
|
|
118
|
+
};
|
|
119
|
+
}, [messagesContainer, conversationId]);
|
|
89
120
|
return (React.createElement("div", { className: "yext-chat w-full h-full" },
|
|
90
121
|
React.createElement("div", { className: cssClasses.container },
|
|
91
122
|
header,
|
|
92
123
|
React.createElement("div", { className: cssClasses.messagesScrollContainer },
|
|
93
|
-
React.createElement("div", { ref: messagesContainer, className: cssClasses.messagesContainer },
|
|
124
|
+
React.createElement("div", { ref: messagesContainer, className: cssClasses.messagesContainer, "aria-label": "Chat Panel Messages Container" },
|
|
94
125
|
messages.map((message, index) => (React.createElement("div", { key: index, ref: setMessagesRef(index) },
|
|
95
126
|
React.createElement(MessageBubble, { ...props, customCssClasses: cssClasses.messageBubbleCssClasses, message: message, linkTarget: linkTarget, onLinkClick: onLinkClick })))),
|
|
96
127
|
loading && (React.createElement("div", { className: "flex" },
|
|
@@ -101,6 +132,38 @@ function ChatPanel(props) {
|
|
|
101
132
|
React.createElement(ChatInput, { ...props, onSend: onSend, onRetry: onRetry, customCssClasses: cssClasses.inputCssClasses })),
|
|
102
133
|
footer && (React.createElement(Markdown, { content: footer, linkClickEvent: "WEBSITE", linkTarget: linkTarget, onLinkClick: onLinkClick, customCssClasses: footerCssClasses })))));
|
|
103
134
|
}
|
|
135
|
+
const BASE_STATE_LOCAL_STORAGE_KEY = "yext_chat_panel_state";
|
|
136
|
+
function getStateLocalStorageKey(hostname, conversationId) {
|
|
137
|
+
return `${BASE_STATE_LOCAL_STORAGE_KEY}__${hostname}__${conversationId}`;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Loads the {@link PanelState} from local storage.
|
|
141
|
+
*/
|
|
142
|
+
const loadSessionState = (conversationId) => {
|
|
143
|
+
const hostname = window?.location?.hostname;
|
|
144
|
+
if (!localStorage || !hostname) {
|
|
145
|
+
return {};
|
|
146
|
+
}
|
|
147
|
+
const savedState = localStorage.getItem(getStateLocalStorageKey(hostname, conversationId));
|
|
148
|
+
if (savedState) {
|
|
149
|
+
try {
|
|
150
|
+
const parsedState = JSON.parse(savedState);
|
|
151
|
+
return parsedState;
|
|
152
|
+
}
|
|
153
|
+
catch (e) {
|
|
154
|
+
console.warn("Unabled to load saved panel state: error parsing state.");
|
|
155
|
+
localStorage.removeItem(getStateLocalStorageKey(hostname, conversationId));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return {};
|
|
159
|
+
};
|
|
160
|
+
const saveSessionState = (conversationId, state) => {
|
|
161
|
+
const hostname = window?.location?.hostname;
|
|
162
|
+
if (!localStorage || !hostname) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
localStorage.setItem(getStateLocalStorageKey(hostname, conversationId), JSON.stringify(state));
|
|
166
|
+
};
|
|
104
167
|
|
|
105
|
-
export { ChatPanel };
|
|
168
|
+
export { ChatPanel, getStateLocalStorageKey, loadSessionState, saveSessionState };
|
|
106
169
|
//# sourceMappingURL=ChatPanel.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatPanel.mjs","sources":["../../../../src/components/ChatPanel.tsx"],"sourcesContent":["import React, {\n ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { useChatState } from \"@yext/chat-headless-react\";\nimport {\n MessageBubble,\n MessageBubbleCssClasses,\n MessageBubbleProps,\n} from \"./MessageBubble\";\nimport { ChatInput, ChatInputCssClasses, ChatInputProps } from \"./ChatInput\";\nimport { LoadingDots } from \"./LoadingDots\";\nimport { useComposedCssClasses } from \"../hooks\";\nimport { withStylelessCssClasses } from \"../utils/withStylelessCssClasses\";\nimport { useReportAnalyticsEvent } from \"../hooks/useReportAnalyticsEvent\";\nimport { useFetchInitialMessage } from \"../hooks/useFetchInitialMessage\";\nimport {\n MessageSuggestionCssClasses,\n MessageSuggestions,\n} from \"./MessageSuggestions\";\nimport { Markdown, MarkdownCssClasses } from \"./Markdown\";\n\n/**\n * The CSS class interface for the {@link ChatPanel} component.\n *\n * @public\n */\nexport interface ChatPanelCssClasses {\n container?: string;\n messagesContainer?: string;\n messagesScrollContainer?: string;\n inputContainer?: string;\n inputCssClasses?: ChatInputCssClasses;\n messageBubbleCssClasses?: MessageBubbleCssClasses;\n messageSuggestionClasses?: MessageSuggestionCssClasses;\n footer?: string;\n}\n\nconst builtInCssClasses: ChatPanelCssClasses = withStylelessCssClasses(\n \"Panel\",\n {\n container: \"h-full w-full flex flex-col relative shadow-2xl bg-white\",\n messagesScrollContainer: \"flex flex-col mt-auto overflow-hidden\",\n messagesContainer:\n \"flex flex-col gap-y-1 px-4 overflow-auto [&>*:first-child]:mt-3\",\n inputContainer: \"w-full p-4\",\n messageBubbleCssClasses: {\n topContainer: \"mt-1\",\n },\n footer: \"text-center text-slate-400 rounded-b-3xl px-4 pb-4 text-[12px]\",\n }\n);\n\n/**\n * The props for the {@link ChatPanel} component.\n *\n * @public\n */\nexport interface ChatPanelProps\n extends Omit<MessageBubbleProps, \"customCssClasses\" | \"message\">,\n Omit<ChatInputProps, \"customCssClasses\"> {\n /** A header to render at the top of the panel. */\n header?: ReactNode;\n /** A footer markdown string to render at the bottom of the panel. */\n footer?: string;\n /**\n * CSS classes for customizing the component styling.\n */\n customCssClasses?: ChatPanelCssClasses;\n /**\n * A set of pre-written initial messages that the user\n * can click on instead of typing their own.\n */\n messageSuggestions?: string[];\n /** Link target open behavior on click.\n * Defaults to \"_blank\".\n */\n linkTarget?: string;\n /** A callback which is called when user clicks a link. */\n onLinkClick?: (href?: string) => void;\n /**\n * Text to display when retrying.\n * Defaults to \"Error occurred. Retrying\".\n */\n retryText?: string;\n}\n\n/**\n * A component that renders a full panel for chat bot interactions. This includes\n * the message bubbles for the conversation, input box with send button, and header\n * (if provided).\n *\n * @public\n *\n * @param props - {@link ChatPanelProps}\n */\nexport function ChatPanel(props: ChatPanelProps) {\n const {\n header,\n footer,\n customCssClasses,\n stream,\n handleError,\n messageSuggestions,\n linkTarget = \"_blank\",\n onLinkClick,\n onSend: onSendProp,\n onRetry: onRetryProp,\n retryText = \"Error occurred. Retrying\",\n } = props;\n const messages = useChatState((state) => state.conversation.messages);\n const loading = useChatState((state) => state.conversation.isLoading);\n const suggestedReplies = useChatState(\n (state) => state.conversation.notes?.suggestedReplies\n );\n const cssClasses = useComposedCssClasses(builtInCssClasses, customCssClasses);\n const reportAnalyticsEvent = useReportAnalyticsEvent();\n useFetchInitialMessage(handleError, stream);\n\n const [retry, setRetry] = useState(false);\n const onSend = useCallback(\n (message: string) => {\n onSendProp?.(message);\n setRetry(false);\n },\n [onSendProp]\n );\n\n const onRetry = useCallback(\n (e: unknown) => {\n onRetryProp?.(e);\n setRetry(true);\n },\n [onRetryProp]\n );\n\n useEffect(() => {\n reportAnalyticsEvent({\n action: \"CHAT_IMPRESSION\",\n });\n }, [reportAnalyticsEvent]);\n\n const suggestions = useMemo(() => {\n if (\n messages.length === 0 ||\n (messages.length === 1 && messages[0].source === \"BOT\")\n ) {\n return messageSuggestions;\n }\n return suggestedReplies;\n }, [messages, suggestedReplies, messageSuggestions]);\n\n const messagesRef = useRef<Array<HTMLDivElement | null>>([]);\n const messagesContainer = useRef<HTMLDivElement>(null);\n\n // Handle scrolling when messages change\n useEffect(() => {\n let scrollTop = 0;\n messagesRef.current = messagesRef.current.slice(0, messages.length);\n\n // Sums up scroll heights of all messages except the last one\n if (messagesRef?.current.length > 1) {\n scrollTop = messagesRef.current\n .slice(0, -1)\n .map((elem, _) => elem?.scrollHeight ?? 0)\n .reduce((total, height) => total + height);\n }\n\n // Scroll to the top of the last message\n messagesContainer.current?.scroll({\n top: scrollTop,\n behavior: \"smooth\",\n });\n }, [messages]);\n\n const setMessagesRef = useCallback((index) => {\n if (!messagesRef?.current) return null;\n return (message) => (messagesRef.current[index] = message);\n }, []);\n\n const footerCssClasses: MarkdownCssClasses = useMemo(\n () => ({\n container: cssClasses.footer,\n link: \"cursor-pointer hover:underline text-blue-600\",\n }),\n [cssClasses]\n );\n\n return (\n <div className=\"yext-chat w-full h-full\">\n <div className={cssClasses.container}>\n {header}\n <div className={cssClasses.messagesScrollContainer}>\n <div ref={messagesContainer} className={cssClasses.messagesContainer}>\n {messages.map((message, index) => (\n <div key={index} ref={setMessagesRef(index)}>\n <MessageBubble\n {...props}\n customCssClasses={cssClasses.messageBubbleCssClasses}\n message={message}\n linkTarget={linkTarget}\n onLinkClick={onLinkClick}\n />\n </div>\n ))}\n {loading && (\n <div className=\"flex\">\n <LoadingDots />\n {retry && (\n <p className=\"text-slate-500 text-[13px] font-bold\">\n {retryText}\n </p>\n )}\n </div>\n )}\n </div>\n </div>\n <div className={cssClasses.inputContainer}>\n {suggestions && (\n <MessageSuggestions\n stream={stream}\n onSend={onSend}\n onRetry={onRetry}\n handleError={handleError}\n suggestions={suggestions}\n customCssClasses={cssClasses.messageSuggestionClasses}\n />\n )}\n <ChatInput\n {...props}\n onSend={onSend}\n onRetry={onRetry}\n customCssClasses={cssClasses.inputCssClasses}\n />\n </div>\n {footer && (\n <Markdown\n content={footer}\n linkClickEvent=\"WEBSITE\"\n linkTarget={linkTarget}\n onLinkClick={onLinkClick}\n customCssClasses={footerCssClasses}\n />\n )}\n </div>\n </div>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ChatPanel.mjs","sources":["../../../../src/components/ChatPanel.tsx"],"sourcesContent":["import React, {\n ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n useLayoutEffect,\n} from \"react\";\nimport { useChatState } from \"@yext/chat-headless-react\";\nimport {\n MessageBubble,\n MessageBubbleCssClasses,\n MessageBubbleProps,\n} from \"./MessageBubble\";\nimport { ChatInput, ChatInputCssClasses, ChatInputProps } from \"./ChatInput\";\nimport { LoadingDots } from \"./LoadingDots\";\nimport { useComposedCssClasses } from \"../hooks\";\nimport { withStylelessCssClasses } from \"../utils/withStylelessCssClasses\";\nimport { useReportAnalyticsEvent } from \"../hooks/useReportAnalyticsEvent\";\nimport { useFetchInitialMessage } from \"../hooks/useFetchInitialMessage\";\nimport {\n MessageSuggestionCssClasses,\n MessageSuggestions,\n} from \"./MessageSuggestions\";\nimport { Markdown, MarkdownCssClasses } from \"./Markdown\";\n\n/**\n * The CSS class interface for the {@link ChatPanel} component.\n *\n * @public\n */\nexport interface ChatPanelCssClasses {\n container?: string;\n messagesContainer?: string;\n messagesScrollContainer?: string;\n inputContainer?: string;\n inputCssClasses?: ChatInputCssClasses;\n messageBubbleCssClasses?: MessageBubbleCssClasses;\n messageSuggestionClasses?: MessageSuggestionCssClasses;\n footer?: string;\n}\n\nconst builtInCssClasses: ChatPanelCssClasses = withStylelessCssClasses(\n \"Panel\",\n {\n container: \"h-full w-full flex flex-col relative shadow-2xl bg-white\",\n messagesScrollContainer: \"flex flex-col mt-auto overflow-hidden\",\n messagesContainer:\n \"flex flex-col gap-y-1 px-4 overflow-auto [&>*:first-child]:mt-3\",\n inputContainer: \"w-full p-4\",\n messageBubbleCssClasses: {\n topContainer: \"mt-1\",\n },\n footer: \"text-center text-slate-400 rounded-b-3xl px-4 pb-4 text-[12px]\",\n }\n);\n\n/**\n * The props for the {@link ChatPanel} component.\n *\n * @public\n */\nexport interface ChatPanelProps\n extends Omit<MessageBubbleProps, \"customCssClasses\" | \"message\">,\n Omit<ChatInputProps, \"customCssClasses\"> {\n /** A header to render at the top of the panel. */\n header?: ReactNode;\n /** A footer markdown string to render at the bottom of the panel. */\n footer?: string;\n /**\n * CSS classes for customizing the component styling.\n */\n customCssClasses?: ChatPanelCssClasses;\n /**\n * A set of pre-written initial messages that the user\n * can click on instead of typing their own.\n */\n messageSuggestions?: string[];\n /** Link target open behavior on click.\n * Defaults to \"_blank\".\n */\n linkTarget?: string;\n /** A callback which is called when user clicks a link. */\n onLinkClick?: (href?: string) => void;\n /**\n * Text to display when retrying.\n * Defaults to \"Error occurred. Retrying\".\n */\n retryText?: string;\n}\n\n/**\n * A component that renders a full panel for chat bot interactions. This includes\n * the message bubbles for the conversation, input box with send button, and header\n * (if provided).\n *\n * @public\n *\n * @param props - {@link ChatPanelProps}\n */\nexport function ChatPanel(props: ChatPanelProps) {\n const {\n header,\n footer,\n customCssClasses,\n stream,\n handleError,\n messageSuggestions,\n linkTarget = \"_blank\",\n onLinkClick,\n onSend: onSendProp,\n onRetry: onRetryProp,\n retryText = \"Error occurred. Retrying\",\n } = props;\n const messages = useChatState((state) => state.conversation.messages);\n const loading = useChatState((state) => state.conversation.isLoading);\n const suggestedReplies = useChatState(\n (state) => state.conversation.notes?.suggestedReplies\n );\n const conversationId = useChatState(\n (state) => state.conversation.conversationId\n );\n const cssClasses = useComposedCssClasses(builtInCssClasses, customCssClasses);\n const reportAnalyticsEvent = useReportAnalyticsEvent();\n useFetchInitialMessage(handleError, stream);\n\n const [retry, setRetry] = useState(false);\n const onSend = useCallback(\n (message: string) => {\n onSendProp?.(message);\n setRetry(false);\n },\n [onSendProp]\n );\n\n const onRetry = useCallback(\n (e: unknown) => {\n onRetryProp?.(e);\n setRetry(true);\n },\n [onRetryProp]\n );\n\n useEffect(() => {\n reportAnalyticsEvent({\n action: \"CHAT_IMPRESSION\",\n });\n }, [reportAnalyticsEvent]);\n\n const suggestions = useMemo(() => {\n if (\n messages.length === 0 ||\n (messages.length === 1 && messages[0].source === \"BOT\")\n ) {\n return messageSuggestions;\n }\n return suggestedReplies;\n }, [messages, suggestedReplies, messageSuggestions]);\n\n const messagesRef = useRef<Array<HTMLDivElement | null>>([]);\n const messagesContainer = useRef<HTMLDivElement>(null);\n\n // State to help detect initial messages rendering\n const [initialMessagesLength] = useState(messages.length);\n\n const savedPanelState = useMemo(() => {\n if (!conversationId) {\n return {};\n }\n return loadSessionState(conversationId);\n }, [conversationId]);\n\n // Handle scrolling when messages change\n useEffect(() => {\n const isInitialRender = messages.length === initialMessagesLength;\n let scrollPos = 0;\n if (isInitialRender && savedPanelState.scrollPosition !== undefined) {\n // memorized position\n scrollPos = savedPanelState?.scrollPosition;\n } else {\n messagesRef.current = messagesRef.current.slice(0, messages.length);\n // Sums up scroll heights of all messages except the last one\n if (messagesRef?.current.length > 1) {\n // position of the top of the last message\n scrollPos = messagesRef.current\n .slice(0, -1)\n .map((elem, _) => elem?.scrollHeight ?? 0)\n .reduce((total, height) => total + height);\n }\n }\n\n messagesContainer.current?.scroll({\n top: scrollPos,\n behavior: \"smooth\",\n });\n }, [messages, initialMessagesLength, savedPanelState.scrollPosition]);\n\n const setMessagesRef = useCallback((index) => {\n if (!messagesRef?.current) return null;\n return (message) => (messagesRef.current[index] = message);\n }, []);\n\n const footerCssClasses: MarkdownCssClasses = useMemo(\n () => ({\n container: cssClasses.footer,\n link: \"cursor-pointer hover:underline text-blue-600\",\n }),\n [cssClasses]\n );\n\n useLayoutEffect(() => {\n const curr = messagesContainer.current;\n const onScroll = () => {\n if (!conversationId) {\n return;\n }\n saveSessionState(conversationId, {\n scrollPosition: curr?.scrollTop,\n });\n };\n curr?.addEventListener(\"scroll\", onScroll);\n return () => {\n curr?.removeEventListener(\"scroll\", onScroll);\n };\n }, [messagesContainer, conversationId]);\n\n return (\n <div className=\"yext-chat w-full h-full\">\n <div className={cssClasses.container}>\n {header}\n <div className={cssClasses.messagesScrollContainer}>\n <div\n ref={messagesContainer}\n className={cssClasses.messagesContainer}\n aria-label=\"Chat Panel Messages Container\"\n >\n {messages.map((message, index) => (\n <div key={index} ref={setMessagesRef(index)}>\n <MessageBubble\n {...props}\n customCssClasses={cssClasses.messageBubbleCssClasses}\n message={message}\n linkTarget={linkTarget}\n onLinkClick={onLinkClick}\n />\n </div>\n ))}\n {loading && (\n <div className=\"flex\">\n <LoadingDots />\n {retry && (\n <p className=\"text-slate-500 text-[13px] font-bold\">\n {retryText}\n </p>\n )}\n </div>\n )}\n </div>\n </div>\n <div className={cssClasses.inputContainer}>\n {suggestions && (\n <MessageSuggestions\n stream={stream}\n onSend={onSend}\n onRetry={onRetry}\n handleError={handleError}\n suggestions={suggestions}\n customCssClasses={cssClasses.messageSuggestionClasses}\n />\n )}\n <ChatInput\n {...props}\n onSend={onSend}\n onRetry={onRetry}\n customCssClasses={cssClasses.inputCssClasses}\n />\n </div>\n {footer && (\n <Markdown\n content={footer}\n linkClickEvent=\"WEBSITE\"\n linkTarget={linkTarget}\n onLinkClick={onLinkClick}\n customCssClasses={footerCssClasses}\n />\n )}\n </div>\n </div>\n );\n}\n\nconst BASE_STATE_LOCAL_STORAGE_KEY = \"yext_chat_panel_state\";\n\nexport function getStateLocalStorageKey(\n hostname: string,\n conversationId: string\n): string {\n return `${BASE_STATE_LOCAL_STORAGE_KEY}__${hostname}__${conversationId}`;\n}\n\n/**\n * Maintains the panel state of the session.\n */\nexport interface PanelState {\n /** The scroll position of the panel. */\n scrollPosition?: number;\n}\n\n/**\n * Loads the {@link PanelState} from local storage.\n */\nexport const loadSessionState = (conversationId: string): PanelState => {\n const hostname = window?.location?.hostname;\n if (!localStorage || !hostname) {\n return {};\n }\n const savedState = localStorage.getItem(\n getStateLocalStorageKey(hostname, conversationId)\n );\n\n if (savedState) {\n try {\n const parsedState: PanelState = JSON.parse(savedState);\n return parsedState;\n } catch (e) {\n console.warn(\"Unabled to load saved panel state: error parsing state.\");\n localStorage.removeItem(\n getStateLocalStorageKey(hostname, conversationId)\n );\n }\n }\n\n return {};\n};\n\nexport const saveSessionState = (conversationId: string, state: PanelState) => {\n const hostname = window?.location?.hostname;\n if (!localStorage || !hostname) {\n return;\n }\n localStorage.setItem(\n getStateLocalStorageKey(hostname, conversationId),\n JSON.stringify(state)\n );\n};\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yext/chat-ui-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "A library of React Components for powering Yext Chat integrations.",
|
|
5
5
|
"author": "clippy@yext.com",
|
|
6
6
|
"main": "./lib/commonjs/src/index.js",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"@types/jest": "^29.5.1",
|
|
71
71
|
"@types/react": "^18.2.7",
|
|
72
72
|
"@yext/chat-headless-react": "^0.9.1",
|
|
73
|
-
"@yext/eslint-config": "^1.0.
|
|
73
|
+
"@yext/eslint-config": "^1.0.2",
|
|
74
74
|
"babel-jest": "^29.5.0",
|
|
75
75
|
"eslint": "^8.39.0",
|
|
76
76
|
"eslint-plugin-storybook": "^0.6.12",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"prettier": "^2.8.8",
|
|
85
85
|
"react": "^18.2.0",
|
|
86
86
|
"react-dom": "^18.2.0",
|
|
87
|
-
"rollup": "^3.
|
|
87
|
+
"rollup": "^3.29.5",
|
|
88
88
|
"rollup-plugin-typescript2": "^0.35.0",
|
|
89
89
|
"storybook": "^7.5.2",
|
|
90
90
|
"tailwindcss": "^3.3.2",
|
|
@@ -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 {
|
|
@@ -117,6 +118,9 @@ export function ChatPanel(props: ChatPanelProps) {
|
|
|
117
118
|
const suggestedReplies = useChatState(
|
|
118
119
|
(state) => state.conversation.notes?.suggestedReplies
|
|
119
120
|
);
|
|
121
|
+
const conversationId = useChatState(
|
|
122
|
+
(state) => state.conversation.conversationId
|
|
123
|
+
);
|
|
120
124
|
const cssClasses = useComposedCssClasses(builtInCssClasses, customCssClasses);
|
|
121
125
|
const reportAnalyticsEvent = useReportAnalyticsEvent();
|
|
122
126
|
useFetchInitialMessage(handleError, stream);
|
|
@@ -157,25 +161,40 @@ export function ChatPanel(props: ChatPanelProps) {
|
|
|
157
161
|
const messagesRef = useRef<Array<HTMLDivElement | null>>([]);
|
|
158
162
|
const messagesContainer = useRef<HTMLDivElement>(null);
|
|
159
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
|
+
|
|
160
174
|
// Handle scrolling when messages change
|
|
161
175
|
useEffect(() => {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
+
}
|
|
171
191
|
}
|
|
172
192
|
|
|
173
|
-
// Scroll to the top of the last message
|
|
174
193
|
messagesContainer.current?.scroll({
|
|
175
|
-
top:
|
|
194
|
+
top: scrollPos,
|
|
176
195
|
behavior: "smooth",
|
|
177
196
|
});
|
|
178
|
-
}, [messages]);
|
|
197
|
+
}, [messages, initialMessagesLength, savedPanelState.scrollPosition]);
|
|
179
198
|
|
|
180
199
|
const setMessagesRef = useCallback((index) => {
|
|
181
200
|
if (!messagesRef?.current) return null;
|
|
@@ -190,12 +209,32 @@ export function ChatPanel(props: ChatPanelProps) {
|
|
|
190
209
|
[cssClasses]
|
|
191
210
|
);
|
|
192
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
|
+
|
|
193
228
|
return (
|
|
194
229
|
<div className="yext-chat w-full h-full">
|
|
195
230
|
<div className={cssClasses.container}>
|
|
196
231
|
{header}
|
|
197
232
|
<div className={cssClasses.messagesScrollContainer}>
|
|
198
|
-
<div
|
|
233
|
+
<div
|
|
234
|
+
ref={messagesContainer}
|
|
235
|
+
className={cssClasses.messagesContainer}
|
|
236
|
+
aria-label="Chat Panel Messages Container"
|
|
237
|
+
>
|
|
199
238
|
{messages.map((message, index) => (
|
|
200
239
|
<div key={index} ref={setMessagesRef(index)}>
|
|
201
240
|
<MessageBubble
|
|
@@ -250,3 +289,58 @@ export function ChatPanel(props: ChatPanelProps) {
|
|
|
250
289
|
</div>
|
|
251
290
|
);
|
|
252
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
|
+
};
|