Creating Common Loading Component in SUSI.AI

A circular loading component appears whenever an asynchronous event takes time on the front-end, to show the user that content is yet to be fetched and processed.

Creating Common Loading component eased the process of handling circular loading alignment. Using common loader decreased code repetition. There were major cases for loader component, mobile view, and desktop view.

Why Styled-components for SUSI.AI?

  • Painless maintenance
  • On-demand CSS injection: The components being rendered in the page are kept on track and only those component’s styles are injected into the DOM. If we combine react-loadable or some other code splitting, the app becomes really performant.
  • Styled components generate their own class name like sc-g3h4h6 and add it as an attribute to DOM, which avoids class name overlaps and misalignments.
  • Styled components are easier to detect as they can be detected by linter and husky. Whereas, static CSS files are not detected, which leads to dead code in the codebase and make them hard to manage. 
  • Simple dynamic styling: adapting the styling of a component based on its props, without having to manually manage dozens of classes.
  • Automatic vendor prefixing
  • Reuse, Reduce code written for CSS
  • Supports media queries, which makes creating a responsive PWA easier

 Let’s have a look at it’s implemented.

Code Integration

const Container = styled.div`
 display: flex;
 align-items: center;
 justify-content: center;
 ${props =>
   props.height
     ? css`
         height: ${props => props.height + ‘rem’};
         @media (max-width: 512px) {
           height: ${props =>
             props.height > 20
               ? props.height – 15 + ‘rem’
               : props.height + ‘rem’};
         }
       `
     : css`
         margin: 19rem 0;
         height: 100%;
         @media (max-width: 512px) {
           margin: 14rem 0;
         }
       `}
`;

const CircularLoader = ({ height = ‘auto’, color = ‘primary’, size = 64 }) => {
 return (
   <Container height={height}>
     <CircularProgress color={color} size={size} />
   </Container>
 );
};

components/shared/CircularLoader.js

The CircularLoader component by default should acquire size 64 width and size, and color primary i.e. SUSI.AI blue color.

The container created using styled component, by default should be horizontally and vertically centered. This is used to handle the case where the content takes the whole width and height(100%) of page. 

If we want the container to be of specific height(lesser than the page in which the Loader is being rendered), we pass in height props.

For handling the mobile views, in case of height, 100%, margin-top and margin-bottom are reduced by 5rem.

In case of height passed in through component, check if the height is greater(than 20) so that the mobile view user can see the footer as well, reduce height by 15rem. If the height is lesser than 20rem, the footer is in the viewport.

Before PR, let’s have a look at how the Loader was rendered inside component:

{loading ? (
  <LoadingContainer>
    <CircularProgress size={64} />
  </LoadingContainer>
 ) : (
   …
 )}

Using Common shared loader,

{loading ? <CircularLoader height={27} /> : …

Settings/Settings.react.js

Adding styled component across the SUSI.AI web app proved to be of great help, the mobile views were easily made using media queries. Components positioning logic could be changed based on props, and most importantly, styled-components could be reused and were much easier to manage and maintain than static CSS classes or inline styles.

However, for other cases like when single or less number of styles were applied, maintaining everything using styled component proved to be additional overhead. A combination of inline styles and styled-components would be apt depending on the nature of style. 

Resources

Tags

SUSI.AI, FOSSASIA, GSoC19, styled-components, SUSI.AI Chat

Continue Reading

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


Continue Reading

Adding Google Analytics To SUSI.AI

Google analytics provides SUSI.AI Admins with a way to analyze traffic and get advanced metrics. Google Analytics first collects data, computes the data, and showcases it on console dashboard. It is used for keeping track of user behavior on the website.

How Google Analytics Work

Below shown are fields used by Google Analytics to get user data. A cookie is stored into user browser. _ga stays in the browser for 2 years, _gid for 2 days.

Whenever a user performs an event like a mouse click, page change, open popup, add query strings to URL, information is sent to Google Analytics using an API call describing the user event. Below is the photo describing it:

The above-bordered boxes consist of information sent by google analytics. The information consists of:

  • The user identification code
  • The device resolution of screen used by a user
  • User language
  • The URL of the page user is on

How it processes data

When a user with tracking code lands on SUSI.AI, Google Analytics creates a unique random identity and attaches it to the user cookie.

Each new user is given a unique ID. Whenever a new ID is detected, analytics considers it as a new user. But when an existing ID is detected, it’s considered as a returning user and set over with the hit.

A unique ID code is fetched from every new unique user. Whenever a new user ID is detected in the call, Google Analytics treats the unique ID as a new user. If the ID matches from earlier ID, the user is a returning user and calculates the metrics another way. Each new user gets a unique ID.

However, a new user is detected if the same user clears out the browser cookie or uses another device over the same IP address to view the webpage.

When an existing ID is detected, it’s considered as a returning user and set over with the hit.

Code Integration

Google Analytics must be initialized using initialize(gaTrackingID) function before any of the other tracking functions will record any data. Using react-ga ga(‘create’, …), the values are sent to google analytics

import withTracker from ‘./withTracker’;
import GoogleAnalytics from ‘react-ga’;
..

actions.getApiKeys().then(({ payload }) => {
  const {
    keys: { googleAnalyticsKey = null },
   } = payload;
   googleAnalyticsKey && GoogleAnalytics.initialize(googleAnalyticsKey);
});
..
<Route exact path=”/” component={withTracker(BrowseSkill)} />
<Route exact path=”/chat” component={withTracker(ChatApp)} />

src/App.js

Higher Order Component for Tracking page activity

A Higher Order Component is a function that returns an enhanced component by adding some more properties or logic and allows reusing component logic.

Using HOC for tracking page helped by not exposing internal component with tracking. The withTracker HOC wraps all the component, which SUSI.AI wants to track and exposes a trackerPage method. 

  • Whenever a component mounts, we track the new pages using componentDidMount. 
  • Whenever the component updates and location of browser url changes, we track the sub pages using componentDidUpdate lifecycle hook.
import React, { Component } from ‘react’;
import GoogleAnalytics from ‘react-ga’;
import PropTypes from ‘prop-types’;

const withTracker = (WrappedComponent, options = { }) => {
  const trackPage = page => {
    GoogleAnalytics.set({
      page,
      …options,
    });
    GoogleAnalytics.pageview(page);
  };
  const HOC = class extends Component {
    componentDidMount() {
      window.scrollTo(0, 0);
      const page = this.props.location.pathname + this.props.location.search;
      trackPage(page);
    }

    componentDidUpdate(prevProps) {
      const currentPage =
        prevProps.location.pathname + prevProps.location.search;
      const nextPage =
        this.props.location.pathname + this.props.location.search;

      if (currentPage !== nextPage) {
        trackPage(nextPage);
      }
      if (this.props.location.pathname !== prevProps.location.pathname) {
        window.scrollTo(0, 0);
      }
    }

    render() {
      return <WrappedComponent {…this.props} />;
    }
  };

  HOC.propTypes = {
    location: PropTypes.object,
  };

  return HOC;
};

export default withTracker;

src/withTracker.js

The Higher-Order Component pattern turned out to be really useful to achieve D.R.Y (Don’t Repeat Yourself) and keeping component separate from tracking. With React Analytics being added, we can track various metrics, live users on site and see how SUSI.AI traffic is performing over time. 

Resources

Tags

SUSI.AI, FOSSASIA, GSoC19, Google Analytics, Higher Order Components

Continue Reading

Implementing Slideshow Servlet in SUSI.AI Skills

Slideshow shown on SUSI.AI homepage helps SUSI.AI client showcase interesting and new features integrated into the platform. It helps to display the capabilities of SUSI.AI and other interesting areas. The slideshow can be configured from the Admin panel of SUSI.AI. 

For storing slideshow data, images, information, redirect to link on slideshow click, we need to implement a servlet to store data on server-side.

The endpoint is of GET type, and accepts:

  • redirect_link(compulsory): redirect link if a user clicks on the slider image
  • image_name(compulsory): The image relative folder path on the server
  • info: Any relevant information about the slider
  • deleteSlide: True, if the user wants to delete slider

Code Integration

For implementing slideshow service, we need to store the image on the backend using uploadImage service and using the uploaded image file path in the backend to store the full slider details using skillSlideshowService service. 

SkillSlideshowService:

For setting the slideshow, the minimum permission required is ADMIN

@Override
   public UserRole getMinimalUserRole() {
       return UserRole.ADMIN;
   }

   @Override
   public JSONObject getDefaultPermissions(UserRole   baseUserRole) {
       return null;
   }

   @Override
   public String getAPIPath() {
       return “/cms/skillSlideshow.json”;
   }

cms/SkillSlideshowService.java

Let’s have a look at how it is implemented, the redirect_link and image_name are necessary parameters and if not passed throws exception. If appropriate parameters are present, get the user query data using query.call. Access the data on the server side through DAO.skillSlideshow, if slideshow key is present in JsonTray skillSlideshow, get JSONObject with key “slideshow”.

If deleteKey is false, create a new JSONObject and put the query call data inside it and add to skillSlideshow object with redirectUrl as the key.

If deleteKey is true, remove the object associated with redirect_link and create a new object and add.

public ServiceResponse serviceImpl(Query call, HttpServletResponse response, Authorization authorization,
           final JsonObjectWithDefault permissions) throws APIException {
       if (call.get(“redirect_link”, null) == null || call.get(“image_name”, null) == null) {
           throw new APIException(400, “Bad Request. No enough parameter present”);
       }

       String redirectLink = call.get(“redirect_link”, null);
       String imageName = call.get(“image_name”, null);
       String info = call.get(“info”, null);
       boolean deleteSlide = call.get(“deleteSlide”, false);
       JsonTray skillSlideshow = DAO.skillSlideshow;
       JSONObject result = new JSONObject();
       JSONObject skillSlideshowObj = new JSONObject();
       if (skillSlideshow.has(“slideshow”)) {
           skillSlideshowObj = skillSlideshow.getJSONObject(“slideshow”);
       }
       if (!deleteSlide) {
           try {
               JSONObject slideObj = new JSONObject();
               slideObj.put(“image_name”, imageName);
               slideObj.put(“info”, info);
               skillSlideshowObj.put(redirectLink, slideObj);
               skillSlideshow.put(“slideshow”, skillSlideshowObj, true);
               result.put(“accepted”, true);
               result.put(“message”, “Added new Slide ” + call.get(“redirect_link”) + ” successfully !”);
               return new ServiceResponse(result);
           } catch (Exception e) {
               throw new APIException(500,
                       “Failed : Unable to add slide with path ” + call.get(“redirect_link”) + ” !”);
           }
       } else {
           try {
               skillSlideshowObj.remove(redirectLink);
               skillSlideshow.put(“slideshow”, skillSlideshowObj, true);
               result.put(“accepted”, true);
               result.put(“message”, “Removed Slide with path ” + call.get(“redirect_link”) + ” successfully !”);
               return new ServiceResponse(result);
           } catch (Exception e) {
               throw new APIException(501,
                       “Failed to remove Slide: ” + call.get(“redirect_link”) + ” doesn’t exists!”);
           }
       }
   }

cms/SkillSlideshowService.java

GetSkillSlideshow 

For fetching the slideshow data on frontend, GetSkillSlideshow servlet is implemented. The minimum userRole required in ANONYMOUS.

 @Override
   public String getAPIPath() {
       return “/cms/getSkillSlideshow.json”;
   }

   @Override
   public UserRole getMinimalUserRole() {
       return UserRole.ANONYMOUS;
   }

   @Override
   public JSONObject getDefaultPermissions(UserRole baseUserRole) {
       return null;
   }

cms/GetSkillSlideshow.java

For fetching the slider data, access DAO.skillSlideshow, and get the JSONObject associated with key slideshow and put it in result response, put accepted key as true and return the response

public ServiceResponse serviceImpl(Query call, HttpServletResponse response, Authorization rights, final JsonObjectWithDefault permissions) throws APIException {
       JsonTray skillSlideshow = DAO.skillSlideshow;
       JSONObject skillSlideshowObj = skillSlideshow.getJSONObject(“slideshow”);
       JSONObject result = new JSONObject();
       try {
           result.put(“accepted”, true);
           result.put(“slideshow”, skillSlideshowObj);
           result.put(“message”, “Success : Fetched all Skills Slides!”);
           return new ServiceResponse(result);
       } catch (Exception e) {
           throw new APIException(500, “Failed : Unable to fetch Skills Slides!”);
       }
   }

cms/GetSkillSlideshow.java

3 types of endpoints are required for achieving the slider slideshow functionality. First, when a user creates or edits a slider, the user first needs to upload the image on the server using uploadImage.json service.

Image Suffix is the suffix of the file name stored in SUSI.AI server, susi_icon is the suffix in the image shown below.

Once the image is uploaded on the server, the API returns the relative path to the server location. The path on the server gets filled in ImagePath field on client-side(disabled to users).

With GetSkillSlideshow and SkillSlideshowService implemented, the admins can now manage and control the slideshow shown on the SUSI.AI home page, directly from the admin panel. The client can now easily discover new, exciting features as well.

Resources

Tags

SUSI.AI, FOSSASIA, GSoC`19, SUSI.AI Server

Continue Reading

Adding GetReportedSkill API on SUSI.AI Server

The GetReportedSkill API was implemented for Admins to view the reported feedback given by users, admin can then monitor skills, which are working fine and in which users are having problems. This can help in deleting buggy/erroneous skills directly from the reported skills tab in the admin panel.

The endpoint is of GET type, and accept 2 parameters: 

  • access_token(compulsory): It is the access token of the logged-in user. It is of a string data type.
  • search: It is a string param that helps us to fetch a list of feedback related to the search term

The minimal role is set to OPERATOR as Admin section access is required for reported skill list.

API Development

Here is a sample response from api:

{
  “session”: {“identity”: {
    “type”: “host”,
    “name”: “0:0:0:0:0:0:0:1_d9aaded8”,
    “anonymous”: true
  }},
  “accepted”: true,
  “list”: [
    {
      “feedback”: “test”,
      “skill_name”: “GSOC”,
      “email”: “[email protected]”,
      “timestamp”: “2019-06-15 03:25:29.425”
    },
    {
      “feedback”: “test101”,
      “skill_name”: “GSOC”,
      “email”: “[email protected]”,
      “timestamp”: “2019-06-15 12:18:33.641”
    }
  ],
  “message”: “Success: Fetched all Reported Skills”
}

The reported skills are stored in DAO under reportedSkills, for fetching the list we need to traverse it’s JSONObject.

JsonTray reportedSkills = DAO.reportedSkills;
JSONObject reportedSkillsListObj = reportedSkills.toJSON();

api/cms/GetReportSkillService.java

Code

For creating a list we need to access each property of JSONObject of reportedSkill, in the following order:

Model → Group → Language → Skill Name → Reported feedback list

for (String key:JSONObject.getNames(reportedSkillsListObj)) {
  modelName = reportedSkillsListObj.getJSONObject(key);
      if (reportedSkillsListObj.has(key)) {
        for (String group_name : JSONObject.getNames(modelName)) {
          groupName = modelName.getJSONObject(group_name);
          if (modelName.has(group_name)) {
            for (String language_name : JSONObject.getNames(groupName)) {
              languageName = groupName.getJSONObject(language_name);
              if (groupName.has(language_name)) {

api/cms/GetReportSkillService.java

If search parameter is passed, check if skillName matches with search parameter, if both strings are equal, create a new reportedObject and append it to reportList, which is a list of reported skills

if (call.get(“search”, null) != null) {
  String skill_name = call.get(“search”, null);
    if (languageName.has(skill_name)) {
      skillName = languageName.getJSONObject(skill_name);
      reports = skillName.getJSONArray(“reports”);
      for (int i = 0; i < reports.length(); i++) {
        JSONObject reportObject = new JSONObject();
        reportObject = reports.getJSONObject(i);
        reportObject.put(“skill_name”, skill_name);
        reportList.add(reportObject);

api/cms/GetReportSkillService.java

If search parameter is not passed, traversed all reported skills and append it to array(reportList).

getNames returns an array of keys as string stored in JSONObject, we traverse the array and put all the reported skill name, feedback, email and timestamp in reportObject and add it to reportList

} else {
   for (String skill_name : JSONObject.getNames(languageName)) {
      skillName = languageName.getJSONObject(skill_name);
      if (languageName.has(skill_name)) {
        reports = skillName.getJSONArray(“reports”);
           for (int i = 0; i < reports.length(); i++) {
             JSONObject reportObject = new JSONObject();
   reportObject = reports.getJSONObject(i);
   reportObject.put(“skill_name”, skill_name);    reportList.add(reportObject);
                 }
            }
       }
  }

api/cms/GetReportSkillService.java

Once we have the list of reported skills reportList return the service response

try {
   result.put(“list”, reportList);
   result.put(“accepted”, true);
   result.put(“message”, “Success: Fetched all Reported  Skills”);
   return new ServiceResponse(result);
  } catch (Exception e) {
       throw new APIException(500, “Failed to fetch the requested list!”);}

api/cms/GetReportSkillService.java

To conclude, the Admin’s now can take decisions based on reports submitted by the user to delete a skill or ignore the feedback.

Link to PR: https://github.com/fossasia/susi_server/pull/1274

Resources

Tags

SUSI.AI, FOSSASIA, GSoC`19, API Development, SUSI Server, SUSI Skills

Continue Reading
Close Menu