├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── README.md ├── css └── main.css ├── demo └── src │ ├── demo.css │ └── index.js ├── nwb.config.js ├── package.json ├── src ├── components │ ├── Audio.js │ ├── Dialog.js │ ├── DialogGroup.js │ ├── Header.js │ └── Input.js ├── index.js └── lib │ └── dialogflow │ ├── ApiAiClient.js │ ├── ApiAiConstants.js │ ├── Errors.js │ ├── Interfaces.js │ ├── Request │ ├── EventRequest.js │ ├── Request.js │ └── TextRequest.js │ ├── XhrRequest.js │ └── index.js ├── tests ├── .eslintrc └── index-test.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /demo/dist 3 | /es 4 | /lib 5 | /node_modules 6 | /umd 7 | **/.DS_Store 8 | *.log 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | node_js: 5 | - 6 6 | 7 | before_install: 8 | - npm install codecov.io coveralls 9 | 10 | after_success: 11 | - cat ./coverage/lcov.info | ./node_modules/codecov.io/bin/codecov.io.js 12 | - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js 13 | 14 | branches: 15 | only: 16 | - master 17 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Prerequisites 2 | 3 | [Node.js](http://nodejs.org/) >= v4 must be installed. 4 | 5 | ## Installation 6 | 7 | - Running `npm install` in the components's root directory will install everything you need for development. 8 | 9 | ## Demo Development Server 10 | 11 | - `npm start` will run a development server with the component's demo app at [http://localhost:3000](http://localhost:3000) with hot module reloading. 12 | 13 | ## Running Tests 14 | 15 | - `npm test` will run the tests once. 16 | 17 | - `npm run test:coverage` will run the tests and produce a coverage report in `coverage/`. 18 | 19 | - `npm run test:watch` will run the tests on every change. 20 | 21 | ## Building 22 | 23 | - `npm run build` will build the component for publishing to npm and also bundle the demo app. 24 | 25 | - `npm run clean` will delete built resources. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-bot-ui 2 | 3 | [![Travis][build-badge]][build] 4 | [![npm package][npm-badge]][npm] 5 | [![Coveralls][coveralls-badge]][coveralls] 6 | 7 | React component for customizable chatbot UI with Dialogflow integration 8 | 9 | [build-badge]: https://img.shields.io/travis/user/repo/master.png?style=flat-square 10 | [build]: https://travis-ci.org/user/repo 11 | 12 | [npm-badge]: https://img.shields.io/npm/v/npm-package.png?style=flat-square 13 | [npm]: https://www.npmjs.org/package/react-bot-ui 14 | 15 | [coveralls-badge]: https://img.shields.io/coveralls/user/repo/master.png?style=flat-square 16 | [coveralls]: https://coveralls.io/github/user/repo 17 | 18 | ## Demo 19 | 20 | Check out the live [demo](https://hboylan.github.io/react-bot-ui). 21 | 22 | Take a look at the demo source [code](https://github.com/hboylan/react-bot-ui/blob/master/demo/src/index.js). 23 | 24 | ## Installation 25 | 26 | #### NPM: 27 | ``` 28 | npm i --save react-bot-ui 29 | ``` 30 | 31 | #### Yarn: 32 | ``` 33 | yarn add react-bot-ui 34 | ``` 35 | 36 | ## Features 37 | 38 | - React component 39 | - Override default CSS variables 40 | - Integrate with [Dialogflow](https://dialogflow.com) 41 | - Toggle chat window open/close 42 | - Embed within existing page 43 | 44 | ## Usage 45 | 46 | #### Minimum Dialogflow integration 47 | ```jsx 48 | 50 | ``` 51 | 52 | #### All optional property defaults 53 | ```jsx 54 | 60 | ``` 61 | 62 | ## Styling 63 | 64 | Default styling can be overriden using CSS [variables](https://github.com/hboylan/react-bot-ui/blob/56dee38/css/main.css#L8) 65 | 66 | ## TODO 67 | 68 | - Include default images as Base64 69 | - Open chat in new window 70 | - Add non-Dialogflow compatability 71 | - Add prop-types integration -------------------------------------------------------------------------------- /css/main.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Chat UI reference 3 | * https://codepen.io/reimersjan/pen/zpuws?editors=1100 4 | */ 5 | 6 | @import url(https://fonts.googleapis.com/css?family=Roboto:300,400,900); 7 | 8 | :root { 9 | --primary-color: black; 10 | --secondary-color: gray; 11 | 12 | --border-radius: 10px; 13 | --icon-size: 50px; 14 | --font-color: white; 15 | --font-family: 'Roboto'; 16 | --font-size: 1em; 17 | --font-weight: 300; 18 | 19 | --container-bg: white; 20 | --container-right: 0; 21 | --container-width: 100%; 22 | 23 | --message-margin-lg: 60px; 24 | --message-margin-sm: 40px; 25 | --message-margin-y: 5px; 26 | --message-padding: 10px; 27 | 28 | --img-user: url(http://www.libertyspecialtymarkets.com/wp-content/uploads/2017/01/profile-default.jpg); 29 | --img-bot: url(https://image.flaticon.com/icons/svg/270/270137.svg); 30 | } 31 | .container * { 32 | box-sizing: border-box; 33 | margin: 0; 34 | } 35 | .container { 36 | bottom: 0; 37 | position: absolute; 38 | right: var(--container-right); 39 | width: var(--container-width); 40 | } 41 | .container header { 42 | background: var(--primary-color); 43 | cursor: pointer; 44 | height: 45px; 45 | padding: 5px 10px; 46 | } 47 | .container header h1, #demo div h1 { 48 | color: var(--font-color); 49 | margin: 5px; 50 | font-family: var(--font-family); 51 | font-size: 1.3em; 52 | font-weight: var(--font-weight); 53 | } 54 | .container div { 55 | /* transition: all 0.75s ease-in-out; */ 56 | } 57 | .messages-wrapper { 58 | background: var(--container-bg); 59 | position: relative; 60 | display: flex; 61 | flex-direction: column-reverse; 62 | } 63 | .messages { 64 | width: 100%; 65 | padding: 0px 10px; 66 | overflow-y: auto; 67 | } 68 | .img-user { 69 | content: var(--img-user); 70 | } 71 | .img-bot { 72 | content: var(--img-bot); 73 | } 74 | .group { 75 | clear: both; 76 | padding: 10px 0px; 77 | } 78 | .group img { 79 | border-radius: 100%; 80 | height: var(--icon-size); 81 | width: var(--icon-size); 82 | } 83 | .group .message { 84 | padding: var(--message-padding); 85 | } 86 | .group .message:nth-child(2) { 87 | border-top-left-radius: var(--border-radius); 88 | border-top-right-radius: var(--border-radius); 89 | } 90 | .group .message:last-child { 91 | border-bottom-left-radius: var(--border-radius); 92 | border-bottom-right-radius: var(--border-radius); 93 | } 94 | .group-user { 95 | float: right; 96 | } 97 | .group-user img { 98 | float: right; 99 | } 100 | .group-user .message { 101 | background-color: var(--secondary-color); 102 | margin: var(--message-margin-y) var(--message-margin-lg) var(--message-margin-y) var(--message-margin-sm); 103 | } 104 | .group-bot { 105 | float: left; 106 | } 107 | .group-bot img { 108 | float: left; 109 | } 110 | .group-bot .message { 111 | background-color: var(--primary-color); 112 | margin: var(--message-margin-y) var(--message-margin-sm) var(--message-margin-y) var(--message-margin-lg); 113 | } 114 | .message p { 115 | color: var(--font-color); 116 | font-family: var(--font-family); 117 | font-size: var(--font-size); 118 | font-weight: var(--font-weight); 119 | } 120 | .text-form { 121 | border-top: 1px solid var(--primary-color); 122 | display: flex; 123 | height: 35px; 124 | width: 100%; 125 | } 126 | input::-webkit-input-placeholder { 127 | color: var(--primary-color) !important; 128 | } 129 | .text-input { 130 | border-style: none; 131 | color: var(--primary-color); 132 | font-family: var(--font-family); 133 | font-size: var(--font-size); 134 | padding: 0 10px; 135 | width: 100%; 136 | } 137 | .btn-send { 138 | border: none; 139 | background-color: var(--primary-color); 140 | color: var(--font-color); 141 | display: flex; 142 | font-family: var(--font-family); 143 | font-size: var(--font-size); 144 | } 145 | .btn-voice { 146 | border: none; 147 | background-color: transparent; 148 | color: var(--primary-color); 149 | display: flex; 150 | font-size: 1.2em; 151 | text-align: center; 152 | } 153 | 154 | /* 155 | * Typing animation reference: 156 | * https://codepen.io/mattonit/pen/vLoddq 157 | */ 158 | 159 | #wave { 160 | position: relative; 161 | text-align: center; 162 | margin-left: auto; 163 | margin-right: auto; 164 | } 165 | #wave .dot { 166 | display: inline-block; 167 | width: 6px; 168 | height: 6px; 169 | border-radius: 50%; 170 | margin-right: 3px; 171 | background: var(--font-color); 172 | animation: wave 1.3s linear infinite; 173 | } 174 | #wave .dot:nth-child(2) { 175 | animation-delay: -1.1s; 176 | } 177 | 178 | #wave .dot:nth-child(3) { 179 | animation-delay: -0.9s; 180 | } 181 | 182 | @keyframes wave { 183 | 0%, 60%, 100% { 184 | transform: initial; 185 | } 186 | 187 | 30% { 188 | transform: translateY(-6px); 189 | } 190 | } -------------------------------------------------------------------------------- /demo/src/demo.css: -------------------------------------------------------------------------------- 1 | @media only screen and (min-width: 400px) { 2 | 3 | :root { 4 | --container-right: 100px; 5 | --container-width: 300px; 6 | } 7 | } 8 | 9 | body { 10 | background: var(--secondary-color); 11 | height: 100%; 12 | margin: 0px; 13 | } 14 | 15 | header { 16 | border-top-left-radius: var(--border-radius); 17 | border-top-right-radius: var(--border-radius); 18 | } -------------------------------------------------------------------------------- /demo/src/index.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {render} from 'react-dom'; 3 | 4 | import ReactBotUI from '../../src'; 5 | import './demo.css'; 6 | 7 | class Demo extends Component { 8 | constructor(props) { 9 | super(props); 10 | this.handleToggle = this.handleToggle.bind(this); 11 | } 12 | 13 | handleToggle() { 14 | if (this.chat) { 15 | this.chat.handleToggle(); 16 | } 17 | } 18 | 19 | render() { 20 | return ( 21 |
22 |

react-bot-ui demo

23 | 24 | this.chat = el} /> 29 |
30 | ); 31 | } 32 | } 33 | 34 | render(, document.querySelector('#demo')) 35 | -------------------------------------------------------------------------------- /nwb.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | type: 'react-component', 5 | npm: { 6 | esModules: true, 7 | // umd: { 8 | // global: 'ReactBotUI', 9 | // externals: { 10 | // react: 'React' 11 | // } 12 | // } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-bot-ui", 3 | "version": "1.0.5", 4 | "description": "React component for chatbot UI", 5 | "author": "Hugh Boylan (http://hughboylan.com)", 6 | "homepage": "https://hboylan.github.io/react-bot-ui", 7 | "license": "MIT", 8 | "repository": "https://github.com/hboylan/react-bot-ui.git", 9 | "main": "lib/index.js", 10 | "jsnext:main": "es/index.js", 11 | "module": "es/index.js", 12 | "files": [ 13 | "css", 14 | "es", 15 | "lib", 16 | "umd" 17 | ], 18 | "scripts": { 19 | "build": "nwb build-react-component", 20 | "clean": "nwb clean-module && nwb clean-demo", 21 | "start": "nwb serve-react-demo", 22 | "test": "nwb test-react", 23 | "test:coverage": "nwb test-react --coverage", 24 | "test:watch": "nwb test-react --server", 25 | "deploy": "nwb build-react-component && gh-pages -d demo/dist" 26 | }, 27 | "dependencies": { 28 | "react-entypo": "^1.3.0" 29 | }, 30 | "peerDependencies": { 31 | "react": "16.x" 32 | }, 33 | "devDependencies": { 34 | "gh-pages": "^1.0.0", 35 | "nwb": "0.19.x", 36 | "react": "^16.0.0", 37 | "react-dom": "^16.0.0" 38 | }, 39 | "keywords": [ 40 | "api.ai", 41 | "chatbot", 42 | "chatbot-ui", 43 | "dialogflow", 44 | "entypo", 45 | "react", 46 | "react-component" 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /src/components/Audio.js: -------------------------------------------------------------------------------- 1 | class Audio { 2 | constructor(onSpeechStart, onResult, onError) { 3 | const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; 4 | this.recognition = new SpeechRecognition(); 5 | 6 | // basic config 7 | this.recognition.lang = 'en-US'; 8 | this.recognition.interimResults = false; 9 | this.recognition.maxAlternatives = 1; 10 | 11 | // event config 12 | this.recognition.addEventListener('speechstart', onSpeechStart); 13 | this.recognition.addEventListener('speechend', this.recognition.stop); 14 | this.recognition.addEventListener('result', onResult); 15 | this.recognition.addEventListener('error', onError); 16 | } 17 | 18 | listen() { 19 | this.recognition.start(); 20 | } 21 | } 22 | 23 | export default Audio; -------------------------------------------------------------------------------- /src/components/Dialog.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import DialogGroup from './DialogGroup'; 4 | 5 | class Dialog extends Component { 6 | scrollToBottom() { 7 | const end = ReactDOM.findDOMNode(this.scrollTarget); 8 | end.scrollIntoView({behavior: 'smooth'}); 9 | } 10 | 11 | componentDidUpdate() { 12 | this.scrollToBottom(); 13 | } 14 | 15 | render() { 16 | let groups = []; 17 | let group, lastMsgIsUser; 18 | for (let msg of this.props.messages) { 19 | 20 | // next group 21 | if (lastMsgIsUser !== msg.isUser) { 22 | lastMsgIsUser = msg.isUser; 23 | group = {isUser: msg.isUser, messages: []}; 24 | groups.push(group); 25 | } 26 | group.messages.push(msg.text); 27 | } 28 | 29 | // bot is typing 30 | if (this.props.isBotTyping) { 31 | const endIndex = groups.length - 1; 32 | if (groups[endIndex].isUser) { 33 | groups.push({isUser: false, messages: [null]}); 34 | } else { 35 | groups[endIndex].messages.push(null); 36 | } 37 | } 38 | 39 | return ( 40 |
43 |
44 | {groups.map((group, i) => 45 | 49 | )} 50 |
this.scrollTarget = el} /> 53 |
54 |
55 | ); 56 | } 57 | } 58 | export default Dialog; 59 | -------------------------------------------------------------------------------- /src/components/DialogGroup.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | 3 | const Message = (props) => ( 4 |
7 | {props.text === null ? :

{props.text}

} 8 |
9 | ); 10 | 11 | const TypingAnimation = (props) => ( 12 |
13 | 14 | 15 | 16 |
17 | ); 18 | 19 | export const UserImage = (props) => ( 20 | User 21 | ); 22 | 23 | export const BotImage = (props) => ( 24 | Bot 25 | ); 26 | 27 | class DialogGroup extends Component { 28 | render() { 29 | const image = this.props.group.isUser ? this.props.isUserHidden ?
: : ; 30 | const messages = this.props.group.messages.map((text, i) => ( 31 | 35 | )); 36 | 37 | return ( 38 |
39 | {image} 40 | {messages} 41 |
42 | ); 43 | } 44 | } 45 | export default DialogGroup; 46 | -------------------------------------------------------------------------------- /src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | 3 | class Header extends Component { 4 | render(){ 5 | return( 6 |
8 |

{this.props.title}

9 |
10 | ); 11 | } 12 | } 13 | export default Header; 14 | -------------------------------------------------------------------------------- /src/components/Input.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {EntypoPaperPlane, EntypoMic} from 'react-entypo'; 3 | //import Audio from './Audio'; 4 | 5 | class Input extends Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = {value: ''}; 9 | 10 | //this.handleAudio = this.handleAudio.bind(this); 11 | this.handleChange = this.handleChange.bind(this); 12 | //this.handleListen = this.handleListen.bind(this); 13 | this.handleSubmit = this.handleSubmit.bind(this); 14 | } 15 | 16 | /*handleAudio(e) { 17 | const last = e.results.length - 1; 18 | const value = this.state.value + e.results[last][0].transcript; 19 | this.setState({value}); 20 | }*/ 21 | 22 | handleChange(e) { 23 | const value = e.target.value; 24 | if (value.length >= 256) { 25 | alert('You have reached 256 character limit!'); 26 | } 27 | this.setState({value}); 28 | } 29 | 30 | /*handleListen() { 31 | this.audio.listen(); 32 | }*/ 33 | 34 | handleSubmit(e) { 35 | e.preventDefault(); 36 | this.props.onSubmit(this.state.value); 37 | this.setState({value: ''}); 38 | } 39 | 40 | componentDidMount() { 41 | this._text.focus(); 42 | //this.audio = new Audio(this.handleAudioStart, this.handleAudio, this.handleAudioError); 43 | } 44 | 45 | render(){ 46 | return( 47 |
49 | this._text = input} 55 | onChange={this.handleChange} 56 | autoComplete={'false'} 57 | required /> 58 | {/* */} 64 | 69 |
70 | ); 71 | } 72 | } 73 | export default Input; 74 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import ApiAiClient from './lib/dialogflow'; 3 | import Header from './components/Header'; 4 | import Dialog from './components/Dialog'; 5 | import Input from './components/Input'; 6 | import '../css/main.css'; 7 | 8 | const BOT_DELAY = 4000; 9 | const BOT_SPEED = 0.03; 10 | const BOT_MAX_CHARS = 150; 11 | 12 | function getBotDelay(msg, isQuick = false) { 13 | let delay = isQuick ? BOT_DELAY / 2 : BOT_DELAY; 14 | let speed = isQuick ? BOT_SPEED * 2 : BOT_SPEED; 15 | return msg.length > BOT_MAX_CHARS ? delay : Math.floor(msg.length / speed); 16 | } 17 | 18 | export default class ReactBotUI extends Component { 19 | constructor(props) { 20 | super(props); 21 | if (props.dialogflow) { 22 | this.dialogflow = new ApiAiClient(props.dialogflow); 23 | } 24 | this.botQueue = []; 25 | this.isProcessingQueue = false; 26 | this.state = { 27 | title: props.title || 'React Bot UI', 28 | messages: [], 29 | isBotTyping: false, 30 | isOpen: props.isOpen !== undefined ? props.isOpen : true, 31 | isVisible: props.isVisible !== undefined ? props.isVisible : true 32 | }; 33 | 34 | this.appendMessage = this.appendMessage.bind(this); 35 | this.processBotQueue = this.processBotQueue.bind(this); 36 | this.processResponse = this.processResponse.bind(this); 37 | this.getResponse = this.getResponse.bind(this); 38 | this.handleResize = this.handleResize.bind(this); 39 | this.handleSubmitText = this.handleSubmitText.bind(this); 40 | this.handleToggle = this.handleToggle.bind(this); 41 | } 42 | 43 | appendMessage(text, isUser = false, next = () => {}) { 44 | let messages = this.state.messages.slice(); 45 | messages.push({isUser, text}); 46 | this.setState({messages, isBotTyping: this.botQueue.length > 0}, next); 47 | } 48 | 49 | processBotQueue(isQuick = false) { 50 | if (!this.isProcessingQueue && this.botQueue.length) { 51 | this.isProcessingQueue = true; 52 | const nextMsg = this.botQueue.shift(); 53 | setTimeout(() => { 54 | this.isProcessingQueue = false; 55 | this.appendMessage(nextMsg, false, this.processBotQueue); 56 | }, getBotDelay(nextMsg, isQuick)); 57 | } 58 | } 59 | 60 | processResponse(text) { 61 | const messages = text 62 | .match(/[^.!?]+[.!?]*/g) 63 | .map(str => str.trim()); 64 | this.botQueue = this.botQueue.concat(messages); 65 | 66 | // start processing bot queue 67 | const isQuick = !this.state.isBotTyping; 68 | this.setState({isBotTyping: true}, () => this.processBotQueue(isQuick)); 69 | } 70 | 71 | getResponse(text) { 72 | return this.dialogflow.textRequest(text) 73 | .then(data => data.result.fulfillment.speech); 74 | } 75 | 76 | handleSubmitText(text) { 77 | 78 | // append user text 79 | this.appendMessage(text, true); 80 | 81 | // fetch bot text, process as queue 82 | if (this.dialogflow) { 83 | this.getResponse(text) 84 | .then(this.processResponse); 85 | } else if (this.props.getResponse) { 86 | this.props.getResponse(text) 87 | .then(this.processResponse); 88 | } else { 89 | this.processResponse('Sorry, I\'m not configured to respond. :\'(') 90 | } 91 | } 92 | 93 | handleResize(e) { 94 | const window = e.target || e; 95 | const y = window.innerHeight; 96 | const header = document.querySelector('.container header'); 97 | const input = document.querySelector('.container .text-form'); 98 | let dialogHeight = y - header.offsetHeight - input.offsetHeight; 99 | if (dialogHeight < 0 || !dialogHeight) { 100 | dialogHeight = 0; 101 | } else if (this.props.dialogHeightMax && dialogHeight > this.props.dialogHeightMax) { 102 | dialogHeight = this.props.dialogHeightMax; 103 | } 104 | this.setState({dialogHeight}); 105 | } 106 | 107 | handleToggle() { 108 | if (this.state.isVisible) { 109 | this.setState({isOpen: !this.state.isOpen}); 110 | } else { 111 | this.setState({isVisible: true}); 112 | } 113 | } 114 | 115 | componentDidMount() { 116 | window.addEventListener('resize', this.handleResize); 117 | this.handleResize(window); 118 | } 119 | 120 | componentWillUnmount() { 121 | window.removeEventListener('resize'); 122 | } 123 | 124 | render() { 125 | return ( 126 |
127 |
129 |
130 | 134 | 135 |
136 |
137 | ); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/lib/dialogflow/ApiAiClient.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { ApiAiConstants } from "./ApiAiConstants"; 18 | import { ApiAiClientConfigurationError } from "./Errors"; 19 | import { EventRequest } from "./Request/EventRequest"; 20 | import TextRequest from "./Request/TextRequest"; 21 | export * from "./Interfaces"; 22 | export { ApiAiConstants } from "./ApiAiConstants"; 23 | export default class ApiAiClient { 24 | constructor(options) { 25 | if (!options || !options.accessToken) { 26 | throw new ApiAiClientConfigurationError("Access token is required for new ApiAi.Client instance"); 27 | } 28 | this.accessToken = options.accessToken; 29 | this.apiLang = options.lang || ApiAiConstants.DEFAULT_CLIENT_LANG; 30 | this.apiVersion = options.version || ApiAiConstants.DEFAULT_API_VERSION; 31 | this.apiBaseUrl = options.baseUrl || ApiAiConstants.DEFAULT_BASE_URL; 32 | this.sessionId = options.sessionId || this.guid(); 33 | } 34 | textRequest(query, options = {}) { 35 | if (!query) { 36 | throw new ApiAiClientConfigurationError("Query should not be empty"); 37 | } 38 | options.query = query; 39 | return new TextRequest(this, options).perform(); 40 | } 41 | eventRequest(eventName, eventData = {}, options = {}) { 42 | if (!eventName) { 43 | throw new ApiAiClientConfigurationError("Event name can not be empty"); 44 | } 45 | options.event = { name: eventName, data: eventData }; 46 | return new EventRequest(this, options).perform(); 47 | } 48 | // @todo: implement local tts request 49 | /*public ttsRequest(query) { 50 | if (!query) { 51 | throw new ApiAiClientConfigurationError("Query should not be empty"); 52 | } 53 | return new TTSRequest(this).makeTTSRequest(query); 54 | }*/ 55 | /*public userEntitiesRequest(options: IRequestOptions = {}): UserEntitiesRequest { 56 | return new UserEntitiesRequest(this, options); 57 | }*/ 58 | getAccessToken() { 59 | return this.accessToken; 60 | } 61 | getApiVersion() { 62 | return (this.apiVersion) ? this.apiVersion : ApiAiConstants.DEFAULT_API_VERSION; 63 | } 64 | getApiLang() { 65 | return (this.apiLang) ? this.apiLang : ApiAiConstants.DEFAULT_CLIENT_LANG; 66 | } 67 | getApiBaseUrl() { 68 | return (this.apiBaseUrl) ? this.apiBaseUrl : ApiAiConstants.DEFAULT_BASE_URL; 69 | } 70 | setSessionId(sessionId) { 71 | this.sessionId = sessionId; 72 | } 73 | getSessionId() { 74 | return this.sessionId; 75 | } 76 | /** 77 | * generates new random UUID 78 | * @returns {string} 79 | */ 80 | guid() { 81 | const s4 = () => Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1); 82 | return s4() + s4() + "-" + s4() + "-" + s4() + "-" + 83 | s4() + "-" + s4() + s4() + s4(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/lib/dialogflow/ApiAiConstants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export var ApiAiConstants; 18 | (function (ApiAiConstants) { 19 | let AVAILABLE_LANGUAGES; 20 | (function (AVAILABLE_LANGUAGES) { 21 | AVAILABLE_LANGUAGES[AVAILABLE_LANGUAGES["EN"] = "en"] = "EN"; 22 | AVAILABLE_LANGUAGES[AVAILABLE_LANGUAGES["DE"] = "de"] = "DE"; 23 | AVAILABLE_LANGUAGES[AVAILABLE_LANGUAGES["ES"] = "es"] = "ES"; 24 | AVAILABLE_LANGUAGES[AVAILABLE_LANGUAGES["PT_BR"] = "pt-BR"] = "PT_BR"; 25 | AVAILABLE_LANGUAGES[AVAILABLE_LANGUAGES["ZH_HK"] = "zh-HK"] = "ZH_HK"; 26 | AVAILABLE_LANGUAGES[AVAILABLE_LANGUAGES["ZH_CN"] = "zh-CN"] = "ZH_CN"; 27 | AVAILABLE_LANGUAGES[AVAILABLE_LANGUAGES["ZH_TW"] = "zh-TW"] = "ZH_TW"; 28 | AVAILABLE_LANGUAGES[AVAILABLE_LANGUAGES["NL"] = "nl"] = "NL"; 29 | AVAILABLE_LANGUAGES[AVAILABLE_LANGUAGES["FR"] = "fr"] = "FR"; 30 | AVAILABLE_LANGUAGES[AVAILABLE_LANGUAGES["IT"] = "it"] = "IT"; 31 | AVAILABLE_LANGUAGES[AVAILABLE_LANGUAGES["JA"] = "ja"] = "JA"; 32 | AVAILABLE_LANGUAGES[AVAILABLE_LANGUAGES["KO"] = "ko"] = "KO"; 33 | AVAILABLE_LANGUAGES[AVAILABLE_LANGUAGES["PT"] = "pt"] = "PT"; 34 | AVAILABLE_LANGUAGES[AVAILABLE_LANGUAGES["RU"] = "ru"] = "RU"; 35 | AVAILABLE_LANGUAGES[AVAILABLE_LANGUAGES["UK"] = "uk"] = "UK"; 36 | })(AVAILABLE_LANGUAGES = ApiAiConstants.AVAILABLE_LANGUAGES || (ApiAiConstants.AVAILABLE_LANGUAGES = {})); 37 | ApiAiConstants.VERSION = "2.0.0-beta.20"; 38 | ApiAiConstants.DEFAULT_BASE_URL = "https://api.api.ai/v1/"; 39 | ApiAiConstants.DEFAULT_API_VERSION = "20150910"; 40 | ApiAiConstants.DEFAULT_CLIENT_LANG = AVAILABLE_LANGUAGES.EN; 41 | })(ApiAiConstants || (ApiAiConstants = {})); 42 | -------------------------------------------------------------------------------- /src/lib/dialogflow/Errors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export class ApiAiBaseError extends Error { 18 | constructor(message) { 19 | super(message); 20 | this.message = message; 21 | this.stack = new Error().stack; 22 | } 23 | } 24 | export class ApiAiClientConfigurationError extends ApiAiBaseError { 25 | constructor(message) { 26 | super(message); 27 | this.name = "ApiAiClientConfigurationError"; 28 | } 29 | } 30 | export class ApiAiRequestError extends ApiAiBaseError { 31 | constructor(message, code = null) { 32 | super(message); 33 | this.message = message; 34 | this.code = code; 35 | this.name = "ApiAiRequestError"; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/lib/dialogflow/Interfaces.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export var IStreamClient; 18 | (function (IStreamClient) { 19 | let ERROR; 20 | (function (ERROR) { 21 | ERROR[ERROR["ERR_NETWORK"] = 0] = "ERR_NETWORK"; 22 | ERROR[ERROR["ERR_AUDIO"] = 1] = "ERR_AUDIO"; 23 | ERROR[ERROR["ERR_SERVER"] = 2] = "ERR_SERVER"; 24 | ERROR[ERROR["ERR_CLIENT"] = 3] = "ERR_CLIENT"; 25 | })(ERROR = IStreamClient.ERROR || (IStreamClient.ERROR = {})); 26 | let EVENT; 27 | (function (EVENT) { 28 | EVENT[EVENT["MSG_WAITING_MICROPHONE"] = 0] = "MSG_WAITING_MICROPHONE"; 29 | EVENT[EVENT["MSG_MEDIA_STREAM_CREATED"] = 1] = "MSG_MEDIA_STREAM_CREATED"; 30 | EVENT[EVENT["MSG_INIT_RECORDER"] = 2] = "MSG_INIT_RECORDER"; 31 | EVENT[EVENT["MSG_RECORDING"] = 3] = "MSG_RECORDING"; 32 | EVENT[EVENT["MSG_SEND"] = 4] = "MSG_SEND"; 33 | EVENT[EVENT["MSG_SEND_EMPTY"] = 5] = "MSG_SEND_EMPTY"; 34 | EVENT[EVENT["MSG_SEND_EOS_OR_JSON"] = 6] = "MSG_SEND_EOS_OR_JSON"; 35 | EVENT[EVENT["MSG_WEB_SOCKET"] = 7] = "MSG_WEB_SOCKET"; 36 | EVENT[EVENT["MSG_WEB_SOCKET_OPEN"] = 8] = "MSG_WEB_SOCKET_OPEN"; 37 | EVENT[EVENT["MSG_WEB_SOCKET_CLOSE"] = 9] = "MSG_WEB_SOCKET_CLOSE"; 38 | EVENT[EVENT["MSG_STOP"] = 10] = "MSG_STOP"; 39 | EVENT[EVENT["MSG_CONFIG_CHANGED"] = 11] = "MSG_CONFIG_CHANGED"; 40 | })(EVENT = IStreamClient.EVENT || (IStreamClient.EVENT = {})); 41 | })(IStreamClient || (IStreamClient = {})); 42 | -------------------------------------------------------------------------------- /src/lib/dialogflow/Request/EventRequest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import Request from "./Request"; 18 | export class EventRequest extends Request { 19 | } 20 | -------------------------------------------------------------------------------- /src/lib/dialogflow/Request/Request.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { ApiAiRequestError } from "../Errors"; 18 | import XhrRequest from "../XhrRequest"; 19 | class Request { 20 | constructor(apiAiClient, options) { 21 | this.apiAiClient = apiAiClient; 22 | this.options = options; 23 | this.uri = this.apiAiClient.getApiBaseUrl() + "query?v=" + this.apiAiClient.getApiVersion(); 24 | this.requestMethod = XhrRequest.Method.POST; 25 | this.headers = { 26 | Authorization: "Bearer " + this.apiAiClient.getAccessToken(), 27 | }; 28 | this.options.lang = this.apiAiClient.getApiLang(); 29 | this.options.sessionId = this.apiAiClient.getSessionId(); 30 | } 31 | static handleSuccess(xhr) { 32 | return Promise.resolve(JSON.parse(xhr.responseText)); 33 | } 34 | static handleError(xhr) { 35 | let error = new ApiAiRequestError(null); 36 | try { 37 | const serverResponse = JSON.parse(xhr.responseText); 38 | if (serverResponse.status && serverResponse.status.errorDetails) { 39 | error = new ApiAiRequestError(serverResponse.status.errorDetails, serverResponse.status.code); 40 | } 41 | else { 42 | error = new ApiAiRequestError(xhr.statusText, xhr.status); 43 | } 44 | } 45 | catch (e) { 46 | error = new ApiAiRequestError(xhr.statusText, xhr.status); 47 | } 48 | return Promise.reject(error); 49 | } 50 | perform(overrideOptions = null) { 51 | const options = overrideOptions ? overrideOptions : this.options; 52 | return XhrRequest.ajax(this.requestMethod, this.uri, options, this.headers) 53 | .then(Request.handleSuccess.bind(this)) 54 | .catch(Request.handleError.bind(this)); 55 | } 56 | } 57 | export default Request; 58 | -------------------------------------------------------------------------------- /src/lib/dialogflow/Request/TextRequest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import Request from "./Request"; 18 | export default class TextRequest extends Request { 19 | } 20 | -------------------------------------------------------------------------------- /src/lib/dialogflow/XhrRequest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * quick ts implementation of example from 19 | * https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise 20 | * with some minor improvements 21 | * @todo: test (?) 22 | * @todo: add node.js implementation with node's http inside. Just to make SDK cross-platform 23 | */ 24 | class XhrRequest { 25 | // Method that performs the ajax request 26 | static ajax(method, url, args = null, headers = null, options = {}) { 27 | // Creating a promise 28 | return new Promise((resolve, reject) => { 29 | // Instantiates the XMLHttpRequest 30 | const client = XhrRequest.createXMLHTTPObject(); 31 | let uri = url; 32 | let payload = null; 33 | // Add given payload to get request 34 | if (args && (method === XhrRequest.Method.GET)) { 35 | uri += "?"; 36 | let argcount = 0; 37 | for (const key in args) { 38 | if (args.hasOwnProperty(key)) { 39 | if (argcount++) { 40 | uri += "&"; 41 | } 42 | uri += encodeURIComponent(key) + "=" + encodeURIComponent(args[key]); 43 | } 44 | } 45 | } 46 | else if (args) { 47 | if (!headers) { 48 | headers = {}; 49 | } 50 | headers["Content-Type"] = "application/json; charset=utf-8"; 51 | payload = JSON.stringify(args); 52 | } 53 | for (const key in options) { 54 | if (key in client) { 55 | client[key] = options[key]; 56 | } 57 | } 58 | // hack: method[method] is somewhat like .toString for enum Method 59 | // should be made in normal way 60 | client.open(XhrRequest.Method[method], uri, true); 61 | // Add given headers 62 | if (headers) { 63 | for (const key in headers) { 64 | if (headers.hasOwnProperty(key)) { 65 | client.setRequestHeader(key, headers[key]); 66 | } 67 | } 68 | } 69 | payload ? client.send(payload) : client.send(); 70 | client.onload = () => { 71 | if (client.status >= 200 && client.status < 300) { 72 | // Performs the function "resolve" when this.status is equal to 2xx 73 | resolve(client); 74 | } 75 | else { 76 | // Performs the function "reject" when this.status is different than 2xx 77 | reject(client); 78 | } 79 | }; 80 | client.onerror = () => { 81 | reject(client); 82 | }; 83 | }); 84 | } 85 | static get(url, payload = null, headers = null, options = {}) { 86 | return XhrRequest.ajax(XhrRequest.Method.GET, url, payload, headers, options); 87 | } 88 | static post(url, payload = null, headers = null, options = {}) { 89 | return XhrRequest.ajax(XhrRequest.Method.POST, url, payload, headers, options); 90 | } 91 | static put(url, payload = null, headers = null, options = {}) { 92 | return XhrRequest.ajax(XhrRequest.Method.PUT, url, payload, headers, options); 93 | } 94 | static delete(url, payload = null, headers = null, options = {}) { 95 | return XhrRequest.ajax(XhrRequest.Method.DELETE, url, payload, headers, options); 96 | } 97 | static createXMLHTTPObject() { 98 | let xmlhttp = null; 99 | for (const i of XhrRequest.XMLHttpFactories) { 100 | try { 101 | xmlhttp = i(); 102 | } 103 | catch (e) { 104 | continue; 105 | } 106 | break; 107 | } 108 | return xmlhttp; 109 | } 110 | } 111 | XhrRequest.XMLHttpFactories = [ 112 | () => new XMLHttpRequest(), 113 | () => new window["ActiveXObject"]("Msxml2.XMLHTTP"), 114 | () => new window["ActiveXObject"]("Msxml3.XMLHTTP"), 115 | () => new window["ActiveXObject"]("Microsoft.XMLHTTP") 116 | ]; 117 | (function (XhrRequest) { 118 | let Method; 119 | (function (Method) { 120 | Method[Method["GET"] = "GET"] = "GET"; 121 | Method[Method["POST"] = "POST"] = "POST"; 122 | Method[Method["PUT"] = "PUT"] = "PUT"; 123 | Method[Method["DELETE"] = "DELETE"] = "DELETE"; 124 | })(Method = XhrRequest.Method || (XhrRequest.Method = {})); 125 | })(XhrRequest || (XhrRequest = {})); 126 | export default XhrRequest; 127 | -------------------------------------------------------------------------------- /src/lib/dialogflow/index.js: -------------------------------------------------------------------------------- 1 | import ApiAiClient from './ApiAiClient'; 2 | 3 | export default ApiAiClient; -------------------------------------------------------------------------------- /tests/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tests/index-test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import React from 'react' 3 | import {render, unmountComponentAtNode} from 'react-dom' 4 | 5 | import Component from 'src/' 6 | 7 | describe('Component', () => { 8 | let node 9 | 10 | beforeEach(() => { 11 | node = document.createElement('div') 12 | }) 13 | 14 | afterEach(() => { 15 | unmountComponentAtNode(node) 16 | }) 17 | 18 | it('displays a welcome message', () => { 19 | render(, node, () => { 20 | expect(node.innerHTML).toContain('Welcome to React components') 21 | }) 22 | }) 23 | }) 24 | --------------------------------------------------------------------------------