@yext/chat-ui-react 0.11.4 → 0.12.1
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 +39 -10
- 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 +5 -5
- package/src/components/ChatPanel.tsx +107 -13
package/THIRD-PARTY-NOTICES
CHANGED
|
@@ -62,7 +62,7 @@ SOFTWARE.
|
|
|
62
62
|
The following NPM packages may be included in this product:
|
|
63
63
|
|
|
64
64
|
- @types/hast@2.3.4
|
|
65
|
-
- @types/hoist-non-react-statics@3.3.
|
|
65
|
+
- @types/hoist-non-react-statics@3.3.6
|
|
66
66
|
- @types/mdast@3.0.11
|
|
67
67
|
- @types/parse5@5.0.3
|
|
68
68
|
- @types/prop-types@15.7.5
|
|
@@ -100,7 +100,7 @@ MIT License
|
|
|
100
100
|
|
|
101
101
|
The following NPM package may be included in this product:
|
|
102
102
|
|
|
103
|
-
- @yext/analytics@0.
|
|
103
|
+
- @yext/analytics@1.0.1
|
|
104
104
|
|
|
105
105
|
This package contains the following license and notice below:
|
|
106
106
|
|
|
@@ -141,9 +141,9 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
141
141
|
|
|
142
142
|
The following NPM packages may be included in this product:
|
|
143
143
|
|
|
144
|
-
- @yext/chat-core@0.
|
|
145
|
-
- @yext/chat-headless-react@0.9.
|
|
146
|
-
- @yext/chat-headless@0.
|
|
144
|
+
- @yext/chat-core@0.9.1
|
|
145
|
+
- @yext/chat-headless-react@0.9.5
|
|
146
|
+
- @yext/chat-headless@0.12.2
|
|
147
147
|
|
|
148
148
|
These packages each contain the following license and notice below:
|
|
149
149
|
|
|
@@ -277,7 +277,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
|
277
277
|
|
|
278
278
|
The following NPM package may be included in this product:
|
|
279
279
|
|
|
280
|
-
- cross-fetch@3.
|
|
280
|
+
- cross-fetch@3.2.0
|
|
281
281
|
|
|
282
282
|
This package contains the following license and notice below:
|
|
283
283
|
|
|
@@ -798,7 +798,7 @@ THE SOFTWARE.
|
|
|
798
798
|
|
|
799
799
|
The following NPM package may be included in this product:
|
|
800
800
|
|
|
801
|
-
- layerr@
|
|
801
|
+
- layerr@3.0.0
|
|
802
802
|
|
|
803
803
|
This package contains the following license and notice below:
|
|
804
804
|
|
|
@@ -1083,7 +1083,7 @@ SOFTWARE.
|
|
|
1083
1083
|
|
|
1084
1084
|
The following NPM package may be included in this product:
|
|
1085
1085
|
|
|
1086
|
-
- node-fetch@2.
|
|
1086
|
+
- node-fetch@2.7.0
|
|
1087
1087
|
|
|
1088
1088
|
This package contains the following license and notice below:
|
|
1089
1089
|
|
|
@@ -1207,7 +1207,6 @@ The following NPM packages may be included in this product:
|
|
|
1207
1207
|
- react-is@18.2.0
|
|
1208
1208
|
- react@18.2.0
|
|
1209
1209
|
- scheduler@0.23.0
|
|
1210
|
-
- use-sync-external-store@1.2.2
|
|
1211
1210
|
|
|
1212
1211
|
These packages each contain the following license and notice below:
|
|
1213
1212
|
|
|
@@ -1754,7 +1753,7 @@ THE SOFTWARE.
|
|
|
1754
1753
|
|
|
1755
1754
|
The following NPM package may be included in this product:
|
|
1756
1755
|
|
|
1757
|
-
- ulidx@2.
|
|
1756
|
+
- ulidx@2.4.1
|
|
1758
1757
|
|
|
1759
1758
|
This package contains the following license and notice below:
|
|
1760
1759
|
|
|
@@ -1928,6 +1927,36 @@ A React helper hook for storing latest value in ref object (updated in useEffect
|
|
|
1928
1927
|
|
|
1929
1928
|
-----------
|
|
1930
1929
|
|
|
1930
|
+
The following NPM package may be included in this product:
|
|
1931
|
+
|
|
1932
|
+
- use-sync-external-store@1.4.0
|
|
1933
|
+
|
|
1934
|
+
This package contains the following license and notice below:
|
|
1935
|
+
|
|
1936
|
+
MIT License
|
|
1937
|
+
|
|
1938
|
+
Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
1939
|
+
|
|
1940
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
1941
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
1942
|
+
in the Software without restriction, including without limitation the rights
|
|
1943
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
1944
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
1945
|
+
furnished to do so, subject to the following conditions:
|
|
1946
|
+
|
|
1947
|
+
The above copyright notice and this permission notice shall be included in all
|
|
1948
|
+
copies or substantial portions of the Software.
|
|
1949
|
+
|
|
1950
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
1951
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
1952
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
1953
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
1954
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
1955
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
1956
|
+
SOFTWARE.
|
|
1957
|
+
|
|
1958
|
+
-----------
|
|
1959
|
+
|
|
1931
1960
|
The following NPM package may be included in this product:
|
|
1932
1961
|
|
|
1933
1962
|
- vfile-message@2.0.4
|
|
@@ -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.1",
|
|
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",
|
|
@@ -69,8 +69,8 @@
|
|
|
69
69
|
"@testing-library/user-event": "^14.4.3",
|
|
70
70
|
"@types/jest": "^29.5.1",
|
|
71
71
|
"@types/react": "^18.2.7",
|
|
72
|
-
"@yext/chat-headless-react": "^0.9.
|
|
73
|
-
"@yext/eslint-config": "^1.0.
|
|
72
|
+
"@yext/chat-headless-react": "^0.9.5",
|
|
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,14 +84,14 @@
|
|
|
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",
|
|
91
91
|
"typescript": "^5.0.4"
|
|
92
92
|
},
|
|
93
93
|
"peerDependencies": {
|
|
94
|
-
"@yext/chat-headless-react": "^0.9.
|
|
94
|
+
"@yext/chat-headless-react": "^0.9.5",
|
|
95
95
|
"react": "^16.14 || ^17 || ^18",
|
|
96
96
|
"react-dom": "^16.14 || ^17 || || ^18"
|
|
97
97
|
},
|
|
@@ -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
|
+
};
|