├── .babelrc ├── .eslintrc ├── .gitignore ├── .mocharc.yml ├── .npmignore ├── .nycrc ├── .prettierrc ├── .reuse └── dep5 ├── CHANGELOG.md ├── LICENSE ├── LICENSES └── MIT.txt ├── README.md ├── assets ├── .gitkeep ├── header-webchat.png ├── header.png ├── webchat-600.gif └── webchat-github.png ├── bin ├── build-dev.js ├── build-lib.js └── build.js ├── package-lock.json ├── package.json ├── src ├── actions │ ├── channel.js │ ├── conversation.js │ └── messages.js ├── components │ ├── Button │ │ ├── index.js │ │ └── style.scss │ ├── Expander │ │ ├── index.js │ │ └── style.scss │ ├── Header │ │ ├── index.js │ │ └── style.scss │ ├── Input │ │ ├── index.js │ │ └── style.scss │ ├── Live │ │ ├── index.js │ │ └── style.scss │ ├── Menu │ │ ├── index.js │ │ └── style.scss │ ├── Message │ │ ├── Buttons.js │ │ ├── Card.js │ │ ├── Carousel.js │ │ ├── List.js │ │ ├── Picture.js │ │ ├── QuickReplies.js │ │ ├── Slider │ │ │ ├── index.js │ │ │ └── style.scss │ │ ├── Text.js │ │ ├── index.js │ │ ├── isTyping.js │ │ ├── style.scss │ │ ├── styleMin.scss │ │ └── styleThemeMin.scss │ ├── SendButton │ │ ├── index.js │ │ └── style.scss │ ├── arrows │ │ ├── index.js │ │ └── style.scss │ └── svgs │ │ ├── arrowLeft.js │ │ ├── arrowRight.js │ │ ├── menu.js │ │ └── style.scss ├── config.js ├── containers │ ├── App │ │ ├── index.js │ │ └── style.scss │ └── Chat │ │ ├── index.js │ │ └── style.scss ├── helpers.js ├── index.html ├── index.js ├── middlewares │ └── api.js ├── reducers │ ├── conversation.js │ ├── index.js │ ├── messages.js │ └── reducer.js ├── script.js ├── store.js └── test │ ├── actions │ ├── channels.test.js │ ├── conversation.test.js │ └── messages.test.js │ ├── codyEmulateTest.js │ ├── components │ ├── Button │ │ └── Button.test.js │ ├── Expander │ │ └── Expander.test.js │ ├── Header │ │ └── Header.test.js │ ├── Input │ │ └── Input.test.js │ ├── Live │ │ └── Live.test.js │ ├── Menu │ │ └── Menu.test.js │ ├── Message │ │ ├── Buttons.test.js │ │ ├── Card.test.js │ │ ├── Carousel.test.js │ │ ├── List.test.js │ │ ├── Message.test.js │ │ ├── Picture.test.js │ │ ├── QuickReplies.test.js │ │ ├── Slider │ │ │ └── Slider.test.js │ │ ├── Text.test.js │ │ └── isTyping.test.js │ └── arrows │ │ └── arrows.test.js │ ├── containers │ ├── App │ │ └── App.test.js │ ├── Chat │ │ └── Chat.test.js │ └── middlewares │ │ └── api.test.js │ ├── helpers.test.js │ ├── messageUtil.js │ ├── mockStore.js │ ├── preferenceUtil.js │ └── reducers │ ├── conversation.test.js │ └── messages.test.js ├── test ├── dom.js ├── errorHandle.js └── helpers.js └── webpack ├── dev.js ├── lib.js └── prod.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", { 5 | "targets": { 6 | "browsers": [">1%", "not op_mini all", "ie 11"], 7 | "node": "current" 8 | }, 9 | "useBuiltIns": "usage", 10 | "corejs": { 11 | "version": "3.1", 12 | "proposals": true 13 | }, 14 | "modules": "auto", 15 | "debug": false 16 | } 17 | ], 18 | "@babel/preset-react" 19 | ], 20 | "plugins": [ 21 | "@babel/plugin-transform-regenerator", 22 | ["@babel/plugin-transform-arrow-functions", { 23 | "spec": false 24 | }], 25 | ["@babel/plugin-proposal-decorators", { 26 | "legacy": true 27 | }], 28 | ["module-resolver", { 29 | "root": ["./src"] 30 | }], 31 | "lodash", 32 | "@babel/plugin-transform-runtime", 33 | "@babel/plugin-proposal-class-properties", 34 | "@babel/plugin-syntax-dynamic-import", 35 | ["react-intl", { 36 | "messagesDir": "./.messages", 37 | "extractSourceLocation": false 38 | }] 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "react": { 4 | "version": "16.1.1" 5 | } 6 | }, 7 | "extends": ["prettier", "prettier/react", "zavatta", "zavatta-react"], 8 | "rules": { 9 | "id-length": 0, 10 | "react/no-array-index-key": 0, 11 | "camelcase": 0, 12 | "no-unused-vars": 0, 13 | "no-inline-comments": 0, 14 | "no-console": 0, 15 | "max-nested-callbacks": 0 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | #package-lock.json 4 | dist 5 | lib 6 | .idea/ 7 | lib 8 | coverage 9 | .nyc_output 10 | -------------------------------------------------------------------------------- /.mocharc.yml: -------------------------------------------------------------------------------- 1 | # This is the Mocha config file 2 | allow-uncaught: true 3 | async-only: false 4 | bail: false 5 | check-leaks: false 6 | color: true 7 | delay: false 8 | diff: true 9 | exit: false 10 | extension: 'js' 11 | forbid-only: false 12 | forbid-pending: false 13 | full-trace: true 14 | growl: false 15 | inline-diffs: false 16 | recursive: false 17 | require: ['./test/errorHandle.js','esm','@babel/register','mock-local-storage','jsdom-global/register','ignore-styles','./test/helpers.js','./test/dom.js'] 18 | retries: 1 19 | slow: 75 20 | sort: false 21 | timeout: false # same as "no-timeout: true" or "timeout: 0" 22 | trace-warnings: true # node flags ok 23 | ui: 'bdd' 24 | v8-stack-trace-limit: 100 # V8 flags are prepended with "v8-" 25 | watch: false 26 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "lines": 80, 3 | "statements": 80, 4 | "functions": 80, 5 | "branches": 80, 6 | "exclude": ["test", "src/test", "src/__test__", "dist", "webpack", "bin"] 7 | } 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | printWidth: 100 2 | semi: false 3 | singleQuote: true 4 | trailingComma: all 5 | -------------------------------------------------------------------------------- /.reuse/dep5: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: Webchat 3 | Upstream-Contact: ospo@sap.com 4 | Source: 5 | Disclaimer: The code in this project may include calls to APIs ("API Calls") of 6 | SAP or third-party products or services developed outside of this project 7 | ("External Products"). 8 | "APIs" means application programming interfaces, as well as their respective 9 | specifications and implementing code that allows software to communicate with 10 | other software. 11 | API Calls to External Products are not licensed under the open source license 12 | that governs this project. The use of such API Calls and related External 13 | Products are subject to applicable additional agreements with the relevant 14 | provider of the External Products. In no event shall the open source license 15 | that governs this project grant any rights in or to any External Products,or 16 | alter, expand or supersede any terms of the applicable additional agreements. 17 | If you have a valid license agreement with SAP for the use of a particular SAP 18 | External Product, then you may make use of any API Calls included in this 19 | project's code for that SAP External Product, subject to the terms of such 20 | license agreement. If you do not have a valid license agreement for the use of 21 | a particular SAP External Product, then you may only make use of any API Calls 22 | in this project for that SAP External Product for your internal, non-productive 23 | and non-commercial test and evaluation of such API Calls. Nothing herein grants 24 | you any rights to use or access any SAP External Product, or provide any third 25 | parties the right to use of access any SAP External Product, through API Calls. 26 | 27 | Files: *.* 28 | Copyright: 2022 SAP SE or an SAP affiliate company and ai-core-samples contributors 29 | License: MIT -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Version 1.4.6 (July 2, 2019) 2 | 3 | - Added markdown support using react-markdown npm library 4 | 5 | ## Version 1.4.5 (Jan 16, 2019) 6 | 7 | Revert renaming className to be compatible with people who are overwritting the style. 8 | 9 | ## Version 1.4.4 (Jan 15, 2019) 10 | 11 | - Use the new domain name https://cai.tools.sap 12 | 13 | ## Version 1.4.3 (Dec 19, 2018) 14 | 15 | Improve bundle size 16 | - reduce dist bundle size by 34% (551kb). 17 | - reduce lib bundle size by 61% (527kb). 18 | 19 | Fix input height for IE11. 20 | Lodash dependency has been removed. 21 | 22 | ## Version 1.4.2 (Dec 18, 2018) 23 | 24 | Revert by adding `react-slick` dependency temporary for IE11 compatibility. 25 | 26 | ## Version 1.4.1 (Dec 11, 2018) 27 | 28 | Fix style of Slider for Quickreplies message when having not a lot items. 29 | 30 | ## Version 1.4.0 (Nov 28, 2018) 31 | 32 | Action delay behavior has been added. 33 | Bot memory is now available when using the script. 34 | Fix eslint 35 | 36 | ## Version 1.3.0 (Nov 28, 2018) 37 | 38 | The dependency `react-slick` has been removed. It was used for `carousel` and `quickreplies` messages. It is replaced by a component written ourself. 39 | 40 | Feature action delay is now supported. 41 | 42 | ## Version 1.2.0 (Sept 26, 2018) 43 | 44 | Before version 1.2.0, the placeholder text displayed in the user's input was 'Write a reply'. 45 | In versions 1.2.0 and above, it's now configurable. 46 | Available as `userInputPlaceholder` (string) in the `preferences` object fetched via `getChannelPreferences()`. 47 | 48 | ## Version 1.1.1 (Sept 21, 2018) 49 | 50 | Patch preventing the apparition of "null" in the webchat's input on Edge 51 | 52 | ## Version 1.1.0 (Sept 08, 2018) 53 | 54 | TL;DR: Improve the way buttons and quickReplies are handled by the webchat. 55 | 56 | A button or a quickReply is composed of: 57 | 58 | - a title, displayed in the webchat (ex: "Let's the show begin!") 59 | - a value, sent to the Bot Connector when the button is clicked (ex: "RANDOM_BORING_INTERNAL_VALUE") 60 | 61 | Previously, the value was sent as a text message, and appeared as such in the webchat as a reply from the user. This mean that when clicking on "Let's the show beging", a user message would appear in the webchat with the content "RANDOM_BORING_INTERNAL_VALUE". 62 | 63 | Now, when clicking on a button, both the value and the title are sent. This way, the Bot Connector still receive the value, but the webchat can display the more user-friendly title. 64 | 65 | ## Version 1.0.0 (Jun 21, 2018) 66 | 67 | The first versioned release of the webchat. 68 | 69 | Every breaking change, bug fix or improvement will be referenced here. 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 SAP Conversational AI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSES/MIT.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecation Notice 2 | 3 | This public repository is read-only and no longer maintained. 4 | 5 | ![](https://img.shields.io/badge/STATUS-NOT%20CURRENTLY%20MAINTAINED-red.svg?longCache=true&style=flat) 6 | 7 |
8 | 9 |
10 | 11 | # SAP Conversational AI Webchat 12 | | [Default Usage](#usage) | [Self-Hosted Installation](#self-hosted-webchat) | [Getting Started on SAP Conversational AI]( #getting-started-with-sap-conversational-ai) | [License](#license) | 13 | |---|---|---|---| 14 |
15 | 💬 Questions / Comments? Join the discussion on our community Slack channel! 16 |
17 | 18 | [![REUSE status](https://api.reuse.software/badge/github.com/SAP/Webchat)](https://api.reuse.software/info/github.com/SAP/Webchat) 19 | 20 | ## What is a webchat? 21 | 22 | The SAP Conversational AI webchat let you **deploy a bot straight to a website**. It will be embed and available through a chat box on your pages. 23 | The webchat is one of the many channels available on [SAP Conversational AI](https://cai.tools.sap), and end-to-end bot building platform. 24 | This webchat is built using the [React](https://github.com/facebook/react) library, along with [Redux](https://github.com/reactjs/redux) for state managment. 25 | 26 |
27 | 28 |
29 | 30 | ## Compatibility 31 | 32 | This webchat is supported by all mobile and desktop browsers in their latest versions. 33 | Internet Explorer support starts at version 9.0. 34 | 35 | ## Usage 36 | 37 | Three different installations on the webchat module are possible. 38 | - The default is the simplest and fatest route, and offers some customization options. 39 | - The self-hosted webchat offers even more customization option, but you'll have to deal with the hosting and maintenance of the module. 40 | - Use it as a React component 41 | 42 | ### Default webchat 43 | 44 | To use the webchat, you need an account on [SAP Conversational AI](https://cai.tools.sap) and a bot. 45 | Then, go to the **CONNECT** tab and click on **Webchat**. It will open a window that lets you adjust your webchat settings, including: 46 | * color scheme, 47 | * header customization, 48 | * bot and user pictures, 49 | * webchat logo and call to action, 50 | * conversation duration 51 | 52 | Once you're satisfied with the settings, click on the **SAVE** button. A script tag appears, and you just have to copy paste it in your web page to embed the webchat. The script must be placed in the `` tag. 53 | 54 |
55 | 56 |
57 | 58 | 59 | ### Self-hosted webchat 60 | 61 | If you want to customize your webchat even more, you can opt for a self-hosted installatiton. Just fork this project to get started! 62 | 63 | #### Installation 64 | 65 | Clone the repository you forked, and install the dependencies. 66 | 67 | ``` 68 | $> git clone YOUR_REPO_URL 69 | $> cd webchat 70 | $> npm install 71 | ``` 72 | 73 | #### Run in development mode 74 | 75 | ``` 76 | $> npm run start 77 | ``` 78 | 79 | #### Eslint + prettier 80 | 81 | ``` 82 | $> npm run prettier 83 | ``` 84 | 85 | #### Build for production 86 | 87 | ``` 88 | $> npm run build 89 | ``` 90 | 91 | #### Use your webchat 92 | 93 | Once you're done, build it and host it. 94 | To use it instead of the default one provided by SAP Conversational AI, you need to set up the Webchat channel in the **CONNECT** tab of your bot. 95 | You'll be using the same script as the default installation, but you have **to replace the src field by your own URL**. 96 | 97 | 98 | ``` 99 | 103 | ``` 104 | 105 | ### React component 106 | You can import the webchat as a React component like the following example: 107 | ``` js 108 | import CaiWebchat from 'webchat'; 109 | 110 | export default class ReactWebchat extends Component { 111 | render() { 112 | return ( 113 | { 115 | this.webchat = ref; 116 | }} 117 | channelId={YOUR_CHANNEL_ID} 118 | token={YOUR_TOKEN} 119 | preferences={{ 120 | accentColor: '#E05A47', 121 | complementaryColor: '#FFFFFF', 122 | botMessageColor: '#707070', 123 | botMessageBackgroundColor: '#F6F6F6', 124 | backgroundColor: '#FFFFFF', 125 | headerLogo: 'https://cdn.cai.tools.sap/webchat/webchat-logo.svg', 126 | headerTitle: 'My awesome chatbot', 127 | botPicture: 'https://cdn.cai.tools.sap/webchat/bot.png', 128 | userPicture: 'https://cdn.cai.tools.sap/webchat/user.png', 129 | onboardingMessage: 'Come speak to me!', 130 | expanderLogo: 'https://cdn.cai.tools.sap/webchat/webchat-logo.svg', 131 | expanderTitle: 'Click on me!', 132 | conversationTimeToLive: 24, 133 | openingType: 'never', 134 | welcomeMessage: 'Hello world !', 135 | }} 136 | getLastMessage={message => { 137 | console.log(message) 138 | }} 139 | /> 140 | ); 141 | } 142 | } 143 | ``` 144 | 145 | #### Props 146 | |Name|Type|Required|Description| 147 | |---|---|---|--| 148 | |onRef|function|false| Function which returns ref of the webchat| 149 | |channelId|string|true|Channel id (you can get in SAP Conversational AI)| 150 | |token|string|true|Token (you can get in React.ai)| 151 | |preferences|object|true| Object containing some settings| 152 | |getLastMessage|function|false|Function which returns the last message sent by the webchat 153 | 154 | #### Methods 155 | You can access these methods by using the reference of the component (use `OnRef`) 156 | ``` 157 | this.webchat = ref } 159 | > 160 | ... 161 | 162 | this.webchat.clearMessages(); 163 | ``` 164 | |Name|Description| 165 | |---|---| 166 | |clearMessages()|Clear all messages in the webchat| 167 | 168 | ### Bot Memory management 169 | One thing you might want to do is to send custom data from your website to the bot, like the name of the logged in user, his ID, the page he is currently on (to send product suggestions for example). To do that, you can define a `window.webchatMethods.getMemory` function, the webchat will call it before sending user messages, and send your arbitrary payload along with the message to the bot. 170 | 171 | If you use SAP Conversational AI's bot-builder (you should :)), your payload will be put in the memory of the conversation, meaning that you will be able to access this data in your bot-builder. Let's say you send this as payload : `{ "userName": "Dominik", "userId": 123456 }`, you will then be able to send this as a greeting message : `Hello {{ memory.userName }} ! How do you do ?`. 172 | 173 | `window.webchatMethods.getMemory` must return a JSON object or a Promise resolving a JSON object : 174 | - `{ "memory": { ... }, "merge": }` 175 | where `{ ... }` is your arbitrary payload. `merge` is an instruction for the bot-builder. If set to true, the payload will be merged with the existing memory, overriding common keys but keeping the ones absent from the payload. If set to false, the memory will be replaced entirely by your payload. 176 | 177 | If your `getMemory` function takes more than 10 seconds, the message will be sent anyway, without waiting for your function to finish. 178 | 179 | #### Examples : 180 | ```html 181 | 182 | 183 | 192 | 193 | 194 | 199 | 200 | 201 | ``` 202 | 203 | ```javascript 204 | window.webchatMethods = { 205 | getMemory: (conversationId) => { 206 | const getCookie = (name) => { 207 | const value = document.cookie.match('(^|;) ?' + name + '=([^;]*)(;|$)') 208 | return value ? value[2] : null 209 | } 210 | const userName = getCookie('userName') 211 | const memory = { userName, currentUrl: window.location.href } 212 | return { memory, merge: true } 213 | } 214 | } 215 | ``` 216 | 217 | ```javascript 218 | window.webchatData = {} 219 | window.webchatMethods = { 220 | getMemory: (conversationId) => { 221 | if (window.webchatData.savedUserData) { 222 | return { memory: window.webchatData.savedUserData, merge: true } 223 | } 224 | return new Promise((resolve, reject) => { 225 | axios.get('/current_user') 226 | .then((response) => { 227 | const memory = { userName: response.data.name, userId: response.data.id } 228 | window.webchatData.savedUserData = memory 229 | resolve({ memory, merge: true }) 230 | }) 231 | .catch(reject) 232 | }) 233 | } 234 | } 235 | ``` 236 | 237 | ```javascript 238 | window.webchatData = {} 239 | window.webchatMethods = { 240 | getMemory: (conversationId) => { 241 | if (!window.webchatData.oriUrl) { 242 | window.webchatData.oriUrl = window.location.href 243 | } 244 | // merge: false - reset the conversation if the user 245 | // switched to another page since the first message 246 | if (window.webchatData.oriUrl !== window.location.href) { 247 | return { memory: {}, merge: false } 248 | } 249 | return { memory: { userName: 'Dominik' }, merge: true } 250 | } 251 | } 252 | ``` 253 | 254 | 255 | ## Getting started with SAP Conversational AI 256 | 257 | We build products to help enterprises and developers have a better understanding of user inputs. 258 | 259 | - **NLP API**: a unique API for text processing, and augmented training. 260 | - **Bot Building Tools**: all you need to create smart bots powered by SAP Conversational AI's NLP API. Design even the most complex conversation flow, use all rich messaging formats and connect to external APIs and services. 261 | - **Bot Connector API**: standardizes the messaging format across all channels, letting you connect your bots to any channel in minutes. 262 | 263 | Learn more about: 264 | 265 | | [API Documentation](https://cai.tools.sap/docs/api-reference/) | [Discover the platform](https://cai.tools.sap/docs/create-your-bot) | [First bot tutorial](https://cai.tools.sap/blog/build-your-first-bot-with-cai-ai/) | [Advanced NodeJS tutorial](https://cai.tools.sap/blog/nodejs-chatbot-movie-bot/) | [Advanced Python tutorial](https://cai.tools.sap/blog/python-cryptobot/) | 266 | |---|---|---|---|---| 267 | 268 | ## License 269 | 270 | Copyright (c) [2016] SAP Conversational AI 271 | 272 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), 273 | to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 274 | 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: 275 | 276 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 277 | 278 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 279 | FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 280 | 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. 281 | -------------------------------------------------------------------------------- /assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-archive/Webchat/1b49d4fd40ff2f4d656dafb387e96294fcc7dbc7/assets/.gitkeep -------------------------------------------------------------------------------- /assets/header-webchat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-archive/Webchat/1b49d4fd40ff2f4d656dafb387e96294fcc7dbc7/assets/header-webchat.png -------------------------------------------------------------------------------- /assets/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-archive/Webchat/1b49d4fd40ff2f4d656dafb387e96294fcc7dbc7/assets/header.png -------------------------------------------------------------------------------- /assets/webchat-600.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-archive/Webchat/1b49d4fd40ff2f4d656dafb387e96294fcc7dbc7/assets/webchat-600.gif -------------------------------------------------------------------------------- /assets/webchat-github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-archive/Webchat/1b49d4fd40ff2f4d656dafb387e96294fcc7dbc7/assets/webchat-github.png -------------------------------------------------------------------------------- /bin/build-dev.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const webpackConfig = require('../webpack/dev') 3 | 4 | const bundler = webpack(webpackConfig) 5 | 6 | bundler.watch({}, err => { 7 | if (err) { 8 | console.error(err) 9 | return 10 | } 11 | 12 | console.log('Waiting for changes') 13 | }) 14 | -------------------------------------------------------------------------------- /bin/build-lib.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | 3 | const webpackConfig = require('../webpack/lib.js') 4 | 5 | const bundler = webpack(webpackConfig) 6 | 7 | bundler.run((err, stats) => { 8 | 9 | if (err) { 10 | console.error(err) 11 | return 12 | } 13 | }) 14 | -------------------------------------------------------------------------------- /bin/build.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | 3 | const webpackConfig = require('../webpack/prod.js') 4 | 5 | const bundler = webpack(webpackConfig) 6 | 7 | bundler.run((err, stats) => { 8 | 9 | if (err) { 10 | console.error(err) 11 | return 12 | } 13 | }) 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webchat", 3 | "version": "1.4.48", 4 | "description": "", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "clean:lib": "rm -rf lib", 8 | "clean:dist": "rm -rf dist", 9 | "build": "npm run clean:dist && NODE_ENV=production npm run build:webpack", 10 | "build:webpack": "node bin/build.js", 11 | "build:js": "babel src -d lib", 12 | "start": "npm run clean:dist && node bin/build-dev", 13 | "lib": "npm run clean:lib && node bin/build-lib", 14 | "lib:dev": "NODE_ENV=development npm run lib", 15 | "prettier": "prettier --write \"src/**/*.?(js|?(s)css|json)\" && eslint --fix src", 16 | "lint": "if [ -n \"$HUDSON_URL\" ]; then echo '!! linter crashes and is therefore not executed on xmake !!'; else eslint src; fi", 17 | "test": "MOCHA_TEST=true nyc --reporter=text mocha 'src/**/*.test.js'", 18 | "testHtml": "MOCHA_TEST=true nyc --reporter=html --report-dir coverage/html mocha --reporter mocha-junit-reporter --reporter-options mochaFile=./reports/mocha.xml 'src/**/*.test.js'", 19 | "coverage": "nyc report --cache false --reporter=html --report-dir coverage/html", 20 | "coverage:clover": "nyc report --cache false --reporter=clover --report-dir coverage/clover", 21 | "coverage:cobertura": "nyc report --cache false --reporter=cobertura --report-dir coverage/cobertura", 22 | "coverage:html": "nyc report --cache false --reporter=html --report-dir coverage/html", 23 | "coverage:lcov": "nyc report --cache false --reporter=lcov --report-dir coverage/lcov" 24 | }, 25 | "keywords": [], 26 | "author": "", 27 | "license": "MIT", 28 | "dependencies": { 29 | "@braintree/sanitize-url": "^6.0.0", 30 | "@reduxjs/toolkit": "^1.8.1", 31 | "@types/react": "16.8.22", 32 | "axios": "^0.26.1", 33 | "classnames": "^2.2.5", 34 | "prop-types": "^15.8.1", 35 | "query-string": "^5.0.1", 36 | "ramda": "^0.27.1", 37 | "react": "^16.13.1", 38 | "react-app-polyfill": "^1.0.6", 39 | "react-dom": "^16.13.1", 40 | "react-markdown": "5.0.2", 41 | "react-redux": "^7.2.8", 42 | "react-slick": "^0.25.2", 43 | "redux": "^4.2.0", 44 | "redux-actions": "^2.6.5", 45 | "redux-thunk": "^2.4.1", 46 | "remark-gfm": "^1.0.0", 47 | "sanitize-html-react": "^1.13.0", 48 | "valid-url": "^1.0.9" 49 | }, 50 | "devDependencies": { 51 | "@babel/cli": "^7.17.10", 52 | "@babel/core": "^7.17.10", 53 | "@babel/generator": "^7.17.10", 54 | "@babel/plugin-proposal-class-properties": "^7.16.7", 55 | "@babel/plugin-proposal-decorators": "^7.17.9", 56 | "@babel/plugin-transform-arrow-functions": "^7.16.7", 57 | "@babel/plugin-transform-regenerator": "^7.17.9", 58 | "@babel/plugin-transform-runtime": "^7.17.10", 59 | "@babel/preset-env": "^7.17.10", 60 | "@babel/preset-react": "^7.16.7", 61 | "@babel/register": "^7.17.7", 62 | "@babel/runtime": "7.12.1", 63 | "@ui5/cli": "^2.14.9", 64 | "autoprefixer": "^9.8.8", 65 | "axios-mock-adapter": "^1.18.2", 66 | "babel-eslint": "^10.0.3", 67 | "babel-loader": "^8.2.5", 68 | "babel-plugin-lodash": "^3.3.4", 69 | "babel-plugin-module-resolver": "^3.2.0", 70 | "babel-plugin-react-intl": "^5.1.10", 71 | "chai": "^4.3.6", 72 | "chai-like": "^1.1.1", 73 | "core-js": "3.3.6", 74 | "css-loader": "^3.2.0", 75 | "enzyme": "^3.11.0", 76 | "enzyme-adapter-react-16": "^1.15.6", 77 | "eslint": "^5.16.0", 78 | "eslint-config-prettier": "^6.1.0", 79 | "eslint-config-zavatta": "^6.0.3", 80 | "eslint-config-zavatta-react": "^2.3.1", 81 | "eslint-plugin-react": "^7.29.4", 82 | "esm": "^3.2.25", 83 | "file-loader": "^1.1.5", 84 | "ignore-styles": "^5.0.1", 85 | "jsdom": "^16.4.0", 86 | "jsdom-global": "^3.0.2", 87 | "mini-css-extract-plugin": "^0.9.0", 88 | "mocha": "^8.3.0", 89 | "mocha-junit-reporter": "^2.0.2", 90 | "mock-local-storage": "^1.1.22", 91 | "nock": "^13.2.4", 92 | "node-sass": "^4.14.0", 93 | "nyc": "^15.1.0", 94 | "path": "^0.12.7", 95 | "postcss-loader": "^3.0.0", 96 | "precss": "^2.0.0", 97 | "prettier": "^1.8.2", 98 | "progress-bar-webpack-plugin": "^1.10.0", 99 | "redux-mock-store": "^1.5.4", 100 | "sass-loader": "^7.3.1", 101 | "sinon": "^9.2.4", 102 | "style-loader": "^1.0.0", 103 | "uglifyjs-webpack-plugin": "2.2.0", 104 | "webpack": "^4.41.2", 105 | "webpack-dev-server": "^3.11.3" 106 | }, 107 | "resolutions": { 108 | "serialize-javascript": "^2.1.2" 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/actions/channel.js: -------------------------------------------------------------------------------- 1 | import config from '../config' 2 | import axios from 'axios' 3 | 4 | export const getChannelPreferences = (channelId, token) => { 5 | const client = axios.create({ 6 | baseURL: config.apiUrl, 7 | headers: { 8 | Authorization: token, 9 | 'X-Token': token, 10 | Accept: 'application/json', 11 | }, 12 | }) 13 | 14 | return client.get(`/webhook/${channelId}/preferences`).then(res => res.data.results) 15 | } 16 | -------------------------------------------------------------------------------- /src/actions/conversation.js: -------------------------------------------------------------------------------- 1 | import { createAction } from 'redux-actions' 2 | 3 | export const setCredentials = createAction('SET_CREDENTIALS') 4 | 5 | export const removeConversationId = createAction('REMOVE_CONVERSATION_ID') 6 | 7 | export const createConversation = createAction('API:CREATE_CONVERSATION', (channelId, token) => ({ 8 | url: `/webhook/${channelId}/conversations`, 9 | method: 'post', 10 | headers: { Authorization: token, 'X-Token': token }, 11 | })) 12 | -------------------------------------------------------------------------------- /src/actions/messages.js: -------------------------------------------------------------------------------- 1 | import { createAction } from 'redux-actions' 2 | 3 | export const postMessage = createAction('API:POST_MESSAGE', (channelId, token, data) => ({ 4 | url: `/webhook/${channelId}`, 5 | method: 'post', 6 | headers: { Authorization: token, 'X-Token': token }, 7 | data, 8 | })) 9 | 10 | export const getMessages = createAction('API:GET_MESSAGES', (channelId, token, conversationId) => ({ 11 | url: `/webhook/${channelId}/conversations/${conversationId}/messages`, 12 | method: 'get', 13 | headers: { Authorization: token, 'X-Token': token }, 14 | })) 15 | 16 | export const pollMessages = createAction( 17 | 'API:POLL_MESSAGES', 18 | (channelId, token, conversationId, lastMessageId) => ({ 19 | url: `/webhook/${channelId}/conversations/${conversationId}/poll`, 20 | method: 'get', 21 | headers: { Authorization: token, 'X-Token': token }, 22 | query: { last_message_id: lastMessageId }, // eslint-disable-line camelcase 23 | }), 24 | ) 25 | 26 | export const removeMessage = createAction('REMOVE_MESSAGE') 27 | 28 | export const setFirstMessage = createAction('SET_FIRST_MESSAGE') 29 | 30 | export const removeAllMessages = createAction('REMOVE_ALL_MESSAGES') 31 | 32 | export const addBotMessage = createAction('ADD_BOT_MESSAGE', (messages, data) => ({ 33 | messages, 34 | data, 35 | })) 36 | 37 | export const addUserMessage = createAction('ADD_USER_MESSAGE') 38 | -------------------------------------------------------------------------------- /src/components/Button/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { sanitizeUrl } from '@braintree/sanitize-url' 4 | import cx from 'classnames' 5 | 6 | import { truncate, validButtonContent } from 'helpers' 7 | 8 | import './style.scss' 9 | 10 | const _getValidTelHref = (button, readOnlyMode) => { 11 | const { value } = button 12 | if (!readOnlyMode && value) { 13 | return value.indexOf('tel:') === 0 ? value : `tel:${value}` 14 | } 15 | return '#' 16 | } 17 | 18 | const _getUrlInfo = (button, readOnlyMode) => { 19 | const { value } = button 20 | const target = readOnlyMode ? '_self' : '_blank' 21 | const href = readOnlyMode || !value ? '#' : value 22 | return { 23 | target, 24 | href, 25 | } 26 | } 27 | 28 | const Button = ({ button, sendMessage, readOnlyMode, isLastMessage }) => { 29 | if (!button) { 30 | return null 31 | } 32 | const { value, title, type } = button 33 | // Increase Button length to 80 characters per SAPMLCONV-3486 34 | const formattedTitle = truncate(title, 80) 35 | const tooltip = title && title.length > 80 ? title : null 36 | const disableButton = readOnlyMode || (!isLastMessage && type === 'trigger_skill') 37 | if (button.type === 'web_url' && sanitizeUrl(value) === 'about:blank') { 38 | return null 39 | } 40 | 41 | let content = null 42 | 43 | // https://sapjira.wdf.sap.corp/browse/SAPMLCONV-4781 - Support the phonenumber options 44 | const linkClassName = cx('RecastAppButton-Link CaiAppButton-Link', { 'CaiAppButton--ReadOnly': disableButton }) 45 | const { href, target } = _getUrlInfo(button, disableButton) 46 | const bData = validButtonContent(button) 47 | switch (type) { 48 | case 'phonenumber': 49 | content = ( 50 | 53 | {formattedTitle} 54 | 55 | ) 56 | break 57 | case 'web_url': 58 | content = ( 59 | 64 | {formattedTitle} 65 | 66 | ) 67 | break 68 | default: 69 | content = ( 70 |
{ 74 | // eslint-disable-next-line no-unused-expressions 75 | !disableButton && sendMessage({ type: 'button', content: bData }, title) 76 | }} 77 | > 78 | {formattedTitle} 79 |
80 | ) 81 | break 82 | } 83 | 84 | return content 85 | } 86 | 87 | Button.propTypes = { 88 | isLastMessage: PropTypes.bool, 89 | button: PropTypes.object, 90 | sendMessage: PropTypes.func, 91 | readOnlyMode: PropTypes.bool, 92 | } 93 | 94 | export default Button 95 | -------------------------------------------------------------------------------- /src/components/Button/style.scss: -------------------------------------------------------------------------------- 1 | .RecastApp .RecastAppButton, .CaiApp .CaiAppButton { 2 | padding: 0.5rem; 3 | 4 | cursor: pointer; 5 | text-align: center; 6 | 7 | font-weight: bold; 8 | color: cornflowerblue; 9 | 10 | overflow: hidden; 11 | word-wrap: break-word; 12 | word-break: break-word; 13 | overflow-wrap: break-word; 14 | 15 | &--ReadOnly { 16 | opacity: 0.5; 17 | cursor: text; 18 | } 19 | } 20 | 21 | .RecastApp, .CaiApp { 22 | .RecastAppButton + .RecastAppButton, .CaiAppButton + .CaiAppButton { 23 | border-top: 1px solid lightgrey; 24 | } 25 | } 26 | 27 | .RecastApp .RecastAppButton-Link, .CaiApp .CaiAppButton-Link { 28 | @extend .CaiAppButton; 29 | @extend .RecastAppButton; 30 | text-decoration: none; 31 | display: block; 32 | &:hover { 33 | text-decoration: none; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/components/Expander/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import cx from 'classnames' 4 | import './style.scss' 5 | 6 | const Expander = ({ onClick, preferences, style, show }) => ( 7 |
16 | {preferences.expanderLogo && ( 17 | 18 | )} 19 | 20 | {preferences.expanderTitle} 21 | 22 | {preferences.onboardingMessage && ( 23 |
{preferences.onboardingMessage}
24 | )} 25 |
26 | ) 27 | 28 | Expander.propTypes = { 29 | preferences: PropTypes.object, 30 | onClick: PropTypes.func.isRequired, 31 | style: PropTypes.object, 32 | show: PropTypes.bool, 33 | } 34 | 35 | export default Expander 36 | -------------------------------------------------------------------------------- /src/components/Expander/style.scss: -------------------------------------------------------------------------------- 1 | .RecastApp .RecastAppExpander, .CaiApp .CaiAppExpander { 2 | position: fixed; 3 | right: 10px; 4 | bottom: 10px; 5 | display: flex; 6 | align-items: center; 7 | 8 | padding: 0.5rem 1rem; 9 | cursor: pointer; 10 | 11 | border-radius: 3px; 12 | box-shadow: 0 1px 6px lightgrey, 0 2px 32px lightgrey; 13 | transition: opacity 0.3s; 14 | 15 | &.open { 16 | opacity: 1; 17 | } 18 | 19 | &.close { 20 | opacity: 0; 21 | } 22 | 23 | &--logo { 24 | margin-right: 0.5rem; 25 | width: 30px; 26 | height: 30px; 27 | } 28 | 29 | &--onboarding { 30 | background-color: white; 31 | 32 | position: absolute; 33 | right: 0; 34 | bottom: 70px; 35 | padding: 0.8rem; 36 | 37 | color: grey; 38 | box-shadow: 0 0 20px 3px lightgrey; 39 | 40 | &::before { 41 | content: ''; 42 | position: absolute; 43 | bottom: -10px; 44 | right: 10px; 45 | 46 | border-style: solid; 47 | border-width: 10px 10px 0px 10px; 48 | border-color: white transparent transparent; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/components/Header/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import './style.scss' 5 | 6 | const Header = ({ closeWebchat, preferences, logoStyle, readOnlyMode }) => { 7 | if (readOnlyMode) { 8 | return null 9 | } 10 | return ( 11 |
18 | 19 | 20 |
{preferences.headerTitle}
21 | 22 |
23 | 24 |
25 |
26 | ) 27 | } 28 | 29 | Header.propTypes = { 30 | closeWebchat: PropTypes.func, 31 | preferences: PropTypes.object, 32 | logoStyle: PropTypes.object, 33 | readOnlyMode: PropTypes.bool, 34 | } 35 | 36 | export default Header 37 | -------------------------------------------------------------------------------- /src/components/Header/style.scss: -------------------------------------------------------------------------------- 1 | .RecastApp .RecastAppHeader, .CaiApp .CaiAppHeader { 2 | display: flex; 3 | align-items: center; 4 | border-radius: 0; 5 | 6 | &--logo { 7 | height: 50px; 8 | width: 50px; 9 | padding: 10px; 10 | } 11 | 12 | &--title { 13 | font-weight: bold; 14 | flex-grow: 1; 15 | } 16 | 17 | &--btn { 18 | cursor: pointer; 19 | margin: 1rem; 20 | width: 15px; 21 | height: 15px; 22 | } 23 | } 24 | 25 | @media only screen and (min-width: 420px) and (min-height: 575px) { 26 | .RecastApp .RecastAppHeader, .CaiApp .CaiAppHeader { 27 | border-radius: 3px 3px 0 0; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/Input/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import append from 'ramda/es/append' 4 | 5 | import SendButton from 'components/SendButton' 6 | import { safeArrayOfItem } from 'helpers' 7 | 8 | import Menu from 'components/Menu' 9 | import MenuSVG from 'components/svgs/menu' 10 | import './style.scss' 11 | import { pathOr } from 'ramda' 12 | 13 | // Number of minimum char to display the char limit. 14 | const NUMBER_BEFORE_LIMIT = 5 15 | 16 | class Input extends Component { 17 | state = { 18 | value: '', 19 | previousValues: [], 20 | historyValues: [], 21 | indexHistory: 0, 22 | menuOpened: false, 23 | isOpen: false, 24 | hasFocus: false, 25 | menuIndexes: [], 26 | } 27 | 28 | static getDerivedStateFromProps (props, state) { 29 | if (!props.isOpen) { 30 | return { isOpen: props.isOpen, hasFocus: false } 31 | } 32 | return { isOpen: props.isOpen } 33 | } 34 | 35 | componentDidMount () { 36 | if (this.state.isOpen) { 37 | this.setFocusState() 38 | } 39 | this._input.value = '' 40 | 41 | this.onInputHeight() 42 | } 43 | 44 | shouldComponentUpdate (nextProps, nextState) { 45 | return ( 46 | nextState.value !== this.state.value 47 | || nextState.menuOpened !== this.state.menuOpened 48 | || nextState.menuIndexes.length !== this.state.menuIndexes.length 49 | || nextState.isOpen !== this.state.isOpen 50 | ) 51 | } 52 | 53 | componentDidUpdate () { 54 | if (this.state.isOpen) { 55 | this.setFocusState() 56 | } 57 | if (!this.state.value) { 58 | // Dirty fix textarea placeholder to reset style correctly 59 | setTimeout(() => { 60 | if (this._input) { 61 | this._input.style.height = '18px' 62 | this._input.value = '' 63 | } 64 | this.onInputHeight() 65 | }, 100) 66 | } 67 | 68 | this.onInputHeight() 69 | } 70 | 71 | setFocusState () { 72 | if (!this.state.hasFocus && this._input) { 73 | setTimeout(() => { 74 | if (this._input) { 75 | this._input.focus() 76 | } 77 | this.setState({ hasFocus: true }) 78 | }, 100) 79 | } 80 | } 81 | 82 | onInputChange = e => { 83 | e.persist() 84 | 85 | const { characterLimit } = this.props 86 | const { value } = e.target 87 | 88 | if (characterLimit && value.length > characterLimit) { 89 | return 90 | } 91 | 92 | this.setState(prevState => { 93 | const newPreviousValues = [...prevState.previousValues] 94 | newPreviousValues[prevState.indexHistory] = value 95 | return { 96 | value: e.target.value, 97 | previousValues: newPreviousValues, 98 | } 99 | }, this.autoGrow) 100 | } 101 | 102 | onInputHeight = () => { 103 | const { onInputHeight } = this.props 104 | if (onInputHeight) { 105 | onInputHeight(this.inputContainer.clientHeight) 106 | } 107 | } 108 | sendMenuSelection = (action) => { 109 | if (action) { 110 | const title = pathOr(null, ['content', 'title'], action) 111 | this.props.onSubmit(action, title) 112 | } 113 | } 114 | sendMessage = () => { 115 | const content = this.state.value.trim() 116 | if (content) { 117 | this.props.onSubmit({ 118 | type: 'text', 119 | content, 120 | }, content.title) 121 | this.setState(prevState => { 122 | const historyValues = append(content, prevState.historyValues) 123 | const previousValues = append('', historyValues) 124 | 125 | return { 126 | value: '', 127 | previousValues, 128 | historyValues, 129 | indexHistory: previousValues.length - 1, 130 | } 131 | }) 132 | } 133 | } 134 | 135 | autoGrow = () => { 136 | this._input.style.height = '18px' 137 | this._input.style.height = `${this._input.scrollHeight}px` 138 | } 139 | 140 | handleKeyboard = keyName => { 141 | const { indexHistory, previousValues } = this.state 142 | if (keyName === 'ArrowUp') { 143 | if (indexHistory > -1) { 144 | this.setState( 145 | prevState => { 146 | const indexHistory = Math.max(prevState.indexHistory - 1, 0) 147 | return { 148 | indexHistory, 149 | value: prevState.previousValues[indexHistory], 150 | } 151 | }, 152 | () => { 153 | // Trick to go to the end of the line when pressing ArrowUp key 154 | setTimeout(() => { 155 | if (this._input) { 156 | this._input.selectionStart = this._input.value.length 157 | this._input.selectionEnd = this._input.value.length 158 | } 159 | }, 10) 160 | }, 161 | ) 162 | } 163 | } else if (keyName === 'ArrowDown') { 164 | if (indexHistory < previousValues.length - 1) { 165 | this.setState(prevState => { 166 | const indexHistory = Math.min( 167 | prevState.indexHistory + 1, 168 | Math.max(prevState.previousValues.length - 1, 0), 169 | ) 170 | return { 171 | indexHistory, 172 | value: prevState.previousValues[indexHistory], 173 | } 174 | }) 175 | } else { 176 | this.setState({ 177 | value: '', 178 | }) 179 | } 180 | } 181 | } 182 | 183 | removeMenuIndex = () => { 184 | const { menuIndexes } = this.state 185 | this.setState({ menuIndexes: menuIndexes.slice(0, -1) }) 186 | } 187 | 188 | addMenuIndex = i => { 189 | const { menuIndexes } = this.state 190 | this.setState({ menuIndexes: [...menuIndexes, i] }) 191 | } 192 | 193 | getCurrentMenu = () => { 194 | const { menuIndexes } = this.state 195 | 196 | return menuIndexes.reduce((currentMenu, i) => currentMenu.call_to_actions[i], this.props.menu) 197 | } 198 | 199 | triggerMenu = () => { 200 | const { menuOpened } = this.state 201 | if (menuOpened) { 202 | return this.setState({ menuOpened: false, menuIndexes: [] }) 203 | } 204 | return this.setState({ menuOpened: true }) 205 | } 206 | 207 | render () { 208 | const { enableHistoryInput, characterLimit, menu, preferences, inputPlaceholder } = this.props 209 | const { value, menuOpened } = this.state 210 | const { call_to_actions } = menu || [] 211 | const menuActions = safeArrayOfItem(call_to_actions) 212 | const showMenuIcon = menuActions.length > 0 213 | 214 | const showLimitCharacter = characterLimit 215 | ? characterLimit - value.length <= NUMBER_BEFORE_LIMIT 216 | : null 217 | 218 | return ( 219 |
{ 222 | this.inputContainer = ref 223 | }} 224 | > 225 | {showMenuIcon && } 226 | 227 | {menuOpened && ( 228 | this.sendMenuSelection(action)} 234 | /> 235 | )} 236 | 237 |