├── README.md ├── src ├── settings.js ├── static │ └── styles │ │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ │ └── bootstrap-paper │ │ ├── _bootswatch.scss │ │ └── _variables.scss ├── store │ ├── store.js │ ├── store.prod.js │ └── store.dev.js ├── index.css ├── utils │ ├── constants.js │ └── helpers.js ├── reducers │ ├── index.js │ └── main.js ├── routes.js ├── containers │ ├── DevTools.js │ ├── App │ │ └── index.js │ └── Main │ │ ├── index.js │ │ └── style.css ├── components │ ├── NoMatch │ │ └── index.js │ ├── Chat │ │ └── index.js │ ├── Form │ │ └── FormControlGroupRF │ │ │ └── index.js │ └── SimpleSearch │ │ └── index.js ├── actions │ ├── actionTypes.js │ ├── main.js │ └── commons.js ├── index.js └── logo.svg ├── public ├── favicon.ico └── index.html ├── Dockerfile ├── .gitignore ├── config ├── jest │ ├── fileTransform.js │ └── cssTransform.js ├── polyfills.js ├── env.js ├── paths.js ├── webpack.config.dev.js └── webpack.config.prod.js ├── .vscode └── settings.json ├── circle.yml ├── scripts ├── test.js ├── build.js └── start.js ├── LICENSE └── package.json /README.md: -------------------------------------------------------------------------------- 1 | # chatbot-demo 2 | chatbot-demo 3 | -------------------------------------------------------------------------------- /src/settings.js: -------------------------------------------------------------------------------- 1 | export const API_ROOT = 'https://beta.floydhub.com/flight/'; -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floydhub/chatbot-demo/master/public/favicon.ico -------------------------------------------------------------------------------- /src/static/styles/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floydhub/chatbot-demo/master/src/static/styles/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /src/static/styles/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floydhub/chatbot-demo/master/src/static/styles/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /src/static/styles/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floydhub/chatbot-demo/master/src/static/styles/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /src/static/styles/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floydhub/chatbot-demo/master/src/static/styles/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /src/store/store.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === 'production') { 2 | module.exports = require('./store.prod') 3 | } else { 4 | module.exports = require('./store.dev') 5 | } -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-box-sizing: border-box; 3 | -moz-box-sizing: border-box; 4 | box-sizing: border-box; 5 | } 6 | 7 | body { 8 | margin: 0; 9 | padding: 0; 10 | font-family: sans-serif; 11 | } 12 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:4.7.0-wheezy 2 | 3 | # Install debug tools 4 | RUN apt-get update 5 | RUN apt-get install -y vim curl 6 | 7 | RUN npm install -g pushstate-server 8 | COPY ./build /demo 9 | 10 | CMD ["pushstate-server", "/demo", "80"] 11 | -------------------------------------------------------------------------------- /src/utils/constants.js: -------------------------------------------------------------------------------- 1 | // HTTP Response types 2 | export const httpresponses = { 3 | JSON: 'json_resp', 4 | TEXT: 'text_resp', 5 | FORM: 'form_resp', 6 | } 7 | 8 | export const users = { 9 | USER: 'user', 10 | BOT: 'bot', 11 | }; 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # testing 7 | coverage 8 | 9 | # production 10 | build 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | npm-debug.log 16 | debug.log -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | // This is a custom Jest transformer turning file imports into filenames. 4 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 5 | 6 | module.exports = { 7 | process(src, filename) { 8 | return 'module.exports = ' + JSON.stringify(path.basename(filename)) + ';'; 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | // This is a custom Jest transformer turning style imports into empty objects. 2 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 3 | 4 | module.exports = { 5 | process() { 6 | return 'module.exports = {};'; 7 | }, 8 | getCacheKey(fileData, filename) { 9 | // The output is always the same. 10 | return 'cssTransform'; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { routerReducer } from 'react-router-redux'; 3 | import { reducer as formReducer } from 'redux-form'; 4 | 5 | import mainReducer from './main'; 6 | 7 | const rootReducer = combineReducers({ 8 | chats: mainReducer, 9 | routing: routerReducer, 10 | form: formReducer, 11 | }); 12 | 13 | export default rootReducer; 14 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, IndexRoute } from 'react-router'; 3 | 4 | import App from 'containers/App'; 5 | import Main from 'containers/Main'; 6 | import NoMatch from 'components/NoMatch'; 7 | 8 | const routes = ( 9 |
10 | 11 | 12 | 13 | 14 |
15 | ); 16 | 17 | export default routes; 18 | -------------------------------------------------------------------------------- /src/containers/DevTools.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createDevTools } from 'redux-devtools'; 3 | import LogMonitor from 'redux-devtools-log-monitor'; 4 | import DockMonitor from 'redux-devtools-dock-monitor'; 5 | 6 | const DevTools = createDevTools( 7 | 12 | 13 | 14 | ); 15 | 16 | export default DevTools; 17 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | // The number of spaces a tab is equal to. 4 | "editor.tabSize": 2, 5 | // Configure glob patterns for excluding files and folders in searches. Inherits all glob patterns from the files.exclude setting. 6 | "search.exclude": { 7 | "**/node_modules": true, 8 | "**/bower_components": true, 9 | "**/dist": true, 10 | "typings": true 11 | }, 12 | "files.associations": { 13 | "*.js": "javascriptreact" 14 | } 15 | } -------------------------------------------------------------------------------- /src/store/store.prod.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import { routerMiddleware } from 'react-router-redux'; 4 | 5 | import rootReducer from 'reducers/'; 6 | import { apiMiddleware } from 'redux-api-middleware'; 7 | 8 | export default function configureStore(history, preloadedState) { 9 | const store = createStore( 10 | rootReducer, 11 | preloadedState, 12 | compose( 13 | applyMiddleware(thunk, apiMiddleware, routerMiddleware(history)) 14 | ) 15 | ); 16 | 17 | return store; 18 | } 19 | -------------------------------------------------------------------------------- /src/components/NoMatch/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | import { ButtonOutline } from 'rebass'; 4 | 5 | const NoMatch = (props) => { 6 | return ( 7 |
8 |

404

9 |
Uh oh, can't find what you're looking for.
10 |
11 | 12 | 13 | Go back to Chat 14 | 15 | 16 |
17 | ); 18 | }; 19 | 20 | export default NoMatch; 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/store/store.dev.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import { routerMiddleware } from 'react-router-redux'; 4 | 5 | import rootReducer from 'reducers/'; 6 | import { apiMiddleware } from 'redux-api-middleware'; 7 | import DevTools from 'containers/DevTools'; 8 | 9 | export default function configureStore(history, preloadedState) { 10 | const store = createStore( 11 | rootReducer, 12 | preloadedState, 13 | compose( 14 | applyMiddleware(thunk, apiMiddleware, routerMiddleware(history)), 15 | DevTools.instrument() 16 | ) 17 | ); 18 | 19 | return store; 20 | } 21 | -------------------------------------------------------------------------------- /config/polyfills.js: -------------------------------------------------------------------------------- 1 | if (typeof Promise === 'undefined') { 2 | // Rejection tracking prevents a common issue where React gets into an 3 | // inconsistent state due to an error, but it gets swallowed by a Promise, 4 | // and the user has no idea what causes React's erratic future behavior. 5 | require('promise/lib/rejection-tracking').enable(); 6 | window.Promise = require('promise/lib/es6-extensions.js'); 7 | } 8 | 9 | // fetch() polyfill for making API calls. 10 | require('whatwg-fetch'); 11 | 12 | // Object.assign() is commonly used with React. 13 | // It will use the native implementation if it's present and isn't buggy. 14 | Object.assign = require('object-assign'); 15 | -------------------------------------------------------------------------------- /src/utils/helpers.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import jQuery from 'jquery'; 3 | 4 | // Converts an object into url params string 5 | export function getUrlParams(params, noPrefix=false) { 6 | if(params == null) return ''; 7 | if(noPrefix) return jQuery.param(params); 8 | return '?' + jQuery.param(params); 9 | } 10 | 11 | // Filters an object `obj` to keep only items whose keys are specified in the array `arr` 12 | // Returns an array of the filtered values, whose order is the samr as `arr` 13 | export function filterObjByArray(obj, arr) { 14 | let filteredObjVals = []; 15 | arr.forEach((key) => { 16 | if (_.has(obj, key)) { 17 | filteredObjVals.push(obj[key]); 18 | } 19 | }); 20 | return filteredObjVals; 21 | } 22 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | services: 3 | - docker 4 | node: 5 | version: 6.5.0 6 | test: 7 | post: 8 | - npm run build 9 | - docker build -t floyd-on.azurecr.io/chatbot-demo:$CIRCLE_BUILD_NUM . 10 | - docker run -d -p 80:80 floyd-on.azurecr.io/chatbot-demo:$CIRCLE_BUILD_NUM 11 | - curl --retry 10 --retry-delay 3 http://127.0.0.1 12 | deployment: 13 | dockerhub: 14 | branch: master 15 | commands: 16 | - docker login -e floydhub@gmail.com -u ${AZURE_REGISTRY_USER} -p ${AZURE_REGISTRY_PASSWORD} 17 | - docker push floyd-on.azurecr.io/chatbot-demo:$CIRCLE_BUILD_NUM 18 | - docker tag floyd-on.azurecr.io/chatbot-demo:$CIRCLE_BUILD_NUM floyd-on.azurecr.io/floyd-web:latest 19 | - docker push floyd-on.azurecr.io/chatbot-demo:latest 20 | -------------------------------------------------------------------------------- /src/actions/actionTypes.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | // GET Request 4 | export const GET_REQUEST = '_GET_REQUEST'; 5 | export const GET_SUCCESS = '_GET_SUCCESS'; 6 | export const GET_FAILURE = '_GET_FAILURE'; 7 | 8 | // POST Request 9 | export const POST_REQUEST = '_POST_REQUEST'; 10 | export const POST_SUCCESS = '_POST_SUCCESS'; 11 | export const POST_FAILURE = '_POST_FAILURE'; 12 | 13 | // Combines the prefix with suffixes 14 | export function combine(prefix, suffixes) { 15 | if (typeof suffixes === 'string') { 16 | return prefix + suffixes; 17 | } 18 | else if(Array.isArray(suffixes)) { 19 | const result = suffixes.map((suffix) => { 20 | return prefix + suffix; 21 | }); 22 | return result; 23 | } 24 | } 25 | 26 | // Returns true if type (action.type) has the provided prefix 27 | export function hasPrefix(type, prefix) { 28 | return _.startsWith(type, prefix); 29 | } 30 | -------------------------------------------------------------------------------- /src/containers/App/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | const propTypes = { 5 | children: PropTypes.node, 6 | dispatch: PropTypes.func.isRequired, 7 | }; 8 | 9 | const contextTypes = { 10 | router: PropTypes.object, 11 | }; 12 | 13 | class App extends Component { 14 | 15 | componentWillMount() { 16 | const { dispatch } = this.props; 17 | } 18 | 19 | render() { 20 | const { children } = this.props; 21 | 22 | return ( 23 |
24 | {React.cloneElement(children)} 25 |
26 | ); 27 | } 28 | } 29 | 30 | const mapStateToProps = (state) => { 31 | return {}; 32 | }; 33 | 34 | const mapsDispatchToProps = (dispatch) => { 35 | return { 36 | dispatch, 37 | }; 38 | }; 39 | 40 | App.propTypes = propTypes; 41 | App.contextTypes = contextTypes; 42 | export default connect( 43 | mapStateToProps, 44 | mapsDispatchToProps 45 | )(App); 46 | -------------------------------------------------------------------------------- /src/components/Chat/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { Field, reduxForm } from 'redux-form'; 3 | import classnames from 'classnames'; 4 | 5 | import { users } from 'utils/constants'; 6 | 7 | // A simple search form with just an input field 8 | const propTypes = { 9 | chat: PropTypes.object.isRequired, 10 | latest: PropTypes.bool, 11 | }; 12 | 13 | const defaultValues = { 14 | latest: false, 15 | }; 16 | 17 | class Chat extends Component { 18 | 19 | render() { 20 | const { chat, latest } = this.props; 21 | 22 | return ( 23 |
24 |
26 |
27 |

{chat.message}

28 |
29 |
30 |
31 | ); 32 | } 33 | } 34 | 35 | Chat.propTypes = propTypes; 36 | Chat.defaultValues = defaultValues; 37 | export default Chat; 38 | -------------------------------------------------------------------------------- /src/components/Form/FormControlGroupRF/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { FormGroup, FormControl, ControlLabel, HelpBlock } from 'react-bootstrap'; 3 | 4 | // A complete form field component for use with redux-form's Field 5 | // Comprises of FormGroup, ControlLabel, FormControl, Feedback and HelpBlock 6 | class FormControlGroupRF extends Component { 7 | render() { 8 | const { input, id, label, help, type, placeholder, meta: { touched, error }, ...rest } = this.props; 9 | 10 | return ( 11 | 15 | {label && {label}} 16 | 17 | 18 | {touched && error && {error}} 19 | {help && {help}} 20 | 21 | ); 22 | } 23 | } 24 | 25 | export default FormControlGroupRF; 26 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'test'; 2 | process.env.PUBLIC_URL = ''; 3 | 4 | // Load environment variables from .env file. Suppress warnings using silent 5 | // if this file is missing. dotenv will never modify any environment variables 6 | // that have already been set. 7 | // https://github.com/motdotla/dotenv 8 | require('dotenv').config({silent: true}); 9 | 10 | const jest = require('jest'); 11 | const argv = process.argv.slice(2); 12 | 13 | // Watch unless on CI or in coverage mode 14 | if (!process.env.CI && argv.indexOf('--coverage') < 0) { 15 | argv.push('--watch'); 16 | } 17 | 18 | // A temporary hack to clear terminal correctly. 19 | // You can remove this after updating to Jest 18 when it's out. 20 | // https://github.com/facebook/jest/pull/2230 21 | var realWrite = process.stdout.write; 22 | var CLEAR = process.platform === 'win32' ? '\x1Bc' : '\x1B[2J\x1B[3J\x1B[H'; 23 | process.stdout.write = function(chunk, encoding, callback) { 24 | if (chunk === '\x1B[2J\x1B[H') { 25 | chunk = CLEAR; 26 | } 27 | return realWrite.call(this, chunk, encoding, callback); 28 | }; 29 | 30 | 31 | jest.run(argv); 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 floydhub 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 2 | // injected into the application via DefinePlugin in Webpack configuration. 3 | 4 | var REACT_APP = /^REACT_APP_/i; 5 | 6 | function getClientEnvironment(publicUrl) { 7 | var processEnv = Object 8 | .keys(process.env) 9 | .filter(key => REACT_APP.test(key)) 10 | .reduce((env, key) => { 11 | env[key] = JSON.stringify(process.env[key]); 12 | return env; 13 | }, { 14 | // Useful for determining whether we’re running in production mode. 15 | // Most importantly, it switches React into the correct mode. 16 | 'NODE_ENV': JSON.stringify( 17 | process.env.NODE_ENV || 'development' 18 | ), 19 | // Useful for resolving the correct path to static assets in `public`. 20 | // For example, . 21 | // This should only be used as an escape hatch. Normally you would put 22 | // images into the `src` and `import` them in code to get their paths. 23 | 'PUBLIC_URL': JSON.stringify(publicUrl) 24 | }); 25 | return {'process.env': processEnv}; 26 | } 27 | 28 | module.exports = getClientEnvironment; 29 | -------------------------------------------------------------------------------- /src/actions/main.js: -------------------------------------------------------------------------------- 1 | import shortid from 'shortid'; 2 | 3 | import { search } from './commons'; 4 | import { users } from 'utils/constants'; 5 | 6 | export const prefix = 'MAIN'; // Prefix for common action types 7 | export const endpoint = 'chat2'; // Endpoint for REST API requests 8 | 9 | export function fetch(query, storeQuery) { 10 | return (dispatch) => { 11 | if (storeQuery) { 12 | dispatch({ 13 | type: 'STORE_USER_CHAT', 14 | payload: { 15 | id: shortid.generate(), 16 | message: query, 17 | from: users.USER, 18 | time: Date.now(), 19 | } 20 | }); 21 | } 22 | 23 | return search(query, prefix, endpoint, dispatch, {input: query}); 24 | }; 25 | } 26 | 27 | // // Submits an item to the backend for creation 28 | // export function submitModule(data) { 29 | // // The data provided is of type FormData 30 | // const headers = { 31 | // Accept: 'application/json, application/xml, text/plain, text/html, *.*', 32 | // // 'Content-Type': 'application/json', 33 | // 'Access-Control-Allow-Origin': '*', 34 | // }; 35 | // return (dispatch, getState) => { 36 | // return submit(data, prefix, endpoint, headers, {}, dispatch); 37 | // }; 38 | // } 39 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 | Floyd - Chatbot Demo 17 | 18 | 19 |
20 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { browserHistory, Router } from 'react-router'; 4 | import { Provider } from 'react-redux'; 5 | import { syncHistoryWithStore } from 'react-router-redux'; 6 | 7 | import routes from './routes'; 8 | import configureStore from './store/store'; 9 | import DevTools from './containers/DevTools'; 10 | 11 | import { users } from 'utils/constants'; 12 | import 'static/styles/bootstrap-paper/bootstrap_paper.css'; 13 | import './index.css'; 14 | 15 | const initialState = { 16 | chats: { 17 | conversations: [ 18 | { 19 | id: 1, 20 | message: 'Hey there', 21 | from: users.BOT, 22 | time: Date.now(), 23 | }, 24 | ], 25 | payloads: { 26 | userInput: { 27 | message: "Let's chat...", 28 | }, 29 | botOutput: { 30 | 31 | } 32 | } 33 | } 34 | }; 35 | 36 | export const store = configureStore(browserHistory, initialState); 37 | const history = syncHistoryWithStore(browserHistory, store); 38 | 39 | ReactDOM.render( 40 | 41 |
42 | 43 | {process.env.NODE_ENV !== 'production' && 44 | 45 | } 46 |
47 |
, 48 | document.getElementById('app') 49 | ); 50 | -------------------------------------------------------------------------------- /src/components/SimpleSearch/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { Field, reduxForm } from 'redux-form'; 3 | 4 | import { Form } from 'react-bootstrap'; 5 | 6 | import FormControlGroupRF from 'components/Form/FormControlGroupRF'; 7 | 8 | // A simple search form with just an input field 9 | const propTypes = { 10 | form: PropTypes.string.isRequired, 11 | onSearch: PropTypes.func.isRequired, 12 | handleSubmit: PropTypes.func.isRequired, 13 | className: PropTypes.string, 14 | placeholder: PropTypes.string, 15 | autoComplete: PropTypes.string, 16 | }; 17 | 18 | const defaultValues = { 19 | placeholder: 'Type something...', 20 | autoComplete: 'off', 21 | }; 22 | 23 | class SimpleSearch extends Component { 24 | 25 | render() { 26 | const { onSearch, handleSubmit, className, placeholder, autoComplete } = this.props; 27 | 28 | return ( 29 |
30 | 38 | 39 | ); 40 | } 41 | } 42 | 43 | SimpleSearch.propTypes = propTypes; 44 | SimpleSearch.defaultValues = defaultValues; 45 | SimpleSearch = reduxForm({ 46 | //form: 'modulesearch', // Pass a unique form name as prop, e.g. 47 | })(SimpleSearch); 48 | 49 | export default SimpleSearch; 50 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('fs'); 3 | 4 | // Make sure any symlinks in the project folder are resolved: 5 | // https://github.com/facebookincubator/create-react-app/issues/637 6 | var appDirectory = fs.realpathSync(process.cwd()); 7 | function resolveApp(relativePath) { 8 | return path.resolve(appDirectory, relativePath); 9 | } 10 | 11 | // We support resolving modules according to `NODE_PATH`. 12 | // This lets you use absolute paths in imports inside large monorepos: 13 | // https://github.com/facebookincubator/create-react-app/issues/253. 14 | 15 | // It works similar to `NODE_PATH` in Node itself: 16 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 17 | 18 | // We will export `nodePaths` as an array of absolute paths. 19 | // It will then be used by Webpack configs. 20 | // Jest doesn’t need this because it already handles `NODE_PATH` out of the box. 21 | 22 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 23 | // Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. 24 | // https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421 25 | 26 | var nodePaths = (process.env.NODE_PATH || '') 27 | .split(process.platform === 'win32' ? ';' : ':') 28 | .filter(Boolean) 29 | .filter(folder => !path.isAbsolute(folder)) 30 | .map(resolveApp); 31 | 32 | // config after eject: we're in ./config/ 33 | module.exports = { 34 | appBuild: resolveApp('build'), 35 | appPublic: resolveApp('public'), 36 | appHtml: resolveApp('public/index.html'), 37 | appIndexJs: resolveApp('src/index.js'), 38 | appPackageJson: resolveApp('package.json'), 39 | appSrc: resolveApp('src'), 40 | yarnLockFile: resolveApp('yarn.lock'), 41 | testsSetup: resolveApp('src/setupTests.js'), 42 | appNodeModules: resolveApp('node_modules'), 43 | ownNodeModules: resolveApp('node_modules'), 44 | nodePaths: nodePaths 45 | }; 46 | -------------------------------------------------------------------------------- /src/reducers/main.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import shortid from 'shortid'; 3 | 4 | import { users } from 'utils/constants'; 5 | import * as at from 'actions/actionTypes'; 6 | import { prefix } from 'actions/main'; 7 | 8 | const getRequest = at.combine(prefix, at.GET_REQUEST); 9 | const getSuccess = at.combine(prefix, at.GET_SUCCESS); 10 | const getFailure = at.combine(prefix, at.GET_FAILURE); 11 | 12 | function mainStoreReducer(state = {}, action) { 13 | switch (action.type) { 14 | case 'STORE_USER_CHAT': 15 | let oldConversations = _.clone(state.conversations); 16 | return _.merge({}, state, { 17 | conversations: _.concat(oldConversations, action.payload), 18 | payloads: { 19 | userInput: { message: action.payload.message }, 20 | } 21 | }) 22 | case getRequest: 23 | return _.merge({}, state, { 24 | isFetching: true, 25 | payloads: { 26 | botOutput: { message: 'Fetching response from server...' } 27 | } 28 | }); 29 | case getSuccess: 30 | oldConversations = _.clone(state.conversations); 31 | 32 | let a = _.trimStart(action.payload.output, 'b\''); 33 | a = _.trimEnd(a, '\\n\''); 34 | let i; 35 | for (i=0; i<10; i++) { 36 | a = _.replace(a, '\\\\\\', ''); 37 | } 38 | // a = _.replace(a, RegExp('/\\\\\\/', "g"), ''); 39 | console.log('Output: ', a); 40 | let j = JSON.parse(a); 41 | console.log('JSON: ', JSON.parse(a)) 42 | const response = j[0][0].tgt.join(' '); 43 | console.log(response) 44 | return _.assign({}, state, { 45 | isFetching: false, 46 | conversations: _.concat(oldConversations, { 47 | id: shortid.generate(), 48 | message: response, 49 | from: users.BOT, 50 | time: Date.now(), 51 | }), 52 | payloads: _.merge({}, state.payloads, { 53 | botOutput: { message: JSON.stringify(j, null, 2) } 54 | }), 55 | lastUpdated: Date.now(), 56 | }); 57 | case getFailure: 58 | return _.merge({}, state, { 59 | isFetching: false, 60 | payloads: { 61 | botOutput: { message: 'ERROR: Failed to fetch response from server...' } 62 | } 63 | }); 64 | default: 65 | return state; 66 | } 67 | } 68 | 69 | export default mainStoreReducer; 70 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/actions/commons.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | import * as at from './actionTypes'; 4 | import { CALL_API, getJSON } from 'redux-api-middleware'; 5 | 6 | import { API_ROOT } from 'settings'; 7 | import { getUrlParams } from 'utils/helpers'; 8 | import { httpresponses } from 'utils/constants'; 9 | 10 | // Abstracting out the common methods between the different action modules 11 | 12 | // Fetches data from server using the REST API 13 | // Can add an (optional) meta object to the each of the dispatched actions 14 | export function get(prefix, endpoint, params, meta={}, responseType=httpresponses.JSON) { 15 | const urlParams = getUrlParams(params); 16 | 17 | return { 18 | [CALL_API]: { 19 | types: [ 20 | { 21 | type: at.combine(prefix, at.GET_REQUEST), 22 | meta: meta, 23 | }, 24 | { 25 | type: at.combine(prefix, at.GET_SUCCESS), 26 | meta: meta, 27 | // NOTE: This is a source of slight inconsistency. All other responses (GET failure, 28 | // POST success/failure, etc.) have the response at action.payload.response. Since 29 | // we are overriding the payload for GET success, the response for just this action type 30 | // is at action.payload. Hence, all reducers and middlewares (e.g. notifications) need 31 | // to look for the response object in action.payload (and not in action.payload.response) 32 | // for this action type alone. 33 | payload: (action, state, res) => { 34 | switch(responseType) { 35 | case httpresponses.JSON: 36 | return getJSON(res); 37 | case httpresponses.TEXT: 38 | return res.text(); 39 | default: // default try to get JSON response 40 | return getJSON(res); 41 | } 42 | } 43 | }, 44 | { 45 | type: at.combine(prefix, at.GET_FAILURE), 46 | meta: meta, 47 | // payload: (action, state, res) => { 48 | // console.log('Inside get failure payload. res: ', res); 49 | // if (res) { 50 | // return { 51 | // status: res.status, 52 | // statusText: res.statusText, 53 | // }; 54 | // } else { 55 | // return { 56 | // status: 'Network request failed', 57 | // }; 58 | // } 59 | // } 60 | } 61 | ], 62 | endpoint: `${API_ROOT}${endpoint}${urlParams}`, 63 | method: 'GET', 64 | // schema: {}, 65 | } 66 | }; 67 | } 68 | 69 | // Fetch an object given a particular Id 70 | // export function fetch(meta, prefix, endpoint, params) { 71 | export function fetch(meta, prefix, endpoint, params, dispatch) { 72 | return dispatch(get(prefix, endpoint, params, meta)); 73 | // return dispatch => dispatch(get(prefix, endpoint, params, meta)); 74 | } 75 | 76 | // Search the given endpoint using searchTerm as URL param 77 | // Used for search results for Modules, Containers and Experiments 78 | export function search(searchTerm, prefix, endpoint, dispatch, params={}) { 79 | return dispatch(get(prefix, endpoint, params)); 80 | } 81 | 82 | // POSTs data to the server using the REST API 83 | function post(data, prefix, endpoint, headers, params) { 84 | const urlParams = getUrlParams(params); 85 | 86 | return { 87 | [CALL_API]: { 88 | types: at.combine(prefix, [at.POST_REQUEST, at.POST_SUCCESS, at.POST_FAILURE]), 89 | endpoint: `${API_ROOT}${endpoint}${urlParams}`, 90 | method: 'POST', 91 | headers: headers, 92 | body: data, 93 | } 94 | }; 95 | } 96 | 97 | export function submit(data, prefix, endpoint, headers, params, dispatch) { 98 | return ( 99 | dispatch(post(data, prefix, endpoint, headers, params)) 100 | // .then(() => {}) // do other actions here 101 | ); 102 | } -------------------------------------------------------------------------------- /src/containers/Main/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { reset } from 'redux-form'; 4 | 5 | import SimpleSearch from 'components/SimpleSearch'; 6 | import Chat from 'components/Chat'; 7 | import { fetch } from 'actions/main'; 8 | import { users } from 'utils/constants'; 9 | 10 | import './style.css'; 11 | 12 | const propTypes = { 13 | }; 14 | 15 | const contextTypes = { 16 | router: PropTypes.object, 17 | }; 18 | 19 | class Main extends Component { 20 | constructor(props) { 21 | super(props); 22 | 23 | this.handleChatInput = this.handleChatInput.bind(this); 24 | this.scrollToBottom = this.scrollToBottom.bind(this); 25 | } 26 | 27 | componentDidUpdate() { 28 | this.scrollToBottom(); // Scroll to bottom of chat window on each conv update 29 | } 30 | 31 | handleChatInput(data, dispatch) { 32 | dispatch(reset('chatInputForm')); // Reset the chat input text 33 | dispatch(fetch(data.search, true)); 34 | } 35 | 36 | scrollToBottom(){ 37 | var element = document.getElementById("scrollingChat"); 38 | element.scrollTop = element.scrollHeight; 39 | } 40 | 41 | render() { 42 | const { conversations, payloads } = this.props; 43 | const { userInput, botOutput } = payloads; 44 | return ( 45 |
46 | 47 |
48 | 49 | {/** Chat Column */} 50 |
51 |
52 | 53 | {/** Conversations */} 54 |
55 | {conversations.map((chat, i) => 56 | 57 | )} 58 |
59 | 60 | {/** Chat Input Textbox */} 61 | 69 | 70 |
71 |
72 | 73 | {/** Payload Column */} 74 |
75 |
Floyd Experiment
76 | 81 | {/** Payload Request */} 82 |
83 |
User Input
84 |
85 |
 86 |                 
87 |
 88 |                   {userInput.message}
 89 |                 
90 |
91 |
92 | 93 | {/** Payload Response */} 94 |
95 |
Model Output
96 |
97 |
 98 |                 
99 |
100 |                   {botOutput.message}
101 |                 
102 |
103 |
104 |
105 | 106 |
107 |
108 | ); 109 | } 110 | 111 | } 112 | 113 | Main.propTypes = propTypes; 114 | Main.contextTypes = contextTypes; 115 | const mapStateToProps = (state, ownProps) => { 116 | const { chats } = state; 117 | const { conversations, payloads } = chats; 118 | 119 | return { 120 | conversations, 121 | payloads 122 | }; 123 | }; 124 | 125 | const mapDispatchToProps = (dispatch) => { 126 | return {}; 127 | }; 128 | 129 | export default connect( 130 | mapStateToProps, 131 | mapDispatchToProps 132 | )(Main); 133 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatbot-demo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "devDependencies": { 6 | "autoprefixer": "6.5.1", 7 | "babel-core": "6.17.0", 8 | "babel-eslint": "7.1.1", 9 | "babel-jest": "17.0.2", 10 | "babel-loader": "6.2.7", 11 | "babel-plugin-transform-class-properties": "^6.19.0", 12 | "babel-plugin-transform-object-rest-spread": "^6.20.2", 13 | "babel-plugin-transform-react-constant-elements": "^6.9.1", 14 | "babel-plugin-transform-regenerator": "^6.21.0", 15 | "babel-plugin-transform-runtime": "^6.15.0", 16 | "babel-polyfill": "^6.20.0", 17 | "babel-preset-es2015": "^6.18.0", 18 | "babel-preset-latest": "^6.16.0", 19 | "babel-preset-react-app": "^2.0.1", 20 | "babel-runtime": "^6.20.0", 21 | "case-sensitive-paths-webpack-plugin": "1.1.4", 22 | "chalk": "1.1.3", 23 | "connect-history-api-fallback": "1.3.0", 24 | "cross-spawn": "4.0.2", 25 | "css-loader": "0.26.0", 26 | "detect-port": "1.0.1", 27 | "dotenv": "2.0.0", 28 | "es6-promise": "^4.0.5", 29 | "eslint": "3.8.1", 30 | "eslint-config-react-app": "^0.5.0", 31 | "eslint-loader": "1.6.0", 32 | "eslint-plugin-babel": "^4.0.0", 33 | "eslint-plugin-flowtype": "2.21.0", 34 | "eslint-plugin-import": "2.0.1", 35 | "eslint-plugin-jsx-a11y": "2.2.3", 36 | "eslint-plugin-react": "6.4.1", 37 | "extract-text-webpack-plugin": "1.0.1", 38 | "file-loader": "0.9.0", 39 | "filesize": "3.3.0", 40 | "fs-extra": "0.30.0", 41 | "gzip-size": "3.0.0", 42 | "html-webpack-plugin": "2.24.0", 43 | "http-proxy-middleware": "0.17.2", 44 | "imports-loader": "^0.7.0", 45 | "jest": "17.0.2", 46 | "json-loader": "0.5.4", 47 | "less": "^2.7.2", 48 | "less-loader": "^2.2.3", 49 | "node-sass": "^4.2.0", 50 | "object-assign": "4.1.0", 51 | "path-exists": "2.1.0", 52 | "postcss-loader": "1.0.0", 53 | "postcss-nested": "^1.0.0", 54 | "promise": "7.1.1", 55 | "react-dev-utils": "^0.4.2", 56 | "recursive-readdir": "2.1.0", 57 | "redux-devtools": "^3.3.1", 58 | "redux-devtools-dock-monitor": "^1.1.1", 59 | "redux-devtools-log-monitor": "^1.2.0", 60 | "sass-loader": "^4.1.1", 61 | "strip-ansi": "3.0.1", 62 | "style-loader": "0.13.1", 63 | "url-loader": "0.5.7", 64 | "webpack": "1.14.0", 65 | "webpack-dev-middleware": "^1.9.0", 66 | "webpack-dev-server": "1.16.2", 67 | "webpack-hot-middleware": "^2.15.0", 68 | "webpack-manifest-plugin": "1.1.0", 69 | "whatwg-fetch": "1.0.0" 70 | }, 71 | "dependencies": { 72 | "babel-runtime": "^6.20.0", 73 | "bootstrap": "^3.3.7", 74 | "bootstrap-sass": "^3.3.7", 75 | "chat-template": "0.0.22", 76 | "classnames": "^2.2.5", 77 | "codemirror": "^5.22.0", 78 | "isomorphic-fetch": "^2.2.1", 79 | "jquery": "^3.1.1", 80 | "lodash": "^4.17.4", 81 | "normalize.css": "^5.0.0", 82 | "react": "^15.4.2", 83 | "react-bootstrap": "^0.30.7", 84 | "react-codemirror": "^0.3.0", 85 | "react-dom": "^15.4.2", 86 | "react-icons": "^2.2.3", 87 | "react-redux": "^5.0.2", 88 | "react-router": "^3.0.0", 89 | "react-router-redux": "^4.0.7", 90 | "react-textarea-autosize": "^4.0.5", 91 | "react-timeago": "^3.1.3", 92 | "rebass": "^0.3.3", 93 | "redux": "^3.6.0", 94 | "redux-api-middleware": "^1.0.2", 95 | "redux-form": "^6.4.3", 96 | "redux-thunk": "^2.1.0", 97 | "shortid": "^2.2.6" 98 | }, 99 | "scripts": { 100 | "start": "node scripts/start.js", 101 | "build": "node scripts/build.js", 102 | "test": "node scripts/test.js --env=jsdom" 103 | }, 104 | "jest": { 105 | "collectCoverageFrom": [ 106 | "src/**/*.{js,jsx}" 107 | ], 108 | "setupFiles": [ 109 | "\\config\\polyfills.js" 110 | ], 111 | "testPathIgnorePatterns": [ 112 | "[/\\\\](build|docs|node_modules)[/\\\\]" 113 | ], 114 | "testEnvironment": "node", 115 | "testURL": "http://localhost", 116 | "transform": { 117 | "^.+\\.(js|jsx)$": "/node_modules/babel-jest", 118 | "^.+\\.css$": "\\config\\jest\\cssTransform.js", 119 | "^(?!.*\\.(js|jsx|css|json)$)": "\\config\\jest\\fileTransform.js" 120 | }, 121 | "transformIgnorePatterns": [ 122 | "[/\\\\]node_modules[/\\\\].+\\.(js|jsx)$" 123 | ], 124 | "moduleNameMapper": { 125 | "^react-native$": "react-native-web" 126 | } 127 | }, 128 | "babel": { 129 | "presets": [ 130 | "react-app" 131 | ] 132 | }, 133 | "eslintConfig": { 134 | "extends": "react-app" 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/containers/Main/style.css: -------------------------------------------------------------------------------- 1 | div { 2 | word-wrap: break-word; 3 | line-height: 1.25rem; 4 | } 5 | 6 | #contentParent { 7 | height: 100%; 8 | } 9 | 10 | .responsive-columns-wrapper { 11 | display: -ms-flexbox; 12 | display: -webkit-flex; 13 | display: flex; 14 | flex-direction: row; 15 | -ms-display: flex; 16 | -ms-flex-direction: row; 17 | } 18 | 19 | #chat-column-holder { 20 | text-align: center; 21 | } 22 | 23 | .responsive-column { 24 | -webkit-flex: 1; 25 | -ms-flex: 1; 26 | flex: 1; 27 | overflow: auto; 28 | } 29 | 30 | .chat-column { 31 | height: 100%; 32 | padding: 0.9375rem 0 0.625rem 0; 33 | margin: auto; 34 | text-align: left; 35 | max-width: 45rem; 36 | min-width: 20rem; 37 | } 38 | 39 | #scrollingChat { 40 | margin: 0.75rem; 41 | overflow-y: auto; 42 | overflow-x: hidden; 43 | height: calc(100% - 8rem); 44 | } 45 | 46 | #payload-column { 47 | font-family: Monaco, monospace; 48 | font-size: 0.75rem; 49 | letter-spacing: 0; 50 | line-height: 1.125rem; 51 | background-color: #23292A; 52 | color: #fff; 53 | overflow-x: auto; 54 | 55 | width: 45%; 56 | max-width: 40rem; 57 | min-width: 20rem; 58 | } 59 | 60 | #payload-column.full { 61 | width: 100%; 62 | max-width: none; 63 | min-width: initial; 64 | } 65 | 66 | #payload-column .header-text, #payload-column #payload-initial-message { 67 | font-family: Helvetica Neue, Helvetica, Arial, sans-serif; 68 | font-size: 1.525rem; 69 | color: #9E9E9E; 70 | letter-spacing: 0.03875rem; 71 | padding: 0.5rem; 72 | padding-left: 2.5rem; 73 | background: #383D3E; 74 | } 75 | 76 | .payload .line-numbers, .payload .payload-text { 77 | padding: 0.5rem; 78 | } 79 | 80 | 81 | 82 | 83 | .hide { 84 | display: none; 85 | } 86 | 87 | .line-numbers { 88 | width: 2rem; 89 | color: #898989; 90 | text-align: right; 91 | } 92 | 93 | pre { 94 | margin: 0; 95 | word-wrap: normal; 96 | } 97 | 98 | .string { 99 | color: #54EED0; 100 | } 101 | 102 | .boolean, .null, .number { 103 | color: #CE8EFF; 104 | } 105 | 106 | .key { 107 | color: #66B7FF; 108 | } 109 | 110 | html{ 111 | font-size: 16px; 112 | } 113 | 114 | @media only screen and (max-width: 1000px) { 115 | html { 116 | font-size: 15px; 117 | } 118 | } 119 | 120 | @media only screen and (max-width: 600px) { 121 | html { 122 | font-size: 14px; 123 | } 124 | 125 | .chat-column { 126 | padding-top: 4rem; 127 | } 128 | 129 | #payload-column { 130 | width: 0; 131 | max-width: none; 132 | min-width: initial; 133 | } 134 | } 135 | 136 | .message-inner { 137 | opacity: 0; 138 | margin-top: 0.9375rem; 139 | -webkit-transition-property: opacity, margin-top; 140 | -webkit-transition-duration: 0.75s; 141 | -webkit-transition-timing-function: ease-in; 142 | -moz-transition-property: opacity, margin-top; 143 | -moz-transition-duration: 0.75s; 144 | -moz-transition-timing-function: ease-in; 145 | -o-transition-property: opacity, margin-top; 146 | -o-transition-duration: 0.75s; 147 | -o-transition-timing-function: ease-in; 148 | -ms-transition-property: opacity, margin-top; 149 | -ms-transition-duration: 0.75s; 150 | -ms-transition-timing-function: ease-in; 151 | transition-property: opacity, margin-top; 152 | transition-duration: 0.75s; 153 | transition-timing-function: ease-in; 154 | } 155 | 156 | .load .message-inner { 157 | opacity: 1; 158 | margin-top: 0.3125rem; 159 | } 160 | 161 | .from-user { 162 | text-align: right; 163 | } 164 | 165 | .from-user .message-inner { 166 | position: relative; 167 | font-size: 1.75rem; 168 | color: #fff; 169 | letter-spacing: 0.02rem; 170 | line-height: 3rem; 171 | background: #00B4A0; 172 | border-radius: 1.25rem; 173 | border-bottom-right-radius: 0; 174 | text-align: left; 175 | display: inline-block; 176 | margin-left: 2.5rem; 177 | min-width: 2.5rem; 178 | } 179 | 180 | .from-user .message-inner p { 181 | margin: 0.3125rem; 182 | padding: 0 0.9375rem; 183 | } 184 | 185 | .from-user .message-inner:before, .from-user .message-inner:after { 186 | content: ""; 187 | position: absolute; 188 | } 189 | 190 | .from-user .message-inner:before { 191 | z-index: -2; 192 | bottom: -0.375rem; 193 | right: 0; 194 | height: 0.375rem; 195 | width: 0.5rem; 196 | background: #1cb3a0; 197 | } 198 | 199 | .from-user .message-inner:after { 200 | z-index: -1; 201 | bottom: -0.5rem; 202 | right: 0; 203 | height: 0.5rem; 204 | width: 0.5rem; 205 | background: #fff; 206 | border-top-right-radius: 1.25rem; 207 | } 208 | 209 | .from-bot .message-inner { 210 | position: relative; 211 | border-radius: 1.5625rem; 212 | font-size: 1.75rem; 213 | color: #B5B5B5; 214 | letter-spacing: 0.015rem; 215 | line-height: 3rem; 216 | } 217 | 218 | .from-bot.latest .message-inner { 219 | color: #323232; 220 | } 221 | 222 | .from-bot p { 223 | margin: 0.3125rem; 224 | padding: 0 1.25rem; 225 | } 226 | 227 | /*.from-bot.latest.top p:before { 228 | content: "."; 229 | color: #9855D4; 230 | background-size: 0.3125rem 1.3125rem; 231 | position: absolute; 232 | z-index: 2; 233 | left: 0.4375rem; 234 | width: 0.3125rem; 235 | height: 1.3125rem; 236 | line-height: 1.3125rem; 237 | }*/ 238 | 239 | 240 | ::-webkit-input-placeholder { 241 | color: #B5B5B5; 242 | } 243 | 244 | ::-moz-placeholder { 245 | color: #B5B5B5; 246 | opacity: 1; 247 | } 248 | 249 | input:-moz-placeholder { 250 | color: #B5B5B5; 251 | opacity: 1; 252 | } 253 | 254 | :-ms-input-placeholder { 255 | color: #B5B5B5; 256 | } 257 | 258 | ::-ms-clear { 259 | display: none; 260 | } -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | // Do this as the first thing so that any code reading it knows the right env. 2 | process.env.NODE_ENV = 'production'; 3 | 4 | // Load environment variables from .env file. Suppress warnings using silent 5 | // if this file is missing. dotenv will never modify any environment variables 6 | // that have already been set. 7 | // https://github.com/motdotla/dotenv 8 | require('dotenv').config({silent: true}); 9 | 10 | var chalk = require('chalk'); 11 | var fs = require('fs-extra'); 12 | var path = require('path'); 13 | var pathExists = require('path-exists'); 14 | var filesize = require('filesize'); 15 | var gzipSize = require('gzip-size').sync; 16 | var webpack = require('webpack'); 17 | var config = require('../config/webpack.config.prod'); 18 | var paths = require('../config/paths'); 19 | var checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 20 | var recursive = require('recursive-readdir'); 21 | var stripAnsi = require('strip-ansi'); 22 | 23 | var useYarn = pathExists.sync(paths.yarnLockFile); 24 | 25 | // Warn and crash if required files are missing 26 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 27 | process.exit(1); 28 | } 29 | 30 | // Input: /User/dan/app/build/static/js/main.82be8.js 31 | // Output: /static/js/main.js 32 | function removeFileNameHash(fileName) { 33 | return fileName 34 | .replace(paths.appBuild, '') 35 | .replace(/\/?(.*)(\.\w+)(\.js|\.css)/, (match, p1, p2, p3) => p1 + p3); 36 | } 37 | 38 | // Input: 1024, 2048 39 | // Output: "(+1 KB)" 40 | function getDifferenceLabel(currentSize, previousSize) { 41 | var FIFTY_KILOBYTES = 1024 * 50; 42 | var difference = currentSize - previousSize; 43 | var fileSize = !Number.isNaN(difference) ? filesize(difference) : 0; 44 | if (difference >= FIFTY_KILOBYTES) { 45 | return chalk.red('+' + fileSize); 46 | } else if (difference < FIFTY_KILOBYTES && difference > 0) { 47 | return chalk.yellow('+' + fileSize); 48 | } else if (difference < 0) { 49 | return chalk.green(fileSize); 50 | } else { 51 | return ''; 52 | } 53 | } 54 | 55 | // First, read the current file sizes in build directory. 56 | // This lets us display how much they changed later. 57 | recursive(paths.appBuild, (err, fileNames) => { 58 | var previousSizeMap = (fileNames || []) 59 | .filter(fileName => /\.(js|css)$/.test(fileName)) 60 | .reduce((memo, fileName) => { 61 | var contents = fs.readFileSync(fileName); 62 | var key = removeFileNameHash(fileName); 63 | memo[key] = gzipSize(contents); 64 | return memo; 65 | }, {}); 66 | 67 | // Remove all content but keep the directory so that 68 | // if you're in it, you don't end up in Trash 69 | fs.emptyDirSync(paths.appBuild); 70 | 71 | // Start the webpack build 72 | build(previousSizeMap); 73 | 74 | // Merge with the public folder 75 | copyPublicFolder(); 76 | }); 77 | 78 | // Print a detailed summary of build files. 79 | function printFileSizes(stats, previousSizeMap) { 80 | var assets = stats.toJson().assets 81 | .filter(asset => /\.(js|css)$/.test(asset.name)) 82 | .map(asset => { 83 | var fileContents = fs.readFileSync(paths.appBuild + '/' + asset.name); 84 | var size = gzipSize(fileContents); 85 | var previousSize = previousSizeMap[removeFileNameHash(asset.name)]; 86 | var difference = getDifferenceLabel(size, previousSize); 87 | return { 88 | folder: path.join('build', path.dirname(asset.name)), 89 | name: path.basename(asset.name), 90 | size: size, 91 | sizeLabel: filesize(size) + (difference ? ' (' + difference + ')' : '') 92 | }; 93 | }); 94 | assets.sort((a, b) => b.size - a.size); 95 | var longestSizeLabelLength = Math.max.apply(null, 96 | assets.map(a => stripAnsi(a.sizeLabel).length) 97 | ); 98 | assets.forEach(asset => { 99 | var sizeLabel = asset.sizeLabel; 100 | var sizeLength = stripAnsi(sizeLabel).length; 101 | if (sizeLength < longestSizeLabelLength) { 102 | var rightPadding = ' '.repeat(longestSizeLabelLength - sizeLength); 103 | sizeLabel += rightPadding; 104 | } 105 | console.log( 106 | ' ' + sizeLabel + 107 | ' ' + chalk.dim(asset.folder + path.sep) + chalk.cyan(asset.name) 108 | ); 109 | }); 110 | } 111 | 112 | // Print out errors 113 | function printErrors(summary, errors) { 114 | console.log(chalk.red(summary)); 115 | console.log(); 116 | errors.forEach(err => { 117 | console.log(err.message || err); 118 | console.log(); 119 | }); 120 | } 121 | 122 | // Create the production build and print the deployment instructions. 123 | function build(previousSizeMap) { 124 | console.log('Creating an optimized production build...'); 125 | webpack(config).run((err, stats) => { 126 | if (err) { 127 | printErrors('Failed to compile.', [err]); 128 | process.exit(1); 129 | } 130 | 131 | if (stats.compilation.errors.length) { 132 | printErrors('Failed to compile.', stats.compilation.errors); 133 | process.exit(1); 134 | } 135 | 136 | if (process.env.CI && stats.compilation.warnings.length) { 137 | printErrors('Failed to compile.', stats.compilation.warnings); 138 | process.exit(1); 139 | } 140 | 141 | console.log(chalk.green('Compiled successfully.')); 142 | console.log(); 143 | 144 | console.log('File sizes after gzip:'); 145 | console.log(); 146 | printFileSizes(stats, previousSizeMap); 147 | console.log(); 148 | 149 | var openCommand = process.platform === 'win32' ? 'start' : 'open'; 150 | var appPackage = require(paths.appPackageJson); 151 | var homepagePath = appPackage.homepage; 152 | var publicPath = config.output.publicPath; 153 | if (homepagePath && homepagePath.indexOf('.github.io/') !== -1) { 154 | // "homepage": "http://user.github.io/project" 155 | console.log('The project was built assuming it is hosted at ' + chalk.green(publicPath) + '.'); 156 | console.log('You can control this with the ' + chalk.green('homepage') + ' field in your ' + chalk.cyan('package.json') + '.'); 157 | console.log(); 158 | console.log('The ' + chalk.cyan('build') + ' folder is ready to be deployed.'); 159 | console.log('To publish it at ' + chalk.green(homepagePath) + ', run:'); 160 | // If script deploy has been added to package.json, skip the instructions 161 | if (typeof appPackage.scripts.deploy === 'undefined') { 162 | console.log(); 163 | if (useYarn) { 164 | console.log(' ' + chalk.cyan('yarn') + ' add --dev gh-pages'); 165 | } else { 166 | console.log(' ' + chalk.cyan('npm') + ' install --save-dev gh-pages'); 167 | } 168 | console.log(); 169 | console.log('Add the following script in your ' + chalk.cyan('package.json') + '.'); 170 | console.log(); 171 | console.log(' ' + chalk.dim('// ...')); 172 | console.log(' ' + chalk.yellow('"scripts"') + ': {'); 173 | console.log(' ' + chalk.dim('// ...')); 174 | console.log(' ' + chalk.yellow('"deploy"') + ': ' + chalk.yellow('"npm run build&&gh-pages -d build"')); 175 | console.log(' }'); 176 | console.log(); 177 | console.log('Then run:'); 178 | } 179 | console.log(); 180 | console.log(' ' + chalk.cyan(useYarn ? 'yarn' : 'npm') + ' run deploy'); 181 | console.log(); 182 | } else if (publicPath !== '/') { 183 | // "homepage": "http://mywebsite.com/project" 184 | console.log('The project was built assuming it is hosted at ' + chalk.green(publicPath) + '.'); 185 | console.log('You can control this with the ' + chalk.green('homepage') + ' field in your ' + chalk.cyan('package.json') + '.'); 186 | console.log(); 187 | console.log('The ' + chalk.cyan('build') + ' folder is ready to be deployed.'); 188 | console.log(); 189 | } else { 190 | // no homepage or "homepage": "http://mywebsite.com" 191 | console.log('The project was built assuming it is hosted at the server root.'); 192 | if (homepagePath) { 193 | // "homepage": "http://mywebsite.com" 194 | console.log('You can control this with the ' + chalk.green('homepage') + ' field in your ' + chalk.cyan('package.json') + '.'); 195 | console.log(); 196 | } else { 197 | // no homepage 198 | console.log('To override this, specify the ' + chalk.green('homepage') + ' in your ' + chalk.cyan('package.json') + '.'); 199 | console.log('For example, add this to build it for GitHub Pages:') 200 | console.log(); 201 | console.log(' ' + chalk.green('"homepage"') + chalk.cyan(': ') + chalk.green('"http://myname.github.io/myapp"') + chalk.cyan(',')); 202 | console.log(); 203 | } 204 | console.log('The ' + chalk.cyan('build') + ' folder is ready to be deployed.'); 205 | console.log('You may also serve it locally with a static server:') 206 | console.log(); 207 | if (useYarn) { 208 | console.log(' ' + chalk.cyan('yarn') + ' global add pushstate-server'); 209 | } else { 210 | console.log(' ' + chalk.cyan('npm') + ' install -g pushstate-server'); 211 | } 212 | console.log(' ' + chalk.cyan('pushstate-server') + ' build'); 213 | console.log(' ' + chalk.cyan(openCommand) + ' http://localhost:9000'); 214 | console.log(); 215 | } 216 | }); 217 | } 218 | 219 | function copyPublicFolder() { 220 | fs.copySync(paths.appPublic, paths.appBuild, { 221 | dereference: true, 222 | filter: file => file !== paths.appHtml 223 | }); 224 | } 225 | -------------------------------------------------------------------------------- /config/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | var autoprefixer = require('autoprefixer'); 2 | var webpack = require('webpack'); 3 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | var CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin'); 5 | var InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin'); 6 | var WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin'); 7 | var getClientEnvironment = require('./env'); 8 | var paths = require('./paths'); 9 | 10 | 11 | 12 | // Webpack uses `publicPath` to determine where the app is being served from. 13 | // In development, we always serve from the root. This makes config easier. 14 | var publicPath = '/'; 15 | // `publicUrl` is just like `publicPath`, but we will provide it to our app 16 | // as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript. 17 | // Omit trailing slash as %PUBLIC_PATH%/xyz looks better than %PUBLIC_PATH%xyz. 18 | var publicUrl = ''; 19 | // Get environment variables to inject into our app. 20 | var env = getClientEnvironment(publicUrl); 21 | 22 | // This is the development configuration. 23 | // It is focused on developer experience and fast rebuilds. 24 | // The production configuration is different and lives in a separate file. 25 | module.exports = { 26 | // You may want 'eval' instead if you prefer to see the compiled output in DevTools. 27 | // See the discussion in https://github.com/facebookincubator/create-react-app/issues/343. 28 | devtool: 'cheap-module-source-map', 29 | // These are the "entry points" to our application. 30 | // This means they will be the "root" imports that are included in JS bundle. 31 | // The first two entry points enable "hot" CSS and auto-refreshes for JS. 32 | entry: [ 33 | // Include an alternative client for WebpackDevServer. A client's job is to 34 | // connect to WebpackDevServer by a socket and get notified about changes. 35 | // When you save a file, the client will either apply hot updates (in case 36 | // of CSS changes), or refresh the page (in case of JS changes). When you 37 | // make a syntax error, this client will display a syntax error overlay. 38 | // Note: instead of the default WebpackDevServer client, we use a custom one 39 | // to bring better experience for Create React App users. You can replace 40 | // the line below with these two lines if you prefer the stock client: 41 | // require.resolve('webpack-dev-server/client') + '?/', 42 | // require.resolve('webpack/hot/dev-server'), 43 | require.resolve('react-dev-utils/webpackHotDevClient'), 44 | // We ship a few polyfills by default: 45 | require.resolve('./polyfills'), 46 | // Finally, this is your app's code: 47 | paths.appIndexJs 48 | // We include the app code last so that if there is a runtime error during 49 | // initialization, it doesn't blow up the WebpackDevServer client, and 50 | // changing JS code would still trigger a refresh. 51 | ], 52 | output: { 53 | // Next line is not used in dev but WebpackDevServer crashes without it: 54 | path: paths.appBuild, 55 | // Add /* filename */ comments to generated require()s in the output. 56 | pathinfo: true, 57 | // This does not produce a real file. It's just the virtual path that is 58 | // served by WebpackDevServer in development. This is the JS bundle 59 | // containing code from all our entry points, and the Webpack runtime. 60 | filename: 'static/js/bundle.js', 61 | // This is the URL that app is served from. We use "/" in development. 62 | publicPath: publicPath 63 | }, 64 | resolve: { 65 | // This allows you to set a fallback for where Webpack should look for modules. 66 | // We read `NODE_PATH` environment variable in `paths.js` and pass paths here. 67 | // We use `fallback` instead of `root` because we want `node_modules` to "win" 68 | // if there any conflicts. This matches Node resolution mechanism. 69 | // https://github.com/facebookincubator/create-react-app/issues/253 70 | fallback: paths.nodePaths, 71 | // These are the reasonable defaults supported by the Node ecosystem. 72 | // We also include JSX as a common component filename extension to support 73 | // some tools, although we do not recommend using it, see: 74 | // https://github.com/facebookincubator/create-react-app/issues/290 75 | extensions: ['.js', '.json', '', '.scss', '.less', '.css'], 76 | modulesDirectories: [ 77 | 'node_modules', 78 | 'src', 79 | ], 80 | alias: { 81 | // Support React Native Web 82 | // https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/ 83 | 'react-native': 'react-native-web' 84 | } 85 | }, 86 | 87 | module: { 88 | // First, run the linter. 89 | // It's important to do this before Babel processes the JS. 90 | preLoaders: [ 91 | { 92 | test: /\.(js|jsx)$/, 93 | loader: 'eslint', 94 | include: paths.appSrc, 95 | } 96 | ], 97 | loaders: [ 98 | // Default loader: load all assets that are not handled 99 | // by other loaders with the url loader. 100 | // Note: This list needs to be updated with every change of extensions 101 | // the other loaders match. 102 | // E.g., when adding a loader for a new supported file extension, 103 | // we need to add the supported extension to this loader too. 104 | // Add one new line in `exclude` for each loader. 105 | // 106 | // "file" loader makes sure those assets get served by WebpackDevServer. 107 | // When you `import` an asset, you get its (virtual) filename. 108 | // In production, they would get copied to the `build` folder. 109 | // "url" loader works like "file" loader except that it embeds assets 110 | // smaller than specified limit in bytes as data URLs to avoid requests. 111 | // A missing `test` is equivalent to a match. 112 | { 113 | exclude: [ 114 | /\.html$/, 115 | /\.(js|jsx)$/, 116 | /\.css$/, 117 | /\.json$/, 118 | /\.svg$/ 119 | ], 120 | loader: 'url', 121 | query: { 122 | limit: 10000, 123 | name: 'static/media/[name].[hash:8].[ext]' 124 | } 125 | }, 126 | // Process JS with Babel. 127 | { 128 | test: /\.(js|jsx)$/, 129 | include: paths.appSrc, 130 | loader: 'babel', 131 | query: { 132 | 133 | // This is a feature of `babel-loader` for webpack (not Babel itself). 134 | // It enables caching results in ./node_modules/.cache/babel-loader/ 135 | // directory for faster rebuilds. 136 | cacheDirectory: true 137 | } 138 | }, 139 | // "postcss" loader applies autoprefixer to our CSS. 140 | // "css" loader resolves paths in CSS and adds assets as dependencies. 141 | // "style" loader turns CSS into JS modules that inject