Integrating Redux with SUSI.AI Web Clients

In this blog post, we are going to go through the implementation of the Redux integration on the SUSI.AI web clients. The existing SUSI.AI WebChat codebase has Flux integrated into it, but integrating Redux would make it a lot easier to manage the app state in a single store. And would result in a more maintainable and performant application. Let us go through the implementation in the blog –

The key steps involved the following –

  • Restructuring the directory structure of the repository to enable better maintenance.
  • Creating a Redux store and configuring the middlewares.
  • Standardizing the format for writing actions and make API calls on dispatching an action.
  • Standardizing the format for writing reducers.
  • Hook the components to the Redux store.

Restructuring the directory structure

DIrectory structure for https://chat.susi.ai
  • All the redux related files and utils are put into the redux directory, to avoid any sort of confusion, better maintenance and enhanced discoverability. The prime reason for it also because the integration was done side-by-side the existing Flux implementation.
  • The actions and reducers directory each has a index.js, which exports all the actions and reducers respectively, so as to maintain a single import path for the components and this also helped to easily split out different types of actions/reducers.

Creating Redux store and configure middlewares

import { createStore as _createStore, applyMiddleware } from 'redux';
import { routerMiddleware } from 'react-router-redux';
import reduxPromise from 'redux-promise';
import reducers from './reducers';

export default function createStore(history) {
 // Sync dispatched route actions to the history
 const reduxRouterMiddleware = routerMiddleware(history);
 const middleware = [reduxRouterMiddleware, reduxPromise];

 let finalCreateStore;
 finalCreateStore = applyMiddleware(...middleware)(_createStore);

 const store = finalCreateStore(
   reducers,
   {},
   window.__REDUX_DEVTOOLS_EXTENSION__ &&
     window.__REDUX_DEVTOOLS_EXTENSION__(),
 );

 return store;
}
  • The function createStore takes in the browserHistory (provided by React Router) and returns a single store object that is passed on to the entry point component of the App.
  • The store is passed to the application via the <Provider> component, provided by the react-redux. It is wrapped to the <App> component as follows –

ReactDOM.render(
 <Provider store={store} key="provider">
   <App />
 </Provider>,
 document.getElementById('root'),
);
  • The 2 middlewares used are routerMiddleware provided by the react-router-redux and the reduxPromise provided by redux-promise.
  • The routerMiddleware enhances a history instance to allow it to synchronize any changes it receives into application state.
  • The reduxPromise returns a promise to the caller so that it can wait for the operation to finish before continuing. This is useful to assist the application to tackle async behaviour.

Standardizing the actions and making API calls on action dispatch

  • The actions file contains the following –

import { createAction } from 'redux-actions';
import actionTypes from '../actionTypes';
import * as apis from '../../apis';

const returnArgumentsFn = function(payload) {
 return Promise.resolve(payload);
};

export default {
// API call on action dispatch
 getApiKeys: createAction(actionTypes.APP_GET_API_KEYS, apis.fetchApiKeys),
// Returns a promise for actions not requiring API calls
 logout: createAction(actionTypes.APP_LOGOUT, returnArgumentsFn),
};

  • As new actions are added, it can be added to the actionTypes file and can be added in the export statement of the above snippet. This enables very standard and easy to manage actions.
  • This approach allows to handle both types of action dispatch – with and without API call. In case of API call on dispatch, the action resolves with the payload of the API.
  • The APIs are called via the AJAX helper (Check out this blog – https://blog.fossasia.org/make-a-helper-for-ajax-requests-using-axios/ ).

Standardizing the reducers and combining them

  • The reducers file contains the following –

import { handleActions } from 'redux-actions';
import actionTypes from '../actionTypes';

const defaultState = {
 ...
 apiKeys: {},
 ...
};

export default handleActions({
  [actionTypes.APP_GET_API_KEYS](state, { payload }) {
     const { keys } = payload;
     return {
       ...state,
       apiKeys: { ...keys },
     };
  },
  ...
},defaultState);
  • The default application state is defined and the reducer corresponding to each action type returns an immutable object which is the new application state,
  • In the above example, the payload from the getApiKeys API call is received in the reducer, which then updated the store.
  • The reducers/index.js  combines all the reducers using combineReducers provided by redux and exports a single object of reducers.

import { combineReducers } from 'redux';
import { routerReducer } from 'react-router-redux';
import app from './app';
import settings from './settings';
import messages from './messages';

export default combineReducers({
 routing: routerReducer,
 app,
 settings,
 messages,
});

Hook the components to the Redux store

  • After Redux integration and standardization of the reducers, actions, any of the component can be hooked to the store as follows –

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';

class MyComponent extends Component {
 componentDidMount() {
    // Dispatch an action
    this.props.actions.getApiKeys();
 }
 render() {
   const { apiKeys } = this.props;
   return (
     <div>
        /* JSX */
     </div>     
   );
 }
}

function mapStateToProps(store) {
 const { apiKeys } = store.app;
 return {
   apiKeys
 };
}

function mapDispatchToProps(dispatch) {
 return {
   actions: bindActionCreators(actions, dispatch),
 };
}

export default connect(mapStateToProps, mapDispatchToProps)(MyComponent);
  • The mapStateToProps is a function that is used to provide the store data to the component via props, whereas mapDispatchToProps is used to provide the action creators as props to the component.

The above was the implementation of Redux in the SUSI.AI Web Clients. I hope the blog provided a detailed insight of how Redux was integrated and the standards that were followed.

References

Continue ReadingIntegrating Redux with SUSI.AI Web Clients

Make a helper for AJAX requests using axios

In this blog post, we are going to go through the implementation of the helper function that is created for making AJAX requests using axios. Currently, the AJAX calls are made in a very random manner where the calls are written in the component itself and involves rewriting of headers, etc for the API call. Let us go through the implementation in the blog, which will standardise the way to make API calls in SUSI.AI web clients.

The primary changes are –

  • Making a common helper for AJAX requests with the help of axios.
  • Making a common file containing all the API calls across the project.

Going through the implementation

The API calls within the repository were not being made in an organised way, also a lot of redundant code was present. The aim of creating the helper is that, all the API calls is called via this common function. It takes care of the headers and also sending access_token with the API if the user is already logged in for API calls requiring authentication. The function for a API request now looks this simple –

// API call for signup
export function getSignup(payload) {
 const { email, password } = payload;
 const url = `${API_URL}/${AUTH_API_PREFIX}/signup.json`;
 return ajax.get(url, { signup: email, password });
}
  • In the above snippet, the ajax is the common helper used for making API calls. ajax is an object of functions that returns a promise for  various methods of API requests
  • We have primarily taken into consideration GET & POST requests type.
  • The helper function is as follows –
/* Insert imports here*/
const cookies = new Cookies();
const obj = {};

['get', 'post', 'all'].forEach(function(method) {
 obj[method] = function(url, payload, settings = {}) {
   /* Request will be aborted after 30 seconds */
   settings = {
     timeout: 30000,
     dataType: 'json',
     crossDomain: true,
     ...settings,
   };
   
   // Check if logged in
   if (cookies.get('loggedIn')) {
     payload = {
       access_token: cookies.get('loggedIn'),
       ...payload,
     };
   }

   return new Promise(function(resolve, reject) {
     let methodArgs = [];
     if (method === 'post') {
       if (payload && payload instanceof FormData !== true) {
           // Convert to Form Data
           payload = toFormData(payload);
       }

       settings.headers = {
         'Content-Type': 'application/x-www-form-urlencoded',
         ...settings.headers,
       };
     } else if (method === 'get') {
       if (payload) {
         // Add params to the URL   
         url += `?${Object.keys(payload)
           .map(key => key + '=' + payload[key])
           .join('&')}`;
       }
     }

     const methodsToAxiosMethodsMap = {
       get: 'get',
       post: 'post',
       all: 'all',
     };

     if (method === 'all') {
       methodArgs = [url];
     } else if (method === 'get') {
       methodArgs = [url, settings];
     } else {
       methodArgs = [url, payload, settings];
     }

     axios[methodsToAxiosMethodsMap[method]].apply({}, methodArgs).then(
       function(data = {}, ...restSuccessArgs) {
         const statusCode = _.get(data, 'status');
         /*  Send only api response */
         let responseData = { statusCode, ..._.get(data, 'data') };

         if (method === 'all') {
           responseData = data;
           responseData.statusCode = statusCode;
         }

         if (payload) {
           responseData.requestPayload = payload;
         }
         // Mark the promise resolved and return the payload
         resolve(camelizeKeys(responseData), ...restSuccessArgs);
       },
       function(data = {}, ...restErrorArgs) {
         // If request is canceled by user
         if (axios.isCancel(data)) {
           reject(data);
         }

         const statusCode = _.get(data, 'response.status', -1);
         let responseData = { statusCode, ..._.get(data, 'response.data') };

         if (method === 'all') {
           responseData = data;
           responseData.statusCode = statusCode;
         }

         if (payload) {
           responseData.requestPayload = payload;
         }
         // Mark the promise rejected and return the payload
         reject(camelizeKeys(responseData), ...restErrorArgs);
       },
     );
   });
 };
});

export default obj;
  • The above objects contains 3 functions –
    • ajax.get(url, payload, settings) – GET – The helper adds the query params to the URL by iterating through them and appending it to the URL and joining them with &.
    • ajax.post(url, payload, settings) –  POST – The helper checks, if the POST requests contains a payload which is an instance of form data, it converts to toFormData payload.
    • ajax.all(url, payload, settings) – ALL – It helps to deal with concurrent requests.
  • The url is the complete API endpoint, payload consists of the data/request payload. The settings consists of any headers related info, that needs to be added exclusively to the axios config.
  • There is also no need to pass access token to each API request as a payload. The helper check whether the user is logged-in and adds the access_token to the request payload. The snippet below demonstrates it –
if (cookies.get('loggedIn')) {
     payload = {
       access_token: cookies.get('loggedIn'),
       ...payload,
     };
   }
  • The access token, if present in the cookies is added to the payload, therefore authenticating the API call.
  • The keys of the response are changed to camel case before being sent to the caller function. It is done to maintain variable nomenclature standards across the app and also to follow javascript guidelines.
  • The file containing the API calls is structured as follows –
import ajax from '../helpers/ajax';
import urls from '../utils/urls';

const { API_URL } = urls;
const AUTH_API_PREFIX = 'aaa';
const CHAT_API_PREFIX = 'susi';
const CMS_API_PREFIX = 'cms';

// API without request payload
export function fetchApiKeys() {
 const url = `${API_URL}/${AUTH_API_PREFIX}/getApiKeys.json`;
 return ajax.get(url);
}

// API with request payload
export function getLogin(payload) {
 const { email, password } = payload;
 const url = `${API_URL}/${AUTH_API_PREFIX}/login.json`;
 return ajax.get(url, { login: email, password, type: 'access-token' });
}

The above was the implementation of the helper function that is created for making AJAX requests using axios. I hope the blog provided a detailed insight of it helps in making the process of making API calls more standardised and easier.

References

Continue ReadingMake a helper for AJAX requests using axios

Upload Avatar for a User in SUSI.AI Server

In this blog post, we are going to discuss on how the feature to upload the avatar for a user was implemented on the SUSI.AI Server. The API endpoint by which a user can upload his/her avatar image is https://api.susi.ai/aaa/uploadAvatar.json.

  • The endpoint is of POST type.
  • It accepts two request parameters –
  • image – It contains the entire image file sent from the client
  • access_token – It is the access_token for the user

The minimalUserRole is set to USER for this API, as only logged-in users can use this API.

Going through the API development

  • The image and access_token parameters are first extracted via the req object, that is passed to the main function. The  parameters are then stored in variables.
  • There is a check if the access_token and image exists. It it doesn’t, an error is thrown.
  • This code snippet discusses the above two points –

protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    Part imagePart = req.getPart("image");
    if (req.getParameter("access_token") != null) {
        if (imagePart == null) {
            result.put("accepted", false);
            result.put("message", "Image file not received");
        } else {.
        }
    else{
        result.put("message","Access token are not given");
        result.put("accepted",false);
        resp.setContentType("application/json");
        resp.setCharacterEncoding("UTF-8");
        resp.getWriter().write(result.toString());
    }
}

 

  • Then the input stream is extracted from the imagePart and stored. And post that the identity is checked if it is valid.
  • The input stream is converted into the Image type using the ImageIO.read method.
  • The image is eventually converted into a BufferedImage using a function, described below.

public static BufferedImage toBufferedImage(Image img)
{
    if (img instanceof BufferedImage)
        return (BufferedImage) img;

    // Create a buffered image with transparency
    BufferedImage bimage = new BufferedImage(img.getWidth(null),     
    img.getHeight(null), BufferedImage.TYPE_INT_ARGB);
    // Draw the image on to the buffered image
    Graphics2D bGr = bimage.createGraphics();
    bGr.drawImage(img, 0, 0, null);
    bGr.dispose();
    // Return the buffered image
    return bimage;
}

 

  • After that, the file path and name is set. The avatar for each user is stored in the /data/avatar_uploads/<uuid of the user>.jpg.
  • The avatar is written to the path using the ImageIO.write function. Once, the file is stored on the server, the success response is sent and the client side receives it.

Resources

Continue ReadingUpload Avatar for a User in SUSI.AI Server

Displaying Avatar Image of Users using Gravatar on SUSI.AI

This blog discusses how the avatar of the user has been shown at different places in the UI like the app bar, feedback comments, etc using the Gravatar service on SUSI.AI. A Gravatar is a Globally Recognized Avatar. Your Gravatar is an image that follows you from site to site appearing beside your name when you do things like comment or post on a blog. So, the Gravatar service has been integrated in SUSI.AI, so that it helps identify the user via the avatar too.

Going through the implementation

  • The aim is to get an avatar of the user from the email id. For that purpose, Gravatar exposes a publicly available avatar of the user, which can be accessed via the following steps :
    • Creating the Hash of the email
    • Sending the image request
  • For creating the MD5 hash of the email, use the npm library md5. The function takes a string as input and returns the hash of the string.
  • Now, a URL is generated using this hash.
  • The URL format is https://www.gravatar.com/avatar/HASH, where ‘HASH’ is the hash of the email of the user. In case, the hash is invalid, Gravatar returns a default avatar image.
  • Also, append ‘.jpg’ to the URL to maintain image format consistency on the website. When, the generated URL is used in an <img> tag, it behaves like an image and an avatar is returned when the URL is requested by the browser.
  • It has been displayed on various instances in the UI like app bar , feedback comments section, etc. The implementation in the feedback section has been discussed below.
  • The CircleImage component has been used for displaying the avatar, which takes name as a required property and src as the link of the image, if present. Following function returns props to the CircleImage component.

import md5 from 'md5';
import { urls } from './';

// urls.GRAVATAR_URL = ‘https://www.gravatar.com/avatar’;

let getAvatarProps = emailId => {
  const emailHash = md5(emailId);
  const GRAVATAR_IMAGE_URL = `${urls.GRAVATAR_URL}/${emailHash}.jpg`;
  const avatarProps = {
    name: emailId.toUpperCase(),
    src: GRAVATAR_IMAGE_URL,
  };
  return avatarProps;
};

export default getAvatarProps;

 

  • Then pass the returned props on the CircleImage component and set it as the leftAvatar property of the feedback comments ListItem. Following is the snippet –

….
<ListItem
  key={index}
  leftAvatar={<CircleImage {...avatarProps} size="40" />}
  primaryText={
    <div>
      <div>{`${data.email.slice(
        0,
        data.email.indexOf('@') + 1,
      )}...`}</div>
      <div className="feedback-timestamp">
        {this.formatDate(parseDate(data.timestamp))}
      </div>
    </div>
  }
  secondaryText={<p>{data.feedback}</p>}
/>
….
.
.

 

  • This displays the avatar of the user on the UI. The UI changes have been shown below :

References

Continue ReadingDisplaying Avatar Image of Users using Gravatar on SUSI.AI

Overriding the Basic File Attributes while Skill Creation/Editing on Server

In this blog post, we are going to understand the method for overriding basic file attributes while Skill creation/editing on SUSI Server. The need for this arose, when the creationTime for the Skill file that is stored on the server gets changed when the skill was edited.

Need for the implementation

As briefly explained above, the creationTime for the Skill file that is stored on the server gets changed when the skill is edited. Also, the need to override the lastModifiedTime was done, so that the Skills based on metrics gives correct results. Currently, we have two metrics for the SUSI Skills – Recently updated skills and Newest Skills. The former is determined by the lastModifiedTime and the later is determined by the creationTime. Due, to inconsistencies of these attributes, the skills that were shown were out of order. The lastModifiedTime was overridden to save the epoch date during Skill creation, so that the newly created skills don’t show up on the Recently Updated Skills section, whereas the creationTime was overridden to maintain the correct the time.

Going through the implementation

Let us first have a look on how the creationTime was overridden in the ModifySkillService.java file.

.
BasicFileAttributes attr = null;
Path p = Paths.get(skill.getPath());
try {
    attr = Files.readAttributes(p, BasicFileAttributes.class);
} catch (IOException e) {
    e.printStackTrace();
}
FileTime skillCreationTime = null;
if( attr != null ) {
    skillCreationTime = attr.creationTime();
}

if (model_name.equals(modified_model_name) &&
    group_name.equals(modified_group_name) &&
    language_name.equals(modified_language_name) &&
    skill_name.equals(modified_skill_name)) {
    // Writing to File
    try (FileWriter file = new FileWriter(skill)) {
        file.write(content);
        json.put("message", "Skill updated");
        json.put("accepted", true);

    } catch (IOException e) {
        e.printStackTrace();
        json.put("message", "error: " + e.getMessage());
    }
    // Keep the creation time same as previous
    if(attr!=null) {
        try {
            Files.setAttribute(p, "creationTime", skillCreationTime);
        } catch (IOException e) {
            System.err.println("Cannot persist the creation time. " + e);
        }
    }.
}
.
.
.

 

  • Firstly, we get the BasicFileAttributes of the Skill file and store it in the attr variable.
  • Next, we initialise the variable skillCreationTime of type FileTime to null and set the value to the existing creationTime.
  • The new Skill file is saved on the path using the FileWriter instance, which changes the creationTime, lastModifiedTime to the time of editing of the skill.
  • The above behaviour is not desired and hence, we want to override the creationTIme with the FileTime saved in skillCreationTIme. This ensures that the creation time of the skill is persisted, even after editing the skill.
  • Now we are going to see how the lastModifiedTime was overridden in the CreateSkillService.java file.

.
Path newPath = Paths.get(path);
// Override modified date to an older date so that the recently updated metrics works fine
// Set is to the epoch time
try {
  Files.setAttribute(newPath, "lastModifiedTime", FileTime.fromMillis(0));
} catch (IOException e) {
  System.err.println("Cannot override the modified time. " + e);
}
.
.
.

 

  • For this, we get the newPath of the Skill file and then the lastModifiedTime for the Skill File is explicitly set to a particular time.
  • We set it to FileTime.fromMillis(0) i.e, the epoch time.

I hope that I was able to convey my learnings and implementation of the code properly and it proved to be helpful for your understanding.

Resources

Documentation for BasicFileAttributes Interface – https://docs.oracle.com/javase/8/docs/api/java/nio/file/attribute/BasicFileAttributes.html

Continue ReadingOverriding the Basic File Attributes while Skill Creation/Editing on Server

Change Role of User in SUSI.AI Admin section

In this blog post, we are going to implement the functionality to change role of an user from the Admin section of Skills CMS Web-app. The SUSI Server has multiple user role levels with different access levels and functions. We will see how to facilitate the change in roles.

The UI interacts with the back-end server via the following API –

  • Endpoint URL –  https://api.susi.ai/cms/getSkillFeedback.json
  • The minimal user role for hitting the API is ADMIN
  • It takes 4 parameters –
    • user – The email of the user.
    • role – The new role of the user. It can take only selected values that are accepted by the server and whose roles have been defined by the server. They are – USER, REVIEWER, OPERATOR, ADMIN, SUPERADMIN.
    • access_token –  The access token of the user who is making the request

Implementation on the CMS Admin

  • Firstly, a dialog box containing a drop-down was added in the Admin section which contains a list of possible User roles. The dialog box is shown when Edit is clicked, present in each row of the User table.
  • The UI of the dialog box is as follows –

  • The implementation of the UI is done as follows –

….
<Dialog
  title="Change User Role"
  actions={actions} // Contains 2 buttons for Change and Cancel
  modal={true}
  open={this.state.showEditDialog}
>
  <div>
    Select new User Role for
    <span style={{ fontWeight: 'bold', marginLeft: '5px' }}>
      {this.state.userEmail}
    </span>
  </div>
  <div>
    <DropDownMenu
      selectedMenuItemStyle={blueThemeColor}
      onChange={this.handleUserRoleChange}
      value={this.state.userRole}
      autoWidth={false}
    >
      <MenuItem
        primaryText="USER"
        value="user"
        className="setting-item"
      />
      /*
        Similarly for REVIEWER, OPERATOR, ADMIN, SUPERADMIN
        Add Menu Items
     */ 
    </DropDownMenu>
  </div>
</Dialog>
….
.
.
.

 

  • In the above UI immplementation the Material-UI compoenents namely Dialog, DropDownMenu, MenuItems, FlatButton is used.
  • When the Drop down value is changed the handleUserRoleChange function is executed. The function changes the value of the state variables and the definition is as follows –
handleUserRoleChange = (event, index, value) => {
  this.setState({
      userRole: value,
  });
};

 

  • Once, the correct user role to be changed has been selected, the on click handlers for the action button comes into picture.
  • The handler for clicking the Cancel button simply closes the dialog box, whereas the handler for the Change button, makes an API call that changes the user role on the Server.
  • The click handlers for both the buttons is as follows –

// Handler for ‘Change’ button
onChange = () => {
  let url =
   `${urls.API_URL}/aaa/changeRoles.json?user=${this.state.userEmail}&
   role=${this.state.userRole}&access_token=${cookies.get('loggedIn')}`;
  let self = this;
  $.ajax({
    url: url,
    dataType: 'jsonp',
    crossDomain: true,
    timeout: 3000,
    async: false,
    success: function(response) {
      self.setState({ changeRoleDialog: true });
    },
    error: function(errorThrown) {
      console.log(errorThrown);
    },
  });
  this.handleClose();
};

// Handler for ‘Cancel’ button
handleClose = () => {
  this.setState({
    showEditDialog: false,
  });
};
  • In the first function above, the URL endpoint is hit, and on success the Success Dialog is shown and the previous dialog is hidden.
  • In the second function above, only the Dialog box is hidden.
  • The cross domain in the AJAX call is kept as true to enable API usage from multiple domain names and the data type set as jsonp also deals with the same issue.

Resources

Continue ReadingChange Role of User in SUSI.AI Admin section

Adding Filters for Lists of Skills on the SUSI.AI Server

In this blog post, we will learn how to add filters to the API responsible for fetching the list of skills i.e. the endpoint – https://api.susi.ai/cms/getSkillList.json. The purpose of adding filters is to return a list of skills based on some parameters associated with the skill, that would be required to allow the user to get the desired response that s/he may be using to display it on the UI.

Overview of the API

  • API to fetch the list of skills –
    • URL –  https://api.susi.ai/cms/getSkillList.json
    • It takes 5 optional parameters –
      • model – It is the name of the model that user is requesting
      • group – It is the name of the group that user is requesting
      • language – It is the name of the language that user is requesting
      • skill – It is the name of the skill that user is requesting
      • applyFilter – It has true/false values, depending whether filtering is required
    • If the request URL contains the parameter applyFilter as true, in that case the other 2 compulsory parameters are –
      • filter_name – ascending/descending, depending upon the type of sorting the user wants
      • filter_type – lexicographical, rating, etc based on what basis the filtering is going to happen

So, we will now look into adding a new filter_type to the API.

Detailed explanation of the implementation

  • We can add filters based on the key values of the Metadata object of individual skills. The Metadata object for each skill is similar to the following object –

{
  "model": "general",
  "group": "Knowledge",
  "language": "en",
  "developer_privacy_policy": null,
  "descriptions": "A skill that returns the anagrams for a word",
  "image": "images/anagrams.jpg",
  "author": "vivek iyer",
  "author_url": "https://github.com/Remorax",
  "author_email": null,
  "skill_name": "Anagrams",
  "protected": false,
  "terms_of_use": null,
  "dynamic_content": true,
  "examples": ["Anagram for best"],
  "skill_rating": {
    "negative": "0",
    "positive": "0",
    "stars": {
      "one_star": 0,
      "four_star": 0,
      "five_star": 0,
      "total_star": 0,
      "three_star": 0,
      "avg_star": 0,
      "two_star": 0
    },
    "feedback_count": 0
  },
  "creationTime": "2017-12-17T14:32:15Z",
  "lastAccessTime": "2018-06-19T17:50:01Z",
  "lastModifiedTime": "2017-12-17T14:32:15Z"
}

 

  • We will now add provision for URL parameter, filter_type=feedback in the API, which will filter the results based on the feedback_count key, which tells the number of feedback/comments a skill has received.
  • In the serviceImpl method of the ListSkillService class, we can see a code snippet that handles the filtering part, It checks the filter_type parameter received in the URL on if-else block. The code snippet looks like this –

if (filter_type.equals("date")) {
 .
 .
} else if (filter_type.equals("lexicographical")) {
 .
 .
} else if (filter_type.equals("rating")) {
 .
 .
}

 

  • Similarly, we will need to add an else if condition with feedback_type=feedback and write the code block inside it. Here is the code for it, which is explained in detail.

.
.
else if (filter_type.equals("feedback")) {
  if (filter_name.equals("ascending")) {
    Collections.sort(jsonValues, new Comparator<JSONObject>() {

      @Override
      public int compare(JSONObject a, JSONObject b) {
      Integer valA;
      Integer valB;
      int result=0;

      try {
        valA = a.getJSONObject("skill_rating").getInt("feedback_count");
        valB = b.getJSONObject("skill_rating").getInt("feedback_count");
        result = Integer.compare(valA, valB);

      } catch (JSONException e) {
        e.printStackTrace();
      }
      return result;
      }
    });
  }
  else {

    Collections.sort(jsonValues, new Comparator<JSONObject>() {

      @Override
      public int compare(JSONObject a, JSONObject b) {
      Integer valA;
      Integer valB;
      int result=0;

      try {
        valA = a.getJSONObject("skill_rating").getInt("feedback_count");
        valB = b.getJSONObject("skill_rating").getInt("feedback_count");
        result = Integer.compare(valB, valA);
      } catch (JSONException e) {
        e.printStackTrace();
      }
      return result;
    }
  });
  }
}
.
.

Working of the above code snippet

  • The first if condition checks for the filter_type the user has requested and enters the condition if it is equal to feedback
  • The next if-else handles the case of ascending and descending and sorts the list of skills accordingly.
  • The variable jsonValues passed in the Collections.sort function contains a List of JSONObject. Here, each object stands for the metadata object for a skill.
  • Since, the sorting is not a simple linear sort, the sort function is overloaded with a comparator function that specifies the key, based on which the sorting would take place.
  • The feedback_count value is stored in valA and valB for the two variables that is considered at an instance while sorting. For any other key based filtering, we need to replace the feedback_count with the desired key name.
  • The compare() method of Integer class of java.lang package compares two integer values (x, y) given as a parameter and returns the value zero if (x==y), if (x < y) then it returns a value less than zero and if (x > y) then it returns a value greater than zero.
  • The value returned to the sort function determines the order of the sorted array.
  • For, ascending and descending, the parameters of the compare function is swapped, so that we can achieve the exact opposite results from one another.

This was the implementation for the filtering of Skill List based on a key value present in the Skill Metadata object. I hope, you found the blog helpful in making the understanding of the implementation better.

Resources

Continue ReadingAdding Filters for Lists of Skills on the SUSI.AI Server

Implementing the List View of the Skill Cards

In this blog post, we are going to understand the implementation of the UI for the SUSI.AI skill card that is displayed on various routes of the SUSI Skill CMS Web-App. Now, there are two types of views of the views for the skill cards – List view and Grid view. We will learn to implement the List View in this blog.

Final UI of the Skill Card

Going through the implementation

The UI has multiple components –

  • The image thumbnail.
  • The title and author section,
  • Below that we have examples, ratings and the description section.

Fetching the data

  • The Skill Metadata for each skill is passed as props from the parent of the component, where this UI is implemented. This data object contains the various data points that are needed to display the UI. The key values used are –
    • skill_name – Used in the Title of the Skill Card
    • image – Used to display the thumbnail image of the skill
    • model – used to create the link to the Skill Details page
    • group – used to create the link to the Skill Details page
    • language – used to create the link to the Skill Details page
    • skill_tag – used to create the link to the Skill Details page
    • examples – used to display the examples card.
    • author – used to display the Author name
    • skill_rating – Used to display the stars and the total number of ratings of the skill
  • The following image shows the various areas, where the data is being used.

Parsing the data and creating JSX

  • Below is the code used to parse the data and achieving the UI, followed by the explanation.

…..
loadSkillCards = () => {
  let cards = [];
  Object.keys(this.state.skills).forEach(el => {
    let skill = this.state.skills[el];
    let skill_name = 'Name not available', examples = [], image = '', description =      
    'No description available', author_name = 'Author', average_rating = 0, 
    total_rating = 0;
    if (skill.skill_name) 
      skill_name = skill.skill_name.charAt(0).toUpperCase() + skill_name.slice(1);
    ….
    // Similarly parse, image, descriptions, author 
    ….
    if (skill.examples)
      examples = skill.examples.slice(0, 2); // Select max 2 examples
    if (skill.skill_rating) {
      average_rating = parseFloat(skill.skill_rating.stars.avg_star);
      total_rating = parseInt(skill.skill_rating.stars.total_star, 10);
    }
    cards.push(
      <div style={styles.skillCard} key={el}>
        <div style={styles.imageContainer}>
          // Display the image, else default avatar compoennt CircleImage
        </div>
        <div style={styles.content}>
          <div style={styles.header}>
            // Add Link to the skill title
              <div style={styles.title}><span>{skill_name}</span></div>
            <div style={styles.authorName}><span>{author_name}</span></div>
          </div>
          <div style={styles.details}>
            <div style={styles.exampleSection}>
              {examples.map((eg, index) => { return (
                <div key={index} style={styles.example}>&quot;{eg}&quot;</div>);
              })}
            </div>
            <div style={styles.textData}>
              <div style={styles.row}>
                <div style={styles.rating}>
                  // Show the 5-star rating section
                </div>
              </div>
              <div style={styles.row}>
                // Insert the skill description
              </div>
        //Close the div tags
    );
  });
  this.setState({cards});
};

render() {
.
.
  return (<div style={styles.gridList}>{skillDisplay}</div>);
}
.
.

 

  • An array of skills is passed as props and set in the state of the component in the constructor lifecycle method. The loadSkillCards() function is called in the didComponentMount lifecycle method, which is responsible for creating the JSX for all the Skill Cards.
  • In this function, the map property of array is used, to iterate over each skill and the corresponding Skill Card is pushed to the cards array, after successfully parsing the data.
  • At the end of the function definition, the state is updated, which in turn triggers the render function. In the render function, the cards are returned enclosed in a <div> tag. This helps us to create the above UI.

Styling the UI

Some important styling used is shown below. For the full styles object, please follow this link.

const styles = {
  skillCard: {
    width: '100%',
    overflow: 'hidden',
    display: 'flex',
    flexDirection: 'row',
    borderTop: '1px solid #eaeded',
    padding: 7,
  },
  imageContainer: {
    display: 'inline-block',
    alignItems: 'center',
    padding: '10px',
    background: '#fff',
    height: '218px',
    marginBottom: '6px',
  },
  image: {
    position: 'relative',
    height: '180px',
    width: '180px',
    verticalAlign: 'top',
    borderRadius: '50%',
  },
….
  gridlist: {
    marginTop: '20px',
    marginBottom: '40px',
    padding: '0px 10px',
    width: '100%',
….
  example: {
    fontStyle: 'italic',
    fontSize: '14px',
    padding: '14px 18px',
    borderRadius: '4px',
    border: '1px #ddd solid',
    float: 'left',
    display: 'flex row',
    justifyContent: 'center',
    alignItems: 'center',
    width: 192,
  },
….
};

export default styles;

 

I hope the implementation of the UI is clear and proved to be helpful for your understanding.

Resources

Showcase of the Material-UI icons (used for View type icons) – https://material.io/icons/

Continue ReadingImplementing the List View of the Skill Cards

Adding Functionality to Switch between List and Grid View of the Skill Cards on SUSI.AI CMS

In this blog post, we are going to understand the implementation of the functionality that enables the user to switch between the List View and the Grid View UI for the skill cards that is displayed on various routes of the SUSI Skill CMS Web-App. Let us go through the implementation in the blog –

Working of the feature

Going through the implementation

  • The UI for implementing the switching of views was achieved via the use of RadioButtonGroup component of the Material-UI library for React.
  • The type of view currently being shown was stored in the component state of the BrowseSkill component as viewType, whose default value is set to list, indicating that the skills are firstly shown in a List View.

.
.

export default class BrowseSkill extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
        .
        …..
        viewType: 'list',
        ….
    };
  }
….
}

 

  • The RadioButtonGroup component has 2 child components, for each view. The child component that is to be used is RadioButton.
  • The props passed in the RadioButtonGroup are –
    • name : It is the name given to the component.
    • defaultSelected : It is the default view type.
    • style : It contains the style object of the UI.
    • valueSelected : It is set to the state variable assigned for storing view type.
    • onChange : It is the handler which executes, when the radio buttons are clicked.
  • The style for the desktop view and mobile view is different depending on the screen size and is follows –

//Mobile view
Style {
    right: 12,
    position: 'absolute',
    top: 216,
    display: 'flex',
}

//Desktop view
style={
    display: 'flex',
    marginTop: 34
}

 

  • The props passed in the RadioButton are –
    • value : The value stored in the state, that is responsible for the view type. The values for List and Grid view are list and grid respectively.
    • label : The label for the RadioButton.
    • labelStyle : The style object for the label.
    • checkedIcon : The icon used in the checked state.
    • uncheckedIcon : The icon used in the unchecked state.

UI of the Radio Buttons

  • The onClick handler of the radio buttons is –

handleViewChange = (event, value) => {
    this.setState({ viewType: value });
};

 

  • The code snippet for the UI implementation, written inside the render function is as follows :

….
<RadioButtonGroup
  name="view_type"
  defaultSelected="list"
  style={
    window.innerWidth < 430
      ? {
          right: 12,
          position: 'absolute',
          top: 216,
          display: 'flex',
        }
      : { display: 'flex', marginTop: 34 }
  }
  valueSelected={this.state.viewType}
  onChange={this.handleViewChange}
>
  <RadioButton
    value="list"
    label="List view"
    labelStyle={{ display: 'none' }}
    style={{ width: 'fit-content' }}
    checkedIcon={
      <ActionViewStream style={{ fill: '#4285f4' }} />
    }
    uncheckedIcon={<ActionViewStream />}
  />
  <RadioButton
    value="grid"
    label="Grid view"
    labelStyle={{ display: 'none' }}
    style={{ width: 'fit-content' }}
    checkedIcon={
      <ActionViewModule style={{ fill: '#4285f4' }} />
    }
    uncheckedIcon={<ActionViewModule />}
  />
</RadioButtonGroup>
….

 

I hope the implementation of the switching between Views would be clear after going through the blog and proved to be helpful for your understanding.

References

Continue ReadingAdding Functionality to Switch between List and Grid View of the Skill Cards on SUSI.AI CMS

Make a cumulative API to return skills based on standard metrics in SUSI.AI

In this blog post, we are going to discuss on how to make a cumulative API which returns skills based on different standard metrics. It was implemented to combine various API calls, that were made from various clients, thereby reducing the number of API calls made. It lead to an optimization in the page load time of various clients and also helped to reduce the 503 errors that were received, due to very frequent API hits. The API endpoint for it is https://api.susi.ai/cms/getSkillMetricsData.json.

It accepts 5 optional parameters –

  • model – It represents the model for which the skill are fetched. The default value is set to General.
  • group – It represents the group(category) for which the skill are fetched. The default value is set to Knowledge.
  • language – It represents the language for which the skill are fetched. The default value is set to en.
  • duration – It represents the duration based on which skills are fetched by standard metrics like usage.
  • count – It represents the number of skills to be returned on a particular metric.

The minimalUserRole is set to ANONYMOUS for this API, as the data is required for home-page and doesn’t need the user to be logged-in.

Going through the API development

  • The parameters are first extracted via the call object that is passed to the main function. The  parameters are then stored in variables. If any parameter is absent, then it is set to the default value.
  • There is a check if the count param exists. If exists, then it is checked if it is of valid data-type. If no, an exception is thrown. And if count param doesn’t exist, it is set to default of 10.
  • This code snippet discusses the above two points –
@Override
public ServiceResponse serviceImpl(Query call, HttpServletResponse response, Authorization rights, final JsonObjectWithDefault permissions) throws APIException {

        String model_name = call.get("model", "general");
        File model = new File(DAO.model_watch_dir, model_name);
        String group_name = call.get("group", "All");
        String language_name = call.get("language", "en");
        int duration = call.get("duration", 0);
        JSONArray jsonArray = new JSONArray();
        JSONObject json = new JSONObject(true);
        JSONObject skillObject = new JSONObject();
        String countString = call.get("count", null);
        Integer count = null;

        if(countString != null) {
            if(Integer.parseInt(countString) < 0) {
                throw new APIException(422, "Invalid count value. It should be positive.");
            } else {
                try {
                    count = Integer.parseInt(countString);
                } catch(NumberFormatException ex) {
                    throw new APIException(422, "Invalid count value.");
                }
            }
        } else {
            count = 10;
        }
.
.
.

 

  • Then the skills are fetched based on the group name and then stored in a JSONArray named jsonArray. This array basically contains the metadata objects of each skill.
  • Now, we need to sort and filter these skills based on standard metrics like – Rating, Feedback Count, Usage Count, Latest skills.
  • The implementation for getting the skills based on the creation time is as follows. Rest, all the metrics were also implemented in the same fashion.

JSONObject skillMetrics = new JSONObject();
List<JSONObject> jsonValues = new ArrayList<JSONObject>();

// temporary list to extract objects from skillObject
for (int i = 0; i < jsonArray.length(); i++) {
    jsonValues.add(jsonArray.getJSONObject(i));
}

// Get skills based on creation date - Returns latest skills
Collections.sort(jsonValues, new Comparator<JSONObject>() {
    private static final String KEY_NAME = "creationTime";
    @Override
    public int compare(JSONObject a, JSONObject b) {
        String valA = new String();
        String valB = new String();
        int result = 0;

        try {
            valA = a.get(KEY_NAME).toString();
            valB = b.get(KEY_NAME).toString();
            result = valB.compareToIgnoreCase(valA);
        } catch (JSONException e) {
            e.printStackTrace();
        }
        return result;
    }
});

JSONArray creationDateData = getSlicedArray(jsonValues, count);
skillMetrics.put("latest", creationDateData);

.
.
.

 

  • The above code snippet deals with sorting the skills based on the creation time, that helps us to fetch the latest skills. The latest skills are stored on the skillMetrics object with the key name latest.
  • Similarly, the skills based on metrics like rating, feedback count and usage are stored with key names rating, feedback & usage respectively.

From the above snippet, we can also see a call to the function getSlicedArray. It takes a list of skills and count as input paramters. It returns the first ‘count’ skills from the list depending on the count value. The implementation of it is as follows –

private JSONArray getSlicedArray(List<JSONObject> jsonValues, Integer count)
{
    JSONArray slicedArray = new JSONArray();
    for (int i = 0; i < jsonValues.size(); i++) {
        if(count == 0) {
            break;
        } else {
            count --;
        }
        slicedArray.put(jsonValues.get(i));
    }
    return slicedArray;
}

 

  • The response object is then sent with 6 (six) key values mainly, apart from the session object. They are
    • accepted –  true – It tells that the skills have been fetched.
    • message – “Success: Fetched skill data based on metrics”
    • model –  It is the model that is sent on request params or the default value.
    • group –  It is the group that is sent on request params or the default value.
    • language – It is the language that is sent on request params or the default value.
    • metrics –  It is the main object of relevance for this API, which contains 4 child keys with values as an array of Skill Metadata objects.

{
  "accepted": true,
  "model": "general",
  "group": "All",
  "language": "en",
  "metrics": {
    "feedback": [
    {skill_1}, {skill_2}, ...
    ],
    "usage": [
    {skill_1}, {skill_2}, ...
    ],
    "rating": [
    {skill_1}, {skill_2}, ...
    ],
    "latest": [
    {skill_1}, {skill_2}, ...
    ]
  },
  "message": "Success: Fetched skill data based on metrics",
  "session": {
   ....
  }
}

 

I hope the development of creating the aforesaid API and the purpose of it is clear and proved to be helpful for your understanding.

References

Continue ReadingMake a cumulative API to return skills based on standard metrics in SUSI.AI