Merged PR 17: chatbot api integration

master
Tudor Stanciu 2020-06-07 20:12:54 +00:00
commit dd329559f2
11 changed files with 290 additions and 25 deletions

View File

@ -1,4 +1,6 @@
import * as types from "./actionTypes"; import * as types from "./actionTypes";
import api from "./api";
import { sendHttpRequest } from "../../redux/actions/httpActions";
export function summonWizard() { export function summonWizard() {
return { type: types.SUMMON_WIZARD }; return { type: types.SUMMON_WIZARD };
@ -7,3 +9,130 @@ export function summonWizard() {
export function dismissBot() { export function dismissBot() {
return { type: types.DISMISS_BOT }; return { type: types.DISMISS_BOT };
} }
export function loadBotSession(botName, userKey, botType) {
return async function(dispatch, getState) {
try {
const state = getState();
const session = state.bot.session[botType];
if (session && (session.loading || session.loaded)) {
//a session exists, so check if a chat is open
if (!state.bot.chat.loaded && !state.bot.chat.loading) {
dispatch(initializeChat(session.sessionId));
}
return;
}
dispatch({ type: types.INITIALIZE_BOT_SESSION_STARTED, botType });
const externalId = state.frontendSession.sessionId;
const clientApplication = state.frontendSession.applicationCode;
const data = await dispatch(
sendHttpRequest(
api.getBotSession(botName, externalId, clientApplication, userKey)
)
);
dispatch({
type: types.INITIALIZE_BOT_SESSION_SUCCESS,
payload: data,
botType
});
dispatch(initializeChat(data.sessionId));
} catch (error) {
throw error;
}
};
}
function initializeChat(sessionId) {
return async function(dispatch) {
try {
dispatch({ type: types.INITIALIZE_BOT_CHAT_STARTED });
const data = await dispatch(
sendHttpRequest(api.initializeChat(sessionId))
);
dispatch({
type: types.INITIALIZE_BOT_CHAT_SUCCESS,
payload: data
});
} catch (error) {
throw error;
}
};
}
export function closeChat() {
return async function(dispatch, getState) {
try {
const { chatId } = getState().bot.chat;
if (!chatId) return;
const data = await dispatch(sendHttpRequest(api.closeChat(chatId)));
dispatch({
type: types.CLOSE_BOT_CHAT_SUCCESS,
payload: data
});
} catch (error) {
throw error;
}
};
}
export function saveMessage(messageSourceId, messageDate, messageContent) {
return async function(dispatch, getState) {
try {
const { chatId } = getState().bot.chat;
if (!chatId) {
//the chat is not yet initialized. The message will be stored on the client and sent to the server next time.
dispatch({
type: types.STORE_BOT_MESSAGE,
message: { messageSourceId, messageDate, messageContent }
});
return;
}
await dispatch(checkStorage(chatId));
const event = await dispatch(
sendHttpRequest(
api.saveMessage(chatId, messageSourceId, messageDate, messageContent)
)
);
dispatch({ type: types.SAVE_BOT_MESSAGE_SUCCESS, payload: event });
return event;
} catch (error) {
throw error;
}
};
}
function checkStorage(chatId) {
return async function(dispatch, getState) {
try {
const messages = getState().bot.storage;
if (messages.length === 0) return;
const promises = [];
messages.forEach(message => {
const promise = dispatch(
sendHttpRequest(
api.saveMessage(
chatId,
message.messageSourceId,
message.messageDate,
message.messageContent
)
)
);
promises.push(promise);
});
//wait to save all stored messages to keep the order
await Promise.all(promises);
//clear stored messages after save
dispatch({ type: types.CLEAR_BOT_STORAGE });
} catch (error) {
throw error;
}
};
}

View File

@ -1,2 +1,11 @@
export const DISMISS_BOT = "DISMISS_BOT"; export const DISMISS_BOT = "DISMISS_BOT";
export const SUMMON_WIZARD = "SUMMON_WIZARD"; export const SUMMON_WIZARD = "SUMMON_WIZARD";
export const INITIALIZE_BOT_SESSION_STARTED = "INITIALIZE_BOT_SESSION_STARTED";
export const INITIALIZE_BOT_SESSION_SUCCESS = "INITIALIZE_BOT_SESSION_SUCCESS";
export const INITIALIZE_BOT_CHAT_STARTED = "INITIALIZE_BOT_CHAT_STARTED";
export const INITIALIZE_BOT_CHAT_SUCCESS = "INITIALIZE_BOT_CHAT_SUCCESS";
export const SAVE_BOT_MESSAGE_SUCCESS = "SAVE_BOT_MESSAGE_SUCCESS";
export const CLOSE_BOT_CHAT_SUCCESS = "CLOSE_BOT_CHAT_SUCCESS";
export const STORE_BOT_MESSAGE = "STORE_BOT_MESSAGE";
export const CLEAR_BOT_STORAGE = "CLEAR_BOT_STORAGE";

View File

@ -0,0 +1,27 @@
import { get, post } from "../../api/axiosApi";
const baseUrl = process.env.CHATBOT_API_URL;
const getBotSession = (botName, externalId, clientApplication, userKey) =>
get(
`${baseUrl}/system/initialize-session/${botName}/${externalId}/${clientApplication}/${userKey}`
);
const initializeChat = sessionId =>
get(`${baseUrl}/chat/initialize/${sessionId}`);
const closeChat = chatId =>
post(`${baseUrl}/chat/close`, {
chatId
});
const saveMessage = (chatId, messageSourceId, messageDate, messageContent) =>
post(`${baseUrl}/chat/message`, {
chatId,
messageSourceId,
messageDate,
messageContent
});
export default {
getBotSession,
initializeChat,
closeChat,
saveMessage
};

View File

@ -1,4 +0,0 @@
export const botType = {
none: Symbol("none"),
wizard: Symbol("wizard")
};

View File

@ -2,10 +2,15 @@ import React, { useEffect, useState } from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { bindActionCreators } from "redux"; import { bindActionCreators } from "redux";
import { botType } from "../botType"; import { botType, bots, userKey } from "../constants";
import Wizard from "./Wizard"; import Wizard from "./Wizard";
import { makeStyles } from "@material-ui/core/styles"; import { makeStyles } from "@material-ui/core/styles";
import { dismissBot } from "../actionCreators"; import {
dismissBot,
loadBotSession,
closeChat,
saveMessage
} from "../actionCreators";
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(theme => ({
bot: { bot: {
@ -24,13 +29,24 @@ const BotsManager = ({ bot, actions }) => {
const classes = useStyles(); const classes = useStyles();
useEffect(() => { useEffect(() => {
if (bot.type) setType(bot.type); if (!bot.type) return;
setType(bot.type);
if (bot.type == botType.none) return;
actions.loadBotSession(bots.Zirhan, userKey.unknown, bot.type);
}, [bot.type]); }, [bot.type]);
const dismissBot = () => {
actions.closeChat();
actions.dismissBot();
};
return ( return (
<div className={classes.botPosition}> <div className={classes.botPosition}>
<div className={classes.bot}> <div className={classes.bot}>
{type === botType.wizard && <Wizard dismissBot={actions.dismissBot} />} {type === botType.wizard && (
<Wizard dismissBot={dismissBot} saveMessage={actions.saveMessage} />
)}
</div> </div>
</div> </div>
); );
@ -49,7 +65,10 @@ function mapStateToProps(state) {
function mapDispatchToProps(dispatch) { function mapDispatchToProps(dispatch) {
return { return {
actions: bindActionCreators({ dismissBot }, dispatch) actions: bindActionCreators(
{ dismissBot, loadBotSession, closeChat, saveMessage },
dispatch
)
}; };
} }

View File

@ -4,8 +4,9 @@ import ChatBot from "react-simple-chatbot";
import { ThemeProvider } from "styled-components"; import { ThemeProvider } from "styled-components";
import { useTheme } from "@material-ui/core/styles"; import { useTheme } from "@material-ui/core/styles";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { bots, messageSource } from "../constants";
const Wizard = ({ dismissBot }) => { const Wizard = ({ dismissBot, saveMessage }) => {
const theme = useTheme(); const theme = useTheme();
const { t } = useTranslation(); const { t } = useTranslation();
@ -21,50 +22,68 @@ const Wizard = ({ dismissBot }) => {
userFontColor: "#4a4a4a" userFontColor: "#4a4a4a"
}; };
const getMessage = message => input => {
const currentDate = new Date();
let messageToSave = message;
if (message.includes("previousValue") && input.previousValue) {
messageToSave = message.replace("{previousValue}", input.previousValue);
}
saveMessage(messageSource.bot, currentDate, messageToSave);
return message;
};
const validate = text => {
const currentDate = new Date();
saveMessage(messageSource.user, currentDate, text);
return true;
};
const steps = [ const steps = [
{ {
id: "1", id: "1",
message: t("Chatbot.Wizard.Message1"), message: getMessage(t("Chatbot.Wizard.Message1")),
trigger: "2" trigger: "2"
}, },
{ {
id: "2", id: "2",
message: t("Chatbot.Wizard.Message2"), message: getMessage(t("Chatbot.Wizard.Message2")),
trigger: "3" trigger: "3"
}, },
{ {
id: "3", id: "3",
message: t("Chatbot.Wizard.Message3"), message: getMessage(t("Chatbot.Wizard.Message3")),
trigger: "4" trigger: "4"
}, },
{ {
id: "4", id: "4",
user: true, user: true,
validator: validate,
trigger: "5" trigger: "5"
}, },
{ {
id: "5", id: "5",
message: t("Chatbot.Wizard.Message5"), message: getMessage(t("Chatbot.Wizard.Message5")),
trigger: "6" trigger: "6"
}, },
{ {
id: "6", id: "6",
user: true, user: true,
validator: validate,
trigger: "7" trigger: "7"
}, },
{ {
id: "7", id: "7",
message: t("Chatbot.Wizard.Message7"), message: getMessage(t("Chatbot.Wizard.Message7")),
trigger: "8" trigger: "8"
}, },
{ {
id: "8", id: "8",
user: true, user: true,
validator: validate,
trigger: "9" trigger: "9"
}, },
{ {
id: "9", id: "9",
message: t("Chatbot.Wizard.Message9"), message: getMessage(t("Chatbot.Wizard.Message9")),
end: true end: true
} }
]; ];
@ -88,14 +107,15 @@ const Wizard = ({ dismissBot }) => {
handleEnd={handleEnd} handleEnd={handleEnd}
steps={steps} steps={steps}
botAvatar={getAvatar()} botAvatar={getAvatar()}
headerTitle="Zirhan" headerTitle={bots.Zirhan}
/> />
</ThemeProvider> </ThemeProvider>
); );
}; };
Wizard.propTypes = { Wizard.propTypes = {
dismissBot: PropTypes.func.isRequired dismissBot: PropTypes.func.isRequired,
saveMessage: PropTypes.func.isRequired
}; };
export default Wizard; export default Wizard;

View File

@ -0,0 +1,17 @@
export const botType = {
none: "none",
wizard: "wizard"
};
export const bots = {
Zirhan: "Zirhan"
};
export const userKey = {
unknown: "Unknown"
};
export const messageSource = {
bot: 1,
user: 2
};

View File

@ -1,6 +1,6 @@
import * as types from "./actionTypes"; import * as types from "./actionTypes";
import initialState from "../../redux/reducers/initialState"; import initialState from "../../redux/reducers/initialState";
import { botType } from "./botType"; import { botType } from "./constants";
export default function chatbotReducer(state = initialState.bot, action) { export default function chatbotReducer(state = initialState.bot, action) {
switch (action.type) { switch (action.type) {
@ -10,6 +10,49 @@ export default function chatbotReducer(state = initialState.bot, action) {
case types.DISMISS_BOT: case types.DISMISS_BOT:
return { ...state, type: botType.none }; return { ...state, type: botType.none };
case types.INITIALIZE_BOT_SESSION_STARTED:
return {
...state,
session: {
...state.session,
[action.botType]: { loading: true, loaded: false }
}
};
case types.INITIALIZE_BOT_SESSION_SUCCESS:
return {
...state,
session: {
...state.session,
[action.botType]: {
loading: false,
loaded: true,
...action.payload
}
}
};
case types.INITIALIZE_BOT_CHAT_STARTED:
return { ...state, chat: { loading: true, loaded: false } };
case types.INITIALIZE_BOT_CHAT_SUCCESS:
return {
...state,
chat: { loading: false, loaded: true, ...action.payload }
};
case types.CLOSE_BOT_CHAT_SUCCESS:
return { ...state, chat: initialState.bot.chat };
case types.STORE_BOT_MESSAGE: {
const storage = [...state.storage];
storage.push(action.message);
return { ...state, storage };
}
case types.CLEAR_BOT_STORAGE:
return { ...state, storage: initialState.bot.storage };
default: default:
return state; return state;
} }

View File

@ -19,7 +19,10 @@ export default {
type: null type: null
}, },
bot: { bot: {
type: null type: null,
session: {},
chat: { loading: false, loaded: false },
storage: []
}, },
ajaxCallsInProgress: 0 ajaxCallsInProgress: 0
}; };

View File

@ -26,7 +26,8 @@ module.exports = {
new webpack.DefinePlugin({ new webpack.DefinePlugin({
"process.env.REVERSE_PROXY_API_URL": JSON.stringify( "process.env.REVERSE_PROXY_API_URL": JSON.stringify(
"http://localhost:5050" "http://localhost:5050"
) ),
"process.env.CHATBOT_API_URL": JSON.stringify("http://localhost:5061")
}), }),
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
template: "src/index.html", template: "src/index.html",

View File

@ -3,7 +3,7 @@ const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin"); const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const webpackBundleAnalyzer = require("webpack-bundle-analyzer"); const webpackBundleAnalyzer = require("webpack-bundle-analyzer");
const CopyPlugin = require('copy-webpack-plugin'); const CopyPlugin = require("copy-webpack-plugin");
process.env.NODE_ENV = "production"; process.env.NODE_ENV = "production";
process.env.PUBLIC_URL = "/reverse-proxy"; process.env.PUBLIC_URL = "/reverse-proxy";
@ -32,6 +32,9 @@ module.exports = {
"process.env.PUBLIC_URL": JSON.stringify(process.env.PUBLIC_URL), "process.env.PUBLIC_URL": JSON.stringify(process.env.PUBLIC_URL),
"process.env.REVERSE_PROXY_API_URL": JSON.stringify( "process.env.REVERSE_PROXY_API_URL": JSON.stringify(
"https://toodle.ddns.net/reverse-proxy-api" "https://toodle.ddns.net/reverse-proxy-api"
),
"process.env.CHATBOT_API_URL": JSON.stringify(
"https://toodle.ddns.net/chatbot-api"
) )
}), }),
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
@ -52,9 +55,7 @@ module.exports = {
} }
}), }),
new CopyPlugin({ new CopyPlugin({
patterns: [ patterns: [{ from: "public", to: "public" }]
{ from: 'public', to: 'public' }
],
}) })
], ],
module: { module: {