├── 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 |
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 |
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 |
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