Build a live comment feature with sentiment analysis using Flask and Vue

Introduction

In this tutorial, we’ll see how we can get the overall feeling of our users after they might have read our post and added their comments. We’ll build a simple blog where users can comment. Then we process the comment to determine the percentages of people that find the post interesting and those who don't.

As technologies are advancing, the way we process data is also taking a huge turn around. Taking advantage of natural language processing, we can determine from a group of comments, how our users feel about our blog post.

We also don’t have to reload a page to see a new comment from a blog post. We can make comments visible in realtime to every user.

We’ll be using Channels, Vue.js and Flask to build the app.

Here is a preview of what the final app will look like:

flask-comments-sentiment-demo

Prerequisite

This tutorial uses the following:

You should have some familiarity with Python development to follow along with this tutorial. If you are not familiar with Vue but still want to follow along, you can go through the basics of Vue in the documentation to get you up to speed in a couple of minutes.

Before we start, let’s get your environment ready. Check that you have the appropriate installation and setup on your machine.

Open up a terminal on your machine and execute the below code:

    $ python --version

If you have a Python 3.6+ installed on your machine, you will have a similar text printed out as python 3.6.0. If you got an output similar to “Command not found”, you need to install Python on your machine. Head over to Python’s official website to download and get it installed.

If you have gotten all that installed, let's proceed.

Creating a Pusher account

We'll use Pusher Channels to handle all realtime functionalities. Before we can start using Pusher Channels, we need to get our API key. We need an account to be able to get the API key.

Head over to Pusher and log in to your account or create a new account if you don’t have one already. Once you are logged in, create a new app and then copy the app API keys.

Setting up the backend app

Let’s create our backend app that will be responsible for handling all communication to Pusher Channels and getting the sentiment of a comment.

Create the following files and folder in a folder named live-comment-sentiment in any convenient location on your system:

1live-comment-sentiment
2      ├── .env
3      ├── .flaskenv
4      ├── app.py
5      ├── requirements.txt
6      ├── static
7      │   ├── custom.js
8      │   └── style.css
9      └── templates
10          └── index.html
11          └── base.html

Creating a virtual environment

It’s a good idea to have an isolated environment when working with Python. virtualenv is a tool to create an isolated Python environment. It creates a folder which contains all the necessary executables to use the packages that a Python project would need.

From your command line, change your directory to the Flask project root folder, execute the below command:

    $ python3 -m venv env

Or:

    $ python -m venv env

The command to use depends on which associates with your Python 3 installation.

Then, activate the virtual environment:

    $ source env/bin/activate

If you are using Windows, activate the virtualenv with the below command:

    > \path\to\env\Scripts\activate

This is meant to be a full path to the activate script. Replace \path\to with your correct path name.

Next, add the Flask configuration setting to the .flaskenv file:

1FLASK_APP=app.py
2    FLASK_ENV=development

This will instruct Flask to use app.py as the main entry file and start up the project in development mode.

Now, add your Pusher API keys to the .env file:

1PUSHER_APP_ID=app_id
2    PUSHER_APP_KEY=key
3    PUSHER_APP_SECRET=secret
4    PUSHER_APP_CLUSTER=cluster

Make sure to replace app_id, key, secret and cluster with your own Pusher keys which you have noted down earlier.

Next, create a Flask instance by adding the below code to app.py:

1# app.py
2    
3    from flask import Flask, jsonify, render_template, request
4    from textblob import TextBlob
5    import pusher
6    import os
7    
8    app = Flask(__name__)
9    
10    @app.route('/')
11    def index():
12        return render_template('index.html')
13    
14    # run Flask app
15    if __name__ == "__main__":
16        app.run()

In the code above, after we instantiate Flask using app = Flask(__name__), we created a new route - / which renders an index.html file from the templates folder.

Now, add the following python packages to the requirements.txt file:

1Flask==1.0.2
2    python-dotenv==0.8.2
3    pusher==2.0.1
4    textblob==0.15.1

The packages we added:

  • python-dotenv: this library will be used by Flask to load environment configurations files.
  • pusher: this is the Pusher Python library that makes it easy to interact with its API.
  • textblob: a Python library which provides a simple API for common natural language processing (NLP).

Next, install the library by executing the below command:

    $ pip install -r requirements.txt

Once the packages are done installing, start up Flask:

    $ flask run

If there is no error, our Flask app will now be available on port 5000. If you visit http://localhost:5000, you will see a blank page. This is because the templates/index.html file is empty, which is ok for now.

Setting up TextBlob

To get the sentiment from comments, we’ll use the TextBlob Python library which provides a simple API for common natural language processing (NLP). We already have the library installed. What we’ll do now is install the necessary data that TextBlob will need.

From your terminal, make sure you are in the project root folder. Also, make sure your virtualenv is activated. Then execute the below function.

1# Download NLTK corpora
2    $ python -m textblob.download_corpora lite

This will download the necessary NLTK corpora (trained models).

Initialize the Pusher Python library

Initialize the Pusher Python library by adding the below code to app.py just after the app = Flask(__name__) line:

1# app.py
2    
3    pusher = pusher.Pusher(
4        app_id=os.getenv('PUSHER_APP_ID'),
5        key=os.getenv('PUSHER_APP_KEY'),
6        secret=os.getenv('PUSHER_APP_SECRET'),
7        cluster=os.getenv('PUSHER_APP_CLUSTER'),
8        ssl=True)

Now we are fully set.

Setting up the frontend

We’ll create a simple page for adding comments. Since we won’t be building a full blog website, we won’t be saving the comments to a database.

Adding the base layout

We’ll use the template inheritance approach to build our views, which makes it possible to reuse the layouts instead of repeating some markup across pages.

Add the following markup to the templates/base.html file:

1<!-- /templates/base.html  -->
2    
3    <!doctype html>
4    <html lang="en">
5      <head>
6          <!-- Required meta tags -->
7        <meta charset="utf-8">
8        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
9        <!-- Bootstrap CSS -->
10        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css">
11        <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
12        <title>Live comment</title>
13      </head>
14      <body>
15          <div class="container" id="app">
16               {% block content %}  {% endblock %}
17            </div>
18        </div>
19        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
20        <script src="https://js.pusher.com/4.1/pusher.min.js"></script>
21        <script src="{{ url_for('static', filename='custom.js')}}"></script>
22      </body>
23    </html>

This is the base layout for our view. All other views will inherit from the base file.

In this file, we have added some libraries. This includes:

The blog page

This will serve as the landing page of the application. Add the following to the templates/index.html file:

1<!-- /templates/index.html  -->
2    
3    
4    {% extends 'base.html' %}
5    
6    {% block content %}
7    <div class="grid-container">
8        <header class="header text-center"> 
9            <img src="https://cdn1.imggmi.com/uploads/2018/10/13/1d5cff977fd6e3aac498e581ef681a1a-full.png">
10        </header>
11        <main class="content">         
12           <div class="content-text">
13              Our pioneering and unique technology is based on state-of-the-art <br/> 
14              machine learning and computer vision techniques. Combining deep neural <br/>
15              networks and spectral graph theory with the computing... <br/>
16            </div>
17        </main>
18        <section class="mood">
19            <div class="row">
20                <div class="col text-center"> 
21                    <div class="mood-percentage">[[ happy ]]%</div>
22                    <div>Happy</div> 
23                </div>
24                <div class="col text-center">
25                    <div class="mood-percentage">[[ neutral ]]%</div>
26                    <div>Neutral</div> 
27                </div>
28                <div class="col text-center"> 
29                    <div class="mood-percentage">[[ sad ]]%</div>
30                    <div>Sad</div> 
31                </div>
32            </div>
33        </section>
34        <section class="comment-section">
35            <div v-for="comment in comments">
36               <comment 
37                    :comment="comment"
38                    v-bind:key="comment.id"
39                >
40                </comment>       
41            </div>
42        </section>
43        <section class="form-section">
44            <form class="form" @submit.prevent="addComment">
45                <div class="form-group">
46                    <input
47                        type="text" 
48                        class="form-control" 
49                        v-model="username"
50                        placeholder="Enter username">
51                </div>
52                <div class="form-group">
53                    <textarea 
54                      class="form-control" 
55                      v-model="comment" 
56                      rows="3"></textarea>
57                </div>
58                <button type="submit" class="btn btn-primary btn-block">Add comment</button>
59            </form>
60        </section>
61    </div>
62    {% endblock %}

In the preceding code:

  • In the <section class="mood">… </section>, we added three placeholders - [[ happy ]], [[ neutral ]] and [[ sad ]], which is the percentages of the moods of users who added comments. These placeholders will be replaced by their actual values when Vue takes over the page DOM (mounted).

    Notice we are using [[ ]] instead of the normal Vue placeholders - {{ }}. This is because we are using Jinja2 template that comes bundled with Flask to render our page. The Jinja2 uses {{ }} placeholder to hold variables that will be substituted to their real values and so do Vue by default. So to avoid conflicts, we will change Vue to use [[ ]] instead.

  • In the <section class="comment-section"> section, we are rendering the comments to the page.

  • Next, is the <section class="form-section">… </section>, where we added a form for adding new comments. Also in the inputs fields, we declare a two-way data binding using the v-model directive.

  • In the form section - <form class="form" @submit.prevent="addComment">…, notice that we have the @submit.prevent directive. This will prevent the form from submitting normally when the user adds a new comment. Then we call the addComment function to add a comment. We don’t have the addComment function declared anywhere yet. We’ll do this when we initialize Vue.

Now, add some styles to the page. Add the below styles to the static/style.css file:

1body {
2        width: 100%;
3        height: 100%;
4    }
5    .grid-container {
6        display: grid;
7        grid-template-rows: 250px auto auto 1fr;
8        grid-template-columns: repeat(3, 1fr);
9        grid-gap: 20px;
10        grid-template-areas:
11            '. header .'
12            'content content content'
13            'mood mood mood'
14            'comment-section comment-section comment-section'
15            'form-section form-section form-section';
16    }
17    .content {
18        grid-area: content;
19    }
20    .comment-section {
21        grid-area: comment-section;
22    }
23    .content-text {
24       font-style: oblique;
25       font-size: 27px;
26    }
27    .mood {
28        grid-area: mood;
29    }
30    .header {
31        grid-area: header;
32    }
33    .form-section {
34        grid-area: form-section;
35    }
36    .comment {
37        border: 1px solid rgb(240, 237, 237);
38        border-radius: 4px;
39        margin: 15px 0px 5px 60px;
40        font-family: monospace;
41    }
42    .comment-text {
43        padding-top: 10px;
44        font-size: 17px;
45    }
46    .form {
47        margin-top: 50px;
48    }
49    .mood-percentage {
50       border: 1px solid gray;
51       min-height: 50px;
52       padding-top: 10px;
53       font-size: 30px;
54       font-weight: bolder;
55    }

Now we have all our user interface ready. If you visit the app URL again, you will see a similar page as below:

flask-comments-sentiment-interface

Initializing Channels

Now let’s initialize Channels. Since we have added the Pusher JavaScript library already, we’ll go ahead and initialize it.

Add the below code to the static/custom.js file:

1// Initiatilze Pusher JavaScript library
2    var pusher = new Pusher('<PUSHER-APP-KEY>', {
3        cluster: '<CLUSTER>',
4        forceTLS: true
5    });

Replace <PUSHER-APP-KEY> and <CLUSTER> with your correct Pusher app details you noted down earlier.

Creating the comment component

If you view the /templates/index.html file, in the <section class="comment-section"> section, you will notice we are calling the <comment> component which we have not created yet. We need to create this component. Also, notice inside the file, we are calling the v-for (v-for="comment in comments") directive to render the comments.

Let’s create the component. Add the below code to static/custom.js:

1Vue.component('comment', {
2        props: ['comment'],
3        template: `
4            <div class="row comment"> 
5                <div class="col-md-2">
6                    <img 
7                       src="https://cdn1.imggmi.com/uploads/2018/10/13/1d5cff977fd6e3aac498e581ef681a1a-full.png" 
8                       class="img-responsive" 
9                       width="90" 
10                       height="90"
11                    >
12                </div>
13                <div class="col-md-10 comment-text text-left" v-html="comment.comment">             </div>
14            </div>
15        `
16    })

Initialize Vue

Now let’s initialize Vue to take over the DOM manipulation.

Add the below code to the static/custom.js file:

1var app = new Vue({
2        el: '#app',
3        delimiters: ['[[', ']]'],
4        data: {
5          username: '',
6          comment: '',
7          comments: [],
8          happy: 0,
9          sad: 0,
10          neutral: 0,
11          socket_id: ""
12        },
13        methods: {},
14        created () {},
15    })

In the preceding code:

  • We initialize Vue using var app = new Vue(… passing to it a key-value object.
  • Next, we tell Vue the part on the page to watch using el: '``#app'. The #app is the ID we have declared in the /templates/base.html.
  • Next, using delimiters: ['[[', ']]'],, we change the default Vue delimiter from {{ }} to [[ ]] so that it does not interfere with that of Jinja2.
  • Then we defined some states using data: {…..
  • Finally, we have methods: {}, and created () {},. We’ll add all the function we’ll declare inside the methods: {} block and then the created () {} is for adding code that will execute once Vue instance is created.

Next, add a function to update the sentiment score. Add the below code to the methods: {} block of the static/custom.js file:

1updateSentiments () {
2                // Initialize the mood to 0
3                let [happy, neutral, sad] = [0, 0, 0];
4                
5                // loop through all comments, then get the total of each mood
6                for (comment of this.comments) {
7                   if (comment.sentiment > 0.4) {
8                      happy++;
9                   } else if (comment.sentiment < 0) {
10                      sad++;
11                   } else {
12                       neutral++;
13                   }
14                }
15                
16                const total_comments = this.comments.length;
17                
18                // Get the percentage of each mood
19                this.sad = ((sad/total_comments) * 100).toFixed();
20                this.happy = ((happy/total_comments) * 100).toFixed();
21                this.neutral = ((neutral/total_comments) * 100).toFixed()
22                
23                // Return an object of the mood values
24                return {happy, neutral, sad}
25            },

In the code above, we created a function that will loop through all the comments to get the number of each mood that appeared. Then we get the percentage of each mood then return their corresponding values.

Next, add a function to add a new comment. Add the below code to the methods: {} block right after the code you added above:

1addComment () {
2               
3               fetch("/add_comment", {
4                    method: "post",
5                    headers: {
6                    'Accept': 'application/json',
7                    'Content-Type': 'application/json'
8                    },
9                    body: JSON.stringify({
10                        id: this.comments.length,
11                        username: this.username,
12                        comment: this.comment,
13                        socket_id: this.socket_id
14                    })
15                })
16                .then( response => response.json() )
17                .then( data => {
18                    // Add the new comment to the comments state data
19                    this.comments.push({
20                        id: data.id,
21                        username: data.username,
22                        comment: data.comment,
23                        sentiment: data.sentiment
24                    })
25                    
26                    // Update the sentiment score
27                    this.updateSentiments();
28                 })
29                 
30               this.username = "";
31               this.comment = "";
32            },

Here, we created a function that makes a request to the /add_comment route to get the sentiment of a comment. Once we receive a response, we add the comment to the comments state. Then we call this.updateSentiments() to update the sentiment percentage. This function will be called each time a user wants to add a new comment.

Next, let’s make comments visible to others in realtime. Add the below code to the created () {} block in the static/custom.js:

1// Set the socket ID
2            pusher.connection.bind('connected', () => {
3                this.socket_id = pusher.connection.socket_id;
4            });
5            
6            // Subscribe to the live-comments channel
7            var channel = pusher.subscribe('live-comments');
8            
9            // Bind the subscribed channel (live-comments) to the new-comment event
10            channel.bind('new-comment', (data) => {
11               this.comments.push(data);
12               
13               // Update the sentiment score
14               this.updateSentiments();
15            });

Get sentiments from comments and make comments realtime

Now, let’s add a function to get the sentiment of a message and then trigger a new-comment event whenever a user adds a comment. Add the below code to app.py

1# ./api/app.py
2    
3    @app.route('/add_comment', methods=["POST"])
4    def add_comment():
5        # Extract the request data
6        request_data = request.get_json()
7        id = request_data.get('id', '')
8        username = request_data.get('username', '')
9        comment = request_data.get('comment', '')
10        socket_id = request_data.get('socket_id', '')
11        
12        # Get the sentiment of a comment
13        text = TextBlob(comment)
14        sentiment =  text.polarity
15        
16        comment_data = {
17            "id": id,
18            "username": username,
19            "comment": comment,
20            "sentiment": sentiment,
21        }
22        
23        #  Trigger an event to Pusher
24        pusher.trigger(
25            "live-comments", 'new-comment', comment_data, socket_id
26        )
27        
28        return jsonify(comment_data)

The sentiment property returns a tuple of the form (polarity, subjectivity) where polarity ranges from -1.0 to 1.0 and subjectivity ranges from 0.0 to 1.0. We will only use the polarity property.

In the pusher.trigger(…, method, we are passing the socket_id so that the user triggering the event won't get back the data sent.

Testing the app

Congrats! Now we have our live comments with sentiments. To test the app, open the app in your browser on two or more different tabs, then add comments and see them appear in realtime on other tabs.

Here is some sample comment you can try out:

  • The post is terrible! - Sad (Negative)
  • I love the way this is going - Happy (Positive)
  • This is amazingly simple to use. What great fun! - Happy (Positive)

If you are getting an error or nothing is working. Stop the server (Press CTRL+C) and then restart it ($ flask run).

Conclusion

In this tutorial, we built a live comment with sentiment analysis. We used Vue for DOM manipulation, Flask for the server side and Channels for realtime functionality. We used the TextBlob python library to detect mood from text.