Adding different metrics sections to the start page

In the initial version of the SUSI.AI Skill CMS we simply displayed all the skills present in the system in the form of cards. Once the skill analytics was incorporated into the CMS we got a bunch of skill statistics and thus we enhanced the start page by incorporating horizontally scrollable skill cards as per skill metrics like top rated skills, most used skills, skills which have received the most feedback etc. I worked on adding the skills with most feedback section and the section for the top games. This post will majorly deal with how the metrics sections are implemented on the start page and how any new metrics can be incorporated into the system and thus displayed on the CMS.

About the API

/cms/getSkillMetricsData.json?language=${language}

Sample API call:

https://api.susi.ai/cms/getSkillMetricsData.json?language=en

 

This will return a JSON which contains the skill data for all the metrics.

{
 "accepted": true,
 "model": "general",
 "group": "All",
 "language": "en",
 "metrics": {
        "newest": [...],
     "rating": [...],
      ...
 }
 "message": "Success: Fetched skill data based on metrics",
   "session": {"identity": {
           "type": "host",
          "name": "162.158.23.7_68cefd16",
          "anonymous": true
   }}
}

 

All of the data for several metics comes from the metrics object of the response which in turn contains arrays of skill data for each metric.

CMS Implementation

Once the BrowseSkill component is mounted we make an API call to the server to fetch all the data and save it to the component state, this data is then fed to the ScrollCardList component as props and the scroll component is rendered with appropriate data for different metrics.

loadMetricsSkills = () => {
   let url;
   url =
           urls.API_URL +
           '/cms/getSkillMetricsData.json?language=' +
           this.state.languageValue;
   let self = this;
   $.ajax({
           url: url,
           dataType: 'jsonp',
           jsonp: 'callback',
           crossDomain: true,
           success: function(data) {
                   self.setState({
                           skillsLoaded: true,
                           staffPicksSkills: data.metrics.staffPicks,
                           topRatedSkills: data.metrics.rating,
                           topUsedSkills: data.metrics.usage,
                           latestUpdatedSkills: data.metrics.latest,
                           newestSkills: data.metrics.newest,
                           topFeedbackSkills: data.metrics.feedback,
                           topGames: data.metrics['Games, Trivia and Accessories'],
                   });
           },
           error: function(e) {
                   console.log('Error while fetching skills based on top metrics', e);
                   return self.loadMetricsSkills();
           },
   });
};

 

We are using a single component for skill metrics and skill listing which show up on applying any filter or visiting any category. Thus we think of a condition when the skill metrics are to be displayed and conditionally render the metrics section depending on the condition.

So the metrics section shows up only when we have not visited any category or language page, there’s no search query in the search bar, there’s no rating refine filter applied and no time filter applied.

let metricsHidden =
         this.props.routeType ||
         this.state.searchQuery.length > 0 ||
         this.state.ratingRefine ||
         this.state.timeFilter;

 

Depending on the section you want to display, pass appropriate data as props to the SkillCardScrollList component, say we want to display the section with most feedback

{this.state.topFeedbackSkills.length &&
!metricsHidden ? (
   <div style={metricsContainerStyle}>
           <div
                   style={styles.metricsHeader}
                   className="metrics-header"
           >
                   <h4>
                           {'"SUSI, what are the skills with most feedback?"'}
                   </h4>
           </div>
           {/* Scroll Id must be unique for all instances of SkillCardList*/}
           {!this.props.routeType && (
                   <SkillCardScrollList
                           scrollId="topFeedback"
                           skills={this.state.topFeedbackSkills}
                           modelValue={this.state.modelValue}
                           languageValue={this.state.languageValue}
                           skillUrl={this.state.skillUrl}
                   />
           )}
   </div>
) : null}

 

So if there are skills preset in the topFeedbackSkills array which was saved in the state from the server initially and the condition to hide metrics is false we render the component and pass appropriate props for scrollId, skills data, language and model values and skill url.

In a similar way any metrics section can be implemented in the CMS, if the data is not present in the API, modify the endpoint to enclose the data you need, fetch data data from the server and just render it.

So I hope after reading through this you have a more clearer understanding about how the metrics sections are implemented on the CMS.

Resources

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

Individual skill usage subsections in SUSI Skill CMS

In SUSI.AI Skills CMS several interactive skill related statistics are displayed on the skill page for each skill which includes user ratings, ratings over time, user feedback and skill usage data displayed interactively. The skill usage section is further subdivided to get more insight into how the skill has been used and from where. Therefore we have three subsections which display Time wise skill usage, device wise usage, and country wise usage. All this data can help evaluate which devices are mostly using the skill or data like in which country the skill is more popular than others. So in this post, we mainly discuss the UI of how these sections are implemented.

Implementation

Adding a Card component to the skill page component at the bottom of the skill page component.

<SkillUsageCard
 skill_usage={this.state.skill_usage}
 device_usage_data={this.state.device_usage_data}
 countryWiseSkillUsage={this.state.countryWiseSkillUsage}
/>

 

In the render function of the newly made component, we import the Paper component from material-ui and render it at the top to contain the subsections to give it a card-like UI.

<div>
   <Paper className="margin-b-md margin-t-md">
           ...
   </Paper>
</div>

 

Create div for the time wise skill usage. Calculate total skill usage for displaying the total skill usage count and also it helps to decide whether we need to render the section or not. So if the total skill usage by time count is greater than zero then render the line chart for visual analysis and display the total skill usage count too.

let totalSkillUsage = 0;
if (this.props.skill_usage) {
 // eslint-disable-next-line
 totalSkillUsage = this.props.skill_usage.reduce((totalCount, day) => {
        if (day) {
         return totalCount + day.count;
        }
        return totalCount;
 }, 0);
}

<div className="time-chart">
 <div>
        <ResponsiveContainer width={this.state.width} height={300}>
         <LineChart
           ...
         >
           <XAxis dataKey="date" padding={{ right: 20 }} />
           <YAxis allowDecimals={false} />
           <Tooltip wrapperStyle={{ height: '60px' }} />
           <Legend />
           <Line
             ...
           />
         </LineChart>
        </ResponsiveContainer>
 </div>
</div>
<div className="total-hits">
 <div className="large-text">{totalSkillUsage}</div>
 Hits this week
</div>

 

Create div for the Device wise usage. Conditionally render it in case the device wise data is available in the props.

<div className="device-usage">
 <div className="sub-title">Device wise Usage</div>
 {this.props.device_usage_data &&
 this.props.device_usage_data.length ? (
        <div className="pie-chart">
         <ResponsiveContainer width={600} height={350}>
           <PieChart>
             <Pie
               ...
             >
               {this.props.device_usage_data.map((entry, index) => (
                 <Cell key={index} fill={entry.color} />
               ))}
             </Pie>
             <Legend wrapperStyle={{ position: 'relative' }} />
           </PieChart>
         </ResponsiveContainer>
        </div>
</div>

 

Create a div for the country wise usage. We get the country wise usage data from the props and then we plug in the data in the geo chart component and also display the data as a table on the side. In case no data comes in or is unavailable we do not render the component at all.

<div>
 {countryWiseSkillUsage && countryWiseSkillUsage.length ? (
        <div className="country-usage-container">
         <div className="country-usage-graph">
           <GeoChart data={countryWiseSkillUsage} />
         </div>
         <div className="country-usage-list">
           <Table>
             ...
         </div>
        </div>
 ) : (
        <div className="unavailable-message">
         Country wise usage distribution is not available.
        </div>
 )}
</div>

 

This is how the three subsection in the skill usage component are implemented

Resources

Adding a feature to delete skills from skill page for admins

SUSI Skill CMS has evolved drastically over the past few months with not only the introduction of skill metrics, skill analytics and powerful sorting features and interactive skill view types we needed the SUSI admins to be able to delete skills directly from the skills page and hence the skill can be deleted without visiting the admin service and then locating the skill and deleting it. This feature can be useful when a skill live on the system needs to be removed instantaneously for any reason like the API used by the skill going down or if it is a redundant skill or anything else. This feature was much needed for the admins and thus it was implemented.

About the API

An API is developed at the server so from the client we call this API to fetch data from the server and plug this data into the chart we wish to render.

Endpoint :

/cms/deleteSkill.json?access_token={access_token}&model={model}&group={group}&language={language}&skill={skill}

 

Parameters :

  • Model
  • Group
  • Skill
  • Language
  • Feedback
  • Access token (taken from the session of the logged in user)

Sample API call :

/cms/deleteSkill.json?access_token=ANecfA1GjP4Bkgv4PwjL0OAW4kODzW&model=general&group=Knowledge&language=en&skill=whois

Displaying a button with delete icon on skill page

The option to delete skill should be available at the skill page for each skill so we add a button with a delete icon for this in the group of edit skills and skill version buttons, clicking over this button will open up a confirmation dialog with two actions notable the delete/confirm button which deletes the skills and the cancel button which can be useful in case the user changes their mind. On clicking the delete button the request to delete the skill is sent to the server and thus the skill is deleted.

Import some required components

import Dialog from 'material-ui/Dialog';
import FlatButton from 'material-ui/FlatButton';
import Cookies from 'universal-cookie';
import $ from 'jquery';

 

Adding some variables to the component state which will help us decide when the delete skill dialog is to be shown.

this.state = {
   ...
   showDeleteDialog: false,
   ...
}

 

Display the delete skill button only when the user is logged in user has admin rights.

{
   cookies.get(showAdmin) ? (
           ...
   ): ''
}

 

Adding some JSX to the component’s render function which includes a div in the skill page section and the Dialog component for the delete skill and some actions which in our case is the the confirmation to delete skill and to cancel the skill deletion in case the user changes their mind. Also a tooltip is shown which appears on hovering over the delete skill button.

<div className="skillDeleteBtn">
 <FloatingActionButton
        onClick={this.handleDeleteToggle}
        data-tip="Delete Skill"
        backgroundColor={colors.header}
 >
        <DeleteBtn />
 </FloatingActionButton>
 <ReactTooltip effect="solid" place="bottom" />
 <Dialog
        title="Delete Skill"
        actions={deleteDialogActions}
        modal={false}
        open={this.state.showDeleteDialog}
        onRequestClose={this.handleDeleteToggle}
 >
        <div>
         Are you sure about deleting{' '}
         <span style={{ fontWeight: 'bold' }}>
           {this.state.skill_name}
         </span>?
        </div>
 </Dialog>
</div>

 

Clicking the delete skill button will change the state variable which decides whether the dialog is to be shown or not.

handleDeleteToggle = () => {
 this.setState({
        showDeleteDialog: !this.state.showDeleteDialog,
 });
};

 

Adding submit and cancel actions for the dialog menu and send them to the dialog as a prop.

const deleteDialogActions = [
 <FlatButton
        label="Delete"
        key="delete"
        style={{ color: 'rgb(66, 133, 244)' }}
        onClick={this.deleteSkill}
 />,
 <FlatButton
        label="Cancel"
        key="cancel"
        style={{ color: 'rgb(66, 133, 244)' }}
        onClick={this.handleDeleteToggle}
 />,
];

Hitting the endpoint for skill deletion.

Adding onClick event handlers for dialog actions, for the cancel button we simply toggle the view of the delete skill dialog and for the submit button hits the endpoint for skill deletion and then we display a snack bar message about the status of request we submit if it succeeded or failed. Once the submit button is clicked we hit the delete skill endpoint by supplying appropriate params and post the request for skill deletion by calling the API through AJAX with appropriate params and thus this concludes the skill deletion workflow.

Build the URL for the AJAX request.

let deleteUrl =    `${urls.API_URL}/cms/deleteSkill.json?model=${this.state.skillModel}&group=${this.state.skillGroup}&language=${this.state.skillLanguagelanguage}&skill=${this.state.skill_name}&access_token=${cookies.get('loggedIn')}`

 

Make an AJAX request to the built API URL and in case the request is successful we set the conditional for displaying the snackbar to true and set the snackbar message depending on whether the skill was deleted successfully or it failed.

$.ajax({
 url: deleteUrl,
 dataType: 'jsonp',
 jsonp: 'callback',
 crossDomain: true,
 success: function(data) {
        // redirect to the index page since the skill page won't be accessible
        this.handleDeleteToggle();
        this.setState({
         dataReceived: true,
        });
        this.props.history.push('/');
 }.bind(this),
 error: function(err) {
        console.log(err);
        this.handleReportToggle();
        this.setState({
         openSnack: true,
         snackMessage: 'Failed to delete the skill.',
        });
 }.bind(this),
});

 

This concludes the workflow of how the skill deletion feature was implemented on the CMS skill page for admins. I hope you found this useful.

Resources

Adding a feature to report skills in the CMS

A lot of interesting features were introduced in the SUSI.AI Skills CMS over the past few months but it lacked the functionality for users to be able to report skills which they find inappropriate or for any other reason. So an API was developed at the server to flag the skills as inappropriate and thus using this API endpoint an option was added at the skill page for each skill to mark the skill as inappropriate or report it. This data could be useful by admins to re-review the skills and see if something is wrong with it and it things seem out of place the skill can be removed or can be disabled.

About the API

An API is developed at the server so from the client we call this API to fetch data from the server and plug this data into the chart we wish to render.

Endpoint :

/cms/reportSkill.json?model={model}&group={group}&skill={skill}&feedback={feedback message}&access_token={access_token}

 

Parameters :

  • Model
  • Group
  • Skill
  • Language
  • Feedback
  • Access token (taken from the session of the logged in user)

Sample API call :

/cms/reportSkill.json?model=general&group=Knowledge&skill=Anime Suggestions&feedback=Not good&access_token=6O7cqoMbzlClxPwg1is31Tz5pjVwo3

Displaying option to report on skill page

The option to report skill should be available at the skill page for each skill so we add a field in the skill details section to the skill page component which will only be visible to the logged in users and on clicking over this field we display a dialog with a text field, the user can enter the message or the reason for reporting the skill and then clicking on the submit button when the user is done writing or click on the cancel button in case the user changes their mind to report the skill. Once the message is submitted we run a function by passing in the feedback message which in turn hits the corresponding endpoint and posts the data on the server.

Import some required components

import Dialog from 'material-ui/Dialog';
import FlatButton from 'material-ui/FlatButton';
import TextField from 'material-ui/TextField';

 

Adding some variables to the component state which will help us decide when the report dialog is to be shown and the feedback message as the user types and some other relevant data.

this.state = {
   ...
   skillTag: '',
   showReportDialog: false,
   feedbackMessage: ''
   ...
}

 

Display the report feature only when the user is logged in.

{
   cookies.get('loggedIn') ? (
           ...
   ): ''
}

 

Adding some jsx to the component’s render function which includes a div in the skill details section and the Dialog component for the report message and confirmation and the dialog contains a text field to take report message and some actions which in our case is the send report action and the cancel report action.

<tr>
 <td>Report: </td>
 <td>
        <div
         style={{ color: '#108ee9', cursor: 'pointer' }}
         onClick={this.handleReportToggle}
        >
         Flag as inappropriate
        </div>
 </td>
 <Dialog
        title="Flag as inappropriate"
        actions={reportDialogActions}
        modal={false}
        open={this.state.showReportDialog}
        onRequestClose={this.handleReportToggle}
 >
        <TextField
         hintText="Leave a feedback message"
         floatingLabelText="Feedback message"
         multiLine
         floatingLabelFocusStyle={{
           color: 'rgb(66, 133, 244)',
         }}
         underlineFocusStyle={{
           borderColor: 'rgb(66, 133, 244)',
         }}
         fullWidth
         onChange={(event, val) =>
           this.saveReportFeedback(val)
         }
        />
 </Dialog>
</tr>

 

Clicking the report as inappropriate will change the state variable which decides whether the dialog is to be shown or not.

handleReportToggle = () => {
        this.setState({
             showReportDialog: !this.state.showReportDialog,
        });
};

 

Adding submit and cancel actions for the dialog menu and send them to the dialog as a prop.

const reportDialogActions = [
 <FlatButton
        label="Cancel"
        key="cancel"
        style={{ color: 'rgb(66, 133, 244)' }}
        onClick={this.handleReportToggle}
 />,
 <FlatButton
        label="Submit"
        key="submit"
        style={{ color: 'rgb(66, 133, 244)' }}
        onClick={this.handleReportSubmit}
 />,
];

Hitting the endpoint for report submit.

Adding onClick event handlers for dialog actions, for the cancel button we simply toggle the view of the report dialog and for the submit button we take the feedback message entered by the user in the text field and hit the endpoint for skill reporting and then we display a snackbar message about the status of report submit if it succeeded or failed.
Once the skill is submitted button is clicked we hit the report endpoint and post the feedback data and call the API through AJAX with appropriate params and thus this concludes the skill reporting workflow.

Build the URL for the AJAX request.

let reportUrl = `${urls.API_URL}/cms/reportSkill.json?model=${
        this.state.skillModel
 }&group=${this.state.skillGroup}&language=${
        this.state.skillLanguage
 }&skill=${this.state.skillTag}&feedback=${
        this.state.feedbackMessage
 }&access_token=${cookies.get('loggedIn')}`;

 

Make an AJAX request to the built API URL and in case the request is successful we set the conditional for displaying the snackbar to true and set the snackbar message depending on whether the skill was reported successfully or it failed.

$.ajax({
 url: reportUrl,
 dataType: 'jsonp',
 jsonp: 'callback',
 crossDomain: true,
 success: function(data) {
        self.handleReportToggle();
        self.setState({
           openSnack: true,
           snackMessage: 'Skill has been reported successfully.',
        });
 },
 error: function(e) {
        self.handleReportToggle();
        self.setState({
           openSnack: true,
           snackMessage: 'Failed to report the skill.',
        });
 },
});

 

This concludes the workflow of how the skill reporting feature was implemented on the CMS. I hope you found this useful.

Resources

Add Info on Skill Usage Distribution for all Skills by an Author in SUSI.AI

SUSI Skill CMS has a dashboard option available at the /dashboard route which displays several data for the logged in user as the skills created by the user and the ratings the user has provided to all the skills, since we have a skill usage section available on all skill pages which depicts the skill usage count for the past week in a line chart. Skill creators didn’t have a functionality to see the skill usage distribution on their skills which can provide some useful insight like how some of the skills they created are performing in comparison to the others so I developed a ‘My Analytics’ section in the dashboard page and displayed the skill usage distribution in the form of pie chart among the skills created by the logged in users.

About the API

An API is developed at the server so from the client we call this API to fetch data from the server and plug this data into the chart we wish to render.

Endpoint :

/cms/getSkillsByAuthor.json?author_email={email}

 

Parameters :

Email ID which is taken from the cookies since it is stored there once the user logs in.

Sample API call :

/cms/getSkillsByAuthor.json?author_email[email protected]

Fetching the data for the component

We first create a separate My Analytics component and require it in the dashboard and make an AJAX call to the appropriate endpoint inside a loadSkillsUsage function which is called inside the componentDidMount hook after which the server returns raw data in the form of JSON. We then pass the response into a saveUsageData function to parse the data for our use and save it to the application state.

loadSKillsUsage = () => {
 let url =
        urls.API_URL +
`/cms/getSkillsByAuthor.json?author_email=${cookies.get('emailId')}`;
 let self = this;
 $.ajax({
        url: url,
        dataType: 'jsonp',
        jsonp: 'callback',
        crossDomain: true,
        success: function(data) {
         self.saveUsageData(data.author_skills || []);
         ...
        },
        error: function(err) {
         ...
        },
 });
};

 

Set the application state with the received data which the pie chart component will use as it’s data source.

saveUsageData = data => {
 const skillUsage = data.map(skill => {
        let dataObject = {};
        dataObject.skill_name = skill.skill_name;
        dataObject.usage_count = skill.usage_count || 0;
        return dataObject;
 });
 this.setState({ skillUsage });
};

Implementing the UI

We create a separate ‘My Analytics’ component which is imported into the dashboard component to make the code cleaner and manageable. So inside the My analytics component, we fetch the data from the server as depicted above and after that, we render the pie chart component after importing from the recharts library.

Importing the pie chart components from the recharts library.

import { Legend, PieChart, Pie, Sector, Cell, ResponsiveContainer } from 'recharts';

 

Rendering the pie chart component while supplying appropriate props most important of which is the data prop which will be used in the chart and that data is available in the application state as saved earlier. We also have other styling props and a function which is triggered when hovering over cells of the pie chart to represent the data of the hovered cell. We also supply the appropriate nameKey and dataKey props as per the data format available in the state.

<ResponsiveContainer width={600} height={350}>
 <PieChart>
        <Pie
         activeIndex={this.state.activePieIndex}
         activeShape={renderActiveShape}
         data={this.state.skillUsage}
         cx={300}
         cy={175}
         innerRadius={80}
         nameKey="skill_name"
         dataKey="usage_count"
         outerRadius={120}
         fill="#8884d8"
         onMouseEnter={this.onPieEnter}
        >

       ...
         </Pie>
        <Legend wrapperStyle={{ position: 'relative' }} />
 </PieChart>
</ResponsiveContainer>

 

Configuring color for each Cell in the pie so it looks more interactive and we have distinguished colors for all devices.

{this.state.skillUsage.map((entry, index) => (
 <Cell
   key={index}
   fill={
     [
       '#0088FE',
       '#00C49F',
       '#FFBB28',
       '#FF8042',
       '#EA4335',
     ][index % 5]
   }
 />
))}

 

Rendering the Pie only when data is available in props so we don’t end up rendering a blank chart which obviously won’t look good.

{
 this.state.skillUsage !== [] ? (
   ...
 ): ''
}

Resources

Implementing feature to filter skills by average customer review

SUSI Skill CMS showcases all the skills on the index page but lacks the functionality to refine skills according to average customer review which is a much-needed feature since some users may only want to try skills which have at least a minimum rating so they can know instantly which skills are performing well in comparison to others. Thus, we implement several star inputs on the sidebar to select skills which have ratings greater than or equal to the selected rating input.

Implementing the UI

Add a menu to the sidebar at the bottom of all categories and display ‘Refine by’ submenu text to denote the section.

<Menu desktop={true} disableAutoFocus={true}>
 <Subheader style={{ fontWeight: 'bold' }}>Refine by</Subheader>
 <h4 style={{ marginLeft: '12px', marginBottom: '4px' }}>
   Avg. Customer Review
 </h4>

...

 

Display rating options to the user by displaying a list of Ratings component imported from react-ratings-declarative, these are to be displayed for all ratings say four stars and above, three stars and above and so on, i.e.

<div
 style={styles.singleRating}
 onClick={() => this.handleRatingRefine(4)}
>
 <Ratings
        rating={4}
        widgetRatedColors="#ffbb28"
        widgetDimensions="20px"
        widgetSpacings="0px"
 >
        <Ratings.Widget />
        <Ratings.Widget />
        <Ratings.Widget />
        <Ratings.Widget />
        <Ratings.Widget />
 </Ratings>
 <div
        style={styles.ratingLabel}
        className={this.state.rating_refine === 4 ? 'bold' : ''}
 >
        & Up
 </div>
</div>

 

We add some styling and attach an onClick listener on each rating component which will handle the refining of skills according to the rating clicked, the idea behind this is to save the rating for the clicked option to the component state and re-render the skill cards

handleRatingRefine = rating => {
 this.setState(
        {
         rating_refine: rating,
        },
        this.loadCards(),
 );
};

 

When the component state is successfully set loadCards function as a callback is called which re-renders the cards by applying filter over the skills which match the average rating criteria which we just set.

if (self.state.rating_refine) {
 data.filteredData = data.filteredData.filter(
        skill =>
         skill.skill_rating.stars.avg_star >= self.state.rating_refine,
 );
}

Displaying a button to clear any refinements made

Once the skills are refined a button is needed to clear any refinements made. Initially when no refinements are made the rating_refine in the state is set to null which indicates that no refinements are made so whenever the value of that state is no null we render a button to clear the refinements or set the rating_refine state to null.

{this.state.rating_refine ? (
 <div
        className="clear-button"
        style={styles.clearButton}
        onClick={() => this.handleRatingRefine(null)}
 >
        Clear
 </div>
) : (
 ''
)}

Resources

Implementing My Rating Section on the SUSI.AI Skills Dashboard

SUSI Skill CMS provides the functionality to rate the skills, therefore users rate skills they use but there isn’t any place where they can see all the skills they rated, thus a ‘My Ratings’ section was implemented on the dashboard page to view these statistics. So to see what ratings they have given to skills they can just login to the cms and navigate to /dashboard and a my ratings components is visible there which lists all the ratings the user has provided in a nice tabular format.

About the API

An API endpoint is implemented on the server which fetches the skill data for skills the user has rated which includes the skill name, stars given and the timestamp.

/cms/getProfileDetails.json?access_token=

 

So we pass the access token of the authenticated user and a JSON response is received which contains all the details as depicted below, this data is then parsed on the frontend and filled in a tabular form on the MyRatings section.

{
 "rated_skills": [
   {"amazon_shopping": {
     "stars": "1",
     "timestamp": "2018-06-10 13:05:32.295"
   }},
   {"aboutsusi": {
     "stars": "2",
     "timestamp": "2018-06-10 13:26:26.222"
   }},
   {"anagrams": {
     "stars": "3",
     "timestamp": "2018-06-10 13:25:31.195"
   }}
 ],
 "session": {"identity": {
   "type": "email",
   "name": "[email protected]",
   "anonymous": false
 }},
 "accepted": true,
 "message": "User ratings fetched."
}

Displaying the results on the web

Make a MyRatings component and render it on the dashboard component

Make an AJAX call to the API and save the returned data to the component state. First create a loadSkills function in componentDidMount which will be called just as the component is mounted to the DOM which will then fetch data from the server, extract the meaningful parts such as skill_name, skill_star and timestamp and push them to an array which in this case is ratingsData. While the data is being fetched we show a circular loader for better UX and once we receive the data we save it in the component state and turn loading to false which will replace the loading animation with the actual data.

loadSkills = () => {
 let url;
 url =
   urls.API_URL +
   '/cms/getProfileDetails.json?access_token=' +
   cookies.get('loggedIn');
 let self = this;
 let ratingsData = [];
 $.ajax({
   url: url,
   jsonpCallback: 'pxcd',
   dataType: 'jsonp',
   jsonp: 'callback',
   crossDomain: true,
   success: function(data) {
     if (data.rated_skills) {
       for (let i of data.rated_skills) {
         let skill_name = Object.keys(i)[0];
         ratingsData.push({
           skill_name: skill_name,
           skill_star: i[skill_name].stars,
           rating_timestamp: i[skill_name].timestamp,
         });
       }
       self.setState({
         ratingsData,
       });
     }
     self.setState({
       loading: false,
     });
   },
   error: function(err) {
     self.setState({
       loading: false,
       openSnackbar: true,
       msgSnackbar: "Error. Couldn't rating data.",
     });
   },
 });
};

 

Display a loading animation when the data is being fetched, we maintain a state in the component called loading which is initially true since we don’t have the data just as the component is rendered so after we receive the data we turn the loading state to false which will hide the circular loader and display the component with the data received.

{this.state.loading ? (
         <div className="center">
           <CircularProgress size={62} color="#4285f5" />
           <h4>Loading</h4>
         </div>
       ) : ( ... )
}

 

Add a table to structure the state data, since we’re using material-ui library throughout the project we use material table component from the library and populate the data received in it with 3 columns namely skill name, skill star, and timestamp.

<div className="table-wrap">
 <Table className="table-root" selectable={false}>
   <TableHeader displaySelectAll={false} adjustForCheckbox={false}>
     <TableRow>
       <TableHeaderColumn>Skill Name</TableHeaderColumn>
       <TableHeaderColumn>Rating</TableHeaderColumn>
       <TableHeaderColumn>Timestamp</TableHeaderColumn>
     </TableRow>
   </TableHeader>
   <TableBody displayRowCheckbox={false}>
     {ratingsData.map((skill, index) => {
       return (
         <TableRow key={index}>
           <TableRowColumn style={{ fontSize: '16px' }}>
             {(
               skill.skill_name.charAt(0).toUpperCase() +
               skill.skill_name.slice(1)
             ).replace(/[_-]/g, ' ')}
           </TableRowColumn>
           <TableRowColumn style={{ fontSize: '16px' }}>
             {skill.skill_star}
           </TableRowColumn>
           <TableRowColumn>
             {this.parseDate(skill.rating_timestamp)}
           </TableRowColumn>
         </TableRow>
       );
     })}
     <TableRow />
   </TableBody>
 </Table>
</div>

 

Display a message when there is no rating data available, let’s say the user has not rated any skills so it does not make sense to display an empty table and therefore we display a message instead for users to rate skills.

{ratingsData.length === 0 &&
!this.state.loading && (
 <div>
   <div className="center">
     <br />
     <h2>
       You have not rated any skill, go to{' '}
       <Link to="/">SUSI Skills Explorer</Link> and rate.
     </h2>
     <br />
   </div>
 </div>
)}

 

Import this component in the dashboard component and render it below My Skills

import MyRatings from './MyRatings';

 

Render the imported MyRatings component on a separate card from material-ui on the dashboard.

<Paper
 style={styles.paperStyle}
 className="botBuilder-page-card"
 zDepth={1}
>
 <h1 className="center">My Ratings</h1>
 <MyRatings />
</Paper>

Resources

Giving users option to switch between All and Reviewed Only Skills on SUSI Skill CMS

There are a lot of Skills on SUSI Skill CMS. Any registered user has the access to creating his/her own Skills. Hence, we need to give the users an option on SUSI Skill CMS whether they want to see all the Skills, or only those Skills that have been tested thoroughly and have been approved by the Admin and higher user roles. This blog post explains how this feature has been implemented on the SUSI Skill CMS.

How is review status of any Skill changed on the server?

The API endpoint which allows Admin and higher user roles to change the review status of any Skill on the server is ‘/cms/changeSkillStatus.json’. It takes the following parameters:

  • model: Model of the Skill
  • group: Group of the Skill
  • language: Language of the Skill
  • skill: Skill name
  • reviewed: A boolean parameter which if true, signifies that the Skill has been approved.

Sample API call:

https://api.susi.ai/cms/changeSkillStatus.json?model=general&group=Knowledge&language=en&skill=aboutsusi&reviewed=true&access_token=yourAccessToken

 

Fetching reviewed only Skills from the server

The ‘/cms/getSkillList.json’ endpoint has been modified to facilitate returning only the Skills whose review status is true. This is done by the following API call:

https://api.susi.ai/cms/getSkillList.json?reviewed=true

 

Creating checkbox to switch between All and Reviewed Only Skills

Checkbox is one of the many Material-UI components. Hence, we need to first import it before we can use it directly in our BrowseSkill component.

import Checkbox from 'material-ui/Checkbox';

 

In the constructor of the BrowseSkill class, we set states for two variables as follows:

constructor(props) {
  super(props);
    this.state = {
      // other state variables
      showSkills: '',
      showReviewedSkills: false,
    };
}

 

In the above code for the constructor, we have set two state variables. ‘showSkills’ is a string which can either be an empty string, or ‘&reviewed=true’. We want to append this string to the ‘/cms/getSkillList.json’ API call because it would determine whether we want to fetch All Skills or reviewed only Skills. The second variable ‘showReviewedSkills’ is a boolean used to keep record of the current state of the page. If it is true, then it means that currently, only the reviewed Skills are being displayed on the CMS site.

Implementation of the checkbox

This is how the Checkbox has been implemented for the purpose of switching between All and Reviewed Only Skills:

 <Checkbox
    label="Show Only Reviewed Skills"
    labelPosition="right"
    className="select"
    checked={this.state.showReviewedSkills}
    labelStyle={{ fontSize: '14px' }}
    iconStyle={{ left: '4px' }}
    style={{
      width: '256px',
      paddingLeft: '8px',
      top: '3px',
    }}
    onCheck={this.handleShowSkills}
  />

 

As can be seen from the above code, the initial state of the checkbox is unchecked as initially, the value of the state variable ‘showReviewedSkills’ is set to false in the constructor. This means that initially all Skills will be shown to the user. On clicking on the checkbox, handleShowSkills() function is called. Its implementation is as follows:

  handleShowSkills = () => {
    let value = !this.state.showReviewedSkills;
    let showSkills = value ? '&reviewed=true' : '';
    this.setState(
      {
        showReviewedSkills: value,
        showSkills: showSkills,
      },
      function() {
        this.loadCards();
      },
    );
  };

 

In the handleShowSkills() function, firstly we store the current value of the state variable ‘showReviewedSkills’. The value of ‘showSkills’ string is determined according to the value of ‘showReviewedSkills’. Then the states of both these variables are updated in the setState() function. Lastly, loadCards() function is called.

In the loadCards() function, we append the value of the state variable ‘showSkills’ to the AJAX call to the ‘/cms/getSkillList.json’ endpoint. The URL used for the API call is as follows:

https://api.susi.ai/cms/getSkillList.json?group=' +
  this.props.routeValue +
  '&language=' +
  this.state.languageValue +
  this.state.filter +
  this.state.showSkills;

 

This is how the implementation of the feature to give users an option to switch between All and Reviewed Only Skills has been done.

Resources

Implementing a device wise usage section on the skill page

SUSI Skill CMS showcases all the skills on the index page as skill cards and users can visit any skill page for any skill by clicking on any of these cards, skill pages for each skill hold some interesting metrics like rating, usage data, country wise usage data etc. But since SUSI runs on different devices so we need something to distribute and showcase how a skill is performing on each device so we implemented a pie chart for visualization of device wise usage data.

About the API

An API is developed at the server so from the client we call this API to fetch data from the server and plug this data into the chart we wish to render.

Endpoint :

/cms/getDeviceWiseSkillUsage.json

 

Parameters :

  • model
  • group
  • language
  • skill

Sample API call :

/cms/getDeviceWiseSkillUsage.json?model=general&group=Knowledge&language=en&skill=ceo

 

Response

{
 "skill_usage": [
   {
     "count": 3,
     "device_type": "Others"
   },
   {
     "count": 39,
     "device_type": "Android"
   },
   {
     "count": 1,
     "device_type": "Web Client"
   }
 ],
 "session": {"identity": {
   "type": "host",
   "name": "162.158.166.37_35449f1b",
   "anonymous": true
 }},
 "skill_name": "news",
 "accepted": true,
 "message": "Device wise skill usage fetched"
}

Fetching the data for the chart

Setting the URL to fetch data from, this URL will be used to make the AJAX call.

let deviceUsageUrl = `${urls.API_URL}/cms/getSkillsByAuthor.json?author_email=${cookies.get('emailId')}`;
deviceUsageUrl = deviceUsageUrl + '?model=' + modelValue + '&group=' + this.groupValue + '&language=' + this.languageValue + '&skill=' + this.name;

 

Make an ajax call to extract data from the response and call a function which saves the data to the application state, this data will later be used to render our chart we wish to render.

$.ajax({
 url: deviceUsageUrl,
 ...
 success: function(data) {
   if (data.skill_usage) {
     self.saveDeviceUsageData(data.skill_usage);
   }
 },
 error: function(e) {
   self.saveDeviceUsageData();
 },
});

 

Set the application state with the received data which the pie chart component will use as it’s data source.

saveDeviceUsageData = (device_usage_data = []) => {
 this.setState({
   device_usage_data,
 });
};

Implementing the UI

We already have a card component for device usage section so we append our device wise usage section to this already present card. We fetch the data in the skillListing component and pass that data as props to the skill usage component so using data from the received props we render our pie chart.

Importing the needed components from recharts library.

import { Tooltip, Legend, PieChart, Pie, Sector, Cell } from 'recharts';

 

Rendering the Piechart component with appropriate props, the data props is the most important which is taken from the application state which we saved earlier.

<PieChart width={600} height={350}>
 <Pie
   data={this.props.device_usage_data}
   nameKey="device_type"
   dataKey="count"
   onMouseEnter={this.onPieEnter}
   ...
 >
   ...
 </Pie>
 <Legend wrapperStyle={{ position: 'relative' }} />
</PieChart>

 

Configuring color for each Cell in the pie so it looks more interactive and we have distinguished colors for all devices.

{this.props.device_usage_data.map((entry, index) => (
 <Cell
   key={index}
   fill={
     [
       '#0088FE',
       '#00C49F',
       '#FFBB28',
       '#FF8042',
       '#EA4335',
     ][index % 5]
   }
 />
))}

 

Rendering the Pie only when data is available in props so we don’t end up rendering a blank chart which obviously won’t look good.

{
 this.props.device_usage_data !== [] ? (
   ...
 ): ''
}

 

Resources

  • Swizec Teller, Rendering a pie chart using react and d3, URL
  • Pie chart example from recharts, URL