diff --git a/THIRD-PARTY-NOTICES b/THIRD-PARTY-NOTICES index 60c54a9..6bc17ac 100644 --- a/THIRD-PARTY-NOTICES +++ b/THIRD-PARTY-NOTICES @@ -31,7 +31,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. The following NPM package may be included in this product: - - @reduxjs/toolkit@1.9.5 + - @reduxjs/toolkit@1.9.7 This package contains the following license and notice below: @@ -62,6 +62,7 @@ SOFTWARE. The following NPM packages may be included in this product: - @types/hast@2.3.4 + - @types/hoist-non-react-statics@3.3.5 - @types/mdast@3.0.11 - @types/parse5@5.0.3 - @types/prop-types@15.7.5 @@ -99,37 +100,7 @@ MIT License The following NPM package may be included in this product: - - @types/hoist-non-react-statics@3.3.1 - -This package contains the following license and notice below: - -MIT License - - Copyright (c) Microsoft Corporation. All rights reserved. - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE - ------------ - -The following NPM package may be included in this product: - - - @yext/analytics@0.6.4 + - @yext/analytics@0.6.6 This package contains the following license and notice below: @@ -170,9 +141,9 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. The following NPM packages may be included in this product: - - @yext/chat-core@0.7.6 - - @yext/chat-headless-react@0.6.1 - - @yext/chat-headless@0.7.1 + - @yext/chat-core@0.8.0 + - @yext/chat-headless-react@0.7.0 + - @yext/chat-headless@0.8.0 These packages each contain the following license and notice below: @@ -827,7 +798,7 @@ THE SOFTWARE. The following NPM package may be included in this product: - - layerr@2.0.1 + - layerr@2.1.0 This package contains the following license and notice below: @@ -1236,7 +1207,7 @@ The following NPM packages may be included in this product: - react-is@18.2.0 - react@18.2.0 - scheduler@0.23.0 - - use-sync-external-store@1.2.0 + - use-sync-external-store@1.2.2 These packages each contain the following license and notice below: @@ -1296,31 +1267,31 @@ SOFTWARE. The following NPM packages may be included in this product: - - react-redux@8.1.2 - - redux@4.2.1 + - react-redux@8.1.3 + - redux-thunk@2.4.2 These packages each contain the following license and notice below: -The MIT License (MIT) - -Copyright (c) 2015-present Dan Abramov - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +The MIT License (MIT) + +Copyright (c) 2015-present Dan Abramov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------- @@ -1356,30 +1327,30 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. The following NPM package may be included in this product: - - redux-thunk@2.4.2 + - redux@4.2.1 This package contains the following license and notice below: -The MIT License (MIT) - -Copyright (c) 2015-present Dan Abramov - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +The MIT License (MIT) + +Copyright (c) 2015-present Dan Abramov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------- @@ -1783,7 +1754,7 @@ THE SOFTWARE. The following NPM package may be included in this product: - - ulidx@2.1.0 + - ulidx@2.3.0 This package contains the following license and notice below: diff --git a/docs/chat-ui-react.chatinput.md b/docs/chat-ui-react.chatinput.md index 868725d..c528c0f 100644 --- a/docs/chat-ui-react.chatinput.md +++ b/docs/chat-ui-react.chatinput.md @@ -9,14 +9,14 @@ A component that allows user to input message and send to Chat API. **Signature:** ```typescript -export declare function ChatInput({ placeholder, stream, inputAutoFocus, handleError, sendButtonIcon, customCssClasses, onSend, }: ChatInputProps): React.JSX.Element; +export declare function ChatInput({ placeholder, stream, inputAutoFocus, handleError, sendButtonIcon, customCssClasses, onSend, onRetry, }: ChatInputProps): React.JSX.Element; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| { placeholder, stream, inputAutoFocus, handleError, sendButtonIcon, customCssClasses, onSend, } | [ChatInputProps](./chat-ui-react.chatinputprops.md) | | +| { placeholder, stream, inputAutoFocus, handleError, sendButtonIcon, customCssClasses, onSend, onRetry, } | [ChatInputProps](./chat-ui-react.chatinputprops.md) | | **Returns:** diff --git a/docs/chat-ui-react.chatinputprops.md b/docs/chat-ui-react.chatinputprops.md index 28ba19e..afc8ea1 100644 --- a/docs/chat-ui-react.chatinputprops.md +++ b/docs/chat-ui-react.chatinputprops.md @@ -19,6 +19,7 @@ export interface ChatInputProps | [customCssClasses?](./chat-ui-react.chatinputprops.customcssclasses.md) | | [ChatInputCssClasses](./chat-ui-react.chatinputcssclasses.md) | _(Optional)_ CSS classes for customizing the component styling. | | [handleError?](./chat-ui-react.chatinputprops.handleerror.md) | | (e: unknown) => void | _(Optional)_ A function which is called when an error occurs from Chat API while processing the user's message. By default, the error is logged to the console and an error message is added to state. | | [inputAutoFocus?](./chat-ui-react.chatinputprops.inputautofocus.md) | | boolean | _(Optional)_ Enable auto focus for the input box. Defaults to false. | +| [onRetry?](./chat-ui-react.chatinputprops.onretry.md) | | (e: unknown) => void | _(Optional)_ A function which is called when a retryable error occurs from Chat API while processing the user's message. | | [onSend?](./chat-ui-react.chatinputprops.onsend.md) | | (message: string) => void | _(Optional)_ A callback which is called when user sends a message. | | [placeholder?](./chat-ui-react.chatinputprops.placeholder.md) | | string | _(Optional)_ The input's placeholder text when no text has been entered by the user. Defaults to "Type a message...". | | [sendButtonIcon?](./chat-ui-react.chatinputprops.sendbuttonicon.md) | | JSX.Element | _(Optional)_ Custom icon for the send button. | diff --git a/docs/chat-ui-react.chatinputprops.onretry.md b/docs/chat-ui-react.chatinputprops.onretry.md new file mode 100644 index 0000000..538a9ea --- /dev/null +++ b/docs/chat-ui-react.chatinputprops.onretry.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/chat-ui-react](./chat-ui-react.md) > [ChatInputProps](./chat-ui-react.chatinputprops.md) > [onRetry](./chat-ui-react.chatinputprops.onretry.md) + +## ChatInputProps.onRetry property + +A function which is called when a retryable error occurs from Chat API while processing the user's message. + +**Signature:** + +```typescript +onRetry?: (e: unknown) => void; +``` diff --git a/docs/chat-ui-react.chatpanelprops.md b/docs/chat-ui-react.chatpanelprops.md index c8e498d..89f5b59 100644 --- a/docs/chat-ui-react.chatpanelprops.md +++ b/docs/chat-ui-react.chatpanelprops.md @@ -22,4 +22,5 @@ export interface ChatPanelProps extends Omit + +[Home](./index.md) > [@yext/chat-ui-react](./chat-ui-react.md) > [ChatPanelProps](./chat-ui-react.chatpanelprops.md) > [retryText](./chat-ui-react.chatpanelprops.retrytext.md) + +## ChatPanelProps.retryText property + +Text to display when retrying. Defaults to "Error occurred. Retrying". + +**Signature:** + +```typescript +retryText?: string; +``` diff --git a/docs/chat-ui-react.md b/docs/chat-ui-react.md index c7d4b8c..e3db0ab 100644 --- a/docs/chat-ui-react.md +++ b/docs/chat-ui-react.md @@ -9,7 +9,7 @@ | Function | Description | | --- | --- | | [ChatHeader({ title, showRestartButton, restartButtonIcon, showCloseButton, closeButtonIcon, onClose, customCssClasses, })](./chat-ui-react.chatheader.md) | A component that renders the header of a chat bot panel, including the title and a button to reset the conversation. | -| [ChatInput({ placeholder, stream, inputAutoFocus, handleError, sendButtonIcon, customCssClasses, onSend, })](./chat-ui-react.chatinput.md) | A component that allows user to input message and send to Chat API. | +| [ChatInput({ placeholder, stream, inputAutoFocus, handleError, sendButtonIcon, customCssClasses, onSend, onRetry, })](./chat-ui-react.chatinput.md) | A component that allows user to input message and send to Chat API. | | [ChatPanel(props)](./chat-ui-react.chatpanel.md) | A component that renders a full panel for chat bot interactions. This includes the message bubbles for the conversation, input box with send button, and header (if provided). | | [ChatPopUp(props)](./chat-ui-react.chatpopup.md) | A component that renders a popup button that displays and hides a panel for chat bot interactions. | | [MessageBubble({ message, showFeedbackButtons, showTimestamp, customCssClasses, formatTimestamp, onLinkClick, })](./chat-ui-react.messagebubble.md) | A component that displays the provided message. | diff --git a/docs/chat-ui-react.messagesuggestionsprops.md b/docs/chat-ui-react.messagesuggestionsprops.md index 4937230..5ae2699 100644 --- a/docs/chat-ui-react.messagesuggestionsprops.md +++ b/docs/chat-ui-react.messagesuggestionsprops.md @@ -18,5 +18,8 @@ export interface MessageSuggestionsProps | --- | --- | --- | --- | | [customCssClasses?](./chat-ui-react.messagesuggestionsprops.customcssclasses.md) | | [MessageSuggestionCssClasses](./chat-ui-react.messagesuggestioncssclasses.md) | _(Optional)_ CSS classes for customizing the component styling. | | [handleError?](./chat-ui-react.messagesuggestionsprops.handleerror.md) | | (e: unknown) => void | _(Optional)_ A function which is called when an error occurs from Chat API while processing the user's message. By default, the error is logged to the console and an error message is added to state. | +| [onRetry?](./chat-ui-react.messagesuggestionsprops.onretry.md) | | (e: unknown) => void | _(Optional)_ A function which is called when a retryable error occurs from Chat API while processing the user's message. | +| [onSend?](./chat-ui-react.messagesuggestionsprops.onsend.md) | | (message: string) => void | _(Optional)_ A callback which is called when user sends a message. | +| [stream?](./chat-ui-react.messagesuggestionsprops.stream.md) | | boolean | _(Optional)_ Enable streaming behavior by making a request to Chat Streaming API. This feature is experimental, and is subject to change. Defaults to false. | | [suggestions](./chat-ui-react.messagesuggestionsprops.suggestions.md) | | string\[\] | List of clickable message suggestions to render. | diff --git a/docs/chat-ui-react.messagesuggestionsprops.onretry.md b/docs/chat-ui-react.messagesuggestionsprops.onretry.md new file mode 100644 index 0000000..0defef1 --- /dev/null +++ b/docs/chat-ui-react.messagesuggestionsprops.onretry.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/chat-ui-react](./chat-ui-react.md) > [MessageSuggestionsProps](./chat-ui-react.messagesuggestionsprops.md) > [onRetry](./chat-ui-react.messagesuggestionsprops.onretry.md) + +## MessageSuggestionsProps.onRetry property + +A function which is called when a retryable error occurs from Chat API while processing the user's message. + +**Signature:** + +```typescript +onRetry?: (e: unknown) => void; +``` diff --git a/docs/chat-ui-react.messagesuggestionsprops.onsend.md b/docs/chat-ui-react.messagesuggestionsprops.onsend.md new file mode 100644 index 0000000..cdc8b26 --- /dev/null +++ b/docs/chat-ui-react.messagesuggestionsprops.onsend.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/chat-ui-react](./chat-ui-react.md) > [MessageSuggestionsProps](./chat-ui-react.messagesuggestionsprops.md) > [onSend](./chat-ui-react.messagesuggestionsprops.onsend.md) + +## MessageSuggestionsProps.onSend property + +A callback which is called when user sends a message. + +**Signature:** + +```typescript +onSend?: (message: string) => void; +``` diff --git a/docs/chat-ui-react.messagesuggestionsprops.stream.md b/docs/chat-ui-react.messagesuggestionsprops.stream.md new file mode 100644 index 0000000..4f832b0 --- /dev/null +++ b/docs/chat-ui-react.messagesuggestionsprops.stream.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/chat-ui-react](./chat-ui-react.md) > [MessageSuggestionsProps](./chat-ui-react.messagesuggestionsprops.md) > [stream](./chat-ui-react.messagesuggestionsprops.stream.md) + +## MessageSuggestionsProps.stream property + +Enable streaming behavior by making a request to Chat Streaming API. This feature is experimental, and is subject to change. Defaults to false. + +**Signature:** + +```typescript +stream?: boolean; +``` diff --git a/etc/chat-ui-react.api.md b/etc/chat-ui-react.api.md index ac3f7f7..ec48788 100644 --- a/etc/chat-ui-react.api.md +++ b/etc/chat-ui-react.api.md @@ -40,7 +40,7 @@ export interface ChatHeaderProps { } // @public -export function ChatInput({ placeholder, stream, inputAutoFocus, handleError, sendButtonIcon, customCssClasses, onSend, }: ChatInputProps): React_2.JSX.Element; +export function ChatInput({ placeholder, stream, inputAutoFocus, handleError, sendButtonIcon, customCssClasses, onSend, onRetry, }: ChatInputProps): React_2.JSX.Element; // @public export interface ChatInputCssClasses { @@ -57,6 +57,7 @@ export interface ChatInputProps { customCssClasses?: ChatInputCssClasses; handleError?: (e: unknown) => void; inputAutoFocus?: boolean; + onRetry?: (e: unknown) => void; onSend?: (message: string) => void; placeholder?: string; sendButtonIcon?: JSX.Element; @@ -93,6 +94,7 @@ export interface ChatPanelProps extends Omit void; + retryText?: string; } // @public @@ -230,6 +232,9 @@ export interface MessageSuggestionCssClasses { export interface MessageSuggestionsProps { customCssClasses?: MessageSuggestionCssClasses; handleError?: (e: unknown) => void; + onRetry?: (e: unknown) => void; + onSend?: (message: string) => void; + stream?: boolean; suggestions: string[]; } diff --git a/package-lock.json b/package-lock.json index c1c0f4a..c92b9b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@yext/chat-ui-react", - "version": "0.8.8", + "version": "0.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@yext/chat-ui-react", - "version": "0.8.8", + "version": "0.9.0", "license": "BSD-3-Clause", "dependencies": { "react-markdown": "^6.0.3", @@ -37,7 +37,7 @@ "@testing-library/user-event": "^14.4.3", "@types/jest": "^29.5.1", "@types/react": "^18.2.7", - "@yext/chat-headless-react": "^0.6.1", + "@yext/chat-headless-react": "^0.7.0", "@yext/eslint-config": "^1.0.0", "babel-jest": "^29.5.0", "eslint": "^8.39.0", @@ -4712,9 +4712,9 @@ } }, "node_modules/@reduxjs/toolkit": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.5.tgz", - "integrity": "sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ==", + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.7.tgz", + "integrity": "sha512-t7v8ZPxhhKgOKtU+uyJT13lu4vL7az5aFi4IdoDs/eS548edn2M8Ik9h8fxgvMjGoAUVFSt6ZC1P5cWmQ014QQ==", "dev": true, "dependencies": { "immer": "^9.0.21", @@ -7491,9 +7491,9 @@ } }, "node_modules/@types/hoist-non-react-statics": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", - "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", + "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", "dev": true, "dependencies": { "@types/react": "*", @@ -8163,9 +8163,9 @@ } }, "node_modules/@yext/analytics": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/@yext/analytics/-/analytics-0.6.4.tgz", - "integrity": "sha512-cB9NJxaTmy6G2cP5BBn+vxVSetbBybsdXuBPE71dJvVjPVMzneio7TaZt3V4PGPuqrZdYMTVXjx7Kgbxep/O8A==", + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/@yext/analytics/-/analytics-0.6.6.tgz", + "integrity": "sha512-LdGE0ZIE295+p/kCW/ps9XKp5M2gn+AM5pDlqzx13nzNKxI4eyOVKtpRwRNroFldIx9sQHVK7uvyQtGshWWB9g==", "dev": true, "dependencies": { "cross-fetch": "^3.1.5", @@ -8173,33 +8173,33 @@ } }, "node_modules/@yext/chat-core": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/@yext/chat-core/-/chat-core-0.7.6.tgz", - "integrity": "sha512-rsZU4mVK6KtAwxTcABvyJIVatRfKPVk58xpFsyw7UoOUhBsT6Sz60WneVm/amvQV9qfCZrK8pljvsKuPaL7PSA==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@yext/chat-core/-/chat-core-0.8.0.tgz", + "integrity": "sha512-LTm/i3YLBLSebkQ8rxqlPRyiTNvcqLiIQEjhU8n9pSp2IZVpgqCm6dH6fJ67XZ1L1OZFjF6TeKpV1qpkuzhj+g==", "dev": true, "dependencies": { "cross-fetch": "^3.1.5" } }, "node_modules/@yext/chat-headless": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@yext/chat-headless/-/chat-headless-0.7.1.tgz", - "integrity": "sha512-jM3UCYDuBd8eANXeE/yu+wT+qtMoTWV1+Iitz9VwDgs9yRWlh2of3Svexvkq4uo4YcCnyne3aXZDkxMnYoKCjA==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@yext/chat-headless/-/chat-headless-0.8.0.tgz", + "integrity": "sha512-2KSrIpnkMOvC8anQJ/VkYhH1VNeOA4Zka5h+HU0STXldtLZ3hKSlSufd/87qR+ie1M3xgcrlZHGBHX8fB2FOyw==", "dev": true, "dependencies": { "@reduxjs/toolkit": "^1.9.5", "@yext/analytics": "^0.6.3", - "@yext/chat-core": "^0.7.0" + "@yext/chat-core": "^0.8.0" } }, "node_modules/@yext/chat-headless-react": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@yext/chat-headless-react/-/chat-headless-react-0.6.1.tgz", - "integrity": "sha512-ArmGj/h1f1y6UMBaFuzhN1ThvnmfXAu9tqQKLNpX910U3AqnCch1FtbS29TfiZjIQbmDndmBJOis7PPTziCXpg==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@yext/chat-headless-react/-/chat-headless-react-0.7.0.tgz", + "integrity": "sha512-4aYNFL2uGApSrTeX6Nf2OOoDhv6YDEfBROsMCWBBRwuo9hEbn574Z3FHr68IjNy/0v8zDid6oektrV1MEhWkrA==", "dev": true, "dependencies": { "@reduxjs/toolkit": "^1.9.5", - "@yext/chat-headless": "^0.7.1", + "@yext/chat-headless": "^0.8.0", "react-redux": "^8.0.5" }, "peerDependencies": { @@ -15944,9 +15944,9 @@ } }, "node_modules/layerr": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/layerr/-/layerr-2.0.1.tgz", - "integrity": "sha512-z0730CwG/JO24evdORnyDkwG1Q7b7mF2Tp1qRQ0YvrMMARbt1DFG694SOv439Gm7hYKolyZyaB49YIrYIfZBdg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/layerr/-/layerr-2.1.0.tgz", + "integrity": "sha512-xDD9suWxfBYeXgqffRVH/Wqh+mqZrQcqPRn0I0ijl7iJQ7vu8gMGPt1Qop59pEW/jaIDNUN7+PX1Qk40+vuflg==", "dev": true }, "node_modules/lazy-universal-dotenv": { @@ -18609,9 +18609,9 @@ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, "node_modules/react-redux": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.2.tgz", - "integrity": "sha512-xJKYI189VwfsFc4CJvHqHlDrzyFTY/3vZACbE+rr/zQ34Xx1wQfB4OTOSeOSNrF6BDVe8OOdxIrAnMGXA3ggfw==", + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.3.tgz", + "integrity": "sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==", "dev": true, "dependencies": { "@babel/runtime": "^7.12.1", @@ -20758,9 +20758,9 @@ } }, "node_modules/ulidx": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ulidx/-/ulidx-2.1.0.tgz", - "integrity": "sha512-DlMi97oP9HASI3kLCjBlOhAG1SoisUrEqC2PJ7itiFbq9q5Zo0JejupXeu2Gke99W62epNzA4MFNToNiq8A5LA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ulidx/-/ulidx-2.3.0.tgz", + "integrity": "sha512-36piWNqcdp9hKlQewyeehCaALy4lyx3FodsCxHuV6i0YdexSkjDOubwxEVr2yi4kh62L/0MgyrxqG4K+qtovnw==", "dev": true, "dependencies": { "layerr": "^2.0.1" @@ -21128,9 +21128,9 @@ "dev": true }, "node_modules/use-sync-external-store": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", - "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", + "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", "dev": true, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" diff --git a/package.json b/package.json index e6fb7fc..7fb1c31 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@yext/chat-ui-react", - "version": "0.8.8", + "version": "0.9.0", "description": "A library of React Components for powering Yext Chat integrations.", "author": "clippy@yext.com", "main": "./lib/commonjs/src/index.js", @@ -69,7 +69,7 @@ "@testing-library/user-event": "^14.4.3", "@types/jest": "^29.5.1", "@types/react": "^18.2.7", - "@yext/chat-headless-react": "^0.6.1", + "@yext/chat-headless-react": "^0.7.0", "@yext/eslint-config": "^1.0.0", "babel-jest": "^29.5.0", "eslint": "^8.39.0", diff --git a/src/components/ChatInput.tsx b/src/components/ChatInput.tsx index 5f05637..3a2cc91 100644 --- a/src/components/ChatInput.tsx +++ b/src/components/ChatInput.tsx @@ -1,10 +1,11 @@ import React, { useCallback, useState } from "react"; -import { useChatActions, useChatState } from "@yext/chat-headless-react"; +import { useChatState } from "@yext/chat-headless-react"; import { ArrowIcon } from "../icons/Arrow"; import { useComposedCssClasses } from "../hooks"; import TextareaAutosize from "react-textarea-autosize"; import { useDefaultHandleApiError } from "../hooks/useDefaultHandleApiError"; import { withStylelessCssClasses } from "../utils/withStylelessCssClasses"; +import { useSendMessageWithRetries } from "../hooks/useSendMessageWithRetries"; /** * The CSS class interface for the {@link ChatInput} component. @@ -58,6 +59,11 @@ export interface ChatInputProps { customCssClasses?: ChatInputCssClasses; /** A callback which is called when user sends a message. */ onSend?: (message: string) => void; + /** + * A function which is called when a retryable error occurs from + * Chat API while processing the user's message. + */ + onRetry?: (e: unknown) => void } /** @@ -79,27 +85,24 @@ export function ChatInput({ sendButtonIcon = , customCssClasses, onSend, + onRetry, }: ChatInputProps) { - const chat = useChatActions(); const [input, setInput] = useState(""); const canSendMessage = useChatState( (state) => state.conversation.canSendMessage ); const defaultHandleApiError = useDefaultHandleApiError(); - + const sendMessageWithRetries = useSendMessageWithRetries(stream, 1, onRetry) const cssClasses = useComposedCssClasses(builtInCssClasses, customCssClasses); const sendMessage = useCallback(async () => { - const res = stream - ? chat.streamNextMessage(input) - : chat.getNextMessage(input); setInput(""); - res - .then(() => { - onSend?.(input); + sendMessageWithRetries(input) + .catch(handleError ?? defaultHandleApiError) + .finally(() => { + onSend?.(input) }) - .catch((e) => (handleError ? handleError(e) : defaultHandleApiError(e))); - }, [chat, input, handleError, defaultHandleApiError, stream, onSend]); + }, [sendMessageWithRetries, input, handleError, defaultHandleApiError, onSend]); const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { diff --git a/src/components/ChatPanel.tsx b/src/components/ChatPanel.tsx index 5950097..562bf12 100644 --- a/src/components/ChatPanel.tsx +++ b/src/components/ChatPanel.tsx @@ -4,6 +4,7 @@ import React, { useEffect, useMemo, useRef, + useState, } from "react"; import { useChatState } from "@yext/chat-headless-react"; import { @@ -76,6 +77,11 @@ export interface ChatPanelProps messageSuggestions?: string[]; /** A callback which is called when user clicks a link. */ onLinkClick?: (href?: string) => void; + /** + * Text to display when retrying. + * Defaults to "Error occurred. Retrying". + */ + retryText?: string; } /** @@ -96,6 +102,9 @@ export function ChatPanel(props: ChatPanelProps) { handleError, messageSuggestions, onLinkClick, + onSend:onSendProp, + onRetry:onRetryProp, + retryText = "Error occurred. Retrying", } = props; const messages = useChatState((state) => state.conversation.messages); const loading = useChatState((state) => state.conversation.isLoading); @@ -106,6 +115,17 @@ export function ChatPanel(props: ChatPanelProps) { const reportAnalyticsEvent = useReportAnalyticsEvent(); useFetchInitialMessage(handleError, stream); + const [retry, setRetry] = useState(false); + const onSend = useCallback((message: string) => { + onSendProp?.(message); + setRetry(false) + }, [onSendProp]) + + const onRetry = useCallback((e: unknown) => { + onRetryProp?.(e); + setRetry(true) + }, [onRetryProp]) + useEffect(() => { reportAnalyticsEvent({ action: "CHAT_IMPRESSION", @@ -174,18 +194,29 @@ export function ChatPanel(props: ChatPanelProps) { /> ))} - {loading && } + {loading &&
+ + {retry &&

{retryText}

} +
}
{suggestions && ( )} - +
{footer && ( void; /** CSS classes for customizing the component styling. */ customCssClasses?: MessageSuggestionCssClasses; + /** {@inheritdoc ChatInputProps.stream} */ + stream?: boolean; + /** {@inheritdoc ChatInputProps.onSend} */ + onSend?: (message: string) => void; + /** {@inheritdoc ChatInputProps.onRetry} */ + onRetry?: (e: unknown) => void } const defaultClassnames: MessageSuggestionCssClasses = withStylelessCssClasses( @@ -51,10 +58,14 @@ export const MessageSuggestions: React.FC = ({ handleError, suggestions, customCssClasses, + stream = false, + onSend, + onRetry, }) => { const actions = useChatActions(); const notes = useChatState((state) => state.conversation.notes); const defaultHandleApiError = useDefaultHandleApiError(); + const sendMessageWithRetries = useSendMessageWithRetries(stream, 1, onRetry) const sendMsg = useCallback( (msg: string) => { const newNotes = { @@ -62,10 +73,13 @@ export const MessageSuggestions: React.FC = ({ suggestedReplies: undefined, } satisfies MessageNotes; actions.setMessageNotes(newNotes); - const res = actions.getNextMessage(msg); - res.catch(handleError ?? defaultHandleApiError); + sendMessageWithRetries(msg) + .catch(handleError ?? defaultHandleApiError) + .finally(() => { + onSend?.(msg) + }) }, - [actions, notes, handleError, defaultHandleApiError] + [notes, actions, sendMessageWithRetries, handleError, defaultHandleApiError, onSend] ); const classes = useComposedCssClasses(defaultClassnames, customCssClasses); diff --git a/src/hooks/useSendMessageWithRetries.ts b/src/hooks/useSendMessageWithRetries.ts new file mode 100644 index 0000000..794f151 --- /dev/null +++ b/src/hooks/useSendMessageWithRetries.ts @@ -0,0 +1,49 @@ +import { ApiError, useChatActions } from "@yext/chat-headless-react"; +import { useCallback } from "react"; + +/** + * Returns a function that sends a message to the chat API with retries + * if the API returns a 5xx status code. + * + * @remarks + * The function will throw the error if the maximum number of retries is reached + * or if the error is not a 5xx status code. + * + * @internal + * + * @param stream - If true, use streaming API + * @param maxRetries - Maximum number of retries + * @param onRetry - Callback to handle error on each retry + * + */ +export function useSendMessageWithRetries( + stream = false, + maxRetries = 0, + onRetry?: (e: unknown) => void +): (input: string) => Promise { + const chat = useChatActions() + return useCallback(async (input: string) => { + let err: unknown; + let text = input; + for (let numRetries = 0; numRetries <= maxRetries; numRetries++) { + if (numRetries > 0 && !!err) { + if (err instanceof ApiError && !!err.statusCode && err.statusCode >= 500) { + onRetry?.(err) + // avoid re-adding user message to conversation history on retry + text = ""; + } else { + throw err; + } + } + try { + await (stream + ? chat.streamNextMessage(text) + : chat.getNextMessage(text)); + return; + } catch (e) { + err = e; + } + } + throw err + }, [chat, maxRetries, onRetry, stream]) +} diff --git a/test-site/package-lock.json b/test-site/package-lock.json index 6a1178b..14e41b7 100644 --- a/test-site/package-lock.json +++ b/test-site/package-lock.json @@ -23,7 +23,7 @@ }, "..": { "name": "@yext/chat-ui-react", - "version": "0.8.8", + "version": "0.9.0", "license": "BSD-3-Clause", "dependencies": { "react-markdown": "^6.0.3", @@ -54,7 +54,7 @@ "@testing-library/user-event": "^14.4.3", "@types/jest": "^29.5.1", "@types/react": "^18.2.7", - "@yext/chat-headless-react": "^0.6.1", + "@yext/chat-headless-react": "^0.7.0", "@yext/eslint-config": "^1.0.0", "babel-jest": "^29.5.0", "eslint": "^8.39.0", diff --git a/tests/hooks/useSendMessageWithRetries.test.ts b/tests/hooks/useSendMessageWithRetries.test.ts new file mode 100644 index 0000000..ac05f0c --- /dev/null +++ b/tests/hooks/useSendMessageWithRetries.test.ts @@ -0,0 +1,64 @@ +import { useSendMessageWithRetries } from "../../src/hooks/useSendMessageWithRetries"; +import { renderHook } from "@testing-library/react"; +import { mockChatActions, spyOnActions } from "../__utils__/mocks"; +import { ApiError } from "@yext/chat-headless-react"; + +const ApiInternalServerError = new ApiError("API Internal Error", 500) +const ApiBadRequestError = new ApiError("API Bad Request Error", 400) + +it("failed request with no retries on 5XX error", async () => { + mockChatActions({ + getNextMessage: jest.fn(() => Promise.reject(ApiInternalServerError)), + }); + const actions = spyOnActions(); + const onRetryMock = jest.fn(); + const { result } = renderHook(() => + useSendMessageWithRetries(false, 0, onRetryMock) + ); + await expect(result.current("mock message")).rejects.toThrow(ApiInternalServerError); + expect(actions.getNextMessage).toBeCalledTimes(1); + expect(onRetryMock).toBeCalledTimes(0); +}); + +it("failed request with retries on 5XX error", async () => { + mockChatActions({ + getNextMessage: jest.fn(() => Promise.reject(ApiInternalServerError)), + }); + const actions = spyOnActions(); + const onRetryMock = jest.fn(); + const { result } = renderHook(() => + useSendMessageWithRetries(false, 1, onRetryMock) + ); + await expect(result.current("mock message")).rejects.toThrow(ApiInternalServerError); + expect(actions.getNextMessage).toBeCalledTimes(2); + expect(onRetryMock).toBeCalledTimes(1); +}); + +it("failed request with no retries on non-5XX error", async () => { + mockChatActions({ + getNextMessage: jest.fn(() => Promise.reject(ApiBadRequestError)), + }); + const actions = spyOnActions(); + const onRetryMock = jest.fn(); + const { result } = renderHook(() => + useSendMessageWithRetries(false, 1, onRetryMock) + ); + await expect(result.current("mock message")).rejects.toThrow(ApiBadRequestError); + expect(actions.getNextMessage).toBeCalledTimes(1); + expect(onRetryMock).toBeCalledTimes(0); +}); + +it("successful request with no retries", async () => { + mockChatActions({ + getNextMessage: jest.fn(() => Promise.resolve()), + }); + const actions = spyOnActions(); + const onRetryMock = jest.fn(); + const { result } = renderHook(() => + useSendMessageWithRetries(false, 1, onRetryMock) + ); + await expect(result.current("mock message")).resolves.toEqual(undefined); + expect(actions.getNextMessage).toBeCalledTimes(1); + expect(onRetryMock).toBeCalledTimes(0); +}); +