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

Continue Reading

Showing skills based on different metrics in SUSI Android App using Nested RecyclerViews

SUSI.AI Android app had an existing skills listing page, which displayed skills under different categories. As a result, there were a number of API calls at almost the same time, which led to slowing down of the app. Thus, the UI of the Skill Listing page has been changed so as to reduce the number of API calls and also to make this page more useful to the user.

API Information

For getting a list of SUSI skills based on various metrics, the endpoint used is /cms/getSkillMetricsData.json

This will give you top ten skills for each metric. Some of the metrics include skill ratings, feedback count, etc. Sample response for top skills based on rating :

"rating": [
  {
    "model": "general",
    "group": "Knowledge",
    "language": "en",
    "developer_privacy_policy": null,
    "descriptions": "A skill to tell atomic mass and elements of periodic table",
    "image": "images/atomic.png",
    "author": "Chetan Kaushik",
    "author_url": "https://github.com/dynamitechetan",
    "author_email": null,
    "skill_name": "Atomic",
    "protected": false,
    "reviewed": false,
    "editable": true,
    "staffPick": false,
    "terms_of_use": null,
    "dynamic_content": true,
    "examples": ["search for atomic mass of radium"],
    "skill_rating": {
      "bookmark_count": 0,
      "stars": {
        "one_star": 0,
        "four_star": 3,
        "five_star": 8,
        "total_star": 11,
        "three_star": 0,
        "avg_star": 4.73,
        "two_star": 0
      },
      "feedback_count": 3
    },
    "usage_count": 0,
    "skill_tag": "atomic",
    "supported_languages": [{
      "name": "atomic",
      "language": "en"
    }],
    "creationTime": "2018-07-25T15:12:25Z",
    "lastAccessTime": "2018-07-30T18:50:41Z",
    "lastModifiedTime": "2018-07-25T15:12:25Z"
  },
  .
  .

]

 

Note : The above response shows only one of the ten objects. There will be ten such skill metadata objects inside the “rating” array. It contains all the details about skills.

Implementation in SUSI.AI Android App

Skill Listing UI of SUSI SKill CMS

Skill Listing UI of SUSI Android App

The UI of skills listing in SUSI Android app displays skills for each metric in a horizontal recyclerview, nested in a vertical recyclerview. Thus, for implementing horizontal recyclerview inside vertical recyclerview, you need two viewholders and two adapters (one each for a recyclerview). Let us go through the implementation.

  • Make a query object consisting of the model and language query parameters that shall be passed in the request to the server.

val queryObject = SkillMetricsDataQuery("general", 
PrefManager.getString(Constant.LANGUAGE,Constant.DEFAULT))

 

  • Fetch the skills based on metrics, by calling fetch in SkillListModel which then makes an API call to fetch groups.

skillListingModel.fetchSkillsMetrics(queryObject, this)

 

  • When the API call is successful, the below mentioned method is called which in turn parses the received response and updates the adapter to display the skills based on different metrics.

override fun onSkillMetricsFetchSuccess(response: Response<ListSkillMetricsResponse>) {
   skillListingView?.visibilityProgressBar(false)
   if (response.isSuccessful && response.body() != null) {
       Timber.d("METRICS FETCHED")
       metricsData = response.body().metrics
       if (metricsData != null) {
           metrics.metricsList.clear()
           metrics.metricsGroupTitles.clear()
           if (metricsData?.rating != null) {
               if (metricsData?.rating?.size as Int > 0) {
                   metrics.metricsGroupTitles.add(utilModel.getString(R.string.metric_rating))
                   metrics.metricsList.add(metricsData?.rating)
                   skillListingView?.updateAdapter(metrics)
               }
           }

           if (metricsData?.usage != null) {
               if (metricsData?.usage?.size as Int > 0) {
                   metrics.metricsGroupTitles.add(utilModel.getString(R.string.metric_usage))
                   metrics.metricsList.add(metricsData?.usage)
                   skillListingView?.updateAdapter(metrics)
               }
           }

           if (metricsData?.newest != null) {
               val size = metricsData?.newest?.size
               if (size is Int) {
                   if (size > 0) {
                       metrics.metricsGroupTitles.add(utilModel.getString(R.string.metric_newest))
                       metrics.metricsList.add(metricsData?.newest)
                       skillListingView?.updateAdapter(metrics)
                   }
               }
           }

           if (metricsData?.latest != null) {
               if (metricsData?.latest?.size as Int > 0) {
                   metrics.metricsGroupTitles.add(utilModel.getString(R.string.metric_latest))
                   metrics.metricsList.add(metricsData?.latest)
                   skillListingView?.updateAdapter(metrics)
               }
           }

           if (metricsData?.feedback != null) {
               if (metricsData?.feedback?.size as Int > 0) {
                   metrics.metricsGroupTitles.add(utilModel.getString(R.string.metric_feedback))
                   metrics.metricsList.add(metricsData?.feedback)
                   skillListingView?.updateAdapter(metrics)
               }
           }

           if (metricsData?.topGames != null) {
               val size = metricsData?.feedback?.size
               if (size is Int) {
                   if (size > 0) {
                       metrics.metricsGroupTitles.add(utilModel.getString(R.string.metrics_top_games))
                       metrics.metricsList.add(metricsData?.topGames)
                       skillListingView?.updateAdapter(metrics)
                   }
               }
           }

           skillListingModel.fetchGroups(this)
       }
   } else {
       Timber.d("METRICS NOT FETCHED")
       skillListingView?.visibilityProgressBar(false)
       skillListingView?.displayError()
   }
}

 

  • When skills are fetched, the data in adapter is updated using skillMetricsAdapter.notifyDataSetChanged()

override fun updateAdapter(metrics: SkillsBasedOnMetrics) {
   swipe_refresh_layout.isRefreshing = false
   if (errorSkillFetch.visibility == View.VISIBLE) {
       errorSkillFetch.visibility = View.GONE
   }
   skillMetrics.visibility = View.VISIBLE
   this.metrics.metricsList.clear()
   this.metrics.metricsGroupTitles.clear()
      this.metrics.metricsList.addAll(metrics.metricsList)
   this.metrics.metricsGroupTitles.addAll(metrics.metricsGroupTitles)
      skillMetricsAdapter.notifyDataSetChanged()
}

 

  • The data is set to the layout in two adapters made earlier. The following is the code to set the title for the metric and adapter to horizontal recyclerview. This is the SkillMetricsAdapter to set data to show item in vertical recyclerview.

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
   if (metrics != null) {
       if (metrics.metricsList[position] != null) {
           holder.groupName?.text = metrics.metricsGroupTitles[position]
       }

       skillAdapterSnapHelper = StartSnapHelper()
       holder.skillList?.setHasFixedSize(true)
       val mLayoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
       holder.skillList?.layoutManager = mLayoutManager
       holder.skillList?.adapter = SkillListAdapter(context, metrics.metricsList[position], skillCallback)
       holder.skillList?.onFlingListener = null
       skillAdapterSnapHelper.attachToRecyclerView(holder.skillList)
   }
}

 

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

Adding a Horizontally scrollable component to display Skills based on metrics

In this blog post, I will discuss about the implementation of a horizontally scrollable component to display skill based on metrics. The purpose of the implementation is to show top skills based on metrics related to usage, ratings, etc in SUSI.AI Skills CMS.

Implementational details

  • We call this component SkillCardScrollList which takes in a list of cards to be displayed along with some other properties and returns an UI, as shown in the above GIF.
  • The parameters that the component takes are:
    • scrollId: It is a required field of the type String. It is the id name of the horizontally scrollable div.
    • skills: It contains an array of cards that are to displayed inside the container.
    • languageValue: It represents the language of the skills that are shown.
    • skillUrl: It contains the URL that the app would be taken to, on clicking individual Skill Card.
    • modelValue: It contains the model that the skill belongs to.
  • Here is a sample of how it is used in the BrowseSkill component, for showing the Top Rated Skills in a SkillCardsScrollList
<SkillCardScrollList
    scrollId="topRated"
    skills={this.state.topRatedSkills}
    modalValue={this.state.modalValue}
    languageValue={this.state.languageValue}
    skillUrl={this.state.skillUrl}
/>

 

  • The reason behind passing an unique scrollId as a prop to the component is that, there was a need to trigger the scroll event of the scrollable div n the click of left and right Floating Action Buttons (FABs) as shown in the UI. And, on multiple imports of this component, there would have been inconsistent scroll behaviour seen, had it not been unique.
  • Following in the code block of the component, which will be explained in details, that deals with the main implementation –
.
.
.
.
  scrollLeft = () => {
    let parentEle = document.getElementById(this.props.scrollId);
    let scrollValue = $(parentEle).scrollLeft() - 200;
    $(parentEle)
      .stop()
      .animate({ scrollLeft: scrollValue }, 100);
  };

  scrollRight = () => {
    // Similar function of scrollLeft
  };

  loadSkillCards = () => {
    let cards = [];
    Object.keys(this.state.skills).forEach(el => {
      .
      /* Each skill object is passed and then pushed to the cards
        array*/
      .
      );
    });
    // Set the cards array in the state 
    this.setState({
      cards,
    });
  };

  render() {
    return (
      <div
        style={{
          marginTop: '20px',
          marginBottom: '40px',
          textAlign: 'justify',
          fontSize: '0.1px',
          width: '100%',
        }}
      >
        <div>
          <div
            id={this.props.scrollId}
            className="scrolling-wrapper"
            style={styles.gridList}
          >
            <FloatingActionButton
              mini={true}
              backgroundColor={'#4285f4'}
              style={styles.leftFab}
              onClick={this.scrollLeft}
            >
              <NavigationChevronLeft />
            </FloatingActionButton>
            {this.state.cards}
            <FloatingActionButton
              mini={true}
              backgroundColor={'#4285f4'}
              style={styles.rightFab}
              onClick={this.scrollRight}
            >
              <NavigationChevronRight />
            </FloatingActionButton>
          </div>
        </div>
      </div>
    );
  }
}

 

  • The div with class scrolling-wrapper is actually scrolled on the click of the left and right FAB. For choosing the correct div to be scrolled, there was a necessary condition of an unique id as explained earlier, which has been set to the div.
  • For making the component horizontally scrollable, specific CSS rules are added to the div. They are –
gridList: {
  margin: '10px',
  textAlign: 'center',
  overflowX: 'scroll',
  overflowY: 'hidden',
  whiteSpace: 'nowrap',
},
leftFab: {
  position: 'absolute',
  left: 260,
  marginTop: 75,
},
rightFab: {
  position: 'absolute',
  right: 0,
  marginTop: 75,
  marginRight: 10,
},

 

  • The CSS rules for the FABs make them fixed in a position and only lets the card list scroll.
  • Lastly, there was a issue regarding the presence of horizontal scroll-bar been shown, which makes the UI look a bit unpleasant.

  • It was hidden with a pseudo selector CSS rule.
div.scrolling-wrapper::-webkit-scrollbar {
    display: none;
}

 

This was the implementation for the horizontally scrollable component for displaying Skill List based on a standard metrics. I hope, you found the blog helpful in making the understanding of the implementation better.

Resources

Continue Reading

Displaying skill rating for each skill on skill page of SUSI SKILL CMS

SUSI exhibits several skills which are managed by the SUSI Skill CMS, it essentially is a client which allows users to create/update skills conveniently since for each skill it is important to have the functionality of rating system so developers can get to know which skills are performing better than the rest and consequently improve them, thus a skill rating system which allows the users to give positive or negative feedback for each skill is implemented on the server.

Fetching skill_rating from the server

  1. Fetch skill data for which ratings are to be displayed through ajax calls
    API Endpoint –

    /cms/getSkillMetadata.json?
    

  2. Parse the received metadata object to get positive and negative ratings for that skill
  3. if(skillData.skill_rating) {
           	let positive_rating = skillData.skill_rating.positive;
            	let negative_rating = skillData.skill_rating.negative;
    }
    

    Sample API response

    {
      "skill_metadata": {
        "model": "general",
        "group": "Knowledge",
        "language": "en",
        "developer_privacy_policy": null,
        "descriptions": "Want to know about fossasia, just ask susi to tell that, Susi tells about the SUSI.AI creators",
        "image": "images/creator_info.png",
        "author": "madhav rathi",
        "author_url": "https://github.com/madhavrathi",
        "author_email": null,
        "skill_name": "Creator Info",
        "terms_of_use": null,
        "dynamic_content": false,
        "examples": [
          "Who created you?",
          "what is fossasia?"
        ],
        "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": "2018-03-17T16:38:29Z",
        "lastAccessTime": "2018-06-15T15:51:50Z",
        "lastModifiedTime": "2018-03-17T16:38:29Z"
      },
      "accepted": true,
      "message": "Success: Fetched Skill's Metadata",
      "session": {"identity": {
        "type": "host",
        "name": "162.158.166.37_d80fb5c9",
        "anonymous": true
      }}
    }
    

  4. Set the react state of the component to store positive and negative rating.
  5. this.setState({
      positive_rating,
      negative_rating
    })
    

  6. Use react-icons to fetch like and dislike icon components from font-awesome.
  7. npm i -S react-icons
    

  8. Import the corresponding icons in the SkillPage component
  9. import { FaThumbsOUp, FaThumbsODown } from 'react-icons/lib/fa/'
    

  10. Display the rating count along with their icons
  11. <div className="rating">
        <div className="positive">
             <FaThumbsOUp />
             {this.state.positive_rating}
         </div>
           <div className="negative">
                 <FaThumbsODown />
                 {this.state.negative_rating}
             </div>
    </div>
    

Example

References

Continue Reading
Close Menu
%d bloggers like this: