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

Continue Reading

Disable editing for non-editable skills for non-admin users

As the Skills in SUSI Skill CMS are publicly editable, any user has the access to edit them. Hence, there needed to be a better control over who can edit the Skills in CMS. We needed to implement a feature to allow Admins and higher user roles to change the status of a Skill to non-editable. The subsequent implementation on CMS would require disabling editing for non-editable Skills for non-admin users. This blog post explains how this feature has been implemented in SUSI.AI.

Adding a boolean parameter ‘editable’ to the Skill metadata

We needed to add a boolean parameter in the Skill metadata for each Skill. The boolean parameter is ‘editable’. If its value is true, then it implies that editing should be allowed for that Skill. If it is set to false, then the Skill should not be editable for non-admin users. By default, its value has been set to true for all Skills. This is implemented as follows in the SusiSkill.java file:

    // in the getSkillMetadata() method
  skillMetadata.put("editable", getSkillEditStatus(model, group, language, skillname));

    // declaration of the getSkillEditStatus() method
    public static boolean getSkillEditStatus(String model, String group, String language, String skillname) {
        // skill status
        JsonTray skillStatus = DAO.skillStatus;
        if (skillStatus.has(model)) {
            JSONObject modelName = skillStatus.getJSONObject(model);
            if (modelName.has(group)) {
                JSONObject groupName = modelName.getJSONObject(group);
                if (groupName.has(language)) {
                    JSONObject languageName = groupName.getJSONObject(language);
                    if (languageName.has(skillname)) {
                        JSONObject skillName = languageName.getJSONObject(skillname);

                        if (skillName.has("editable")) {
                            return false;
                        }
                    }
                }
            }
        }
        return true;
    }

 

Allowing Admin and higher user roles to change edit status of any Skill

This is facilitated by the endpoint ‘/cms/changeSkillStatus.json’. Its minimum base user role is set to Admin so that only Admins and higher user roles are able to change status of any Skill. A sample API call to this endpoint to change the edit status of any Skill to ‘false’ is as follows:

http://127.0.0.1:4000/cms/changeSkillStatus.json?model=general&group=Knowledge&language=en&skill=aboutsusi&editable=false&access_token=zdasIagg71NF9S2Wu060ZxrRdHeFAx

 

If we want to change the edit status of any Skill to ‘false’, then we need to add the Skill to the ‘skillStatus.json’ file. For this, we need to traverse inside the JSONObject in the ‘skillStatus.json’ file. We need to traverse inside the model, group and language as specified in the query parameters. This is done as follows:

   if(editable.equals("false")) {
       skill_status.put("editable", false);
   }

   JsonTray skillStatus = DAO.skillStatus;

   if (skillStatus.has(model_name)) {
        modelName = skillStatus.getJSONObject(model_name);
        if (modelName.has(group_name)) {
            groupName = modelName.getJSONObject(group_name);
            if (groupName.has(language_name)) {
                languageName = groupName.getJSONObject(language_name);

                if (languageName.has(skill_name)) {
                    skillName = languageName.getJSONObject(skill_name);

                    if(editable != null && editable.equals("false")) {
                        skillName.put("editable", false);
                    }
                    else if(editable != null && editable.equals("true")) {
                        skillName.remove("editable");
                    }

                    skillStatus.commit();
                    result.put("accepted", true);
                    result.put("message", "Skill status changed successfully.");
                    return new ServiceResponse(result);
                }
            }
        }
    }

 

If we want to change the edit status of any Skill to ‘true’, then we need to remove the Skill from the ‘skillStatus.json’ file. We also need to remove all the empty JSONObjects inside the ‘skillStatus.json’ file, if they are created in the process of removing Skills from it. This is done as follows:

   if (skillStatus.has(model_name)) {
        modelName = skillStatus.getJSONObject(model_name);
        if (modelName.has(group_name)) {
            groupName = modelName.getJSONObject(group_name);
            if (groupName.has(language_name)) {
                languageName = groupName.getJSONObject(language_name);
                if (languageName.has(skill_name)) {
                    skillName = languageName.getJSONObject(skill_name);
                    if(editable != null && editable.equals("true")) {
                        skillName.remove("editable");
                    }
                    if(skillName.length() == 0) {
                        languageName.remove(skill_name);
                        if(languageName.length() == 0) {
                            groupName.remove(language_name);
                            if(groupName.length() == 0) {
                                modelName.remove(group_name);
                                if(modelName.length() == 0) {
                                    skillStatus.remove(model_name);
                                }
                            }
                        }
                    }
                    skillStatus.commit();
                }
            }
        }
    }

 

Disabling editing for non-editable Skills for non-admin users on Skill CMS

For the Skills whose edit status has been set to ‘false’ by the Admins, we need to allow the non-admin users to only be able to view the code of the Skill, and not permit them to change the code and save the changes to the Skill. We need to display a message to the users about the possible reasons. All the code for displaying the message is put in an if() condition as follows:

   if (
      cookies.get('loggedIn') &&
      !this.state.editable &&
      !this.state.showAdmin
    )

 

This is how the Skill edit page for a non-editable Skill would look like for a non-admin user:

For an Admin user, this would look exactly same like an editable Skill page. Admin user would be able to edit and make changes to the Skill code and save the changes.

This is how editing of non-editable Skills have been disabled for non-admin users.

Resources

Continue Reading

Implementing a skill rating over time graph section in SUSI Skill CMS

In SUSI.AI skill ratings is an invaluable aspect which greatly helps the users to know which skills are performing better than the rest and are more popular than the others. A robust skill rating system for the skills was developed recently which allows the users to rate skills as per their experience and thus data like average rating, total number of ratings is available but there was no provision to see the rating history or how the skills rating has changed over time, this could be an important aspect for users or developers to know what changes to the skill made it less/more popular. An API is developed at the server to retrieve the ratings over time data, we can use these details to render attractive components for a better visual understanding of how the skill is performing and get statistics like how the skill’s rating has changed over time.

About the API

Endpoint : /cms/getRatingsOverTime.json

Parameters

  • model
  • group
  • language
  • skill

After consuming these params the API will return the number of times a skill is called along with

the date on which it is called. We use that data as an input for the line chart component that we want to render. 

Fetching data from the server and storing in the application state

Make an AJAX call to the server to fetch the data from the URL which holds the server endpoint, on successfully receiving the data we do some formatting with the timestamp that comes along the data to make it more convenient to understand and then we call a saveRatingOverTime function which saves the data received from the server to the application state.

let ratingOverTimeUrl = `${urls.API_URL}/cms/getRatingsOverTime.json`;
skillUsageUrl = skillUsageUrl + '?model=' + modelValue + '&group=' + this.groupValue + '&language=' + this.languageValue + '&skill=' + this.name;
// Fetch the skill ratings over time
$.ajax({
 url: ratingOverTimeUrl,
 dataType: 'json',
 crossDomain: true,
 success: function(data) {
        if (data.ratings_over_time) {
         const ratingData = data.ratings_over_time.map(item => {
             return {
               rating: item.rating,
               count: item.count,
               timestamp: parseDate(item.timestamp)
                 .split(' ')
                 .slice(2, 4)
                 .join(' '),
                 };
           });
         self.saveRatingOverTime(ratingData);
        }
 },
 error: function(e) {
        console.log(e);
        self.saveRatingOverTime();
 },
});

 

Save the skill usage details in the component state.

// Save ratings over time data in the component state
saveRatingOverTime = (ratings_over_time = []) => {
 this.setState({
        ratings_over_time,
 });
};

 

Send the received data as props to the Skill Rating component and render it.

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

Implementing the UI

Importing the packages for rendering the chart in the Skill Ratings component.

import { XAxis, YAxis, Tooltip, LineChart, Line, Legend, ResponsiveContainer } from 'recharts';

 

Display a small heading for the section in the ratings card and Render a Responsive container component which will form a parent component for out Chart which will be rendered when the ratings over time data received in the props is not empty.

<div className="sub-title" style={{ alignSelf: 'flex-start' }}>
 Rating over time
</div>
{this.props.ratings_over_time.length ? (
 <div>
        <ResponsiveContainer
         height={300}
         width={
           window.innerWidth < 660
             ? this.state.width
             : this.state.width * 1.5
         }
         debounce={1}
        >
         ...
        </ResponsiveContainer>
 </div>
) : (
 <div>No ratings data over time is present</div>
)}

 

Render a LineChart and supply data from the data prop received from the Skill Listing component, add X-Axis and Y-Axis by supplying corresponding dataKey props depending on the data received, Add a tooltip to describe points on the line chart and a legend which describes the lines, After that we have a Line component which depicts the change in ratings over time on the chart.

<LineChart
 data={this.props.ratings_over_time}
 margin={{
        top: 5,
        right: 30,
        left: 20,
        bottom: 5,
 }}
>
 <XAxis dataKey="timestamp" padding={{ right: 20 }} />
 <YAxis dataKey="rating" />
 <Tooltip wrapperStyle={{ height: '60px' }} />
 <Legend />
 <Line
        name="Average rating"
        type="monotone"
        dataKey="rating"
        stroke="#82ca9d"
        activeDot={{ r: 8 }}
 />
</LineChart>

 

So I hope after going through this blog it is more clear how the ratings over time section is implemented in the Skill CMS.

Resources

 

Continue Reading

Appending a rating section of SUSI SKILL CMS to the skill page

Ratings is an essential component of skills which provides the developers an insight into how the SUSI Skill is functioning and how to further improve it which ultimately leads to great user experience so this was the motivation to allow users to be able to rate skills, once the rating system is implemented we need to show some statistics like average rating, total users who have rated the skills etc on the skill page for each skill, and this will also enable users to get top rated skills and thus users can get to use the best skills rated by the community. So we implemented a rating section to SUSI SKILL CMS

Implementation

Server –

  1. Two APIs were implemented by the analytics team on the server which allows the user to rate skill and fetch rating for each skill.
    1. To rate the skill (Sample)

      /cms/getSkillRating.json?model=general&group=Knowledge&language=en&skill=aboutsusi&callback=pc&_=1525446551181
      

       

    2. To get the ratings data for any skill (Sample)

      /cms/fiveStarRateSkill.json?model=general&group=Knowledge&language=en&skill=aboutsusi&stars=3&callback=p&_=1526813916145
      

       

CMS –

    1. When visiting any skill make an ajax call to the server to fetch the skill data for the visited skill. The call takes in the URL from which we have to fetch data from and of course a datatype which is jsonp since server returns data in the JSON format, when the request succeeds we save the received rating to the application state and in the case or any errors we log the error for developers to debug.

// Fetch ratings for the visited skill
           $.ajax({
               url: skillRatingUrl,
               jsonpCallback: 'pc',
               dataType: 'jsonp',
               jsonp: 'callback',
               crossDomain: true,
               success: function (data) {
                   self.saveSkillRatings(data.skill_rating)
               },
               error: function(e) {
                   console.log(e);
               }
           });
    1. Save the fetched data to the application state, this data saved in the state will be used in several components and graph present on the ratings section.

saveSkillRatings = (skill_ratings) => {
    this.setState({
        skill_ratings: data
    })
 }
  1. Plug in the data received to the Bar chart component to visualize how ratings are divide.
    1. Import the required components on the top of the file from the recharts library which provides us with several interactive charts.
    2. import {BarChart, Cell, LabelList, Bar, XAxis, YAxis, Tooltip} from 'recharts';
      
        1. Plug the data to the BarChart component through the data prop and render them to the page, this data is coming from the application state which we saved earlier. After that we define keys and styling for the X-Axis and Y-Axis and an interactive tooltip which shows up on hovering over any bar of that chart. We have 5 bars on the chart for each star rating all of different and unique colors and labels which appear on the right of each bar.

      <div className="rating-bar-chart">
         <BarChart layout='vertical' width={400} height={250}
              data={this.state.skill_ratings}>
              <XAxis type="number" padding={{right: 20}} />
              <YAxis dataKey="name" type="category"/>
              <Tooltip
      
                   wrapperStyle={{height: '60px'}} />
              <Bar name="Skill Rating" dataKey="value" fill="#8884d8">
                   <LabelList dataKey="value" position="right" />
                       {
                           this.state.skill_ratings
                            .map((entry, index) =>
                              <Cell key={index} fill={
      
                                 ['#0088FE', '#00C49F', '#FFBB28',
                                '#FF8042', '#FF2323'][index % 5]
                             }/>)
                       }
              </Bar>
          </BarChart>
      </div>
      
    3. Display the average rating of the skill along with the stars
        1. Import the stars component from the react-ratings-declarative component.

                  import Ratings from 'react-ratings-declarative';
          

           

        2. Render the average ratings and the stars component which is available in the app state as saved before.

          <div className="average">
                  Average Rating
          <div>
                     {this.state.avg_rating ? this.state.avg_rating : 2.5}
                  </div>
                  <Ratings
                     rating={this.state.avg_rating || 2.5}
                     widgetDimensions="20px"
                     widgetSpacings="5px"
                   >
                     <Ratings.Widget />
                     <Ratings.Widget />
                     <Ratings.Widget />
                     <Ratings.Widget />
                     <Ratings.Widget />
                  </Ratings>
          </div> 
          

           

    1. Display the total no of people who rated the skill, again, by using the ratings data saved in the state and calculating the total users who rated the skill by using a reduce function in ES6.

      <div className="total-rating">
              Total Ratings
              <div>
                 {this.state.skill_ratings.reduce((total, num) => {
                     return total + num.value
                 }, 0)}
              </div>
      </div>
      

      Outcome –

      I hope this post helped you in understanding how the rating system is implemented in the CMS.

      References –

Continue Reading

Multiple Languages Filter in SUSI.AI Skills CMS and Server

There are numerous users of SUSI.AI globally. Most of the users use skills in English languages while some prefer their native languages. Also,there are some users who want SUSI skills of multiple languages. So the process of fetching skills from multiple languages has been explained in this blog.

Server side implementation

The language parameter in ListSkillService.java is modified to accept a string that contains the required languages separated by a comma. Then this parameter is split by comma symbol which returns an array of the required languages.

String language_list = call.get("language", "en");
String[] language_names = language_list.split(",");

Then simple loop over this array language by language and keep adding the the skills’ metadata, in that language into the response object.

for (String language_name : language_names) {
	// fetch the skills in this language.
}

CMS side implementation

Convert the state variable languageValue, in BrowseSkill.js, from strings to an array so that multiple languages can be kept in it.

languageValue: ['en']

Change the language dropdown menu to allow selection of multiple values and attach an onChange listener to it. Its value is the same as that of state variable languageValue and its content is filled by calling a function languageMenuItems().

<SelectField
    multiple={true}
    hintText="Languages"
    value={languageValue}
    onChange={this.handleLanguageChange}
  >
    {this.languageMenuItems(languageValue)}
</SelectField>

The languageMenuItems() function gets the list of checked languages as a parameter. The whole list of languages are stored in a global variable called languages. So this function loops over the list of all the languages and check / uncheck them based on the values passed in the argument. It build a menu item for each language and put the ISO6391 native name of that language into the menu item.

languageMenuItems(values) {
  return languages.map(name => (
    <MenuItem
      insetChildren={true}
      checked={values && values.indexOf(name) > -1}
      value={name}
      primaryText={
        ISO6391.getNativeName(name)
          ? ISO6391.getNativeName(name)
          : 'Universal'
      }
    />
  ));
}

While the language change handler gets the values of the selected languages in the form of an array, from the drop down menu. It simply assigns this value to the state variable languageValue and calls the loadCards() function to load the skills based on the new filter.

this.setState({ languageValue: values }, function() {
    this.loadCards();
  });

 References

Continue Reading

Showing top metrics from skill groups

SUSI.AI shows top metrics on the home page. They include highest rated skills, most used skills, latest skills and skills with most feedbacks etc. Now the idea is to include top skills from a particular category also. For example “SUSI, what are your top games”? So how to fetch the required metrics in a generalized way.

Updating the skill metrics data API

Add an API parameter in SkillMetricsDataService.java to specify the names of groups to fetch the required metrics. It accepts a semicolon (;) separated list of group names. If no group is passed then by default it shows the top games.

String metrics_list = call.get("metrics", "Games, Trivia and Accessories");
String[] metrics_names = metrics_list.split(";");

Split the metrics parameter by semicolon and store in an array. This array contains all the groups of which top skills are to be displayed on the CMS home page. Loop over the array, group by group and filter out the skills that don’t belong to the current metrics group. Sort the filtered skills in decreasing order of the overall rating. But this sorting does not simply arrange the skills in decreasing order of their overall rating. Actually, it divides the skills into 2 halves. The first half contains the skills that have been rated by at least 10 users in decreasing order of the overall ratings. And the second half contains the rest of the skills in decreasing order of their overall rating.

for (String metric_name : metrics_names) {
    try {
        metric_name = metric_name.trim();
        List<JSONObject> groupJsonValues = new ArrayList<JSONObject>();
        for (int i = 0; i < jsonArray.length(); i++) {
            if (jsonArray.getJSONObject(i).get("group").toString().equals(metric_name)) {
                groupJsonValues.add(jsonArray.getJSONObject(i));
            }
        }
        // Get skills based on ratings of a particular group
        SusiSkill.sortByAvgStar(groupJsonValues, false);

        JSONArray topGroup = new JSONArray();
        topGroup = getSlicedArray(groupJsonValues, count);
        skillMetrics.put(metric_name, topGroup);
    }
    catch (Exception e) {
        e.printStackTrace();
    }
}

So if top games and news skills are to be shown on the CMS then the API endpoint looks like :

https://api.susi.ai/cms/getSkillMetricsData.json?metrics=Games, Trivia and Accessories; News

And the response is like :

{
	accepted: true,
	model: "general",
	group: "All",
	language: "en",
	metrics: {
		+newest: [...],
		+latest: [...],
		+rating: [...],
		+usage: [...],
		+feedback: [...],
		+staffPicks: [...],
		+Games, Trivia and Accessories: [...],
		+News: [...]
	},
	message: "Success: Fetched skill data based on metrics"
}

Resources

  • NA
Continue Reading

5 Star Skill Rating System in the SUSI.AI CMS

For making a system more reliable and robust, continuous evaluation is quite important. So is in the case of SUSI AI. User feedback is important to improve SUSI skills and create new ones. Previously we had only thumbs up / thumbs down as a feedback method, from the SUSI chat client. But now a 5 star rating system has been added to the SUSI Skill CMS so that users can rate a skill there. Before the implementation of API  let’s look how data is stored in SUSI AI Susi_server uses DAO in which skill rating is stored as JSONTray. The rating system has been implemented on SkillListing.js file on the CMS.

The CMS side

  1. Install the Recharts Data Visualization library.

$ npm install --save recharts
  1. Install the Rating library for react.

$ npm install --save react-ratings-declarative
  1. Import the Barchart, Cell, LabelList, Bar, XAxis, YAxis and Tooltip components from Recharts.

import {BarChart, Cell, LabelList, Bar, XAxis, YAxis, Tooltip} from 'recharts';
  1. Import the Ratings components from React Ratings Declarative.

import Ratings from 'react-ratings-declarative';
  1. Add average rating, total ratings, rating counts on each star and rating given by the users as state variables.

this.state = {
    ...
    avg_rating: '',
    total_star: '',
    skill_ratings: [],
    rating : 0
    ...
}
  1. Load the skill ratings as soon as the page loads. getSkillRating.json API is used to get skill ratings. Fetch the stars related data from the API response and save them in the state variable using saveSkillRatings() function.

$.ajax({
    url: skillRatingUrl,
    success: function (data) {
      self.saveSkillRatings(data.skill_rating.stars)
    },
);
  1.  Store the skills rating data to be visualized on the charts. The rating data is kept in an array and store in a state variable. Put zero if the rating count is not available. This state variable is used by the Recharts library as an input for data visualization.

saveSkillRatings = (skill_ratings) => {
       const ratings_data = [
             {name: '5 ⭐', value: skill_ratings.five_star || 0},
             {name: '4 ⭐', value: skill_ratings.four_star || 0},
             {name: '3 ⭐', value: skill_ratings.three_star || 0},
             {name: '2 ⭐', value: skill_ratings.two_star || 0},
             {name: '1 ⭐', value: skill_ratings.one_star || 0}];
       this.setState({
           skill_ratings: ratings_data,
           avg_rating: skill_ratings.avg_star,
           total_star: skill_ratings.total_star
       })
   }
  1. Create a function to that informs the server about the rating given by the current user. The changeRating() function calls the fiveStarRateSkill.json API with the parameters like who has rated the skill and what rating has been given.

changeRating = (newRating) => {
    $.ajax({
        url: changeRatingUrl,
        success: function(data) {
            console.log('Ratings accepted');
        },
    this.setState({
        rating: newRating
    });
};
  1.  Check if the user is logged in and display the rating bar. The rating bar should appear only if the user is logged in. The document’s cookies hold the information about the logged in user.

{
  cookies.get('loggedIn') ?
  <Ratings
     rating={this.state.rating}
     changeRating={this.changeRating}
    >
     <Ratings.Widget />
     <Ratings.Widget />
     <Ratings.Widget />
     <Ratings.Widget />
     <Ratings.Widget />
  </Ratings>
  :
  null
}
  1.  Display the BarChart of existing ratings, total ratings and average rating. It shows the count on each star.

If the average rating is available in the state then show it, otherwise put zero in the average rating.

Add a Bar Chart and define the data to be visualized i.e. the skill rating data. The Y-axis of the chart maps to the particular rating and the X-axis shows the count of that rating. The value of bar and label is set from the count of each star rating.

<BarChart layout='vertical' width={400} height={250} data={this.state.skill_ratings}>
    <XAxis type="number" padding={{right: 20}} />
    <YAxis dataKey="name" type="category"/>
    <Bar name="Skill Rating" dataKey="value" fill="#8884d8">
        <LabelList dataKey="value" position="right" />
        {
            this.state.skill_ratings
                .map((entry, index) =>
                    <Cell key={index} fill={
                        ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#FF2323'][index % 5]
                    }/>)
        }
    </Bar>
</BarChart>

If the total number of ratings is available in the state then show it, otherwise put zero in the total number of ratings.

Conclusion

So this 5 star rating system will help in improving the SUSI skills. Also, it will help in making better decisions when we have multiple similar skills and we have to choose one to respond to the user query.

References

Continue Reading

Implementing API to facilitate changing review status of a Skill

As any registered user can make Skills in SUSI Skill CMS, sometimes the Skill made is not up to the mark to be displayed on the CMS site. There needs to be a feature implemented where the Admin and higher user roles can review a Skill and approve it accordingly. Then only the approved Skills should be displayed on the Skill CMS. This feature required implementation of an API to allow Admin and higher user roles to change the review status of a Skill. This blog post explains how such an API has been implemented.

Implementing a servlet to allow changing review status of a Skill

The basic task of the servlet is to allow Admin and higher user roles to allow changing the review status of a Skill. The review status of a Skill can either be ‘true’, indicating that the Skill has been approved by the Admin, or ‘false’, which would indicate that the Skill still needs some improvements before it is actually displayed on the SUSI Skill CMS.

Here is the implementation of the API:

  1. The API should be usable to only the users who have a user role Admin or higher. Only those with minimum Admin rights should be allowed to control what Skills are displayed on the CMS site. This is implemented as follows:
   @Override
    public UserRole getMinimalUserRole() {
        return UserRole.ADMIN;
    }

 

  1. The endpoint for the API is ‘/cms/changeSkillStatus.json’. This is implemented as follows:
   @Override
    public String getAPIPath() {
        return "/cms/changeSkillStatus.json";
    }

 

  1. The main method of the servlet is the serviceImpl() method. This is where the actual code goes which will be executed each time the API is called. This is implemented as follows:

    @Override
    public ServiceResponse serviceImpl(Query call, HttpServletResponse response, Authorization authorization, JsonObjectWithDefault permissions) throws APIException {

       // Store the query parameters in variables

        if(skill_name == null || !(reviewed.equals("true") || reviewed.equals("false"))) {
            throw new APIException(400, "Bad service call, missing arguments.");
        }

        JSONObject result = new JSONObject();
        JsonTray skillStatus = DAO.skillStatus;
        JSONObject modelName = new JSONObject();
        JSONObject groupName = new JSONObject();
        JSONObject languageName = new JSONObject();
        JSONObject skillName = new JSONObject();

        if(reviewed.equals("true")) {
            JSONObject reviewStatus = new JSONObject();
            reviewStatus.put("reviewed", true);
                // traverse through the skillStatus.json file down to the Skill name and put review status ‘true’ in it
                skillName.put("reviewed", true);
                result.put("accepted", true);
                result.put("message", "Skill review status changed successfully.");
                return new ServiceResponse(result);
            }


            // deleting skill name from skillStatus.json file if it’s review status is being changed to ‘false’
            else {
                if (skillStatus.has(model_name)) {
                    modelName = skillStatus.getJSONObject(model_name);
                    if (modelName.has(group_name)) {
                        groupName = modelName.getJSONObject(group_name);
                        if (groupName.has(language_name)) {
                          languageName = groupName.getJSONObject(language_name);
                            if (languageName.has(skill_name)) {
                                languageName.remove(skill_name);
                                    skillStatus.remove(model_name);
                                }
                                skillStatus.commit();
                            }
                        }
                    }
                }
                result.put("accepted", true);
                result.put("message", "Skill review status changed successfully.");
                return new ServiceResponse(result);
            }
        }

 

The list of reviewed Skills is being stored in the ‘skillStatus.json’ file. So we need to traverse through that file and store the review status of the Skill as required while making the API call.

The API takes 5 parameters:

  • Model of the Skill
  • Group of the Skill
  • Language of the Skill
  • Skill name
  • Review status to be set for the Skill – true or false

On making the API call, if the value of the query parameter ‘reviewed’ is ‘true’, then the Skill name along with its review status is being appended in the ‘skillStatus.json’ file. This is done by traversing through the file using the specified model, group and language info of the Skill.

However, if the value of the query parameter ‘reviewed’ is ‘false’, then we need to check if the Skill is already there in the ‘skillStatus.json’ file. If it is already there, then we remove its entry from the file. If it isn’t in the file already, then it’s review status is already ‘false’. We also need to commit this change to the JsonTray for it to get reflected in the Server.

This is how an API has been implemented which would allow Admin and higher user roles to change the review status of any Skill, which would then facilitate showing only the approved Skills on the CMS site.

  1. Resources

     

Continue Reading

Skill Ratings Over Time

The SUSI SKill CMS provides an option to rate and review a skill. These feedbacks help the skill creators to improve the skills. Also, the ratings and reviews can be updated by the reviewer. But the CMS only provides the current rating of a skill. What if a user or a developer wants to see how that skill has performed over time? Are there any improvements in the skill or not?

For that, we need the skill ratings over time !

Server side implementation

Create a ratingsOverTime.json file to store the monthly average rating of the skills and make a JSONTray object for that in src/ai/susi/DAO.java file. The JSON file contains the timestamp for every month, the average ratings on a skill in that month and the total number of ratings in that month.

public static JsonTray ratingsOverTime;

Path ratingsOverTime_per = susi_skill_rating_dir.resolve("ratingsOverTime.json");
Path ratingsOverTime_vol = susi_skill_rating_dir.resolve("ratingsOverTime_session.json");
ratingsOverTime = new JsonTray(ratingsOverTime_per.toFile(), ratingsOverTime_vol.toFile(), 1000000);
OS.protectPath(ratingsOverTime_per);
OS.protectPath(ratingsOverTime_vol);

Now whenever a user rates a skill, the data in ratingsOverTime.json needs to be updated. For this fetch the overall rating data of the current month. Multiply the average rating with the total number of ratings (count) of that month.

sum = average_rating X number_of_ratings

Then add the rating given by the current user to this sum and divide by count + 1 to again get the new average rating. Also increment the total number of ratings by 1.

new_sum = sum + rating_by_user

new_avg = new_sum/(count+1)

number_of_ratings =  number_of_ratings + 1

float totalRating = skillRating * ratingCount;
float newAvgRating = (totalRating + skill_stars)/(ratingCount + 1);
ratingObject.put("rating", newAvgRating);
ratingObject.put("count", ratingCount + 1);

Now we have got the ratings over time stored in ratingsOverTime.json file. An API to access this data is also required. So create an API GetRatingOverTime.java returns the ratings over time of a particular skill. The API has the following attributes :

Endpoint : /cms/getRatingsOverTime.json

Minimum user role : anonymous

Parameters : model, group, language and skill

JSONArray skillRatings = languageName.getJSONArray(skill_name);
result.put("skill_name", skill_name);
result.put("ratings_over_time", skillRatings);
return new ServiceResponse(result);

It fetches the data corresponding to the skill from ratingsOverTime.json and returns it to the CMS.

Add this API to SusiServer.java

//Skill ratings over time
GetRatingsOverTime.class

References

Continue Reading

Adding “All” in Skill Categories

The SUSI SKill CMS has various filters to explore the skills of interest.  For example skill category and skill language. The skills are stored in the susi_skill_data Github repo in the following structure:

susi_skill_data/models/<model_name>/<group_name>/<language_name>/<skill_name>.txt

NOTE: group and category are same terms and can be used interchangeably

So when a category filter is applied the skills from the corresponding directory are returned.

susi_skill_data/models/<model_name>/<group_name>/<language_name>/<skill_name>.txt

But there’s no directory called “All”,  so how to get skills of all groups? For this, we need to loop through all the directories present in the model.

Server side implementation

Create a helper function that returns a list of all the folders present in a directory. The function accepts the parent directory name and an empty list. First, fetch all the items (files and folders) present in that directory and store them in an array. Then apply a filter over the array to check if the element is a directory and doesn’t start with a dot(.) i.e., it’s not hidden. Add the filtered array to the list.

private void listFoldersForFolder(final File folder, ArrayList<String> fileList) {
    File[] filesInFolder = folder.listFiles();
    if (filesInFolder != null) {
        Arrays.stream(filesInFolder)
                .filter(fileEntry -> fileEntry.isDirectory() && !fileEntry.getName().startsWith("."))
                .forEach(fileEntry -> fileList.add(fileEntry.getName() + ""));
    }
}

Fetch the group name form the request and add a check if the CMS is asking for all skill. Otherwise, return the skills of a particular group only.

String group_name = call.get("group", "Knowledge");
if (group_name.equals("All")) {
  // Return the list of all skills
} else {
  // Return the list of a skills in a particular group only
}

To fetch the list of all skills, call the listFoldersForFolders() function with the model name and an empty list as arguments. The function adds all the directories, present in that model directory, to folderList.

File allGroup = new File(String.valueOf(model));
ArrayList<String> folderList = new ArrayList<String>();
listFoldersForFolder(allGroup, folderList);

Then loop over all the groups present in the list to get all the skills present in that group. This process is the same as the existing process of getting skills of a particular category. Just keep adding the skill list to a global array.

CMS side implementation

The list of categories is first fetched from the API and then added to the dropdown menu. Since the API doesn’t return “All” in it, so we need to push it to the list manually.

groups.push(<MenuItem
                value="All"
                key="All"
                primaryText="All" />);

References

Continue Reading
  • 1
  • 2
Close Menu