Enhanced Skill Tiles in SUSI Skill CMS

The SUSI Skill Wiki is a management system for all the SUSI Skills and the Skill display screen ought to look attractive. The earlier version of the Skill Display was just a display with the skill name populated as cards as shown in the image.

image

So as we progressed over to add more metadata to the SUSI Skills, we had the challenge to show all details which were as follows –

An example of a skill metadata format-

"cricketTest": {
      "image": "images/images.jpg",
      "author_url": "skill.susi.ai",
      "examples": ["Testing Works"],
      "developer_privacy_policy": "na",
      "author": "cms",
      "skill_name": "cricket",
      "dynamic_content": true,
      "terms_of_use": "na",
      "descriptions": "testing",
      "skill_rating": null
    }

To embed the Skill metadata in the Tiles the following steps are to be followed –

  1. We first use the end point at the SUSI Server, http://api.susi.ai/cms/getSkillList.json with the following attributes –
    1. model – The skill follows a general model or maybe a tutorial model
    2. group -The category or group of the skill.
    3. language – The language of the skill output.

2.  An AJAX request to this end point gives us the following response.

{
accepted: true,
model: "general",
group: "Knowledge",
language: "en",
skills: 
{
capital: 
{
image: "images/capital.png",
author_url: "https://github.com/chashmeetsingh",
examples: 
[
"capital of Bangladesh"
],
developer_privacy_policy: null,
author: "chashmeet singh",
skill_name: "capital",
dynamic_content: true,
terms_of_use: null,
descriptions: "a skill to tell user about capital of any country.",
skill_rating: 
{
negative: "0",
positive: "1"
}
}

We use the descriptions, skill_name, examples, image from the skill metadata to create our Skill Tile.

  1. The styles of the cards follow a CSS flexbox structure. A sample mock up of the Skill Card looks as follows.

We first handle all the base cases and show “No name available”, “No description available” for data which does not exist or is found to be “null”. We then create the card mock-up in ReactJS which looks somewhat like this code snippet in the file BrowseSkill.js

                            <Card style={styles.row} key={el}>
                                <div style={styles.right} key={el}>
                                    {image ? <div style={styles.imageContainer}>
                                        <img alt={skill_name} src={image} style={styles.image}/>
                                    </div>:
                                    <CircleImage name={el} size="48"/>}
                                    <div style={styles.titleStyle}>"{examples}"</div>
                                </div>
                                <div style={styles.details}>
                                    <h3 style={styles.name}>{skill_name}</h3>
                                    <p style={styles.description}>{description}</p>
                                </div>
                            </Card>

  1. We then add the following styles to the Card and its contents which complete the look of the View.
imageContainer:{   
        position: 'relative',
        height: '80px',
        width: '80px',
        verticalAlign: 'top'
    },
    name:{
        textAlign: 'left',
        fontSize: '15px',
        color: '#4285f4',
        margin: '4px 0'
    },
    details:{
        paddingLeft:'10px'
    },
    image:{
        maxWidth: '100%',
        border: 0
    },
description:{
        textAlign:'left',
        fontSize: '14px'   
    },
row: {
        width: 280,
        minHeight:'200px',
        margin:"10px",
        overflow:'hidden',
        justifyContent: "center",
        fontSize: '10px',
        textAlign: 'center',
        display: 'inline-block',
        background: '#f2f2f2',
        borderRadius: '5px',
        backgroundColor: '#f4f6f6',
        border: '1px solid #eaeded',
        padding: '4px',
        alignSelf:'flex-start'
    },
titleStyle:{
    textAlign: 'left',
    fontStyle: 'italic',
    fontSize: '16px',
    textOverflow: 'ellipsis',
    overflow: 'hidden',
    width: '138px',
    marginLeft: '15px',
    verticalAlign: 'middle',
    display: 'block'
}

To see the SUSI Skills or to contribute to it, please visit http://skills.susi.ai

Resources

Displaying Blog Posts on SUSI AI Web Chat’s Blog Page and Share Posts

FOSSASIA is maintaining a superior blog and it contains blog posts about projects and programs run by FOSSASIA. While we were implementing SUSI Web Chat Application we got a requirement to implement a blog page. Speciality of this blog page is it is not a separate blog page, it fetches blog posts and other related data by filtering the FOSSASIA’s main blog.

In this blog post I’ll discuss how we fetched and managed those data on front-end and how we made the appearance same as the FOSSASIA main blog.

First we get blog posts as a JSON. For that we used rss2json API. we can get the RSS feed as a JSON by sending our RSS feed URL to the rss2json API. Check the rss2json API documentation here.

It produces all posts as items array. Next we store this array of responses in our application as a state.

This response contains blog post titles featured images’ details and post content and other metadata such as tags, author name and published date.

We had few requirements to fulfill. First one is to show full content of the blogpost in a new blog page.

We can take the full content from response like this,

this.state.posts.slice(this.state.startPage, this.state.startPage + 10).map((posts, i) => {
        let content = posts.content;
})

We can use “cintent” variable to show content but it contains the featured image. We have to skip that image. For that,

let htmlContent = content.replace(/<img.*?>/, '');

Now we have to render this string value as HTML. For that we have to install “test-to-html” package using below command.

npm install html-to-text --save

Now we can convert text into html like this

htmlContent = renderHTML(htmlContent);

We used this HTML content inside the “CardText” tag.

<CardText> {htmlContent}
</CardText>

At the bottom of the post we needed to show author name, tags list and categories list.
Since tags and categories come in one array, we have to separate them.
First we defined an array which contains all the categories in Fossasia blog. Then we compared that array with the categories we got like this.

       const allCategories = ['FOSSASIA','GSoC','SUSI.AI']

Compare two arrays,

          posts.categories.map((cat) => {
                let k = 0;
                for (k = 0; k < allCategories.length; k++) {
                              if (cat === allCategories[k]) {
                                  category.push(cat);
                              }
              	}
          });

we defined this “arrDiff” simple function to get the difference of two arrays.

     var tags=arrDiff(category,posts.categories)

Make the list of categories

let fCategory=category.map((cat) =>
<span key={cat} ><a className="tagname" href={'http://blog.fossasia.org/category/' + cat.replace(/\s+/g, '-').toLowerCase()} rel="noopener noreferrer">{cat}</a></span>
   );

We can use above step to make tags list.

Then after used it in the “CardActions”

<span className='categoryContainer'>
    <i className="fa fa-folder-open-o tagIcon"></i>
    {fCategory}
</span>

 

According to the final requirement we needed to add social media share buttons for Facebook and Twitter.

If we need to make a twitter share button we have to follow up this method. But we can use “react-share” npm package to make these kind of share buttons.

This is how we made Facebook and Twitter share buttons. First of all we have to install “react-share” package using below command.

npm install react-share --save

Then we have to import the installed package.

import { ShareButtons, generateShareIcon } from 'react-share';

Then after we defined Button and Icon like this.

      const {FacebookShareButton,TwitterShareButton} = ShareButtons;
      const FacebookIcon = generateShareIcon('facebook');
      const TwitterIcon = generateShareIcon('twitter');

Now we can use these components.

<TwitterShareButton url={posts.guid} title={posts.title} via='asksusi' hashtags={posts.categories.slice(0, 4)} >                                                                                <TwitterIcon size={32} round={true} />
</TwitterShareButton>
<FacebookShareButton url={posts.link}>
     <FacebookIcon size={32} round={true} />
</FacebookShareButton>

We have to send URL and title of the post with the tweet and tags as hashtags. So we have to pass them into the component as above.
Above code produces this model of tweets.

That’s how “text-to-htm”l and “react-share” works on react. If you would like to contribute to SUSI project or any other FOSSASIA project please fork our repositories on github.

Resources:

Sorting Users and Implementing Animations on SUSI Web Chat Team Page

While we were developing the chat application, we wanted to show details of Developers.  So we planned to build a team page for SUSI Web Chat Application. In this post I discuss few things we built there. Like sorting feature, animations of the page, usage of Material UI.

First we made an array of objects to store user details. In that array we grouped them in sub arrays so we can refer them in different sections separately. We stored following data in “TeamList.js” in an array.

var team = [{
 'mentors': [{
   'name': 'Mario Behling',
   'github': 'http://github.com/mariobehling',
   'avatar': 'http://fossasia.org/img/mariobehling.jpg',
   'twitter': 'https://twitter.com/mariobehling',
   'linkedin': 'https://www.linkedin.com/in/mariobehling/',
   'blog': '#'
 }]
},{ 'server': [{
    }]
}

There was a requirement to sort developers by their name so we had to build a way to sort out array data which are in main array. This is how we built that.
The function we are going to use for sorting.

   function compare(a, b) {
     if (a.name < b.name) { return -1; }
     if (a.name > b.name) { return 1; }
     return 0;
   }

This is how we have used it to sort developers.

import team from './TeamList';
team[1].server.sort(compare);

In this function we took values of object two by two and compared.
Now we have to show these sorted information on view.
Extract data that we want from array and we used material UI Cards to show these data on view.
This is how we extracted data from the array.

   team[1].server.sort(compare);
   let server = team[1].server.map((serv, i) => {
     return ( <Card className='team-card' key={i}>
         <CardMedia className="container" >
           <img src={serv.avatar} alt={serv.name} className="image" />
           <div className="overlay" >
             <div className="text"> <FourButtons member={serv} /> </div>
           </div>
         </CardMedia>
         <CardTitle title={serv.name} subtitle={serv.designation} />
       </Card>)   })

Now it shows data on the view.
“” contains an image of the member. We need to show social media links of the user on mouseover. We did that using simple CSS. I added a block comment around those particular styles. Check those styles here.

.overlay {
 position: absolute;
 bottom: 100%;
 left: 0;
 right: 0;
 background-color: #000;
 overflow: hidden;
 width: 100%;
 height:0;
 transition: .3s ease;
 opacity:0;
}
.container:hover .overlay {
 bottom: 0;
 height: 100%;
 opacity:0.7;
}

Above lines show that how we made the animation of the overlay animation.

Now we want to show social media buttons on the overlay. We made another separate component for buttons and return those like this.

render() {
       let member= this.props.member;
       return (<div>
         <CardActions>
           <IconButton href={member.github} target="_blank" >
  <CardActions>
		</div>)}

Finally we showed those data on Team page. We returned these JSX from render(){} method.

         <div className="team-header">
           <div className="support__heading">Developers</div>
         </div>
         <div className='team-container'>{server}</div>

I have mentioned few resources which I used to implement these features. If you are willing to contribute SUSI AI Web Chat application. Fork our repository on github.

Resources

Documentation of Array.sort https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort

How to use Image overlay CSS effects: https://www.w3schools.com/howto/howto_css_image_overlay.asp

Handling Offline Message Responses in SUSI Web Chat

Previously, the SUSI Web Chat stopped working when there was no Internet connectivity. The application’s overall state was disturbed as one would see the loading message gif and the users were left to wonder on how to proceed. To handle this situation, we required notifying the User in the offline mode, with a message, that there is no Internet Connectivity.

This following image demonstrates the previous state where the application hung.

This image shows how this state was handled currently. One can test this out on http://chat.susi.ai by disconnecting and sending a message to SUSI and then connecting back to the Internet.

To achieve this, one needed to handle the offline and online events of the browser. The following steps can be followed to achieve this.

  1. We make use of the following eventListener functions to know whether the user is connected to the Internet.
// handles the Offlines event
window.addEventListener('offline', handleOffline.bind(this));  

// handles the Online event
window.addEventListener('online', handleOnline.bind(this));
  1. We then set a global offline message which is modified on the connections switching from online to an offline state. They are handled by the following functions.
let offlineMessage = null;

function handleOffline() {
  offlineMessage = 'Sorry, cannot answer that now. I have no net connectivity';
}
function handleOnline() {
  offlineMessage = null;
}
  1. We then handle the action createSUSIMessage() in API.actions.js and send the  AJAX request which we are making according to the offline/online state. This enables us to send the correct message response to the User even in the offline state and not letting the Application state crash.
// So if the offlineMessage variable is not null we call the AJAX 
if(!offlineMessage){
        // handle AJAX
}
else {
    // we create a message saying there is no Internet connectivity.
}
     
  1. The messages on refreshing back restore back to the original state as these are not being stored in the server. Hence the User is able to see the correct History, i.e., only those messages which were sent to the server and successfully responded to by SUSI.

Resources

Implementation of SUSI Web Chat Auto Sizing Message Composer

While we are using SUSI Web Chat Application we may have to send lengthy messages. Existing application’s Message composer supports for lengthy messages but it manages a constant value for every user input. While we were developing the application we got a requirement to build a growing message composer.

Final output of this implementation produces a message composer that grows when user completes a new line until user completes 5 lines and after 5 lines it maintains a fixed size and enables scrolling.

So we tried several packages to get this done. And finally we did this  using react-textarea-autosize  it gives all these features and it gives user to customize the elements furthermore.

First we have to install the npm package:

npm install --save react-textarea-autosize

After the installation we have to import the package on top of the “MessageComposer.react.js”

import TextareaAutosize from 'react-textarea-autosize';

Next we need to use this package like this,

         <TextareaAutosize
           className='scroll'
           id='scroll'
           minRows={1}
           maxRows={5}
           placeholder="Type a message..."
           value={this.state.text}
           onChange={this._onChange.bind(this)}
           onKeyDown={this._onKeyDown.bind(this)}
           ref={(textarea) => { this.nameInput = textarea; }}
           style={{ background: this.props.textarea}}
         />

 

This package provides “minRows” and “maxRows”  attributes and we can define minimum height of the text area and maximum height it can grow. If you need to know more about auto growing text areas and to get examples refer this.

Next we wanted to hide the scrollbar which is displaying when the textarea height is exceeding.

How we hide the scrollbars  on chrome browsers.

.scroll::-webkit-scrollbar {
 	 display: none;
}

This is how we hide the scrollbar on firefox browser.

.scroll {
 	overflow: -moz-scrollbars-none;
}

Now we have to style up the textarea because it comes with default styles. We wrapped up the textarea with the div and applied our styles to that. In my case we wrapped up my textarea with  <div className=“textBack”>

This is how we styled the textarea using the wrapper div.

.textBack{
 background: #fff;
 width: 83%;
 border-radius: 40px;
 padding: 5px 20px;
 display: block;
 position: relative;
 top: 12%;
 box-sizing: content-box;
 margin: 0px 0 10px 0;
}

Our textarea is like this.

It expands when user exceeds the width of textarea.

This is how we implemented the SUSI Web Chat’s growing message composer. If you would like to contribute please fork our repository on github  

Resources:

Implementing a Collapsible Responsive App Bar of SUSI Web Chat

In the SUSI Web Chat application we wanted to make few static pages such as: Overview, Terms, Support, Docs, Blog, Team, Settings and About. The idea was to show them in app bar. Requirements were  to have the capability to collapse on small viewports and to use Material UI components. In this blog post I’m going to elaborate how we built the responsive app bar Using Material UI components.

First we added usual Material UI app bar component  like this.

             <header className="nav-down" id="headerSection">
             <AppBar
               className="topAppBar"
title={<a href={this.state.baseUrl} ><img src="susi-white.svg" alt="susi-logo"  className="siteTitle"/></a>}
               style={{backgroundColor:'#0084ff'}}
               onLeftIconButtonTouchTap={this.handleDrawer}
               iconElementRight={<TopMenu />}
             />
             </header>

We added SUSI logo instead of the text title using below code snippet and linked it to the home page like this.

title={<a href={this.state.baseUrl} ><img src="susi-white.svg" alt="susi-logo"  className="siteTitle"/></a>}

We have defined “this.state.baseUrl” in constructor and it gets the base url of the web application.

this.state = {
       openDrawer: false, 
baseUrl: window.location.protocol + '//' + window.location.host + '/'
       };

We need to open the right drawer when we click on the button on top left corner. So we have to define two methods to open and close drawer as below.

   handleDrawer = () => this.setState({openDrawer: !this.state.openDrawer});
   handleDrawerClose = () => this.setState({openDrawer: false});

Now we have to add components that we need to show on the right side of the app bar. We connect those elements to the app bar like this. “iconElementRight={}”

We defined “TopMenu” Items like this.

   const TopMenu = (props) => (
   <div>
     <div className="top-menu">
     <FlatButton label="Overview"  href="/overview" style={{color:'#fff'}} className="topMenu-item"/>
     <FlatButton label="Team"  href="/team" style={{color:'#fff'}} className="topMenu-item"/>
     </div>

We added FlatButtons to place links to other static pages. After all we needed a FlatButton that gives IconMenu to show login and signup options.

     <IconMenu {...props} iconButtonElement={
         <IconButton iconStyle={{color:'#fff'}} ><MoreVertIcon /></IconButton>
     }>
     <MenuItem primaryText="Chat" containerElement={<Link to="/logout" />}
                   rightIcon={<Chat/>}/>
     </IconMenu>
   </div>
   );

After adding all these correctly you will see this kind of an app bar in your application.

Now our app bar is ready. But it does not collapse on small viewports.
So we planned to hide flat buttons on small sized screens and show the menu button. For that we used media queries.

@media only screen and (max-width: 800px){
   .topMenu-item{ display: none !important;  }
   .topAppBar button{ display: block  !important; }
}

This is how we built the responsive app bar using Material UI components. You can check the preview from this url. If you are willing to contribute to SUSI Web Chat here is the GitHub repository.

Resources:

  • Material UI Components: http://www.material-ui.com/#/components/
  • Learn More about media queries: https://www.w3schools.com/css/css_rwd_mediaqueries.asp

Implementing Speech to Text for Chrome in SUSI Web Chat

SUSI Web Chat now replies to voice inputs. To achieve this, I made use of the Web Speech API. The voice input saves one from the pain of typing and it’s a much needed feature for the Web Chat and to maintain the similarity with the other SUSI Android and SUSI iOS clients.

To test the feature out in SUSI Web Chat, click on the microphone icon beside the text area on chat.susi.ai.

Say the message once the dialog appears, and you will see the message being sent to the Chat List rendered in text.

Let’s achieve the same result following the steps below.

  1. First, initialize the class Voice Recognition with defaults for the Speech Recognition, for that we create a file VoiceRecognition.js
  1. We first initialize the Speech Recognition API with the window object.
  2. We warn the User with a console message if there is no Speech Recognition API available.
  3. If it’s available call the recognition function using the following line

this.recognition = this.createRecognition(SpeechRecognition)

// Initialise the Speech recognition API
const SpeechRecognition = window.SpeechRecognition
      || window.webkitSpeechRecognition
      || window.mozSpeechRecognition
      || window.msSpeechRecognition
      || window.oSpeechRecognition
    // Warn the user if not available otherwise call the createRecognition function
    if (SpeechRecognition != null) {
      this.recognition = this.createRecognition(SpeechRecognition)
    } else {
      console.warn('The current browser does not support the SpeechRecognition API.');
    }
  }
  1. Then we write the createRecognition function
  1. We set our defaults first as “continuous  – true, interimResults – false, and language – ‘en-US’ ”
  2. We pass these options to the recognition object that we created in the above step and finally return the recognition object.
createRecognition = (SpeechRecognition) => {
    const defaults = {
      continuous: true,
      interimResults: false,
      lang: 'en-US'
    }

    const options = Object.assign({}, defaults, this.props)

    let recognition = new SpeechRecognition()

    recognition.continuous = options.continuous
    recognition.interimResults = options.interimResults
    recognition.lang = options.lang

    return recognition
  }
  1. Initialize all the helper functions to be passed as props.
  1. start – This method starts the recognition and invokes the Mic of the browser. It also checks if the browser has the access to the user’s Mic.
  2. stop – Stop method closes the Mic and returns the audio captured so far.
  3. abort – Abort method stops the SpeechRecognition service.
  4. onspeechend – This method is called if there is any inactivity and there is no voice input. Hence, stops the recognition service.
  5. componentWillReceiveProps – This method waits for the stop method and calls it when it has received the stop object.
  6. componentWIllUnmount – This method is invoked just before the component is about to unmount and therefore its function is to abort the Speech Recognition Service
  7. render –  We return null as there is nothing to return in this component and all the converted text of the captured Speech will be sent to the parent element.
start = () => {
    this.recognition.start()
  }

  stop = () => {
    this.recognition.stop()
  }

  abort = () => {
    this.recognition.abort()
  }
  onspeechend = () => {
    console.log('no sound detected');
    this.recognition.stop()
  }

  componentWillReceiveProps ({ stop }) {
    if (stop) {
      this.stop()
    }
  }

  componentWillUnmount () {
    this.abort()
  }

  render () {
    return null
  }
  1. Add event listeners to start and stop functions inside componentDidMount() to ensure every action that we want to perform from the parent element is after the component has successfully mounted itself.
  1. start – The start method is set with an action start so that we can pass the required action name to the VoiceRecognition component that we created
  2. end – The end method similarly is set with an action end
  3. After setting up the actions we finally call the bindResult function with the result that we received.
componentDidMount () {
    const events = [
      { name: 'start', action: this.props.onStart },
      { name: 'end', action: this.props.onEnd },
      { name: 'onspeechend', action: this.props.onspeechend }
    ]

    events.forEach(event => {
      this.recognition.addEventListener(event.name, event.action)
    })

    this.recognition.addEventListener('result', this.bindResult)

    this.start()
  }
  1. Bind the result and send it as the props to the parent element.
  2. Combine all interim results of the recognition and send it to the onResult function as finalTranscript
  3. The function bindResult – The function bindResult does all the binding of the interim results that we received and output a final result as finalTranscript.
  4. Lastly, we add the prop validations to ensure the correct props are being passed to our VoiceRecognition component.
// bindResult function
 bindResult = (event) => {
    let interimTranscript = ''
    let finalTranscript = ''
   // Bind all the results to finalTranscript
    for (let i = event.resultIndex; i < event.results.length; ++i) {
      if (event.results[i].isFinal) {
        finalTranscript += event.results[i][0].transcript
      } else {
        interimTranscript += event.results[i][0].transcript
      }
    }

    this.props.onResult({ interimTranscript, finalTranscript })
  }
// Add Prop Validations
VoiceRecognition.propTypes = {
  onStart: PropTypes.func,
  onEnd : PropTypes.func,
  onResult: PropTypes.func,
  Onspeechend: PropTypes.func,
  continuous: PropTypes.bool,
  lang: PropTypes.string,
  stop: PropTypes.bool
};
// Finally export the VoiceRecognition Component
export default VoiceRecognition
  1. Lastly, call the VoiceRecogntion component and pass the props from the MessageComposer Section to it in the following way.
  1. Initialize the default state in the constructor inside this.state
this.state = {
      text: '',
      start: false, // Starting the VoiceRecognition
      stop: false, // Stop the VoiceRecognition
      open: false, // Maintain the modal state
      result:'' // Maintain the result state
    };
  1. onStart function to call the VoiceRecognition component only when the Mic Button is pressed.
  2. onEnd to end the Speech Recognition service.
  3. onResult to send the message through the Actions.createMessage() function
onResult = ({interimTranscript,finalTranscript }) => {
    let result = interimTranscript;
    let voiceResponse = false;
    this.setState({result:result});
    if(finalTranscript) {
      result = finalTranscript;
      this.setState({
      start: false,
      result:result,
      stop: false,
      open:false,
      animate:false
      });
      if(this.props.speechOutputAlways || this.props.speechOutput){
        voiceResponse = true;
      }
      Actions.createMessage(result, this.props.threadID, voiceResponse);
      setTimeout(()=>this.setState({result: ''}),400);
      this.Button = <Mic />
    }
  }
  1. Fire the component based on the value of start variable and pass the requisite props as given below in the code.
// Only when the start is ‘true’ call the VoiceRecognition component

    {this.state.start && (
          <VoiceRecognition
            onStart={this.onStart}
            onEnd={this.onEnd}
            onResult={this.onResult}
            continuous={true}
            lang="en-US"
            stop={this.state.stop}
          />
        )}

 

  1. Update the text in the “Speak Now” Dialog to show the user the Speech to Text conversion
  1. Update the text in the Modal when it is converted from Speech to Text, i.e. when we set the state of the result variable.
{this.state.result !=='' ? this.state.result :
          'Speak Now...'}

To get access to the full code, go to the repository https://github.com/fossasia/chat.susi.ai

Resources

Implementing Text to Speech on SUSI Web Chat

SUSI Web Chat now gives voice replies while chatting with it similar to SUSI Android and SUSI iOS clients. To test the Voice Output on Chrome,

  1. Visit chat.susi.ai
  2. Click on the Mic input button.
  3. Say something using the Mic when the Speak Now View appears

The simplest way to add text-to-speech to your website is by using the official Speech API currently available for Chrome Browser. The following steps help to achieve it in ReactJS.

  1. Initialize state defaults in a component named, VoicePlayer.
const defaults = {
      text: '',
      volume: 1,
      rate: 1,
      pitch: 1,
      lang: 'en-US'
    } 

There are two states which need to be maintained throughout the component and to be passed as such in our props which will maintain the state. Thus our state is simply

this.state = {
      started: false,
      playing: false
    }
  1. Our next step is to make use of functions of the Web Speech API to carry out the relevant processes.
  1. speak() – window.speechSynthesis.speak(this.speech) – Calls the speak method of the Speech API
  2. cancel() – window.speechSynthesis.cancel() – Calls the cancel method of the Speech API
  1. We then use our component helper functions to assign eventListeners and listen to any changes occurring in the background. For this purpose we make use of the functions componentWillReceiveProps(), shouldComponentUpdate(), componentDidMount(), componentWillUnmount(), and render().
  1. componentWillReceiveProps() – receives the object parameter {pause} to listen to any paused action
  2. shouldComponentUpdate() simply returns false if no updates are to be made in the speech outputs.
  3. componentDidMount() is the master function which listens to the action start, adds the eventListener start and end, and calls the speak() function if the prop play is true.
  4. componentWillUnmount() destroys the speech object and ends the speech.

Here’s a code snippet for Function componentDidMount()

componentDidMount () {
  
    const events = [
      { name: 'start', action: this.props.onStart }
    ]
    // Adding event listeners
    events.forEach(e => {
      this.speech.addEventListener(e.name, e.action)
    })

    this.speech.addEventListener('end', () => {
      this.setState({ started: false })
      this.props.onEnd()
    })

    if (this.props.play) {
      this.speak()
    }
  }
  1. We then add proper props validation in the following way to our VoicePlayer component.
VoicePlayer.propTypes = {
  play: PropTypes.bool,
  text: PropTypes.string,
  onStart: PropTypes.func,
  onEnd: PropTypes.func
};
  1. The next step is to pass the props from a listener view to the VoicePlayer component. Hence the listener here is the component MessageListItem.js from where the voice player is initialized.
  1. First step is to initialise the state.
this.state = {
      play: false,
    }
  onStart = () => {
    this.setState({ play: true });
  }
  onEnd = () => {
    this.setState({ play: false });
  }
  1. Next, we set play to true when we want to pass the props and the text which is to be said and append it to the message lists which have voice set as `true`
 { this.props.message.voice &&
      (<VoicePlayer
             play
             text={voiceOutput}
             onStart={this.onStart}
             onEnd={this.onEnd}
 />)}

Finally, our message lists with voice true will be heard on the speaker as they have been spoken on the microphone. To get access to the full code, go to the repository https://github.com/fossasia/chat.susi.ai or on our chat channel at gitter.im/fossasia/susi_webchat

Resources

  1. Speak-easy-synthesis repository http://mdn.github.io/web-speech-api/speak-easy-synthesis
  2. Web-speech-api repository https://github.com/mdn/web-speech-api/

Showing Offline and Online Status in SUSI Web Chat

A lot of times while chatting on SUSI Web Chat, one does not receive responses, this could either be due to no Internet connection or a down server.

For a better user experience, the user needs to be notified whether he is connected to the SUSI Chat. If one ever loses out on the Internet connection, SUSI Web Chat will notify you what’s the status through a snack bar.

Here are the steps to achieve the above:-

  1. The first step is to initialize the state of the Snackbar and the message.
this.state = {
snackopen: false,
snackMessage: 'It seems you are offline!'
}
  1. Next, we add eventListeners in the component which will bind to our browser’s window object. While this event can be added to any component in the App but we need it in the MessageSection particularly to show the snack bar.

The window object listens to any online offline activity of the browser.

  1. In the file MessageSection.react.js, inside the function componentDidMount()
  1. We initialize window listeners in our constructor section and bind them to the function handleOnline and handleOffline to set states to the opening of the SnackBar.
// handles the Offlines event
window.addEventListener('offline', this.handleOffline.bind(this));  

// handles the Online event
window.addEventListener('online', this.handleOnline.bind(this));
  1. We then create the handleOnline and handleOffline functions which sets our state to make the Snackbar open and close respectively.
// handles the Offlines event
window.addEventListener('offline', this.handleOffline.bind(this));  

// handles the Online event
window.addEventListener('online', this.handleOnline.bind(this));
  1. Next, we create a Snackbar component using the material-ui snackbar component.

The Snackbar is visible as soon as the the snackopen state is set to true with the message which is passed whether offline or online.

<Snackbar
       autoHideDuration={4000}
       open={this.state.snackopen}
       message={this.state.snackMessage}
 />

To get access to the full code, go to the repository https://github.com/fossasia/chat.susi.ai

Resources

Implementing Login Functionality in SUSI Web Chat

SUSI Web Chat is fully equipped with all the accounting features which are being provided by the SUSI.AI API. This blog discloses all the API features one needs to know to embed the Login functionality in SUSI Web Chat.

  1. To embed the Login feature, first we create a form using material-ui.com components with the followng fields
    1. Email
    2. Password
    3. Note: We can also chose a Custom Server while logging in, here I have used the Standard Server ie. http://api.susi.ai to make the user Login

The form can be made with the help of the following fields

  • TextField for Email, props to be passed
    • Name – email
    • Value – this.state.email which gets the value of the current email
    • floatingLabelText is Email,
    • errorText is the message which we want to show when the email does not match the regex or its empty.

Code Snippet –

<TextField name="email" value={this.state.email} onChange={this.handleChange} errorText={this.emailErrorMessage}    floatingLabelText="Email" />
  • PasswordField for Password
    • Name – password
    • Value – this.state.password which gets the value of the current email
    • floatingLabelText is Password,
    • errorText is the message which we want to show when the password is not filled.

Code Snippet-

<PasswordField name='password' value={this.state.password} onChange={this.handleChange} errorText={this.passwordErrorMessage}   floatingLabelText='Password' />
  • The next elements are RadioButton groups taken from material-ui.com. This ensures the user signs into a standard server or even to a custom server. This is not compulsory as of now.
  • And lastly we need a submit button, which is disabled until all the fields are filled.

Code Snippet –

<RaisedButton label="Login" type="submit" labelColor="#fff" disable={!this.state.validForm} />

For the full form, check out this file at Login.react.js

  1. A Sample UI could be as shown in the image
  2. Next after creating the Login Screen, we make the onSubmit prop which is to be hooked up with another function called handleSubmit. An example code snippet from Login.react.js
 handleSubmit = (e) => {
        e.preventDefault();
        // Get the trimmed values from the fields
        var email = this.state.email.trim();
        var password = this.state.password.trim();
        // Set the default server to login
        let BASE_URL = defaults.Server;
            // handle all the details of the chosen server
        let serverUrl = this.state.serverUrl;
        if(serverUrl.slice(-1) === '/'){
            serverUrl = serverUrl.slice(0,-1);
        }
        if(serverUrl !== ''){
            BASE_URL = serverUrl;
        }
// if email and password is filled return true
        if (!email || !password) { return this.state.isFilled; }
// Check the regex of email
        let validEmail = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(email); 
// Pass the parameters to the loginEndPoint
        let loginEndPoint =
            BASE_URL+'/aaa/login.json?type=access-token&login=' +
            this.state.email + '&password=' + this.state.password;
        // If email and password is filled and valid call AJAX
        if (email && validEmail) {
            // AJAX Calls
        }
    }
    1. Then we make the Ajax Calls and store the created token from hitting the URL at http://api.susi.ai/aaa/login.json?type=access-token&login=EMAIL&password=PASSWORD. We store the cookie in browser and generate a session for the user using a package ‘universal-cookies’.
$.ajax({
    url: loginEndPoint,
    dataType: 'jsonp',
    jsonpCallback: 'p',
    jsonp: 'callback',
    crossDomain: true,
    success: function (response) {
        cookies.set('serverUrl', BASE_URL, { path: '/' });
        let accessToken = response.access_token;
        let state = this.state;// Adding the current State
        let time = response.valid_seconds; // Get the valid time of the cookie
        state.isFilled = true; // Set isFilled to true
        state.accessToken = accessToken; // Get the token
        state.success = true; // Set Success to true
        state.msg = response.message; // Get the server message
        state.time = time; // Get the time in the state
        this.setState(state); // Set the  state with the values
/* Pass the token to the binding function handleOnSubmit passing the arguments - token and the valid time */
        this.handleOnSubmit(accessToken, time);
    }.bind(this),
    error: function (errorThrown) {
        let msg = 'Login Failed. Try Again';
        let state = this.state;
        state.msg = msg;
        this.setState(state);
    }.bind(this)
});

 

    1. We then fire up the handleOnSubmit(accessToken, time) which saves the token for the given expiration time from the server.

Here’s the sample code

handleOnSubmit = (loggedIn, time) => {
        let state = this.state;
        if (state.success) {
            cookies.set('loggedIn', loggedIn, { path: '/', maxAge: time }); // set the cookie in the browser to maintain the loggedIn state
            this.props.history.push('/', { showLogin: false });
            window.location.reload();// reload after the loggedIn cookie creation
        }
        else {
            this.setState({
                error: true,
                accessToken: '',
                success: false
            });
        }
    }
  1. We then check the access token and redirect him based on his login state. This is handled in MessageSection.react.js
import Cookies from 'universal-cookie';
const cookies = new Cookies();
if (cookies.get('loggedIn')) {
    //Show all functionalities of loggedIn state
}
else {
//Redirect user to login page
}

 

To have a look at the full project, visit https://github.com/fossasia/chat.susi.ai and feel free to contribute. To test the project visit http://chat.susi.ai

Resources