@yext/chat-ui-react 0.7.2 → 0.8.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.
Files changed (45) hide show
  1. package/README.md +10 -2
  2. package/lib/bundle.css +1 -1
  3. package/lib/commonjs/package.json.js +1 -1
  4. package/lib/commonjs/src/components/ChatPanel.d.ts.map +1 -1
  5. package/lib/commonjs/src/components/ChatPanel.js +3 -14
  6. package/lib/commonjs/src/components/ChatPanel.js.map +1 -1
  7. package/lib/commonjs/src/components/ChatPopUp.d.ts +31 -4
  8. package/lib/commonjs/src/components/ChatPopUp.d.ts.map +1 -1
  9. package/lib/commonjs/src/components/ChatPopUp.js +58 -10
  10. package/lib/commonjs/src/components/ChatPopUp.js.map +1 -1
  11. package/lib/commonjs/src/components/InitialMessagePopUp.d.ts +33 -0
  12. package/lib/commonjs/src/components/InitialMessagePopUp.d.ts.map +1 -0
  13. package/lib/commonjs/src/components/InitialMessagePopUp.js +44 -0
  14. package/lib/commonjs/src/components/InitialMessagePopUp.js.map +1 -0
  15. package/lib/commonjs/src/components/index.d.ts +2 -1
  16. package/lib/commonjs/src/components/index.d.ts.map +1 -1
  17. package/lib/commonjs/src/hooks/useFetchInitialMessage.d.ts +13 -0
  18. package/lib/commonjs/src/hooks/useFetchInitialMessage.d.ts.map +1 -0
  19. package/lib/commonjs/src/hooks/useFetchInitialMessage.js +53 -0
  20. package/lib/commonjs/src/hooks/useFetchInitialMessage.js.map +1 -0
  21. package/lib/esm/index.d.ts +42 -4
  22. package/lib/esm/package.json.mjs +1 -1
  23. package/lib/esm/src/components/ChatPanel.d.ts.map +1 -1
  24. package/lib/esm/src/components/ChatPanel.mjs +4 -15
  25. package/lib/esm/src/components/ChatPanel.mjs.map +1 -1
  26. package/lib/esm/src/components/ChatPopUp.d.ts +31 -4
  27. package/lib/esm/src/components/ChatPopUp.d.ts.map +1 -1
  28. package/lib/esm/src/components/ChatPopUp.mjs +58 -10
  29. package/lib/esm/src/components/ChatPopUp.mjs.map +1 -1
  30. package/lib/esm/src/components/InitialMessagePopUp.d.ts +33 -0
  31. package/lib/esm/src/components/InitialMessagePopUp.d.ts.map +1 -0
  32. package/lib/esm/src/components/InitialMessagePopUp.mjs +38 -0
  33. package/lib/esm/src/components/InitialMessagePopUp.mjs.map +1 -0
  34. package/lib/esm/src/components/index.d.ts +2 -1
  35. package/lib/esm/src/components/index.d.ts.map +1 -1
  36. package/lib/esm/src/hooks/useFetchInitialMessage.d.ts +13 -0
  37. package/lib/esm/src/hooks/useFetchInitialMessage.d.ts.map +1 -0
  38. package/lib/esm/src/hooks/useFetchInitialMessage.mjs +51 -0
  39. package/lib/esm/src/hooks/useFetchInitialMessage.mjs.map +1 -0
  40. package/package.json +2 -2
  41. package/src/components/ChatPanel.tsx +5 -22
  42. package/src/components/ChatPopUp.tsx +159 -33
  43. package/src/components/InitialMessagePopUp.tsx +76 -0
  44. package/src/components/index.ts +3 -2
  45. package/src/hooks/useFetchInitialMessage.ts +58 -0
@@ -0,0 +1,51 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { useChatActions, useChatState } from '@yext/chat-headless-react';
3
+ import { useDefaultHandleApiError } from './useDefaultHandleApiError.mjs';
4
+
5
+ /**
6
+ * Sends a request to Chat API to fetch the initial message when the
7
+ * conversation first start or when the message history is reset.
8
+ *
9
+ * @internal
10
+ *
11
+ * @param handleError - A function which is called when an error occurs while fetching for initial message.
12
+ * By default, the error is logged to the console and an error message is added to state.
13
+ * @param stream - Enable streaming behavior by making a request to Chat Streaming API. Defaults to false.
14
+ * @param customCondition - additional condition for when to fetch initial message
15
+ */
16
+ function useFetchInitialMessage(handleError, stream = false, customCondition = true) {
17
+ const chat = useChatActions();
18
+ const defaultHandleApiError = useDefaultHandleApiError();
19
+ const messages = useChatState((state) => state.conversation.messages);
20
+ const [fetchInitialMessage, setFetchInitialMessage] = useState(messages.length === 0);
21
+ const [messagesLength, setMessagesLength] = useState(messages.length);
22
+ const canSendMessage = useChatState((state) => state.conversation.canSendMessage);
23
+ //handle message history resets
24
+ useEffect(() => {
25
+ const newMessagesLength = messages.length;
26
+ // Fetch data only when the conversation messages changes from non-zero to zero
27
+ if (messagesLength > 0 && newMessagesLength === 0) {
28
+ setFetchInitialMessage(true);
29
+ }
30
+ setMessagesLength(newMessagesLength);
31
+ }, [messages.length, messagesLength]);
32
+ useEffect(() => {
33
+ if (!fetchInitialMessage || !canSendMessage || !customCondition) {
34
+ return;
35
+ }
36
+ setFetchInitialMessage(false);
37
+ const res = stream ? chat.streamNextMessage() : chat.getNextMessage();
38
+ res.catch((e) => (handleError ? handleError(e) : defaultHandleApiError(e)));
39
+ }, [
40
+ chat,
41
+ stream,
42
+ handleError,
43
+ defaultHandleApiError,
44
+ fetchInitialMessage,
45
+ canSendMessage,
46
+ customCondition,
47
+ ]);
48
+ }
49
+
50
+ export { useFetchInitialMessage };
51
+ //# sourceMappingURL=useFetchInitialMessage.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useFetchInitialMessage.mjs","sources":["../../../../src/hooks/useFetchInitialMessage.ts"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport { useChatState, useChatActions } from \"@yext/chat-headless-react\";\nimport { useDefaultHandleApiError } from \"../hooks/useDefaultHandleApiError\";\n\n/**\n * Sends a request to Chat API to fetch the initial message when the\n * conversation first start or when the message history is reset.\n *\n * @internal\n *\n * @param handleError - A function which is called when an error occurs while fetching for initial message.\n * By default, the error is logged to the console and an error message is added to state.\n * @param stream - Enable streaming behavior by making a request to Chat Streaming API. Defaults to false.\n * @param customCondition - additional condition for when to fetch initial message\n */\nexport function useFetchInitialMessage(\n handleError?: (e: unknown) => void,\n stream = false,\n customCondition = true\n) {\n const chat = useChatActions();\n const defaultHandleApiError = useDefaultHandleApiError();\n const messages = useChatState((state) => state.conversation.messages);\n const [fetchInitialMessage, setFetchInitialMessage] = useState(\n messages.length === 0\n );\n const [messagesLength, setMessagesLength] = useState(messages.length);\n const canSendMessage = useChatState(\n (state) => state.conversation.canSendMessage\n );\n\n //handle message history resets\n useEffect(() => {\n const newMessagesLength = messages.length;\n // Fetch data only when the conversation messages changes from non-zero to zero\n if (messagesLength > 0 && newMessagesLength === 0) {\n setFetchInitialMessage(true);\n }\n setMessagesLength(newMessagesLength);\n }, [messages.length, messagesLength]);\n\n useEffect(() => {\n if (!fetchInitialMessage || !canSendMessage || !customCondition) {\n return;\n }\n setFetchInitialMessage(false);\n const res = stream ? chat.streamNextMessage() : chat.getNextMessage();\n res.catch((e) => (handleError ? handleError(e) : defaultHandleApiError(e)));\n }, [\n chat,\n stream,\n handleError,\n defaultHandleApiError,\n fetchInitialMessage,\n canSendMessage,\n customCondition,\n ]);\n}\n"],"names":[],"mappings":";;;;AAIA;;;;;;;;;;AAUG;AACG,SAAU,sBAAsB,CACpC,WAAkC,EAClC,MAAM,GAAG,KAAK,EACd,eAAe,GAAG,IAAI,EAAA;AAEtB,IAAA,MAAM,IAAI,GAAG,cAAc,EAAE,CAAC;AAC9B,IAAA,MAAM,qBAAqB,GAAG,wBAAwB,EAAE,CAAC;AACzD,IAAA,MAAM,QAAQ,GAAG,YAAY,CAAC,CAAC,KAAK,KAAK,KAAK,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;AACtE,IAAA,MAAM,CAAC,mBAAmB,EAAE,sBAAsB,CAAC,GAAG,QAAQ,CAC5D,QAAQ,CAAC,MAAM,KAAK,CAAC,CACtB,CAAC;AACF,IAAA,MAAM,CAAC,cAAc,EAAE,iBAAiB,CAAC,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AACtE,IAAA,MAAM,cAAc,GAAG,YAAY,CACjC,CAAC,KAAK,KAAK,KAAK,CAAC,YAAY,CAAC,cAAc,CAC7C,CAAC;;IAGF,SAAS,CAAC,MAAK;AACb,QAAA,MAAM,iBAAiB,GAAG,QAAQ,CAAC,MAAM,CAAC;;AAE1C,QAAA,IAAI,cAAc,GAAG,CAAC,IAAI,iBAAiB,KAAK,CAAC,EAAE;YACjD,sBAAsB,CAAC,IAAI,CAAC,CAAC;AAC9B,SAAA;QACD,iBAAiB,CAAC,iBAAiB,CAAC,CAAC;KACtC,EAAE,CAAC,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC;IAEtC,SAAS,CAAC,MAAK;QACb,IAAI,CAAC,mBAAmB,IAAI,CAAC,cAAc,IAAI,CAAC,eAAe,EAAE;YAC/D,OAAO;AACR,SAAA;QACD,sBAAsB,CAAC,KAAK,CAAC,CAAC;AAC9B,QAAA,MAAM,GAAG,GAAG,MAAM,GAAG,IAAI,CAAC,iBAAiB,EAAE,GAAG,IAAI,CAAC,cAAc,EAAE,CAAC;QACtE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,WAAW,GAAG,WAAW,CAAC,CAAC,CAAC,GAAG,qBAAqB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC9E,KAAC,EAAE;QACD,IAAI;QACJ,MAAM;QACN,WAAW;QACX,qBAAqB;QACrB,mBAAmB;QACnB,cAAc;QACd,eAAe;AAChB,KAAA,CAAC,CAAC;AACL;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yext/chat-ui-react",
3
- "version": "0.7.2",
3
+ "version": "0.8.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",
@@ -33,7 +33,7 @@
33
33
  "lint": "prettier --write . && eslint --fix --max-warnings=0 .",
34
34
  "test": "jest --config=jest.config.json",
35
35
  "storybook": "storybook dev -p 6006",
36
- "dev": "tsc --watch -p tsconfig.json",
36
+ "dev": "rollup --watch --config rollup.config.mjs",
37
37
  "generate-docs": "api-extractor run --local --verbose && api-documenter markdown --input-folder temp --output-folder docs && rm -rf temp",
38
38
  "generate-notices": "generate-license-file --input package.json --output ./THIRD-PARTY-NOTICES --overwrite",
39
39
  "postcss": "postcss",
@@ -1,5 +1,5 @@
1
1
  import React, { useCallback, useEffect, useRef } from "react";
2
- import { useChatState, useChatActions } from "@yext/chat-headless-react";
2
+ import { useChatState } from "@yext/chat-headless-react";
3
3
  import {
4
4
  MessageBubble,
5
5
  MessageBubbleCssClasses,
@@ -8,9 +8,9 @@ import {
8
8
  import { ChatInput, ChatInputCssClasses, ChatInputProps } from "./ChatInput";
9
9
  import { LoadingDots } from "./LoadingDots";
10
10
  import { useComposedCssClasses } from "../hooks";
11
- import { useDefaultHandleApiError } from "../hooks/useDefaultHandleApiError";
12
11
  import { withStylelessCssClasses } from "../utils/withStylelessCssClasses";
13
12
  import { useReportAnalyticsEvent } from "../hooks/useReportAnalyticsEvent";
13
+ import { useFetchInitialMessage } from "../hooks/useFetchInitialMessage";
14
14
 
15
15
  /**
16
16
  * The CSS class interface for the {@link ChatPanel} component.
@@ -65,16 +65,12 @@ export interface ChatPanelProps
65
65
  * @param props - {@link ChatPanelProps}
66
66
  */
67
67
  export function ChatPanel(props: ChatPanelProps) {
68
- const { header, customCssClasses } = props;
69
- const chat = useChatActions();
68
+ const { header, customCssClasses, stream, handleError } = props;
70
69
  const messages = useChatState((state) => state.conversation.messages);
71
70
  const loading = useChatState((state) => state.conversation.isLoading);
72
- const canSendMessage = useChatState(
73
- (state) => state.conversation.canSendMessage
74
- );
75
71
  const cssClasses = useComposedCssClasses(builtInCssClasses, customCssClasses);
76
- const defaultHandleApiError = useDefaultHandleApiError();
77
72
  const reportAnalyticsEvent = useReportAnalyticsEvent();
73
+ useFetchInitialMessage(handleError, stream);
78
74
 
79
75
  useEffect(() => {
80
76
  reportAnalyticsEvent({
@@ -82,16 +78,6 @@ export function ChatPanel(props: ChatPanelProps) {
82
78
  });
83
79
  }, [reportAnalyticsEvent]);
84
80
 
85
- // Fetch the first message on load, if there are no existing messages or a request being processed
86
- useEffect(() => {
87
- if (messages.length !== 0 || !canSendMessage) {
88
- return;
89
- }
90
- const { stream = false, handleError } = props;
91
- const res = stream ? chat.streamNextMessage() : chat.getNextMessage();
92
- res.catch((e) => (handleError ? handleError(e) : defaultHandleApiError(e)));
93
- }, [chat, props, messages, defaultHandleApiError, canSendMessage]);
94
-
95
81
  const messagesRef = useRef<Array<HTMLDivElement | null>>([]);
96
82
  const messagesContainer = useRef<HTMLDivElement>(null);
97
83
 
@@ -125,10 +111,7 @@ export function ChatPanel(props: ChatPanelProps) {
125
111
  <div className={cssClasses.container}>
126
112
  {header}
127
113
  <div className={cssClasses.messagesScrollContainer}>
128
- <div
129
- ref={messagesContainer}
130
- className={cssClasses.messagesContainer}
131
- >
114
+ <div ref={messagesContainer} className={cssClasses.messagesContainer}>
132
115
  {messages.map((message, index) => (
133
116
  <div key={index} ref={setMessagesRef(index)}>
134
117
  <MessageBubble
@@ -10,6 +10,12 @@ import { twMerge } from "tailwind-merge";
10
10
  import { useComposedCssClasses } from "../hooks";
11
11
  import { withStylelessCssClasses } from "../utils/withStylelessCssClasses";
12
12
  import { useReportAnalyticsEvent } from "../hooks/useReportAnalyticsEvent";
13
+ import {
14
+ InitialMessagePopUp,
15
+ InitialMessagePopUpCssClasses,
16
+ } from "./InitialMessagePopUp";
17
+ import { useChatState } from "@yext/chat-headless-react";
18
+ import { useFetchInitialMessage } from "../hooks/useFetchInitialMessage";
13
19
 
14
20
  /**
15
21
  * The CSS class interface for the {@link ChatPopUp} component.
@@ -22,11 +28,16 @@ export interface ChatPopUpCssClasses {
22
28
  panel__display?: string;
23
29
  panel__hidden?: string;
24
30
  button?: string;
25
- button__display?: string;
26
- button__hidden?: string;
27
31
  buttonIcon?: string;
32
+ ctaLabelContainer?: string;
33
+ ctaLabel?: string;
34
+ notification?: string;
35
+ closedPopupContainer?: string;
36
+ closedPopupContainer__display?: string;
37
+ closedPopupContainer__hidden?: string;
28
38
  headerCssClasses?: ChatHeaderCssClasses;
29
39
  panelCssClasses?: ChatPanelCssClasses;
40
+ initialMessagePopUpCssClasses?: InitialMessagePopUpCssClasses;
30
41
  }
31
42
 
32
43
  const fixedPosition = "fixed bottom-6 right-4 lg:bottom-14 lg:right-10 z-50 ";
@@ -39,13 +50,20 @@ const builtInCssClasses: ChatPopUpCssClasses = withStylelessCssClasses(
39
50
  "w-80 max-[480px]:right-0 max-[480px]:bottom-0 max-[480px]:w-full max-[480px]:h-full lg:w-96 h-[75vh]",
40
51
  panel__display: "duration-300 translate-y-0",
41
52
  panel__hidden: "duration-300 translate-y-[20%] opacity-0 invisible",
42
- button:
53
+ closedPopupContainer:
43
54
  fixedPosition +
44
- "p-2 w-12 h-12 lg:w-16 lg:h-16 flex justify-center items-center text-white shadow-xl rounded-full bg-gradient-to-br from-blue-600 to-blue-700 hover:-translate-y-2 duration-150",
45
- button__display: "duration-300 transform translate-y-0",
46
- button__hidden:
55
+ "flex gap-x-2.5 items-center hover:-translate-y-2 duration-150",
56
+ closedPopupContainer__display: "duration-300 transform translate-y-0",
57
+ closedPopupContainer__hidden:
47
58
  "duration-300 transform translate-y-[20%] opacity-0 invisible",
59
+ button:
60
+ "p-2 w-12 h-12 lg:w-16 lg:h-16 flex justify-center items-center text-white shadow-xl rounded-full bg-gradient-to-br from-blue-600 to-blue-700",
48
61
  buttonIcon: "text-blue-600 w-[28px] h-[28px] lg:w-[40px] lg:h-[40px]",
62
+ ctaLabelContainer: "max-w-60 -mr-8 line-clamp-1",
63
+ ctaLabel:
64
+ "p-3 pr-8 flex items-center whitespace-nowrap animate-expand-left font-bold rounded-l-full bg-white text-blue-700 h-10 lg:h-14 text-sm lg:text-base",
65
+ notification:
66
+ "fixed animate-fade-in bg-red-700 -right-1 top-0 rounded-full w-5 lg:w-6 h-5 lg:h-6 items-center flex justify-center text-sm lg:text-base text-white",
49
67
  headerCssClasses: {
50
68
  container: "max-[480px]:rounded-none rounded-t-3xl",
51
69
  },
@@ -67,10 +85,31 @@ export interface ChatPopUpProps
67
85
  Omit<ChatPanelProps, "header" | "customCssClasses"> {
68
86
  /** Custom icon for the popup button to open the panel. */
69
87
  openPanelButtonIcon?: JSX.Element;
88
+ /** CSS classes for customizing the component styling. */
89
+ customCssClasses?: ChatPopUpCssClasses;
90
+ /** Whether to show the panel on load. Defaults to false. */
91
+ openOnLoad?: boolean;
70
92
  /**
71
- * CSS classes for customizing the component styling.
93
+ * Whether to show the initial message popup when the panel is hidden on load.
94
+ * Defaults to false.
72
95
  */
73
- customCssClasses?: ChatPopUpCssClasses;
96
+ showInitialMessagePopUp?: boolean;
97
+ /**
98
+ * Whether to show a heartbeat animation on the popup button when the panel is hidden.
99
+ * Defaults to false.
100
+ */
101
+ showHeartBeatAnimation?: boolean;
102
+ /**
103
+ * Whether to show notification showing number of unread messages.
104
+ * Defaults to false.
105
+ */
106
+ showUnreadNotification?: boolean;
107
+ /**
108
+ * The "Call to Action" label to be displayed next to the popup button.
109
+ * By default, the CTA is not shown.
110
+ * This prop will override the "showInitialMessagePopUp" prop, if specified.
111
+ */
112
+ ctaLabel?: string;
74
113
  }
75
114
 
76
115
  /**
@@ -87,63 +126,150 @@ export function ChatPopUp(props: ChatPopUpProps) {
87
126
  customCssClasses,
88
127
  showRestartButton = true,
89
128
  onClose: customOnClose,
129
+ handleError,
130
+ openOnLoad = false,
131
+ showInitialMessagePopUp = false,
132
+ showHeartBeatAnimation = false,
133
+ showUnreadNotification = false,
134
+ ctaLabel,
90
135
  title,
91
136
  } = props;
92
- const reportAnalyticsEvent = useReportAnalyticsEvent();
93
137
 
138
+ const reportAnalyticsEvent = useReportAnalyticsEvent();
94
139
  useEffect(() => {
95
140
  reportAnalyticsEvent({
96
141
  action: "CHAT_IMPRESSION",
97
142
  });
98
143
  }, [reportAnalyticsEvent]);
99
144
 
145
+ const messages = useChatState((s) => s.conversation.messages);
146
+ const [numReadMessages, setNumReadMessagesLength] = useState<number>(0);
147
+ const [numUnreadMessages, setNumUnreadMessagesLength] = useState<number>(0);
148
+
149
+ useFetchInitialMessage(
150
+ showInitialMessagePopUp ? console.error : handleError,
151
+ false,
152
+ showUnreadNotification || showInitialMessagePopUp
153
+ );
154
+
155
+ const [showInitialMessage, setshowInitialMessage] = useState(
156
+ //only show initial message popup (if specified) when CTA label is not provided
157
+ !ctaLabel && showInitialMessagePopUp
158
+ );
159
+ const onCloseInitialMessage = useCallback(() => {
160
+ setshowInitialMessage(false);
161
+ }, []);
162
+
163
+ // control CSS behavior on open/close state of the panel
100
164
  const [showChat, setShowChat] = useState(false);
165
+
166
+ // control the actual DOM rendering of the panel. Start rendering on first open state
167
+ // to avoid message requests immediately on load while the popup is still "hidden"
168
+ const [renderChat, setRenderChat] = useState(false);
169
+
170
+ // update in useEffect, instead of having openOnLoad as initial state for show/renderChat,
171
+ // in order to maintain the fade-in CSS animation when opening the panel on load
172
+ useEffect(() => {
173
+ if (openOnLoad) {
174
+ setShowChat(true);
175
+ setRenderChat(true);
176
+ setshowInitialMessage(false);
177
+ }
178
+ }, [openOnLoad]);
179
+
101
180
  const onClick = useCallback(() => {
102
181
  setShowChat(!showChat);
182
+ setRenderChat(true);
183
+ setshowInitialMessage(false);
103
184
  }, [showChat]);
104
185
 
105
186
  const onClose = useCallback(() => {
106
187
  setShowChat(false);
107
188
  customOnClose?.();
108
- }, [customOnClose]);
189
+ // consider all the messages are read while the panel was open
190
+ setNumReadMessagesLength(messages.length);
191
+ }, [customOnClose, messages]);
192
+
193
+ useEffect(() => {
194
+ //update number of unread messages if there are new messages added while the panel is closed
195
+ setNumUnreadMessagesLength(messages.length - numReadMessages);
196
+ }, [messages, numReadMessages]);
109
197
 
110
198
  const cssClasses = useComposedCssClasses(builtInCssClasses, customCssClasses);
111
199
  const panelCssClasses = twMerge(
112
200
  cssClasses.panel,
113
201
  showChat ? cssClasses.panel__display : cssClasses.panel__hidden
114
202
  );
115
- const buttonCssClasses = twMerge(
116
- cssClasses.button,
117
- showChat ? cssClasses.button__hidden : cssClasses.button__display
203
+ const closedPopupContainerCssClasses = twMerge(
204
+ cssClasses.closedPopupContainer,
205
+ showChat
206
+ ? cssClasses.closedPopupContainer__hidden
207
+ : cssClasses.closedPopupContainer__display
118
208
  );
119
209
 
120
210
  return (
121
211
  <div className="yext-chat w-full h-full">
122
212
  <div className={cssClasses.container}>
123
213
  <div className={panelCssClasses} aria-label="Chat Popup Panel">
124
- <ChatPanel
125
- {...props}
126
- customCssClasses={cssClasses.panelCssClasses}
127
- header={
128
- <ChatHeader
129
- title={title}
130
- showRestartButton={showRestartButton}
131
- showCloseButton={true}
132
- onClose={onClose}
133
- customCssClasses={cssClasses.headerCssClasses}
134
- />
135
- }
136
- />
214
+ {renderChat && (
215
+ <ChatPanel
216
+ {...props}
217
+ customCssClasses={cssClasses.panelCssClasses}
218
+ header={
219
+ <ChatHeader
220
+ title={title}
221
+ showRestartButton={showRestartButton}
222
+ showCloseButton={true}
223
+ onClose={onClose}
224
+ customCssClasses={cssClasses.headerCssClasses}
225
+ />
226
+ }
227
+ />
228
+ )}
137
229
  </div>
138
- <button
139
- aria-label="Chat Popup Button"
140
- onClick={onClick}
141
- className={buttonCssClasses}
230
+ <div
231
+ className={closedPopupContainerCssClasses}
232
+ aria-label="Chat Closed Popup Container"
142
233
  >
143
- {openPanelButtonIcon ?? (
144
- <ChatIcon className={cssClasses.buttonIcon} />
234
+ {showInitialMessage && (
235
+ <InitialMessagePopUp
236
+ onClose={onCloseInitialMessage}
237
+ customCssClasses={cssClasses.initialMessagePopUpCssClasses}
238
+ />
145
239
  )}
146
- </button>
240
+ {ctaLabel && (
241
+ // the div container is needed to islate the expand CSS animation
242
+ <div className={cssClasses.ctaLabelContainer}>
243
+ <button
244
+ onClick={onClick}
245
+ aria-label="CTA Label"
246
+ className={cssClasses.ctaLabel}
247
+ >
248
+ {ctaLabel}
249
+ </button>
250
+ </div>
251
+ )}
252
+ <button
253
+ aria-label="Chat Popup Button"
254
+ onClick={onClick}
255
+ className={
256
+ cssClasses.button +
257
+ (showHeartBeatAnimation ? " animate-heartbeat" : "")
258
+ }
259
+ >
260
+ {openPanelButtonIcon ?? (
261
+ <ChatIcon className={cssClasses.buttonIcon} />
262
+ )}
263
+ {showUnreadNotification && !!numUnreadMessages && (
264
+ <div
265
+ aria-label="Unread Messages Notification"
266
+ className={cssClasses.notification}
267
+ >
268
+ {numUnreadMessages}
269
+ </div>
270
+ )}
271
+ </button>
272
+ </div>
147
273
  </div>
148
274
  </div>
149
275
  );
@@ -0,0 +1,76 @@
1
+ import React, { useMemo } from "react";
2
+ import { CrossIcon } from "../icons/Cross";
3
+ import { useComposedCssClasses } from "../hooks";
4
+ import { withStylelessCssClasses } from "../utils/withStylelessCssClasses";
5
+ import { MessageSource, useChatState } from "@yext/chat-headless-react";
6
+
7
+ /**
8
+ * The CSS class interface for the InitialMessagePopUp component.
9
+ *
10
+ * @public
11
+ */
12
+ export interface InitialMessagePopUpCssClasses {
13
+ container?: string;
14
+ closeButton?: string;
15
+ closeButtonIcon?: string;
16
+ message?: string;
17
+ }
18
+
19
+ /**
20
+ * The props for the {@link InitialMessagePopUp} component.
21
+ *
22
+ * @internal
23
+ */
24
+ interface InitialMessagePopUpProps {
25
+ /** CSS classes for customizing the component styling. */
26
+ customCssClasses?: InitialMessagePopUpCssClasses;
27
+ /** Function to call when user click on the close button */
28
+ onClose: () => void;
29
+ }
30
+
31
+ const builtInCssClasses: InitialMessagePopUpCssClasses =
32
+ withStylelessCssClasses("InitialMessagePopUp", {
33
+ container: "flex gap-x-1 animate-fade-in",
34
+ closeButton: "bg-white w-4 h-4 rounded-full border border-slate-300",
35
+ closeButtonIcon: "",
36
+ message:
37
+ "line-clamp-2 w-60 p-2.5 bg-white rounded-xl shadow-xl text-sm lg:text-base",
38
+ });
39
+
40
+ /**
41
+ * A component that renders a popup bubble displaying the initial message from chat bot.
42
+ *
43
+ * @internal
44
+ *
45
+ * @param props - {@link InitialMessagePopUpProps}
46
+ */
47
+ export function InitialMessagePopUp({
48
+ onClose,
49
+ customCssClasses,
50
+ }: InitialMessagePopUpProps) {
51
+ const messages = useChatState((s) => s.conversation.messages);
52
+ const cssClasses = useComposedCssClasses(builtInCssClasses, customCssClasses);
53
+
54
+ const firstBotMessage: string = useMemo(() => {
55
+ return messages.length !== 0 && messages[0].source === MessageSource.BOT
56
+ ? messages[0].text
57
+ : "";
58
+ }, [messages]);
59
+
60
+ if (firstBotMessage.length === 0) {
61
+ return <></>;
62
+ }
63
+
64
+ return (
65
+ <div className={cssClasses.container}>
66
+ <button
67
+ aria-label="Close Initial Message"
68
+ onClick={onClose}
69
+ className={cssClasses.closeButton}
70
+ >
71
+ <CrossIcon className={cssClasses.closeButtonIcon} />
72
+ </button>
73
+ <div className={cssClasses.message}>{firstBotMessage}</div>
74
+ </div>
75
+ );
76
+ }
@@ -10,10 +10,11 @@ export type {
10
10
  MessageBubbleProps,
11
11
  } from "./MessageBubble";
12
12
 
13
- export type { FeedbackButtonsCssClasses } from "./FeedbackButtons";
14
-
15
13
  export { ChatPanel } from "./ChatPanel";
16
14
  export type { ChatPanelCssClasses, ChatPanelProps } from "./ChatPanel";
17
15
 
18
16
  export { ChatPopUp } from "./ChatPopUp";
19
17
  export type { ChatPopUpCssClasses, ChatPopUpProps } from "./ChatPopUp";
18
+
19
+ export type { FeedbackButtonsCssClasses } from "./FeedbackButtons";
20
+ export type { InitialMessagePopUpCssClasses } from "./InitialMessagePopUp";
@@ -0,0 +1,58 @@
1
+ import { useEffect, useState } from "react";
2
+ import { useChatState, useChatActions } from "@yext/chat-headless-react";
3
+ import { useDefaultHandleApiError } from "../hooks/useDefaultHandleApiError";
4
+
5
+ /**
6
+ * Sends a request to Chat API to fetch the initial message when the
7
+ * conversation first start or when the message history is reset.
8
+ *
9
+ * @internal
10
+ *
11
+ * @param handleError - A function which is called when an error occurs while fetching for initial message.
12
+ * By default, the error is logged to the console and an error message is added to state.
13
+ * @param stream - Enable streaming behavior by making a request to Chat Streaming API. Defaults to false.
14
+ * @param customCondition - additional condition for when to fetch initial message
15
+ */
16
+ export function useFetchInitialMessage(
17
+ handleError?: (e: unknown) => void,
18
+ stream = false,
19
+ customCondition = true
20
+ ) {
21
+ const chat = useChatActions();
22
+ const defaultHandleApiError = useDefaultHandleApiError();
23
+ const messages = useChatState((state) => state.conversation.messages);
24
+ const [fetchInitialMessage, setFetchInitialMessage] = useState(
25
+ messages.length === 0
26
+ );
27
+ const [messagesLength, setMessagesLength] = useState(messages.length);
28
+ const canSendMessage = useChatState(
29
+ (state) => state.conversation.canSendMessage
30
+ );
31
+
32
+ //handle message history resets
33
+ useEffect(() => {
34
+ const newMessagesLength = messages.length;
35
+ // Fetch data only when the conversation messages changes from non-zero to zero
36
+ if (messagesLength > 0 && newMessagesLength === 0) {
37
+ setFetchInitialMessage(true);
38
+ }
39
+ setMessagesLength(newMessagesLength);
40
+ }, [messages.length, messagesLength]);
41
+
42
+ useEffect(() => {
43
+ if (!fetchInitialMessage || !canSendMessage || !customCondition) {
44
+ return;
45
+ }
46
+ setFetchInitialMessage(false);
47
+ const res = stream ? chat.streamNextMessage() : chat.getNextMessage();
48
+ res.catch((e) => (handleError ? handleError(e) : defaultHandleApiError(e)));
49
+ }, [
50
+ chat,
51
+ stream,
52
+ handleError,
53
+ defaultHandleApiError,
54
+ fetchInitialMessage,
55
+ canSendMessage,
56
+ customCondition,
57
+ ]);
58
+ }