diff --git a/.eslintrc b/.eslintrc
deleted file mode 100755
index ea7803a7..00000000
--- a/.eslintrc
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "extends": ["airbnb", "plugin:prettier/recommended", "prettier/react"],
- "plugins": ["prettier"],
- "parser": "babel-eslint",
- "globals": {
- "window": true,
- "Audio": true,
- "document": true,
- "localStorage": true,
- "navigator": true
- },
- "rules": {
- "prettier/prettier": ["error"],
- "no-confusing-arrow": 0,
- "no-return-assign": 0,
- "guard-for-in": 0,
- "no-restricted-syntax": 0,
- "no-param-reassign": 0,
- "react/no-will-update-set-state": 0,
- "new-cap": 0
- }
-}
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 00000000..94391dbe
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,57 @@
+
+
+## Description
+
+
+
+## Related issue
+
+
+
+## Type of change
+
+- [ ] `feat`: new feature
+- [ ] `fix`: bug fix
+- [ ] `docs`: documentation
+- [ ] `refactor` / `perf` / `test`
+- [ ] `build` / `ci` / `chore`
+- [ ] Breaking change (the title has `!`, and the migration is described below)
+
+## How has this been tested?
+
+
+
+## Breaking changes
+
+
+
+## Checklist
+
+- [ ] The title follows Conventional Commits
+- [ ] `npm run lint`, `npm run typecheck` and `npm test` pass
+- [ ] Tests cover the change
+- [ ] The README is updated when the public API changes
diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml
index 63188989..81bfcde5 100644
--- a/.github/workflows/nodejs.yml
+++ b/.github/workflows/nodejs.yml
@@ -1,26 +1,40 @@
name: Node CI
-on: [push]
+on:
+ push:
+ branches: [master]
+ pull_request:
jobs:
build:
-
runs-on: ubuntu-latest
strategy:
matrix:
- node-version: [8.x, 10.x, 12.x]
+ node-version: [22.x, 24.x]
+
+ steps:
+ - uses: actions/checkout@v4
+ - name: Use Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@v4
+ with:
+ node-version: ${{ matrix.node-version }}
+ cache: npm
+ - run: npm ci
+ - run: npm run lint
+ - run: npm run typecheck
+ - run: npm run test:coverage
+ - run: npm run build
+
+ react-18:
+ runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v1
- - name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v1
- with:
- node-version: ${{ matrix.node-version }}
- - name: npm install, build, and test
- run: |
- npm ci
- npm run build --if-present
- npm test
- env:
- CI: true
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 24.x
+ cache: npm
+ - run: npm ci
+ - run: npm install --no-save react@18 react-dom@18
+ - run: npm test
diff --git a/.gitignore b/.gitignore
index 0c5e34ea..0a1fcda5 100755
--- a/.gitignore
+++ b/.gitignore
@@ -28,3 +28,9 @@ pids
# Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
node_modules
+
+# Coverage report
+coverage.lcov
+
+# Build output (generated on publish)
+/dist
diff --git a/.prettierrc b/.prettierrc
index ea9ebdb2..31fb5177 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -1,4 +1,6 @@
{
"printWidth": 100,
- "singleQuote": true
-}
\ No newline at end of file
+ "singleQuote": true,
+ "trailingComma": "none",
+ "arrowParens": "avoid"
+}
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index a5d9e581..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-language: node_js
-node_js:
- - 8
-before_install:
- - "npm install react@16.4.1 react-dom@16.4.1 styled-components@4.1.3"
diff --git a/README.md b/README.md
index 2f104f26..ac36efed 100755
--- a/README.md
+++ b/README.md
@@ -1,13 +1,8 @@
-> [!WARNING]
-> React Simple Chatbot is no longer maintained. I recommend using [react-chatbotify](https://github.com/tjtanjin/react-chatbotify) as an alternative.
-
-
-
# React Simple Chatbot
-
+
-
+
A simple chatbot component to create conversation chats
@@ -15,18 +10,23 @@ A simple chatbot component to create conversation chats
## Getting Start
+`react-simple-chatbot` uses [styled-components](https://styled-components.com) as a peer dependency, so install both:
+
```bash
-npm install react-simple-chatbot --save
+npm install react-simple-chatbot styled-components
```
+It works with React 18 or 19 and styled-components 5.1 or newer. The library is written in TypeScript, so the types are included.
+
## Usage
There are several examples on the [website](https://lucasbassetti.github.io/react-simple-chatbot/). Here is the first one to get you started:
-``` javascript
-import ChatBot from 'react-simple-chatbot';
+```tsx
+import { createRoot } from 'react-dom/client';
+import ChatBot, { type Step } from 'react-simple-chatbot';
-const steps = [
+const steps: Step[] = [
{
id: '0',
message: 'Welcome to react chatbot!',
@@ -39,33 +39,20 @@ const steps = [
},
];
-ReactDOM.render(
-
-
-
,
- document.getElementById('root')
-);
+createRoot(document.getElementById('root')!).render();
```
-## React Simple Chatbot with AI
+### Tips
-1. [CodeParrot AI](https://codeparrot.ai/oracle?owner=LucasBassetti&repo=react-simple-chatbot) - Bot will help you understand this repository better. You can ask for code examples, installation guide, debugging help and much more.
+- **Restart the conversation**: render the chatbot with a new `key` (e.g. ``). If `cache` is enabled, also clear it with `localStorage.removeItem(cacheName)`.
+- **Change steps after mount**: updates to `steps`, the avatars, the delays and `botName` are used by the steps that were not rendered yet.
+- **Style the chatbot**: every element has a stable `rsc-*` class name (e.g. `.rsc-ts-bubble`, `.rsc-input`) that you can target with CSS, and the `*Style` props accept inline styles.
+- **Custom components**: the components of the steps receive `step`, `steps`, `previousStep` and `triggerNextStep`, typed as `CustomComponentProps`.
## React Simple Chatbot on media
1. [webdesignerdepot](https://www.webdesignerdepot.com/2017/08/whats-new-for-designers-august-2017/)
-2. [blogduwebdesign](http://www.blogduwebdesign.com/webdesign/ressources-web-du-lundi-aout-164/2507)
-3. [codrops](https://tympanus.net/codrops/collective/collective-335/)
-
-## Build with `react-simple-chatbot`
-
-1. [Seth Loh Website](https://github.com/lackdaz/lackdaz.github.io) - Personal website of Seth Loh ([demo](https://www.sethloh.com))
-2. [Paul's Website](https://psheon.github.io/) - Personal website of Paul Jiang ([demo](https://psheon.github.io/archives/))
-3. [Cisco Partner Support API Chatbot](https://github.com/btotharye/cisco-pss-api-chatbot) - Code with screenshots to have your own Cisco Serial lookup chatbot.
-4. [Chatcompose](https://www.chatcompose.com/en.html) - Chatbot Platform for Conversational Marketing and Support.
-5. [Mixat](https://www.svt.se/mixat) - News Chatbot for tweenies. Also as app ([iOS](https://apps.apple.com/se/app/mixat-h%C3%A4r-f%C3%A5r-du-koll/id1239444432) or [Android](https://play.google.com/store/apps/details?id=se.svt.mixat))
-
-Built something with `react-simple-chatbot`? Submit a PR and add it to this list!
+2. [codrops](https://tympanus.net/codrops/collective/collective-335/)
## How to Contribute
diff --git a/babel.config.js b/babel.config.js
deleted file mode 100644
index af37e236..00000000
--- a/babel.config.js
+++ /dev/null
@@ -1,8 +0,0 @@
-const presets = ['@babel/env', '@babel/preset-react'];
-const plugins = [
- '@babel/plugin-transform-arrow-functions',
- '@babel/plugin-proposal-class-properties',
- '@babel/plugin-transform-object-assign'
-];
-
-module.exports = { presets, plugins };
diff --git a/contributing.md b/contributing.md
index d11da078..e02d429b 100644
--- a/contributing.md
+++ b/contributing.md
@@ -17,8 +17,17 @@ Pull requests are very welcome. Note that if you are going to propose drastic ch
Fork the repository, clone it locally and create a branch for your proposed bug fix or new feature. Avoid working directly on the master branch.
+The library is written in TypeScript with React function components and hooks. Useful commands:
+
+- `npm start`: run the example (`example/`) with Vite at http://localhost:5173
+- `npm test` / `npm run test:coverage`: run the tests (Vitest + Testing Library)
+- `npm run lint` and `npm run typecheck`: check the code
+- `npm run build`: build `dist/` (ES module, UMD and type declarations)
+
Implement your bug fix or feature, write tests to cover it and make sure all tests are passing (run a final `npm test` to make sure everything is correct). Then commit your changes, push your bug fix/feature branch to the origin (your forked repo) and open a pull request to the upstream (the repository you originally forked)'s master branch.
+The pull request title must follow [Conventional Commits](https://www.conventionalcommits.org): `(): `, where the type is one of `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore` or `revert`, e.g. `fix(cache): restore options without value`. Add `!` after the type for breaking changes (e.g. `feat!: drop React 17 support`). Fill the pull request template, it asks for the description, the related issue, how the change was tested and the breaking changes.
+
## Documentation
Documentation is extremely important and takes a fair deal of time and effort to write and keep updated. Please submit any and all improvements you can make to the repository's docs.
diff --git a/coverage.lcov b/coverage.lcov
deleted file mode 100644
index e69de29b..00000000
diff --git a/dist/react-simple-chatbot.js b/dist/react-simple-chatbot.js
deleted file mode 100644
index 0e388b2d..00000000
--- a/dist/react-simple-chatbot.js
+++ /dev/null
@@ -1,2 +0,0 @@
-!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("react"),require("styled-components")):"function"==typeof define&&define.amd?define(["react","styled-components"],t):"object"==typeof exports?exports.ReactSimpleChatbot=t(require("react"),require("styled-components")):e.ReactSimpleChatbot=t(e.react,e["styled-components"])}("undefined"!=typeof self?self:this,function(e,t){return function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="dist/",n(n.s=9)}([function(e,t,n){e.exports=n(5)()},function(t,n){t.exports=e},function(e,n){e.exports=t},function(e,t){var n=function(e,t){return{parse:function(t,r){var a=JSON.parse(t,i).map(o),s=a[0],u=r||n,c="object"==typeof s&&s?function t(n,r,o,i){return Object.keys(o).reduce(function(o,a){var s=o[a];if(s instanceof e){var u=n[s];"object"!=typeof u||r.has(u)?o[a]=i.call(o,a,u):(r.add(u),o[a]=i.call(o,a,t(n,r,u,i)))}else o[a]=i.call(o,a,s);return o},o)}(a,new Set,s,u):s;return u.call({"":c},"",c)},stringify:function(e,o,i){for(var a,s=new Map,u=[],c=[],l=o&&typeof o==typeof u?function(e,t){if(""===e||-11&&void 0!==arguments[1]?arguments[1]:1,n=function(e){e=e.replace(/^#?([a-f\d])([a-f\d])([a-f\d])$/i,function(e,t,n,r){return t+t+n+n+r+r});var t=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(e);return t?{r:parseInt(t[1],16),g:parseInt(t[2],16),b:parseInt(t[3],16)}:null}(e);return"rgba(".concat(n.r,", ").concat(n.g,", ").concat(n.b,", ").concat(t,")")};function f(){var e=g(["\n 0% { box-shadow: 0 0 0 0 ","; }\n 70% { box-shadow: 0 0 0 10px ","; }\n 100% { box-shadow: 0 0 0 0 ","; }\n"]);return f=function(){return e},e}function d(){var e=g(["\n 25% { transform: rotate(-1deg); }\n 100% { transform: rotate(1deg); }\n"]);return d=function(){return e},e}function b(){var e=g(["\n 100% { transform: scale(1); }\n"]);return b=function(){return e},e}function h(){var e=g(["\n 0% { opacity: .2; }\n 20% { opacity: 1; }\n 100% { opacity: .2; }\n"]);return h=function(){return e},e}function g(e,t){return t||(t=e.slice(0)),Object.freeze(Object.defineProperties(e,{raw:{value:Object.freeze(t)}}))}var y=Object(c.keyframes)(h()),v=Object(c.keyframes)(b()),m=Object(c.keyframes)(d());function S(){var e=function(e,t){t||(t=e.slice(0));return Object.freeze(Object.defineProperties(e,{raw:{value:Object.freeze(t)}}))}(["\n animation: "," 1.4s infinite both;\n animation-delay: ",";\n"]);return S=function(){return e},e}var x=l.a.span(S(),y,function(e){return e.delay}),O=function(){return o.a.createElement("span",{className:"rsc-loading"},o.a.createElement(x,{delay:"0s"},"."),o.a.createElement(x,{delay:".2s"},"."),o.a.createElement(x,{delay:".4s"},"."))};function w(){var e=function(e,t){t||(t=e.slice(0));return Object.freeze(Object.defineProperties(e,{raw:{value:Object.freeze(t)}}))}(["\n background: #fff;\n border-radius: 5px;\n box-shadow: rgba(0, 0, 0, 0.15) 0px 1px 2px 0px;\n display: flex;\n justify-content: center;\n margin: 0 6px 10px 6px;\n padding: 16px;\n"]);return w=function(){return e},e}var j=l.a.div(w());function k(e){return(k="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function E(e,t){for(var n=0;n0&&void 0!==arguments[0]?arguments[0]:nt,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:nt,r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:nt,o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"en";return function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),tt||(tt=this),this.state={inputValue:"",lang:o,onChange:t,onEnd:n,onStop:r},this.onResult=this.onResult.bind(this),this.onEnd=this.onEnd.bind(this),this.setup(),tt}return et(e,null,[{key:"isSupported",value:function(){return"webkitSpeechRecognition"in window}}]),et(e,[{key:"onChange",value:function(e){var t=this.state.onChange;this.setState({inputValue:e}),t(e)}},{key:"onFinal",value:function(e){this.setState({inputValue:e}),this.recognition.stop()}},{key:"onEnd",value:function(){var e=this.state,t=e.onStop,n=e.onEnd,r=e.force;this.setState({speaking:!1}),r?t():n()}},{key:"onResult",value:function(e){for(var t="",n="",r=e.resultIndex;r0?t.length-1:0,o=n.generateRenderedStepsById(),i=t[r].value;return"function"==typeof e?e({previousValue:i,steps:o}):e}),mt(vt(vt(n)),"generateRenderedStepsById",function(){for(var e=n.state.previousSteps,t={},r=0,o=e.length;r0?r[t-1]:{};return b&&!h?o.a.createElement(R,{key:t,speak:n.speak,step:e,steps:g,style:c,previousStep:y,previousValue:y.value,triggerNextStep:n.triggerNextStep}):d?o.a.createElement(Z,{key:t,step:e,previousValue:y.value,triggerNextStep:n.triggerNextStep,bubbleOptionStyle:u}):o.a.createElement(de,{key:t,step:e,steps:g,speak:n.speak,previousStep:y,previousValue:y.value,triggerNextStep:n.triggerNextStep,avatarStyle:a,bubbleStyle:s,hideBotAvatar:l,hideUserAvatar:p,speechSynthesis:f,isFirst:n.isFirstPosition(e),isLast:n.isLastPosition(e)})}),n.content=null,n.input=null,n.supportsScrollBehavior=!1,n.setContentRef=function(e){n.content=e},n.setInputRef=function(e){n.input=e},n.state={renderedSteps:[],previousSteps:[],currentStep:{},previousStep:{},steps:{},disabled:!0,opened:e.opened||!e.floating,inputValue:"",inputInvalid:!1,speaking:!1,recognitionEnable:e.recognitionEnable&&rt.isSupported(),defaultUserSettings:{}},n.speak=ft(e.speechSynthesis),n}var n,i,a;return function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function");e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),t&&yt(e,t)}(t,r["Component"]),n=t,a=[{key:"getDerivedStateFromProps",value:function(e,t){var n=e.opened;return void 0!==e.toggleFloating&&void 0!==n&&n!==t.opened?function(e){for(var t=1;t {\n // http://stackoverflow.com/a/5624139\n const shorthandRegex = /^#?([a-f\\d])([a-f\\d])([a-f\\d])$/i;\n hex = hex.replace(shorthandRegex, (m, r, g, b) => r + r + g + g + b + b);\n\n const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n return result\n ? {\n r: parseInt(result[1], 16),\n g: parseInt(result[2], 16),\n b: parseInt(result[3], 16)\n }\n : null;\n};\n\n/**\n * Transform hex+alpha to rgba\n * @param {string} hex hex color code\n * @param {number} [alpha=1]\n * @returns {string} the rgba as string\n */\nconst rgba = (hex, alpha = 1) => {\n const color = hexToRgb(hex);\n return `rgba(${color.r}, ${color.g}, ${color.b}, ${alpha})`;\n};\n\nexport default rgba;\n","import { keyframes } from 'styled-components';\nimport rgba from './rgba';\n\nconst loading = keyframes`\n 0% { opacity: .2; }\n 20% { opacity: 1; }\n 100% { opacity: .2; }\n`;\n\nconst scale = keyframes`\n 100% { transform: scale(1); }\n`;\n\nconst invalidInput = keyframes`\n 25% { transform: rotate(-1deg); }\n 100% { transform: rotate(1deg); }\n`;\n\nconst pulse = color => keyframes`\n 0% { box-shadow: 0 0 0 0 ${rgba(color, 0.4)}; }\n 70% { box-shadow: 0 0 0 10px ${rgba(color, 0)}; }\n 100% { box-shadow: 0 0 0 0 ${rgba(color, 0)}; }\n`;\n\nexport { loading, scale, invalidInput, pulse };\n","import styled from 'styled-components';\nimport { loading } from '../../common/animations';\n\nconst LoadingStep = styled.span`\n animation: ${loading} 1.4s infinite both;\n animation-delay: ${props => props.delay};\n`;\n\nexport default LoadingStep;\n","import React from 'react';\nimport LoadingStep from './LoadingStep';\n\nconst Loading = () => (\n \n .\n .\n .\n \n);\n\nexport default Loading;\n","import styled from 'styled-components';\n\nconst ChatStepContainer = styled.div`\n background: #fff;\n border-radius: 5px;\n box-shadow: rgba(0, 0, 0, 0.15) 0px 1px 2px 0px;\n display: flex;\n justify-content: center;\n margin: 0 6px 10px 6px;\n padding: 16px;\n`;\n\nexport default ChatStepContainer;\n","import React, { Component } from 'react';\nimport PropTypes from 'prop-types';\nimport Loading from '../common/Loading';\nimport CustomStepContainer from './CustomStepContainer';\n\nclass CustomStep extends Component {\n state = {\n loading: true\n };\n\n componentDidMount() {\n const { speak, step, previousValue, triggerNextStep } = this.props;\n const { delay, waitAction } = step;\n\n setTimeout(() => {\n this.setState({ loading: false }, () => {\n if (!waitAction && !step.rendered) {\n triggerNextStep();\n }\n speak(step, previousValue);\n });\n }, delay);\n }\n\n renderComponent = () => {\n const { step, steps, previousStep, triggerNextStep } = this.props;\n const { component } = step;\n\n return React.cloneElement(component, {\n step,\n steps,\n previousStep,\n triggerNextStep\n });\n };\n\n render() {\n const { loading } = this.state;\n const { style } = this.props;\n\n return (\n \n {loading ? : this.renderComponent()}\n \n );\n }\n}\n\nCustomStep.propTypes = {\n previousStep: PropTypes.objectOf(PropTypes.any).isRequired,\n previousValue: PropTypes.oneOfType([\n PropTypes.string,\n PropTypes.bool,\n PropTypes.number,\n PropTypes.object,\n PropTypes.array\n ]),\n speak: PropTypes.func,\n step: PropTypes.objectOf(PropTypes.any).isRequired,\n steps: PropTypes.objectOf(PropTypes.any).isRequired,\n style: PropTypes.objectOf(PropTypes.any).isRequired,\n triggerNextStep: PropTypes.func.isRequired\n};\nCustomStep.defaultProps = {\n previousValue: '',\n speak: () => {}\n};\n\nexport default CustomStep;\n","import styled from 'styled-components';\nimport { scale } from '../../common/animations';\n\nconst Option = styled.li`\n animation: ${scale} 0.3s ease forwards;\n cursor: pointer;\n display: inline-block;\n margin: 2px;\n transform: scale(0);\n`;\n\nexport default Option;\n","export default {\n background: '#f5f8fb',\n fontFamily: 'monospace',\n headerBgColor: '#6e48aa',\n headerFontColor: '#fff',\n headerFontSize: '16px',\n botBubbleColor: '#6E48AA',\n botFontColor: '#fff',\n userBubbleColor: '#fff',\n userFontColor: '#4a4a4a'\n};\n","import styled from 'styled-components';\nimport defaultTheme from '../../theme';\n\nconst OptionElement = styled.button`\n background: ${({ theme }) => theme.botBubbleColor};\n border: 0;\n border-radius: 22px;\n box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.15);\n color: ${({ theme }) => theme.botFontColor};\n display: inline-block;\n font-size: 14px;\n padding: 12px;\n\n &:hover {\n opacity: 0.7;\n }\n &:active,\n &:hover:focus {\n outline:none;\n }\n`;\n\nOptionElement.defaultProps = {\n theme: defaultTheme\n};\n\nexport default OptionElement;\n","import styled from 'styled-components';\n\nconst Options = styled.ul`\n margin: 2px 0 12px 0;\n padding: 0 6px;\n`;\n\nexport default Options;\n","import styled from 'styled-components';\n\nconst OptionsStepContainer = styled.div``;\n\nexport default OptionsStepContainer;\n","import React, { Component } from 'react';\nimport PropTypes from 'prop-types';\nimport Option from './Option';\nimport OptionElement from './OptionElement';\nimport Options from './Options';\nimport OptionsStepContainer from './OptionsStepContainer';\n\nclass OptionsStep extends Component {\n onOptionClick = ({ value }) => {\n const { triggerNextStep } = this.props;\n\n triggerNextStep({ value });\n };\n\n renderOption = option => {\n const { bubbleOptionStyle, step } = this.props;\n const { user } = step;\n const { value, label } = option;\n\n return (\n \n );\n };\n\n render() {\n const { step } = this.props;\n const { options } = step;\n\n return (\n \n \n {Object.keys(options).map(key => options[key]).map(this.renderOption)}\n \n \n );\n }\n}\n\nOptionsStep.propTypes = {\n bubbleOptionStyle: PropTypes.objectOf(PropTypes.any).isRequired,\n step: PropTypes.objectOf(PropTypes.any).isRequired,\n triggerNextStep: PropTypes.func.isRequired\n};\n\nexport default OptionsStep;\n","import styled from 'styled-components';\nimport { scale } from '../../common/animations';\nimport defaultTheme from '../../theme';\n\nconst Bubble = styled.div`\n animation: ${scale} 0.3s ease forwards;\n background: ${props => (props.user ? props.theme.userBubbleColor : props.theme.botBubbleColor)};\n border-radius: ${props => {\n const { isFirst, isLast, user } = props;\n\n if (!isFirst && !isLast) {\n return user ? '18px 0 0 18px' : '0 18px 18px 0px';\n }\n\n if (!isFirst && isLast) {\n return user ? '18px 0 18px 18px' : '0 18px 18px 18px';\n }\n\n return props.user ? '18px 18px 0 18px' : '18px 18px 18px 0';\n }};\n box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.15);\n color: ${props => (props.user ? props.theme.userFontColor : props.theme.botFontColor)};\n display: inline-block;\n font-size: 14px;\n max-width: 50%;\n margin: ${props => {\n const { isFirst, showAvatar, user } = props;\n\n if (!isFirst && showAvatar) {\n return user ? '-8px 46px 10px 0' : '-8px 0 10px 46px';\n }\n\n if (!isFirst && !showAvatar) {\n return user ? '-8px 0px 10px 0' : '-8px 0 10px 0px';\n }\n\n return '0 0 10px 0';\n }};\n overflow: hidden;\n position: relative;\n padding: 12px;\n transform: scale(0);\n transform-origin: ${props => {\n const { isFirst, user } = props;\n\n if (isFirst) {\n return user ? 'bottom right' : 'bottom left';\n }\n\n return user ? 'top right' : 'top left';\n }};\n`;\n\nBubble.defaultProps = {\n theme: defaultTheme\n};\n\nexport default Bubble;\n","import styled from 'styled-components';\nimport { scale } from '../../common/animations';\n\nconst Image = styled.img`\n animation: ${scale} 0.3s ease forwards;\n border-radius: ${props => (props.user ? '50% 50% 50% 0' : '50% 50% 0 50%')};\n box-shadow: rgba(0, 0, 0, 0.15) 0px 1px 2px 0px;\n height: 40px;\n min-width: 40px;\n padding: 3px;\n transform: scale(0);\n transform-origin: ${props => (props.user ? 'bottom left' : 'bottom right')};\n width: 40;\n`;\n\nexport default Image;\n","import styled from 'styled-components';\n\nconst ImageContainer = styled.div`\n display: inline-block;\n order: ${props => (props.user ? '1' : '0')};\n padding: 6px;\n`;\n\nexport default ImageContainer;\n","import styled from 'styled-components';\n\nconst TextStepContainer = styled.div`\n align-items: flex-end;\n display: flex;\n justify-content: ${props => (props.user ? 'flex-end' : 'flex-start')};\n`;\n\nexport default TextStepContainer;\n","import React, { Component } from 'react';\nimport PropTypes from 'prop-types';\nimport Bubble from './Bubble';\nimport Image from './Image';\nimport ImageContainer from './ImageContainer';\nimport Loading from '../common/Loading';\nimport TextStepContainer from './TextStepContainer';\n\nclass TextStep extends Component {\n /* istanbul ignore next */\n state = {\n loading: true\n };\n\n componentDidMount() {\n const { step, speak, previousValue, triggerNextStep } = this.props;\n const { component, delay, waitAction } = step;\n const isComponentWatingUser = component && waitAction;\n\n setTimeout(() => {\n this.setState({ loading: false }, () => {\n if (!isComponentWatingUser && !step.rendered) {\n triggerNextStep();\n }\n speak(step, previousValue);\n });\n }, delay);\n }\n\n getMessage = () => {\n const { previousValue, step } = this.props;\n const { message } = step;\n\n return message ? message.replace(/{previousValue}/g, previousValue) : '';\n };\n\n renderMessage = () => {\n const { step, steps, previousStep, triggerNextStep } = this.props;\n const { component } = step;\n\n if (component) {\n return React.cloneElement(component, {\n step,\n steps,\n previousStep,\n triggerNextStep\n });\n }\n\n return this.getMessage();\n };\n\n render() {\n const {\n step,\n isFirst,\n isLast,\n avatarStyle,\n bubbleStyle,\n hideBotAvatar,\n hideUserAvatar\n } = this.props;\n const { loading } = this.state;\n const { avatar, user, botName } = step;\n\n const showAvatar = user ? !hideUserAvatar : !hideBotAvatar;\n\n const imageAltText = user ? \"Your avatar\" : `${botName}'s avatar`;\n\n return (\n \n \n {isFirst && showAvatar && (\n \n )}\n \n \n {loading ? : this.renderMessage()}\n \n \n );\n }\n}\n\nTextStep.propTypes = {\n avatarStyle: PropTypes.objectOf(PropTypes.any).isRequired,\n isFirst: PropTypes.bool.isRequired,\n isLast: PropTypes.bool.isRequired,\n bubbleStyle: PropTypes.objectOf(PropTypes.any).isRequired,\n hideBotAvatar: PropTypes.bool.isRequired,\n hideUserAvatar: PropTypes.bool.isRequired,\n previousStep: PropTypes.objectOf(PropTypes.any),\n previousValue: PropTypes.oneOfType([\n PropTypes.string,\n PropTypes.bool,\n PropTypes.number,\n PropTypes.object,\n PropTypes.array\n ]),\n speak: PropTypes.func,\n step: PropTypes.objectOf(PropTypes.any).isRequired,\n steps: PropTypes.objectOf(PropTypes.any),\n triggerNextStep: PropTypes.func.isRequired\n};\n\nTextStep.defaultProps = {\n previousStep: {},\n previousValue: '',\n speak: () => {},\n steps: {}\n};\n\nexport default TextStep;\n","export default [\n {\n key: 'id',\n types: ['string', 'number'],\n required: true\n },\n {\n key: 'user',\n types: ['boolean'],\n required: true\n },\n {\n key: 'hideExtraControl',\n types: ['boolean'],\n required: false\n },\n {\n key: 'trigger',\n types: ['string', 'number', 'function'],\n required: false\n },\n {\n key: 'validator',\n types: ['function'],\n required: false\n },\n {\n key: 'end',\n types: ['boolean'],\n required: false\n },\n {\n key: 'placeholder',\n types: ['string'],\n required: false\n },\n {\n key: 'inputAttributes',\n types: ['object'],\n required: false\n },\n {\n key: 'metadata',\n types: ['object'],\n required: false\n }\n];\n","export default [\n {\n key: 'id',\n types: ['string', 'number'],\n required: true\n },\n {\n key: 'message',\n types: ['string', 'function'],\n required: true\n },\n {\n key: 'avatar',\n types: ['string'],\n required: false\n },\n {\n key: 'trigger',\n types: ['string', 'number', 'function'],\n required: false\n },\n {\n key: 'delay',\n types: ['number'],\n required: false\n },\n {\n key: 'end',\n types: ['boolean'],\n required: false\n },\n {\n key: 'placeholder',\n types: ['string'],\n required: false\n },\n {\n key: 'hideInput',\n types: ['boolean'],\n required: false\n },\n {\n key: 'hideExtraControl',\n types: ['boolean'],\n required: false\n },\n {\n key: 'inputAttributes',\n types: ['object'],\n required: false\n },\n {\n key: 'metadata',\n types: ['object'],\n required: false\n }\n];\n","export default [\n {\n key: 'id',\n types: ['string', 'number'],\n required: true\n },\n {\n key: 'options',\n types: ['object'],\n required: true\n },\n {\n key: 'end',\n types: ['boolean'],\n required: false\n },\n {\n key: 'placeholder',\n types: ['string'],\n required: false\n },\n {\n key: 'hideInput',\n types: ['boolean'],\n required: false\n },\n {\n key: 'hideExtraControl',\n types: ['boolean'],\n required: false\n },\n {\n key: 'inputAttributes',\n types: ['object'],\n required: false\n },\n {\n key: 'metadata',\n types: ['object'],\n required: false\n }\n];\n","export default [\n {\n key: 'id',\n types: ['string', 'number'],\n required: true\n },\n {\n key: 'component',\n types: ['any'],\n required: true\n },\n {\n key: 'avatar',\n types: ['string'],\n required: false\n },\n {\n key: 'replace',\n types: ['boolean'],\n required: false\n },\n {\n key: 'waitAction',\n types: ['boolean'],\n required: false\n },\n {\n key: 'asMessage',\n types: ['boolean'],\n required: false\n },\n {\n key: 'trigger',\n types: ['string', 'number', 'function'],\n required: false\n },\n {\n key: 'delay',\n types: ['number'],\n required: false\n },\n {\n key: 'end',\n types: ['boolean'],\n required: false\n },\n {\n key: 'placeholder',\n types: ['string'],\n required: false\n },\n {\n key: 'hideInput',\n types: ['boolean'],\n required: false\n },\n {\n key: 'hideExtraControl',\n types: ['boolean'],\n required: false\n },\n {\n key: 'inputAttributes',\n types: ['object'],\n required: false\n },\n {\n key: 'metadata',\n types: ['object'],\n required: false\n }\n];\n","export default [\n {\n key: 'id',\n types: ['string', 'number'],\n required: true\n },\n {\n key: 'update',\n types: ['string', 'number'],\n required: true\n },\n {\n key: 'trigger',\n types: ['string', 'number', 'function'],\n required: true\n },\n {\n key: 'placeholder',\n types: ['string'],\n required: false\n },\n {\n key: 'inputAttributes',\n types: ['object'],\n required: false\n },\n {\n key: 'metadata',\n types: ['object'],\n required: false\n }\n];\n","import userSchema from './userSchema';\nimport textSchema from './textSchema';\nimport optionsSchema from './optionsSchema';\nimport customSchema from './customSchema';\nimport updateSchema from './updateSchema';\nimport { stringify } from 'flatted/cjs';\n\nconst schema = {\n parse(step) {\n let parser = [];\n\n if (step.user) {\n parser = userSchema;\n } else if (step.message) {\n parser = textSchema;\n } else if (step.options) {\n parser = optionsSchema;\n } else if (step.component) {\n parser = customSchema;\n } else if (step.update) {\n parser = updateSchema;\n } else {\n throw new Error(`The step ${stringify(step)} is invalid`);\n }\n\n for (let i = 0, len = parser.length; i < len; i += 1) {\n const { key, types, required } = parser[i];\n\n if (!step[key] && required) {\n throw new Error(`Key '${key}' is required in step ${stringify(step)}`);\n } else if (step[key]) {\n if (types[0] !== 'any' && types.indexOf(typeof step[key]) < 0) {\n throw new Error(\n `The type of '${key}' value must be ${types.join(' or ')} instead of ${typeof step[\n key\n ]}`\n );\n }\n }\n }\n\n const keys = parser.map(p => p.key);\n\n for (const key in step) {\n if (keys.indexOf(key) < 0) {\n console.error(`Invalid key '${key}' in step '${step.id}'`);\n delete step[key];\n }\n }\n\n return step;\n },\n\n checkInvalidIds(steps) {\n for (const key in steps) {\n const step = steps[key];\n const triggerId = steps[key].trigger;\n\n if (typeof triggerId !== 'function') {\n if (step.options) {\n const triggers = step.options.filter(option => typeof option.trigger !== 'function');\n const optionsTriggerIds = triggers.map(option => option.trigger);\n\n for (let i = 0, len = optionsTriggerIds.length; i < len; i += 1) {\n const optionTriggerId = optionsTriggerIds[i];\n if (optionTriggerId && !steps[optionTriggerId]) {\n throw new Error(\n `The id '${optionTriggerId}' triggered by option ${i + 1} in step '${\n steps[key].id\n }' does not exist`\n );\n }\n }\n } else if (triggerId && !steps[triggerId]) {\n throw new Error(\n `The id '${triggerId}' triggered by step '${steps[key].id}' does not exist`\n );\n }\n }\n }\n }\n};\n\nexport default schema;\n","import { stringify, parse } from 'flatted/cjs'\n\n/* istanbul ignore next */\nconst getData = (params, callback) => {\n const { cacheName, cache, firstStep, steps } = params;\n const currentStep = firstStep;\n const renderedSteps = [steps[currentStep.id]];\n const previousSteps = [steps[currentStep.id]];\n const previousStep = {};\n const unParsedCache = localStorage.getItem(cacheName);\n\n if (cache && unParsedCache) {\n try {\n const data = parse(unParsedCache);\n const lastStep = data.renderedSteps[data.renderedSteps.length - 1];\n\n if (lastStep && lastStep.end) {\n localStorage.removeItem(cacheName);\n } else {\n for (let i = 0, len = data.renderedSteps.length; i < len; i += 1) {\n const renderedStep = data.renderedSteps[i];\n // remove delay of cached rendered steps\n data.renderedSteps[i].delay = 0;\n // flag used to avoid call triggerNextStep in cached rendered steps\n data.renderedSteps[i].rendered = true;\n\n // an error is thrown when render a component from localStorage.\n // So it's necessary reassing the component\n if (renderedStep.component) {\n const { id } = renderedStep;\n data.renderedSteps[i].component = steps[id].component;\n }\n }\n\n const { trigger, end, options } = data.currentStep;\n const { id } = data.currentStep;\n\n if (options) {\n delete data.currentStep.rendered;\n }\n\n // add trigger function to current step\n if (!trigger && !end) {\n if (options) {\n for (let i = 0; i < options.length; i += 1) {\n data.currentStep.options[i].trigger = steps[id].options[i].trigger;\n }\n } else {\n data.currentStep.trigger = steps[id].trigger;\n }\n }\n\n // execute callback function to enable input if last step is\n // waiting user type\n if (data.currentStep.user) {\n callback();\n }\n\n return data;\n }\n } catch (error) {\n console.info(`Unable to parse cache named:${cacheName}. \\nThe cache where probably created with an older version of react-simple-chatbot.\\n`, error);\n }\n }\n\n return {\n currentStep,\n previousStep,\n previousSteps,\n renderedSteps\n };\n};\n\n/* istanbul ignore next */\nconst setData = (cacheName, cachedData) => {\n const data = parse(stringify(cachedData));\n // clean components\n for (const key in data) {\n for (let i = 0, len = data[key].length; i < len; i += 1) {\n if (data[key][i].component) {\n data[key][i].component = data[key][i].id;\n }\n }\n }\n\n localStorage.setItem(cacheName, stringify(data));\n};\n\nexport { getData, setData };\n","import styled from 'styled-components';\nimport defaultTheme from '../theme';\n\nconst ChatBotContainer = styled.div`\n background: ${({ theme }) => theme.background};\n border-radius: 10px;\n box-shadow: 0 12px 24px 0 rgba(0, 0, 0, 0.15);\n font-family: ${({ theme }) => theme.fontFamily};\n overflow: hidden;\n position: ${({ floating }) => (floating ? 'fixed' : 'relative')};\n bottom: ${({ floating, floatingStyle }) =>\n floating ? floatingStyle.bottom || '32px' : 'initial'};\n top: ${({ floating, floatingStyle }) => (floating ? floatingStyle.top || 'initial' : 'initial')};\n right: ${({ floating, floatingStyle }) => (floating ? floatingStyle.right || '32px' : 'initial')};\n left: ${({ floating, floatingStyle }) =>\n floating ? floatingStyle.left || 'initial' : 'initial'};\n width: ${({ width }) => width};\n height: ${({ height }) => height};\n z-index: 999;\n transform: ${({ opened }) => (opened ? 'scale(1)' : 'scale(0)')};\n transform-origin: ${({ floatingStyle }) => floatingStyle.transformOrigin || 'bottom right'};\n transition: transform 0.3s ease;\n\n @media screen and (max-width: 568px) {\n border-radius: ${({ floating }) => (floating ? '0' : '')};\n bottom: 0 !important;\n left: initial !important;\n height: 100%;\n right: 0 !important;\n top: initial !important;\n width: 100%;\n }\n`;\n\nChatBotContainer.defaultProps = {\n theme: defaultTheme\n};\n\nexport default ChatBotContainer;\n","import styled from 'styled-components';\n\nconst Content = styled.div`\n height: calc(${props => props.height} - ${props => (props.hideInput ? '56px' : '112px')});\n overflow-y: scroll;\n margin-top: 2px;\n padding-top: 6px;\n\n @media screen and (max-width: 568px) {\n height: ${props => (props.floating ? 'calc(100% - 112px)' : '')};\n }\n`;\n\nexport default Content;\n","import styled from 'styled-components';\nimport defaultTheme from '../theme';\n\nconst Header = styled.div`\n align-items: center;\n background: ${({ theme }) => theme.headerBgColor};\n color: ${({ theme }) => theme.headerFontColor};\n display: flex;\n fill: ${({ theme }) => theme.headerFontColor};\n height: 56px;\n justify-content: space-between;\n padding: 0 10px;\n`;\n\nHeader.defaultProps = {\n theme: defaultTheme\n};\n\nexport default Header;\n","import styled from 'styled-components';\nimport defaultTheme from '../theme';\n\nconst HeaderTitle = styled.h2`\n margin: 0;\n font-size: ${({ theme }) => theme.headerFontSize};\n`;\n\nHeaderTitle.defaultProps = {\n theme: defaultTheme\n};\n\nexport default HeaderTitle;\n","import styled from 'styled-components';\n\nconst HeaderIcon = styled.a`\n cursor: pointer;\n`;\n\nexport default HeaderIcon;\n","import styled from 'styled-components';\n\nconst FloatButton = styled.a`\n align-items: center;\n cursor: pointer;\n background: ${({ theme }) => theme.headerBgColor};\n bottom: 32px;\n border-radius: 100%;\n box-shadow: 0 12px 24px 0 rgba(0, 0, 0, 0.15);\n display: flex;\n fill: ${({ theme }) => theme.headerFontColor};\n height: 56px;\n justify-content: center;\n position: fixed;\n right: 32px;\n transform: ${props => (props.opened ? 'scale(0)' : 'scale(1)')};\n transition: transform 0.3s ease;\n width: 56px;\n z-index: 999;\n`;\n\nFloatButton.defaultProps = {\n theme: {\n headerBgColor: '#6e48aa',\n headerFontColor: '#fff'\n }\n};\n\nexport default FloatButton;\n","import styled from 'styled-components';\n\nconst FloatingIcon = styled.img`\n height: 24px;\n width: 24px;\n`;\n\nexport default FloatingIcon;\n","import styled from 'styled-components';\n\nconst Footer = styled.div`\n position: relative;\n`;\n\nexport default Footer;\n","import { invalidInput } from '../common/animations';\nimport styled, { css } from 'styled-components';\n\nconst Input = styled.input`\n animation: ${props =>\n props.invalid\n ? css`\n ${invalidInput} .2s ease\n `\n : ''};\n border: 0;\n border-radius: 0;\n border-bottom-left-radius: 10px;\n border-bottom-right-radius: 10px;\n border-top: ${props => (props.invalid ? '0' : '1px solid #eee')};\n box-shadow: ${props => (props.invalid ? 'inset 0 0 2px #E53935' : 'none')};\n box-sizing: border-box;\n color: ${props => (props.invalid ? '#E53935' : '')};\n font-size: 16px;\n opacity: ${props => (props.disabled && !props.invalid ? '.5' : '1')};\n outline: none;\n padding: ${props => (props.hasButton ? '16px 52px 16px 10px' : '16px 10px')};\n width: 100%;\n -webkit-appearance: none;\n\n &:disabled {\n background: #fff;\n }\n\n @media screen and (max-width: 568px) {\n border-bottom-left-radius: ${props => (props.floating ? '0' : '10px')};\n border-bottom-right-radius: ${props => (props.floating ? '0' : '10px')};\n }\n`;\n\nexport default Input;\n","import styled, { css } from 'styled-components';\nimport defaultTheme from '../theme';\nimport { pulse } from '../common/animations';\n\nconst fillFunc = props => {\n const { speaking, invalid, theme } = props;\n\n if (speaking) {\n return theme.headerBgColor;\n }\n return invalid ? '#E53935' : '#4a4a4a';\n};\n\nconst SubmitButton = styled.button`\n background-color: transparent;\n border: 0;\n border-bottom-right-radius: 10px;\n box-shadow: none;\n cursor: ${props => (props.disabled ? 'default' : 'pointer')};\n fill: ${fillFunc};\n opacity: ${props => (props.disabled && !props.invalid ? '.5' : '1')};\n outline: none;\n padding: 14px 16px 12px 16px;\n &:before {\n content: '';\n position: absolute;\n width: 23px;\n height: 23px;\n border-radius: 50%;\n animation: ${({ theme, speaking }) =>\n speaking\n ? css`\n ${pulse(theme.headerBgColor)} 2s ease infinite\n `\n : ''};\n }\n &:not(:disabled):hover {\n opacity: 0.7;\n }\n`;\n\nSubmitButton.defaultProps = {\n theme: defaultTheme\n};\n\nexport default SubmitButton;\n","let instance = null;\n\nconst noop = () => {};\n\nexport default class Recognition {\n static isSupported() {\n return 'webkitSpeechRecognition' in window;\n }\n\n /**\n * Creates an instance of Recognition.\n * @param {function} [onChange] callback on change\n * @param {function} [onEnd] callback on and\n * @param {function} [onStop] callback on stop\n * @param {string} [lang='en'] recognition lang\n * @memberof Recognition\n * @constructor\n */\n constructor(onChange = noop, onEnd = noop, onStop = noop, lang = 'en') {\n if (!instance) {\n instance = this;\n }\n this.state = {\n inputValue: '',\n lang,\n onChange,\n onEnd,\n onStop\n };\n\n this.onResult = this.onResult.bind(this);\n this.onEnd = this.onEnd.bind(this);\n\n this.setup();\n\n return instance;\n }\n\n /**\n * Handler for recognition change event\n * @param {string} interimTranscript\n * @memberof Recognition\n * @private\n */\n onChange(interimTranscript) {\n const { onChange } = this.state;\n this.setState({\n inputValue: interimTranscript\n });\n onChange(interimTranscript);\n }\n\n /**\n * Handler for recognition change event when its final\n * @param {string} finalTranscript\n * @memberof Recognition\n * @private\n */\n onFinal(finalTranscript) {\n this.setState({\n inputValue: finalTranscript\n });\n this.recognition.stop();\n }\n\n /**\n * Handler for recognition end event\n * @memberof Recognition\n * @private\n */\n onEnd() {\n const { onStop, onEnd, force } = this.state;\n this.setState({ speaking: false });\n if (force) {\n onStop();\n } else {\n onEnd();\n }\n }\n\n /**\n * Handler for recognition result event\n * @memberof Recognition\n * @private\n */\n onResult(event) {\n let interimTranscript = '';\n let finalTranscript = '';\n\n for (let i = event.resultIndex; i < event.results.length; i += 1) {\n if (event.results[i].isFinal) {\n finalTranscript += event.results[i][0].transcript;\n this.onFinal(finalTranscript);\n } else {\n interimTranscript += event.results[i][0].transcript;\n this.onChange(interimTranscript);\n }\n }\n }\n\n /**\n * method for updating the instance state\n * @param {object} nextState\n * @memberof Recognition\n * @private\n */\n setState(nextState) {\n this.state = Object.assign({}, this.state, nextState);\n }\n\n /**\n * setup the browser recognition\n * @returns {Recognition}\n * @memberof Recognition\n * @public\n */\n setup() {\n if (!Recognition.isSupported()) {\n return this;\n }\n\n const { webkitSpeechRecognition } = window;\n\n this.recognition = new webkitSpeechRecognition();\n this.recognition.continuous = true;\n this.recognition.interimResults = true;\n this.recognition.lang = this.state.lang;\n this.recognition.onresult = this.onResult;\n this.recognition.onend = this.onEnd;\n return this;\n }\n\n /**\n * change the recognition lang and resetup\n * @param {string} lang the new lang\n * @returns {Recognition}\n * @memberof Recognition\n * @public\n */\n setLang(lang) {\n this.setState({ lang });\n this.setup();\n return this;\n }\n\n /**\n * toggle the recognition\n * @returns {Recognition}\n * @memberof Recognition\n * @public\n */\n speak() {\n if (!Recognition.isSupported()) {\n return this;\n }\n const { speaking } = this.state;\n if (!speaking) {\n this.recognition.start();\n this.setState({\n speaking: true,\n inputValue: ''\n });\n } else {\n this.setState({\n force: true\n });\n this.recognition.stop();\n }\n return this;\n }\n}\n","import React from 'react';\n\nconst ChatIcon = () => (\n \n);\n\nexport default ChatIcon;\n","import React from 'react';\n\nconst CloseIcon = () => (\n \n);\n\nexport default CloseIcon;\n","import React from 'react';\nimport PropTypes from 'prop-types';\n\nconst SubmitIcon = ({ size }) => (\n \n);\n\nSubmitIcon.propTypes = {\n size: PropTypes.number\n};\n\nSubmitIcon.defaultProps = {\n size: 20\n};\n\nexport default SubmitIcon;\n","import React from 'react';\nimport PropTypes from 'prop-types';\n\nconst MicIcon = ({ size }) => (\n \n);\n\nMicIcon.propTypes = {\n size: PropTypes.number\n};\n\nMicIcon.defaultProps = {\n size: 20\n};\n\nexport default MicIcon;\n","export const isMobile = () => /iphone|ipod|android|ie|blackberry|fennec/i.test(navigator.userAgent);\n\nexport const isString = value => typeof value === 'string';\n","import { isString } from './utils';\n\nexport const getSpeakText = step => {\n const { message, metadata = {} } = step;\n if (isString(metadata.speak)) {\n return metadata.speak;\n }\n if (isString(message)) {\n return message;\n }\n return '';\n};\n\nexport const speakFn = speechSynthesisOptions => (step, previousValue) => {\n const { lang, voice, enable } = speechSynthesisOptions;\n const { user } = step;\n\n if (!window.SpeechSynthesisUtterance || !window.speechSynthesis) {\n return;\n }\n if (user) {\n return;\n }\n if (!enable) {\n return;\n }\n const text = getSpeakText(step);\n const msg = new window.SpeechSynthesisUtterance();\n msg.text = text.replace(/{previousValue}/g, previousValue);\n msg.lang = lang;\n msg.voice = voice;\n window.speechSynthesis.speak(msg);\n};\n","import React, { Component } from 'react';\nimport PropTypes from 'prop-types';\nimport Random from 'random-id';\nimport { CustomStep, OptionsStep, TextStep } from './steps_components';\nimport schema from './schemas/schema';\nimport * as storage from './storage';\nimport {\n ChatBotContainer,\n Content,\n Header,\n HeaderTitle,\n HeaderIcon,\n FloatButton,\n FloatingIcon,\n Footer,\n Input,\n SubmitButton\n} from './components';\nimport Recognition from './recognition';\nimport { ChatIcon, CloseIcon, SubmitIcon, MicIcon } from './icons';\nimport { isMobile } from './utils';\nimport { speakFn } from './speechSynthesis';\n\nclass ChatBot extends Component {\n /* istanbul ignore next */\n constructor(props) {\n super(props);\n\n this.content = null;\n this.input = null;\n\n this.supportsScrollBehavior = false;\n\n this.setContentRef = element => {\n this.content = element;\n };\n\n this.setInputRef = element => {\n this.input = element;\n };\n\n this.state = {\n renderedSteps: [],\n previousSteps: [],\n currentStep: {},\n previousStep: {},\n steps: {},\n disabled: true,\n opened: props.opened || !props.floating,\n inputValue: '',\n inputInvalid: false,\n speaking: false,\n recognitionEnable: props.recognitionEnable && Recognition.isSupported(),\n defaultUserSettings: {}\n };\n\n this.speak = speakFn(props.speechSynthesis);\n }\n\n componentDidMount() {\n const { steps } = this.props;\n const {\n botDelay,\n botAvatar,\n botName,\n cache,\n cacheName,\n customDelay,\n enableMobileAutoFocus,\n userAvatar,\n userDelay\n } = this.props;\n const chatSteps = {};\n\n const defaultBotSettings = { delay: botDelay, avatar: botAvatar, botName };\n const defaultUserSettings = {\n delay: userDelay,\n avatar: userAvatar,\n hideInput: false,\n hideExtraControl: false\n };\n const defaultCustomSettings = { delay: customDelay };\n\n for (let i = 0, len = steps.length; i < len; i += 1) {\n const step = steps[i];\n let settings = {};\n\n if (step.user) {\n settings = defaultUserSettings;\n } else if (step.message || step.asMessage) {\n settings = defaultBotSettings;\n } else if (step.component) {\n settings = defaultCustomSettings;\n }\n\n chatSteps[step.id] = Object.assign({}, settings, schema.parse(step));\n }\n\n schema.checkInvalidIds(chatSteps);\n\n const firstStep = steps[0];\n\n if (firstStep.message) {\n const { message } = firstStep;\n firstStep.message = typeof message === 'function' ? message() : message;\n chatSteps[firstStep.id].message = firstStep.message;\n }\n\n const { recognitionEnable } = this.state;\n const { recognitionLang } = this.props;\n\n if (recognitionEnable) {\n this.recognition = new Recognition(\n this.onRecognitionChange,\n this.onRecognitionEnd,\n this.onRecognitionStop,\n recognitionLang\n );\n }\n\n this.supportsScrollBehavior = 'scrollBehavior' in document.documentElement.style;\n\n if (this.content) {\n this.content.addEventListener('DOMNodeInserted', this.onNodeInserted);\n window.addEventListener('resize', this.onResize);\n }\n\n const { currentStep, previousStep, previousSteps, renderedSteps } = storage.getData(\n {\n cacheName,\n cache,\n firstStep,\n steps: chatSteps\n },\n () => {\n // focus input if last step cached is a user step\n this.setState({ disabled: false }, () => {\n if (enableMobileAutoFocus || !isMobile()) {\n if (this.input) {\n this.input.focus();\n }\n }\n });\n }\n );\n\n this.setState({\n currentStep,\n defaultUserSettings,\n previousStep,\n previousSteps,\n renderedSteps,\n steps: chatSteps\n });\n }\n\n static getDerivedStateFromProps(props, state) {\n const { opened, toggleFloating } = props;\n if (toggleFloating !== undefined && opened !== undefined && opened !== state.opened) {\n return {\n ...state,\n opened\n };\n }\n return state;\n }\n\n componentWillUnmount() {\n if (this.content) {\n this.content.removeEventListener('DOMNodeInserted', this.onNodeInserted);\n window.removeEventListener('resize', this.onResize);\n }\n }\n\n onNodeInserted = event => {\n const { currentTarget: target } = event;\n const { enableSmoothScroll } = this.props;\n\n if (enableSmoothScroll && this.supportsScrollBehavior) {\n target.scroll({\n top: target.scrollHeight,\n left: 0,\n behavior: 'smooth'\n });\n } else {\n target.scrollTop = target.scrollHeight;\n }\n };\n\n onResize = () => {\n this.content.scrollTop = this.content.scrollHeight;\n };\n\n onRecognitionChange = value => {\n this.setState({ inputValue: value });\n };\n\n onRecognitionEnd = () => {\n this.setState({ speaking: false });\n this.handleSubmitButton();\n };\n\n onRecognitionStop = () => {\n this.setState({ speaking: false });\n };\n\n onValueChange = event => {\n this.setState({ inputValue: event.target.value });\n };\n\n getTriggeredStep = (trigger, value) => {\n const steps = this.generateRenderedStepsById();\n return typeof trigger === 'function' ? trigger({ value, steps }) : trigger;\n };\n\n getStepMessage = message => {\n const { previousSteps } = this.state;\n const lastStepIndex = previousSteps.length > 0 ? previousSteps.length - 1 : 0;\n const steps = this.generateRenderedStepsById();\n const previousValue = previousSteps[lastStepIndex].value;\n return typeof message === 'function' ? message({ previousValue, steps }) : message;\n };\n\n generateRenderedStepsById = () => {\n const { previousSteps } = this.state;\n const steps = {};\n\n for (let i = 0, len = previousSteps.length; i < len; i += 1) {\n const { id, message, value, metadata } = previousSteps[i];\n\n steps[id] = {\n id,\n message,\n value,\n metadata\n };\n }\n\n return steps;\n };\n\n triggerNextStep = data => {\n const { enableMobileAutoFocus } = this.props;\n const { defaultUserSettings, previousSteps, renderedSteps, steps } = this.state;\n\n let { currentStep, previousStep } = this.state;\n const isEnd = currentStep.end;\n\n if (data && data.value) {\n currentStep.value = data.value;\n }\n if (data && data.hideInput) {\n currentStep.hideInput = data.hideInput;\n }\n if (data && data.hideExtraControl) {\n currentStep.hideExtraControl = data.hideExtraControl;\n }\n if (data && data.trigger) {\n currentStep.trigger = this.getTriggeredStep(data.trigger, data.value);\n }\n\n if (isEnd) {\n this.handleEnd();\n } else if (currentStep.options && data) {\n const option = currentStep.options.filter(o => o.value === data.value)[0];\n const trigger = this.getTriggeredStep(option.trigger, currentStep.value);\n delete currentStep.options;\n\n // replace choose option for user message\n currentStep = Object.assign({}, currentStep, option, defaultUserSettings, {\n user: true,\n message: option.label,\n trigger\n });\n\n renderedSteps.pop();\n previousSteps.pop();\n renderedSteps.push(currentStep);\n previousSteps.push(currentStep);\n\n this.setState({\n currentStep,\n renderedSteps,\n previousSteps\n });\n } else if (currentStep.trigger) {\n if (currentStep.replace) {\n renderedSteps.pop();\n }\n\n const trigger = this.getTriggeredStep(currentStep.trigger, currentStep.value);\n let nextStep = Object.assign({}, steps[trigger]);\n\n if (nextStep.message) {\n nextStep.message = this.getStepMessage(nextStep.message);\n } else if (nextStep.update) {\n const updateStep = nextStep;\n nextStep = Object.assign({}, steps[updateStep.update]);\n\n if (nextStep.options) {\n for (let i = 0, len = nextStep.options.length; i < len; i += 1) {\n nextStep.options[i].trigger = updateStep.trigger;\n }\n } else {\n nextStep.trigger = updateStep.trigger;\n }\n }\n\n nextStep.key = Random(24);\n\n previousStep = currentStep;\n currentStep = nextStep;\n\n this.setState({ renderedSteps, currentStep, previousStep }, () => {\n if (nextStep.user) {\n this.setState({ disabled: false }, () => {\n if (enableMobileAutoFocus || !isMobile()) {\n if (this.input) {\n this.input.focus();\n }\n }\n });\n } else {\n renderedSteps.push(nextStep);\n previousSteps.push(nextStep);\n\n this.setState({ renderedSteps, previousSteps });\n }\n });\n }\n\n const { cache, cacheName } = this.props;\n if (cache) {\n setTimeout(() => {\n storage.setData(cacheName, {\n currentStep,\n previousStep,\n previousSteps,\n renderedSteps\n });\n }, 300);\n }\n };\n\n handleEnd = () => {\n const { handleEnd } = this.props;\n\n if (handleEnd) {\n const { previousSteps } = this.state;\n\n const renderedSteps = previousSteps.map(step => {\n const { id, message, value, metadata } = step;\n\n return {\n id,\n message,\n value,\n metadata\n };\n });\n\n const steps = [];\n\n for (let i = 0, len = previousSteps.length; i < len; i += 1) {\n const { id, message, value, metadata } = previousSteps[i];\n\n steps[id] = {\n id,\n message,\n value,\n metadata\n };\n }\n\n const values = previousSteps.filter(step => step.value).map(step => step.value);\n\n handleEnd({ renderedSteps, steps, values });\n }\n };\n\n isInputValueEmpty = () => {\n const { inputValue } = this.state;\n return !inputValue || inputValue.length === 0;\n };\n\n isLastPosition = step => {\n const { renderedSteps } = this.state;\n const { length } = renderedSteps;\n const stepIndex = renderedSteps.map(s => s.key).indexOf(step.key);\n\n if (length <= 1 || stepIndex + 1 === length) {\n return true;\n }\n\n const nextStep = renderedSteps[stepIndex + 1];\n const hasMessage = nextStep.message || nextStep.asMessage;\n\n if (!hasMessage) {\n return true;\n }\n\n const isLast = step.user !== nextStep.user;\n return isLast;\n };\n\n isFirstPosition = step => {\n const { renderedSteps } = this.state;\n const stepIndex = renderedSteps.map(s => s.key).indexOf(step.key);\n\n if (stepIndex === 0) {\n return true;\n }\n\n const lastStep = renderedSteps[stepIndex - 1];\n const hasMessage = lastStep.message || lastStep.asMessage;\n\n if (!hasMessage) {\n return true;\n }\n\n const isFirst = step.user !== lastStep.user;\n return isFirst;\n };\n\n handleKeyPress = event => {\n if (event.key === 'Enter') {\n this.submitUserMessage();\n }\n };\n\n handleSubmitButton = () => {\n const { speaking, recognitionEnable } = this.state;\n\n if ((this.isInputValueEmpty() || speaking) && recognitionEnable) {\n this.recognition.speak();\n if (!speaking) {\n this.setState({ speaking: true });\n }\n return;\n }\n\n this.submitUserMessage();\n };\n\n submitUserMessage = () => {\n const { defaultUserSettings, inputValue, previousSteps, renderedSteps } = this.state;\n let { currentStep } = this.state;\n\n const isInvalid = currentStep.validator && this.checkInvalidInput();\n\n if (!isInvalid) {\n const step = {\n message: inputValue,\n value: inputValue\n };\n\n currentStep = Object.assign({}, defaultUserSettings, currentStep, step);\n\n renderedSteps.push(currentStep);\n previousSteps.push(currentStep);\n\n this.setState(\n {\n currentStep,\n renderedSteps,\n previousSteps,\n disabled: true,\n inputValue: ''\n },\n () => {\n if (this.input) {\n this.input.blur();\n }\n }\n );\n }\n };\n\n checkInvalidInput = () => {\n const { enableMobileAutoFocus } = this.props;\n const { currentStep, inputValue } = this.state;\n const result = currentStep.validator(inputValue);\n const value = inputValue;\n\n if (typeof result !== 'boolean' || !result) {\n this.setState(\n {\n inputValue: result.toString(),\n inputInvalid: true,\n disabled: true\n },\n () => {\n setTimeout(() => {\n this.setState(\n {\n inputValue: value,\n inputInvalid: false,\n disabled: false\n },\n () => {\n if (enableMobileAutoFocus || !isMobile()) {\n if (this.input) {\n this.input.focus();\n }\n }\n }\n );\n }, 2000);\n }\n );\n\n return true;\n }\n\n return false;\n };\n\n toggleChatBot = opened => {\n const { toggleFloating } = this.props;\n\n if (toggleFloating) {\n toggleFloating({ opened });\n } else {\n this.setState({ opened });\n }\n };\n\n renderStep = (step, index) => {\n const { renderedSteps } = this.state;\n const {\n avatarStyle,\n bubbleStyle,\n bubbleOptionStyle,\n customStyle,\n hideBotAvatar,\n hideUserAvatar,\n speechSynthesis\n } = this.props;\n const { options, component, asMessage } = step;\n const steps = this.generateRenderedStepsById();\n const previousStep = index > 0 ? renderedSteps[index - 1] : {};\n\n if (component && !asMessage) {\n return (\n \n );\n }\n\n if (options) {\n return (\n \n );\n }\n\n return (\n \n );\n };\n\n render() {\n const {\n currentStep,\n disabled,\n inputInvalid,\n inputValue,\n opened,\n renderedSteps,\n speaking,\n recognitionEnable\n } = this.state;\n const {\n className,\n contentStyle,\n extraControl,\n controlStyle,\n floating,\n floatingIcon,\n floatingStyle,\n footerStyle,\n headerComponent,\n headerTitle,\n hideHeader,\n hideSubmitButton,\n inputStyle,\n placeholder,\n inputAttributes,\n recognitionPlaceholder,\n style,\n submitButtonStyle,\n width,\n height\n } = this.props;\n\n const header = headerComponent || (\n \n {headerTitle}\n {floating && (\n this.toggleChatBot(false)}>\n \n \n )}\n \n );\n\n let customControl;\n if (extraControl !== undefined) {\n customControl = React.cloneElement(extraControl, {\n disabled,\n speaking,\n invalid: inputInvalid\n });\n }\n\n const icon =\n (this.isInputValueEmpty() || speaking) && recognitionEnable ? : ;\n\n const inputPlaceholder = speaking\n ? recognitionPlaceholder\n : currentStep.placeholder || placeholder;\n\n const inputAttributesOverride = currentStep.inputAttributes || inputAttributes;\n\n return (\n \n {floating && (\n
this.toggleChatBot(true)}\n >\n {typeof floatingIcon === 'string' ? : floatingIcon}\n \n )}\n
\n {!hideHeader && header}\n \n {renderedSteps.map(this.renderStep)}\n \n \n \n
\n );\n }\n}\n\nChatBot.propTypes = {\n avatarStyle: PropTypes.objectOf(PropTypes.any),\n botAvatar: PropTypes.string,\n botName: PropTypes.string,\n botDelay: PropTypes.number,\n bubbleOptionStyle: PropTypes.objectOf(PropTypes.any),\n bubbleStyle: PropTypes.objectOf(PropTypes.any),\n cache: PropTypes.bool,\n cacheName: PropTypes.string,\n className: PropTypes.string,\n contentStyle: PropTypes.objectOf(PropTypes.any),\n customDelay: PropTypes.number,\n customStyle: PropTypes.objectOf(PropTypes.any),\n controlStyle: PropTypes.objectOf(PropTypes.any),\n enableMobileAutoFocus: PropTypes.bool,\n enableSmoothScroll: PropTypes.bool,\n extraControl: PropTypes.objectOf(PropTypes.element),\n floating: PropTypes.bool,\n floatingIcon: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),\n floatingStyle: PropTypes.objectOf(PropTypes.any),\n footerStyle: PropTypes.objectOf(PropTypes.any),\n handleEnd: PropTypes.func,\n headerComponent: PropTypes.element,\n headerTitle: PropTypes.string,\n height: PropTypes.string,\n hideBotAvatar: PropTypes.bool,\n hideHeader: PropTypes.bool,\n hideSubmitButton: PropTypes.bool,\n hideUserAvatar: PropTypes.bool,\n inputAttributes: PropTypes.objectOf(PropTypes.any),\n inputStyle: PropTypes.objectOf(PropTypes.any),\n opened: PropTypes.bool,\n toggleFloating: PropTypes.func,\n placeholder: PropTypes.string,\n recognitionEnable: PropTypes.bool,\n recognitionLang: PropTypes.string,\n recognitionPlaceholder: PropTypes.string,\n speechSynthesis: PropTypes.shape({\n enable: PropTypes.bool,\n lang: PropTypes.string,\n voice:\n typeof window !== 'undefined'\n ? PropTypes.instanceOf(window.SpeechSynthesisVoice)\n : PropTypes.any\n }),\n steps: PropTypes.arrayOf(PropTypes.object).isRequired,\n style: PropTypes.objectOf(PropTypes.any),\n submitButtonStyle: PropTypes.objectOf(PropTypes.any),\n userAvatar: PropTypes.string,\n userDelay: PropTypes.number,\n width: PropTypes.string\n};\n\nChatBot.defaultProps = {\n avatarStyle: {},\n botDelay: 1000,\n botName: 'The bot',\n bubbleOptionStyle: {},\n bubbleStyle: {},\n cache: false,\n cacheName: 'rsc_cache',\n className: '',\n contentStyle: {},\n customStyle: {},\n controlStyle: { position: 'absolute', right: '0', top: '0' },\n customDelay: 1000,\n enableMobileAutoFocus: false,\n enableSmoothScroll: false,\n extraControl: undefined,\n floating: false,\n floatingIcon: ,\n floatingStyle: {},\n footerStyle: {},\n handleEnd: undefined,\n headerComponent: undefined,\n headerTitle: 'Chat',\n height: '520px',\n hideBotAvatar: false,\n hideHeader: false,\n hideSubmitButton: false,\n hideUserAvatar: false,\n inputStyle: {},\n opened: undefined,\n placeholder: 'Type the message ...',\n inputAttributes: {},\n recognitionEnable: false,\n recognitionLang: 'en',\n recognitionPlaceholder: 'Listening ...',\n speechSynthesis: {\n enable: false,\n lang: 'en',\n voice: null\n },\n style: {},\n submitButtonStyle: {},\n toggleFloating: undefined,\n userDelay: 1000,\n width: '350px',\n botAvatar:\n \"data:image/svg+xml,%3csvg version='1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath d='M303 70a47 47 0 1 0-70 40v84h46v-84c14-8 24-23 24-40z' fill='%2393c7ef'/%3e%3cpath d='M256 23v171h23v-84a47 47 0 0 0-23-87z' fill='%235a8bb0'/%3e%3cpath fill='%2393c7ef' d='M0 240h248v124H0z'/%3e%3cpath fill='%235a8bb0' d='M264 240h248v124H264z'/%3e%3cpath fill='%2393c7ef' d='M186 365h140v124H186z'/%3e%3cpath fill='%235a8bb0' d='M256 365h70v124h-70z'/%3e%3cpath fill='%23cce9f9' d='M47 163h419v279H47z'/%3e%3cpath fill='%2393c7ef' d='M256 163h209v279H256z'/%3e%3cpath d='M194 272a31 31 0 0 1-62 0c0-18 14-32 31-32s31 14 31 32z' fill='%233c5d76'/%3e%3cpath d='M380 272a31 31 0 0 1-62 0c0-18 14-32 31-32s31 14 31 32z' fill='%231e2e3b'/%3e%3cpath d='M186 349a70 70 0 1 0 140 0H186z' fill='%233c5d76'/%3e%3cpath d='M256 349v70c39 0 70-31 70-70h-70z' fill='%231e2e3b'/%3e%3c/svg%3e\",\n userAvatar:\n \"data:image/svg+xml,%3csvg viewBox='-208.5 21 100 100' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3e%3ccircle cx='-158.5' cy='71' fill='%23F5EEE5' r='50'/%3e%3cdefs%3e%3ccircle cx='-158.5' cy='71' id='a' r='50'/%3e%3c/defs%3e%3cclipPath id='b'%3e%3cuse overflow='visible' xlink:href='%23a'/%3e%3c/clipPath%3e%3cpath clip-path='url(%23b)' d='M-108.5 121v-14s-21.2-4.9-28-6.7c-2.5-.7-7-3.3-7-12V82h-30v6.3c0 8.7-4.5 11.3-7 12-6.8 1.9-28.1 7.3-28.1 6.7v14h100.1z' fill='%23E6C19C'/%3e%3cg clip-path='url(%23b)'%3e%3cdefs%3e%3cpath d='M-108.5 121v-14s-21.2-4.9-28-6.7c-2.5-.7-7-3.3-7-12V82h-30v6.3c0 8.7-4.5 11.3-7 12-6.8 1.9-28.1 7.3-28.1 6.7v14h100.1z' id='c'/%3e%3c/defs%3e%3cclipPath id='d'%3e%3cuse overflow='visible' xlink:href='%23c'/%3e%3c/clipPath%3e%3cpath clip-path='url(%23d)' d='M-158.5 100.1c12.7 0 23-18.6 23-34.4 0-16.2-10.3-24.7-23-24.7s-23 8.5-23 24.7c0 15.8 10.3 34.4 23 34.4z' fill='%23D4B08C'/%3e%3c/g%3e%3cpath d='M-158.5 96c12.7 0 23-16.3 23-31 0-15.1-10.3-23-23-23s-23 7.9-23 23c0 14.7 10.3 31 23 31z' fill='%23F2CEA5'/%3e%3c/svg%3e\"\n};\n\nexport default ChatBot;\n","import ChatBot from './ChatBot';\nimport Loading from './steps_components/common/Loading';\n\nexport default ChatBot;\nexport { Loading };\n"],"sourceRoot":""}
\ No newline at end of file
diff --git a/eslint.config.mjs b/eslint.config.mjs
new file mode 100644
index 00000000..2e26668f
--- /dev/null
+++ b/eslint.config.mjs
@@ -0,0 +1,33 @@
+import { defineConfig } from 'eslint/config';
+import js from '@eslint/js';
+import tseslint from 'typescript-eslint';
+import react from 'eslint-plugin-react';
+import reactHooks from 'eslint-plugin-react-hooks';
+import jsxA11y from 'eslint-plugin-jsx-a11y';
+import prettierRecommended from 'eslint-plugin-prettier/recommended';
+import globals from 'globals';
+
+export default defineConfig(
+ { ignores: ['dist', 'coverage', 'tests/helpers/corti.ts'] },
+ js.configs.recommended,
+ tseslint.configs.recommended,
+ react.configs.flat.recommended,
+ jsxA11y.flatConfigs.recommended,
+ {
+ plugins: { 'react-hooks': reactHooks },
+ languageOptions: {
+ globals: globals.browser
+ },
+ settings: {
+ react: { version: 'detect' }
+ },
+ rules: {
+ 'react-hooks/rules-of-hooks': 'error',
+ 'react-hooks/exhaustive-deps': 'error',
+ 'react/prop-types': 'off',
+ '@typescript-eslint/no-explicit-any': 'off',
+ '@typescript-eslint/no-unused-vars': ['error', { ignoreRestSiblings: true }]
+ }
+ },
+ prettierRecommended
+);
diff --git a/example/components/Example.jsx b/example/components/Example.jsx
deleted file mode 100644
index 5cd87354..00000000
--- a/example/components/Example.jsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import React from 'react';
-import { ThemeProvider } from 'styled-components';
-import ChatBot from '../../lib/index';
-
-const otherFontTheme = {
- background: '#f5f8fb',
- fontFamily: 'Helvetica Neue',
- headerBgColor: '#6e48aa',
- headerFontColor: '#fff',
- headerFontSize: '16px',
- botBubbleColor: '#6E48AA',
- botFontColor: '#fff',
- userBubbleColor: '#fff',
- userFontColor: '#4a4a4a'
-};
-
-const steps = [
- {
- id: '1',
- message: 'Hello World',
- end: true
- }
-];
-
-const ThemedExample = () => (
-
-
-
-
-
-);
-
-export default ThemedExample;
diff --git a/example/components/Example.tsx b/example/components/Example.tsx
new file mode 100644
index 00000000..7e2a2c60
--- /dev/null
+++ b/example/components/Example.tsx
@@ -0,0 +1,44 @@
+import React from 'react';
+import { ThemeProvider } from 'styled-components';
+import ChatBot, { type ChatBotTheme, type CustomComponentProps, type Step } from '../../src';
+
+const otherFontTheme: ChatBotTheme = {
+ background: '#f5f8fb',
+ fontFamily: 'Helvetica Neue',
+ headerBgColor: '#6e48aa',
+ headerFontColor: '#fff',
+ headerFontSize: '16px',
+ botBubbleColor: '#6E48AA',
+ botFontColor: '#fff',
+ userBubbleColor: '#fff',
+ userFontColor: '#4a4a4a'
+};
+
+const Summary = ({ steps }: CustomComponentProps) => (
+
+ {steps?.name.value} likes {steps?.fruit.value}
+
+);
+
+const steps: Step[] = [
+ { id: '1', message: 'Hello! What is your name?', trigger: 'name' },
+ { id: 'name', user: true, trigger: '3' },
+ { id: '3', message: 'Hi {previousValue}! Which fruit do you like?', trigger: 'fruit' },
+ {
+ id: 'fruit',
+ options: [
+ { label: 'Apple', trigger: 'summary' },
+ { label: 'Banana', trigger: 'summary' }
+ ]
+ },
+ { id: 'summary', component: , asMessage: true, trigger: 'end' },
+ { id: 'end', message: 'Thanks!', end: true }
+];
+
+const ThemedExample = () => (
+
+
+
+);
+
+export default ThemedExample;
diff --git a/example/index.html b/example/index.html
index 52adae25..7fc0bc1e 100644
--- a/example/index.html
+++ b/example/index.html
@@ -6,6 +6,6 @@
-
+