diff --git a/src/features/chatbot/actionCreators.js b/src/features/chatbot/actionCreators.js index 4384283..e16f0d7 100644 --- a/src/features/chatbot/actionCreators.js +++ b/src/features/chatbot/actionCreators.js @@ -1,4 +1,6 @@ import * as types from "./actionTypes"; +import api from "./api"; +import { sendHttpRequest } from "../../redux/actions/httpActions"; export function summonWizard() { return { type: types.SUMMON_WIZARD }; @@ -7,3 +9,130 @@ export function summonWizard() { export function dismissBot() { 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; + } + }; +} diff --git a/src/features/chatbot/actionTypes.js b/src/features/chatbot/actionTypes.js index 4ba513d..d0d7b30 100644 --- a/src/features/chatbot/actionTypes.js +++ b/src/features/chatbot/actionTypes.js @@ -1,2 +1,11 @@ export const DISMISS_BOT = "DISMISS_BOT"; 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"; diff --git a/src/features/chatbot/api.js b/src/features/chatbot/api.js new file mode 100644 index 0000000..482a32f --- /dev/null +++ b/src/features/chatbot/api.js @@ -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 +}; diff --git a/src/features/chatbot/botType.js b/src/features/chatbot/botType.js deleted file mode 100644 index 3512fa9..0000000 --- a/src/features/chatbot/botType.js +++ /dev/null @@ -1,4 +0,0 @@ -export const botType = { - none: Symbol("none"), - wizard: Symbol("wizard") -}; diff --git a/src/features/chatbot/components/BotsManager.js b/src/features/chatbot/components/BotsManager.js index 64e9ef9..c9d1106 100644 --- a/src/features/chatbot/components/BotsManager.js +++ b/src/features/chatbot/components/BotsManager.js @@ -2,10 +2,15 @@ import React, { useEffect, useState } from "react"; import PropTypes from "prop-types"; import { connect } from "react-redux"; import { bindActionCreators } from "redux"; -import { botType } from "../botType"; +import { botType, bots, userKey } from "../constants"; import Wizard from "./Wizard"; import { makeStyles } from "@material-ui/core/styles"; -import { dismissBot } from "../actionCreators"; +import { + dismissBot, + loadBotSession, + closeChat, + saveMessage +} from "../actionCreators"; const useStyles = makeStyles(theme => ({ bot: { @@ -24,13 +29,24 @@ const BotsManager = ({ bot, actions }) => { const classes = useStyles(); 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]); + const dismissBot = () => { + actions.closeChat(); + actions.dismissBot(); + }; + return (
- {type === botType.wizard && } + {type === botType.wizard && ( + + )}
); @@ -49,7 +65,10 @@ function mapStateToProps(state) { function mapDispatchToProps(dispatch) { return { - actions: bindActionCreators({ dismissBot }, dispatch) + actions: bindActionCreators( + { dismissBot, loadBotSession, closeChat, saveMessage }, + dispatch + ) }; } diff --git a/src/features/chatbot/components/Wizard.js b/src/features/chatbot/components/Wizard.js index aa80c25..b528ce7 100644 --- a/src/features/chatbot/components/Wizard.js +++ b/src/features/chatbot/components/Wizard.js @@ -4,8 +4,9 @@ import ChatBot from "react-simple-chatbot"; import { ThemeProvider } from "styled-components"; import { useTheme } from "@material-ui/core/styles"; import { useTranslation } from "react-i18next"; +import { bots, messageSource } from "../constants"; -const Wizard = ({ dismissBot }) => { +const Wizard = ({ dismissBot, saveMessage }) => { const theme = useTheme(); const { t } = useTranslation(); @@ -21,50 +22,68 @@ const Wizard = ({ dismissBot }) => { 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 = [ { id: "1", - message: t("Chatbot.Wizard.Message1"), + message: getMessage(t("Chatbot.Wizard.Message1")), trigger: "2" }, { id: "2", - message: t("Chatbot.Wizard.Message2"), + message: getMessage(t("Chatbot.Wizard.Message2")), trigger: "3" }, { id: "3", - message: t("Chatbot.Wizard.Message3"), + message: getMessage(t("Chatbot.Wizard.Message3")), trigger: "4" }, { id: "4", user: true, + validator: validate, trigger: "5" }, { id: "5", - message: t("Chatbot.Wizard.Message5"), + message: getMessage(t("Chatbot.Wizard.Message5")), trigger: "6" }, { id: "6", user: true, + validator: validate, trigger: "7" }, { id: "7", - message: t("Chatbot.Wizard.Message7"), + message: getMessage(t("Chatbot.Wizard.Message7")), trigger: "8" }, { id: "8", user: true, + validator: validate, trigger: "9" }, { id: "9", - message: t("Chatbot.Wizard.Message9"), + message: getMessage(t("Chatbot.Wizard.Message9")), end: true } ]; @@ -88,14 +107,15 @@ const Wizard = ({ dismissBot }) => { handleEnd={handleEnd} steps={steps} botAvatar={getAvatar()} - headerTitle="Zirhan" + headerTitle={bots.Zirhan} /> ); }; Wizard.propTypes = { - dismissBot: PropTypes.func.isRequired + dismissBot: PropTypes.func.isRequired, + saveMessage: PropTypes.func.isRequired }; export default Wizard; diff --git a/src/features/chatbot/constants.js b/src/features/chatbot/constants.js new file mode 100644 index 0000000..81703eb --- /dev/null +++ b/src/features/chatbot/constants.js @@ -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 +}; diff --git a/src/features/chatbot/reducer.js b/src/features/chatbot/reducer.js index b8220c9..605895a 100644 --- a/src/features/chatbot/reducer.js +++ b/src/features/chatbot/reducer.js @@ -1,6 +1,6 @@ import * as types from "./actionTypes"; import initialState from "../../redux/reducers/initialState"; -import { botType } from "./botType"; +import { botType } from "./constants"; export default function chatbotReducer(state = initialState.bot, action) { switch (action.type) { @@ -10,6 +10,49 @@ export default function chatbotReducer(state = initialState.bot, action) { case types.DISMISS_BOT: 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: return state; } diff --git a/src/redux/reducers/initialState.js b/src/redux/reducers/initialState.js index 026f328..0bd3aa2 100644 --- a/src/redux/reducers/initialState.js +++ b/src/redux/reducers/initialState.js @@ -19,7 +19,10 @@ export default { type: null }, bot: { - type: null + type: null, + session: {}, + chat: { loading: false, loaded: false }, + storage: [] }, ajaxCallsInProgress: 0 }; diff --git a/webpack.config.dev.js b/webpack.config.dev.js index 7b03efd..c5e0b88 100644 --- a/webpack.config.dev.js +++ b/webpack.config.dev.js @@ -26,7 +26,8 @@ module.exports = { new webpack.DefinePlugin({ "process.env.REVERSE_PROXY_API_URL": JSON.stringify( "http://localhost:5050" - ) + ), + "process.env.CHATBOT_API_URL": JSON.stringify("http://localhost:5061") }), new HtmlWebpackPlugin({ template: "src/index.html", diff --git a/webpack.config.prod.js b/webpack.config.prod.js index 1c205a6..0275ec0 100644 --- a/webpack.config.prod.js +++ b/webpack.config.prod.js @@ -3,7 +3,7 @@ const path = require("path"); const HtmlWebpackPlugin = require("html-webpack-plugin"); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 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.PUBLIC_URL = "/reverse-proxy"; @@ -32,6 +32,9 @@ module.exports = { "process.env.PUBLIC_URL": JSON.stringify(process.env.PUBLIC_URL), "process.env.REVERSE_PROXY_API_URL": JSON.stringify( "https://toodle.ddns.net/reverse-proxy-api" + ), + "process.env.CHATBOT_API_URL": JSON.stringify( + "https://toodle.ddns.net/chatbot-api" ) }), new HtmlWebpackPlugin({ @@ -52,9 +55,7 @@ module.exports = { } }), new CopyPlugin({ - patterns: [ - { from: 'public', to: 'public' } - ], + patterns: [{ from: "public", to: "public" }] }) ], module: {