Integrating Redux in SkillCreator and BotBuilder

SkillCreator in SUSI.AI used for creating new skills and Botbuilder for creating new bots had parent-child communication as seen in React components. Many components were using a state, which was lifted up to its parent and passed to its children (Design, Configure, Deploy, SkillCreator) as props. This architecture has many problems, maintainability and readability being the prime one.

For maintaining the state of SkillCreator and Botbuilder we used Redux, which lets us use global state and we don’t have to worry about passing props just for the purpose of sending data deep down. With Redux, the code size reduced and we successfully eliminated a lot of code redundancy. 

Basic Data Flow in Redux

  1. A UI Event like onClick happens
  2. The UI event dispatches an action, mapDispatchToAction gives access to actions defined in action files.
  3. Perform synchronous or asynchronous tasks like fetching data from external API.
  4. The payload is used by reducer function, which updates the global store based on payload and previous state.
  5. The global store changes and using the mapStateToProps present, the function is subscribed to changes over time and gets the updated values of the store.

Code Integration 

The state object consists of 3 nested objects. Each view, .i.e on botWizard, for Build tab we update the skill object. For the Design tab, design object and etc. The objective of storing in Redux store is, we require the data to persist when a user navigates/change to other tabs for example Design Tab, the data of skills tab is mapped from the store so if user switches back to skills tab, no data loss will be there. All such required fields are stored in the store.

Default States: 

const defaultState = {
 skill: {
   name: '',
   file: null,
   category: null,
   language: '',
   image: avatarsIcon,
   imageUrl: '<image_name>',
   code:
     '::name <Bot_name>\n::category <Category>\n::language <Language>\n::author <author_name>\n::author_url <author_url>\n::description <description> \n::dynamic_content <Yes/No>\n::developer_privacy_policy <link>\n::image images/<image_name>\n::terms_of_use <link>\n\n\nUser query1|query2|quer3....\n!example:<The question that should be shown in public skill displays>\n!expect:<The answer expected for the above example>\nAnswer for the user query',
   author: '',
 },
 design: {
   botbuilderBackgroundBody: '#ffffff',
   botbuilderBodyBackgroundImg: '',
   botbuilderUserMessageBackground: '#0077e5',
   botbuilderUserMessageTextColor: '#ffffff',
   botbuilderBotMessageBackground: '#f8f8f8',
   botbuilderBotMessageTextColor: '#455a64',
   botbuilderIconColor: '#000000',
   botbuilderIconImg: botIcon,
   code:
     '::bodyBackground #ffffff\n::bodyBackgroundImage \n::userMessageBoxBackground #0077e5\n::userMessageTextColor #ffffff\n::botMessageBoxBackground #f8f8f8\n::botMessageTextColor #455a64\n::botIconColor #000000\n::botIconImage ',
 },
 configCode:
   "::allow_bot_only_on_own_sites no\n!Write all the domains below separated by commas on which you want to enable your chatbot\n::allowed_sites \n!Choose if you want to enable the default susi skills or not\n::enable_default_skills yes\n!Choose if you want to enable chatbot in your devices or not\n::enable_bot_in_my_devices no\n!Choose if you want to enable chatbot in other user's devices or not\n::enable_bot_for_other_users no",
 view: 'code',
 loading: true,

reducers/create.js

The CREATE_SET_VIEW reducer function updates the view, e.g it can be ‘code’, ‘ui’, ‘tree’. The action setView is dispatched to update the view tab. The action setSkillData is dispatched when we want to update the skill object in our global store.Reducer function for it:

[actionTypes.CREATE_SET_SKILL_DATA](state, { payload }) {
  return {
    ...state,
    skill: {
      ...state.skill,
      ...payload,
    },
  };
},

reducers/create.js

The setSkillData is called whenever the user updates the code of skills.

Updating Design Code

The setDesignData is dispatched whenever we update the code in AceEditor of design tab. generateDesignData function helps in generating design object from the designCode to update the UI view of design tab.

   [actionTypes.CREATE_SET_DESIGN_DATA](state, { payload }) {
     const { code } = payload;
     return {
       ...state,
       design: {
         code,
         ...generateDesignData(code),
     },
   };
 },

reducers/create.js

updateDesignData action is called for changing the design object in global state. In UIView,

handleRemoveUrlBody = () => {
   let {
     actions,
     design: { code },
   } = this.props;
   code = code.replace(
     /^::bodyBackgroundImage\s(.*)$/m,
     '::bodyBackgroundImage ',
   );
   actions.updateDesignData({
     code,
     botbuilderBodyBackgroundImg: '',
     botbuilderBodyBackgroundImgName: '',
   });
 };

reducers/create.js

In reducer, the design state is updated by extracting payload using the spread operator and adding new key-value pairs to design object.

[actionTypes.CREATE_UPDATE_DESIGN_DATA](state, { payload }) {
     return {
       ...state,
       design: {
         ...state.design,
         ...payload,
       },
     };
   },

reducers/create.js

For changing the color, handleChangeColor function in present in UIView to handle color change, which dispatches action setDesignComponentColor

The CREATE_SET_DESIGN_COMPONENT_COLOR updates the design code accordingly to match the UI changes done in the UI tab of the design view.

[actionTypes.CREATE_SET_DESIGN_COMPONENT_COLOR](state, { payload }) {
     let code = '';
     const { component, color } = payload;
     if (component === 'botbuilderBackgroundBody') {
       code = state.design.code.replace(
         /^::bodyBackground\s(.*)$/m,
         `::bodyBackground ${color}`,
       );
     } else if (component === 'botbuilderUserMessageBackground') {
       code = state.design.code.replace(
         /^::userMessageBoxBackground\s(.*)$/m,
         `::userMessageBoxBackground ${color}`,
       );
     } else if (component === 'botbuilderUserMessageTextColor') {
       code = state.design.code.replace(
         /^::userMessageTextColor\s(.*)$/m,
         `::userMessageTextColor ${color}`,
       );
     } else if (component === 'botbuilderBotMessageBackground') {
       code = state.design.code.replace(
         /^::botMessageBoxBackground\s(.*)$/m,
         `::botMessageBoxBackground ${color}`,
       );
     } else if (component === 'botbuilderBotMessageTextColor') {
       code = state.design.code.replace(
         /^::botMessageTextColor\s(.*)$/m,
         `::botMessageTextColor ${color}`,
       );
     } else if (component === 'botbuilderIconColor') {
       code = state.design.code.replace(
         /^::botIconColor\s(.*)$/m,
         `::botIconColor ${color}`,
       );
     }
     return {
       ...state,
       design: {
         ...state.design,
         code,
         [component]: color,
       },
     };
   },

reducers/create.js

The reducer generates the new designCode and also sets the component color for it to be used in UI View. Similar logic is applied for handling the configured state in the reducer.

To conclude, shifting to redux architecture instead of prop drilling proved to be of great advantage. The code became more performant, modular and easy to manage. The state persisted even after component unmounted.

Resources

Tags

SUSI.AI, FOSSASIA, GSoC19, Redux, Skills CMS, SUSI Chat


Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.