Emoji suggestions in an Angular chat application using sentiment analysis
You should have Node and npm installed on your machine. A basic understanding of Angular and Node is required.
Introduction
Sentiment analysis is the process of computationally identifying and categorizing opinions expressed in a piece of text, especially to determine whether the writer’s attitude towards a particular topic, product, etc. is positive, negative, or neutral. Using sentiment analysis, we can suggest emojis to be used as replies to messages based on the context of the received message.
Using Angular, you can extend the template language with your components and use a wide array of existing components, and with Pusher we can enable realtime messaging in the chat using pusher’s pub/sub pattern.
We’ll be building a realtime chat application using Pusher, Angular and the sentiment library for emoji suggestions based on the context of messages received.
Using our application users can get emoji suggestions as replies to received messages while engaging in a chat with a third party
Here’s a demo of the final product:
Prerequisites
To follow this tutorial a basic understanding of Angular and Node.js is required. Please ensure that you have Node and npm installed before you begin.
We’ll be using these tools to build out our application:
We’ll be sending messages to the server and using Pusher’s pub/sub pattern, we’ll listen to and receive messages in realtime. To make use of Pusher you’ll have to create an account here.
After account creation, visit the dashboard. Click Create new Channels app, fill out the details, click Create my app, and make a note of the details on the App Keys tab.
Let’s build!
Setup and folder structure
Using the Angular CLI (command line interface) provided by the Angular team, we’ll initialize our project. To initialize the project, first, install the CLI by running npm install @angular/cli
in your terminal. NPM is a package manager used for installing packages. It will be available on your PC if you have Node installed.
To create a new Angular project using the CLI, open a terminal and run:
ng new angular-pusher-chat --style=scss
The command is simply telling the CLI to create a new project called angular-pusher-chat
and it should make use of the CSS pre-processor SCSS rather than CSS for styling.
Open the newly created folder angular-pusher-chat
, and your folder structure should look something like this:
angular-pusher-chat/
e2e/
node_modules/
src/
app/
app.component.html
app.component.ts
app.component.css
...
Open a terminal inside the project folder and start the application by running ng serve
or npm start
. If you open your browser and visit the link http://localhost:4200
you should see the screenshot below if everything went well.
Building our server
Now that we have our Angular application running, let’s build out a basic part of our server.
To do this we’ll need to install Express. Express is a fast, unopinionated, minimalist web framework for Node.js. We’ll use this to receive requests from our Angular application.
To install express, run npm install express
in a terminal in the root folder of your project.
Create a file called server.js
in the root of the project and update it with the code snippet below
// server.js
require('dotenv').config();
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
const port = process.env.PORT || 4000;
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header(
'Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept'
);
next();
});
app.listen(port, () => {
console.log(`Server started on port ${port}`);
});
We referenced three packages that haven’t been installed, body-parser
, pusher
and dotenv
. Install these packages by running the following command in your terminal.
npm i body-parser pusher dotenv
- body-parser is a package used to parse incoming request bodies in a middleware before your handlers, available under the
req.body
property. - dotenv is a zero-dependency module that loads environment variables from a
.env
file into process.env. This package is used so sensitive information like theappId
andsecret
aren’t added to our codebase directly. - The dotenv package will load the variables provided in our
.env
file into our environment. - CORS: The calls to our endpoint will be coming in from a different origin, therefore we need to make sure we include the CORS headers (
Access-Control-Allow-Origin
). If you are unfamiliar with the concept of CORS headers, you can find more information here.
If you noticed, I added the dotenv
package at the start of our file. This is because we need to load the variables as early as possible to make them available throughout the application.
Also, you’ll notice that we installed Pusher library as a dependency. Follow the steps above to create a Pusher account if you haven’t done so already
Let’s create a .env
file to load the variables we’ll be needing into the Node environment. Create the file in the root folder of your project and update it with the code below.
// .env
PUSHER_APP_ID=<APP_ID>
PUSHER_KEY=<PUSHER_KEY>
PUSHER_SECRET=<PUSHER_SECRET>
Please ensure you replace the placeholder values above with your Pusher appId
, key
and secret
.
This is a standard Node application configuration, nothing specific to our app.
Sending messages
To enable users send and receive messages, we’ll create a route to handle incoming requests. Update your server.js
file with the code below.
// server.js
require('dotenv').config();
const express = require('express');
const bodyParser = require('body-parser');
const Pusher = require('pusher');
const pusher = new Pusher({
appId: process.env.PUSHER_APP_ID,
key: process.env.PUSHER_KEY,
secret: process.env.PUSHER_SECRET,
cluster: 'eu',
encrypted: true,
});
...
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header(
'Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept'
);
next();
});
app.post('/messages', (req, res) => {
const { body } = req;
const { text, id } = body;
const data = {
text,
id,
timeStamp: new Date(),
};
pusher.trigger('chat', 'message', data);
res.json(data);
});
...
- We created a
POST /messages
route which, when hit, triggers a Pusher event. - We used object destructuring to get the body of the request, we also got the
text
andid
in the request body sent by the user. - The
data
object contains thetext
andid
sent by the user. We’ll also added a timestamp to the it. - The trigger is achieved using the
trigger
method which takes the trigger identifier(chat
), an event name (message
), and a payload(data
). - The payload can be any value, but in this case, we have a JS object.
- We still go ahead to respond with an object containing the
data
variable we created.
Sentiment analysis
Sentiment analysis refers to the use of natural language processing, text analysis, computational linguistics, and biometrics to systematically identify, extract, quantify, and study effective states and subjective information. - Wikipedia
You can read up a bit about sentiment analysis using the following links below:
Using sentiment analysis, we’ll analyse the messages sent to determine the attitude of the sender. With the data gotten from the analysis, we’ll determine the emojis to suggest to the user.
We’ll use the Sentiment JavaScript library for analysis. To install this library, open a terminal in the root folder of your project and run the following command.
npm install sentiment
We’ll update our POST /messages
route to include analysis of the messages being sent in. Update your server.js
with the code below.
// server.js
require('dotenv').config();
const express = require('express');
const bodyParser = require('body-parser');
const Pusher = require('pusher');
const Sentiment = require('sentiment');
const sentiment = new Sentiment();
...
app.post('/messages', (req, res) => {
const { body } = req;
const { text, id } = body;
const result = sentiment.analyze(text);
const comparative = result.comparative;
const tone =
comparative >= 0 ? (comparative >= 1 ? 'positive' : 'neutral') : 'negative';
const data = {
text,
id,
timeStamp: new Date(),
sentiment: {
tone,
score: result.score,
},
};
pusher.trigger('chat', 'message', data);
res.json(data);
});
...
- Include the sentiment library in the project
result
: here, we analyze the message sent in by the user to determine the context of the message.comparative
: this is the comparative score gotten after analyzing the message. This score is used to determine if a message ispositive
,negative
orneutral
.tone
: thetone
variable is the context of the message gotten after analysis. This will benegative
if the comparative score is below0
,neutral
if the score is above0
but below1
. The tone ispositive
if the comparative score is1
and above.- A new object(
sentiment
) property is added to the response data containing the message’s tone and score.
You can now start the server by running node server.js
in the terminal in the root folder of the project.
Chat view
Let’s begin to build out our chat interface. We’ll create a chat
component to hold the chat interface. We’ll create this using the CLI. Run ng generate component chat
in a terminal in the root folder of your project.
Update the recently created files as follows:
// chat.component.html
<div class="main">
<div class="chat-box">
<div class="message-area">
<div class="message"
*ngFor="let message of messages"
[ngClass]="getClasses(message.type)">
<p>{{message.text}}</p>
</div>
</div>
<div class="input-area">
<form (submit)="sendMessage()" name="messageForm" #messageForm="ngForm">
<input type="text" name="message" id="message" [(ngModel)]="message">
<button>
<span data-feather="send"></span>
</button>
</form>
</div>
</div>
</div>
In the code snippet above:
- We loop through the available
messages
in the.message-area
. - We have a form containing an input element and a submit button.
- We using an icon-set called feather-icons in our project. To include feather-icons in your project, simply add the cdn link in your
index.html
file.
// index.html
...
<script src="https://unpkg.com/feather-icons/dist/feather.min.js"></script>
</body>
...
Open the chat.component.ts
file and update with the code below:
// chat.component.ts
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { v4 } from 'uuid';
declare const feather: any;
interface Message {
id: string;
text: string;
timeStamp: Date;
type: string;
}
@Component({
selector: 'app-chat',
templateUrl: './chat.component.html',
styleUrls: ['./chat.component.scss'],
})
export class ChatComponent implements OnInit {
constructor(private http: HttpClient) {}
messages: Array<Message> = [];
message: string = '';
lastMessageId;
sendMessage() {
if (this.message !== '') {
// Assign an id to each outgoing message. It aids in the process of differentiat . ing between outgoing and incoming messages
this.lastMessageId = v4();
const data = {
id: this.lastMessageId,
text: this.message,
};
this.http
.post(`http://localhost:4000/messages`, data)
.subscribe((res: Message) => {
const message = {
...res,
// The message type is added to distinguish between incoming and outgoing messages. It also aids with styling of each message type
type: 'outgoing',
};
this.messages = this.messages.concat(message);
this.message = '';
});
}
}
// This method adds classes to the element based on the message type
getClasses(messageType) {
return {
incoming: messageType === 'incoming',
outgoing: messageType === 'outgoing',
};
}
ngOnInit() {
// Initialize feather icons
feather.replace();
}
}
sendMessage
: this method uses the native HttpClient
to make requests to the server. The POST
method takes a URL and the request body
as parameters. We then append the data returned to the array of messages. We make use of a package called uuid to give each message a unique id
.
Run npm install uuid
in a terminal inside your project’s root folder to install this package.
getClasses
: this method generates classes for a message element based on the messageType
In the ngOnInit
lifecycle, we initialize [feather](https://feathericons.com)
, our chosen icon set.
To make use of the HttpClient
service, we’ll need to import the HttpClientModule
into the app.module.ts
file. Also to make use of form-related directives, we’ll need to import the FormsModule
. So update your app module file as follows:
// app.module.ts
...
import { ChatComponent } from './chat/chat-component';
import {HttpClientModule} from '@angular/common/http';
import {FormsModule} from "@angular/forms";
...
@NgModule({
declarations: [AppComponent, ChatComponent],
imports: [BrowserModule, HttpClientModule, FormsModule],
providers: [],
bootstrap: [AppComponent],
})
...
Styling
Open the chat.component.scss
file and update it with the styles below:
// chat.component.scss
.main {
display: flex;
justify-content: center;
align-items: center;
min-height: 90vh;
.chat-box {
width: 300px;
max-height: 500px;
height: 500px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.12), 0 2px 4px 0 rgba(0, 0, 0, 0.08);
border-radius: 3% 3% 0 0;
position: relative;
padding-bottom: 40px;
.message-area {
max-height: 450px;
height: 450px;
overflow: auto;
padding: 20px 20px;
.message {
p {
color: white;
font-size: 13px;
font-weight: bold;
margin: 0px;
width: 45%;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
padding: 7px 10px;
margin: 5px 0;
}
}
.message.incoming {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
p {
border-radius: 0 11px 11px 11px;
background: goldenrod;
}
}
.message.outgoing {
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: flex-end;
p {
border-radius: 11px 11px 0 11px;
background: darkgrey;
}
}
}
}
}
This first SCSS snippet styles the .chat-area
. Including how messages should look. The next snippet will style the input area and the send button.
// chat.component.scss
.input-area {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 40px;
form {
display: flex;
height: 100%;
input {
width: 85%;
border: none;
background: rgba(0, 0, 0, 0.08);
padding: 5px 10px;
color: rgba(0, 0, 0, 0.3);
font-size: 14px;
font-weight: bold;
font-family: 'Titillium Web', sans-serif;
&:focus {
outline: none;
}
}
button {
width: 15%;
border: none;
background: darkslategray;
color: white;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
}
}
Introducing Pusher and sending messages
So far we have an application that allows users send messages but the messages aren’t delivered in realtime. To solve this problem, we’ll include the Pusher library.
Open a terminal in the root folder of the project and install the package by running the following command:
npm install pusher-js
To make the Pusher library available in our application, we’ll add the library as a third party script to be loaded by Angular CLI. All CLI config is stored in .angular-cli.json
file. Modify the scripts
property to include the link to pusher.min.js
.
// .angular-cli.json
...
"scripts": [
"../node_modules/pusher-js/dist/web/pusher.min.js"
]
...
Now that Pusher has been made available in our project, we’ll create a Pusher service to be used application wide. The Angular CLI can aid in the service creation. Open a terminal in your project’s root folder and run the following command.
ng generate service pusher
This command simply tells the CLI to generate a service named pusher
. Now open the pusher.service.ts
file and update it with the code below.
// pusher.service.ts
import { Injectable } from '@angular/core';
declare const Pusher: any;
@Injectable()
export class PusherService {
constructor() {
var pusher = new Pusher('<PUSHER_KEY>', {
cluster: 'eu',
encrypted: true,
});
this.channel = pusher.subscribe('chat');
}
channel;
public init() {
return this.channel;
}
}
- First, we initialize Pusher in the constructor.
- The
init
method returns the Pusher property we created.
Note: Ensure you replace the
PUSHER_KEY
string with your actual Pusher key.
To make the service available application wide, import it into the module file.
// app.module.ts
import {PusherService} from './pusher.service'
...
@NgModule({
....
providers: [PusherService],
....
})
We’ll make use of this service in our component, by binding to the message event and appending the returned message into the list of messages. This will be done in the ngOnInit
lifecycle.
// chat.component.ts
import { PusherService } from '../pusher.service';
...
// Include the PusherService in the component's constructor
constructor(private http: HttpClient, private pusher: PusherService){}
...
ngOnInit() {
feather.replace();
const channel = this.pusher.init();
channel.bind('message', (data) => {
if (data.id !== this.lastMessageId) {
const message: Message = {
...data,
type: 'incoming',
};
this.messages = this.messages.concat(message);
}
});
}
}
To test the realtime functionality of the application, open two browsers side-by-side and send messages. You should be able to receive a message sent from one browser on the other.
Now let’s place the chat component in the app.component.html
file. This will make our chat component available for viewing. Replace the contents of the file with the code below.
// app.component.html
<div>
<app-chat></app-chat>
</div>
Now we can view our application for the first time, it should currently be able to send messages back and forth in realtime.
Emoji suggestions
To display emoji suggestions during a chat session, we’ll make use of the sentiment
param being sent from the server as a response for each message request. The data being sent from the server should be similar to the snippet below.
{
id: '83d3dd57-6cf0-42dc-aa5b-2d997a562b7c',
text: 'i love pusher',
timeStamp: '2018-04-27T15:04:24.574Z'
sentiment: {
tone: 'positive',
score: 3
}
}
Generate an emoji
component that will hold the emoji section. This component will handle the display of emojis based on the tone of each message received. We’ll generate a new component using the CLI by running the following command in the terminal.
ng generate component emoji-panel
Replace the contents of the emoji-panel.component.html
file with the code snippet below.
// emoji-panel.component.html
<div class="emojis" [hidden]="!showEmojis" [attr.aria-hidden]="!showEmojis">
<div class="emoji-list positive" *ngIf="result.tone === 'positive'">
<span class="emoji" *ngFor="let emoji of emojiList.positive; let i = index;" (click)="onClick('positive', i)">
{{codePoint(emoji)}}
</span>
</div>
<div class="emoji-list neutral" *ngIf="result.tone === 'neutral'">
<span class="emoji" *ngFor="let emoji of emojiList.neutral; let j = index;" (click)="onClick('neutral', j)">
{{codePoint(emoji)}}
</span>
</div>
<div class="emoji-list negative" *ngIf="result.tone === 'negative'">
<span class="emoji" *ngFor="let emoji of emojiList.negative; let k = index;" (click)="onClick('negative', k)">
{{codePoint(emoji)}}
</span>
</div>
</div>
attr.aria-hidden
: here we set the accessibility attribute of the element to either true
or false
based on the showEmojis
variable.
// emoji-panel.component.ts
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-emoji-panel',
templateUrl: './emoji-panel.component.html',
styleUrls: ['./emoji-panel.component.scss'],
})
export class EmojiPanelComponent implements OnInit {
constructor() {}
@Input() result = {};
@Input() showEmojis: boolean = false;
@Output() onEmojiSelect: EventEmitter<string> = new EventEmitter();
emojiList = {
positive: [128512, 128513, 128536, 128516],
neutral: [128528, 128529, 128566, 129300],
negative: [128543, 128577, 128546, 128542],
};
codePoint(emojiCodePoint) {
return String.fromCodePoint(emojiCodePoint);
}
onClick(reaction, index) {
const emoji = this.emojiList[reaction][index];
this.onEmojiSelect.emit(emoji);
}
ngOnInit() {}
}
emojiList
: this is an object containing a list of emojis that’ll be suggested. There’s a list for each message tone.
showEmojis
: an input variable from the parent component(chat
) to determine the visibility of the emoji panel
onClick
: this method takes to parameters. The reaction
param is used to select the list of emojis to check for the provided index
. The selected emoji is then emitted to the parent component.
// emoji-panel.component.scss
.emojis {
&[aria-hidden='true'] {
animation: slideOutDown 0.7s;
}
&[aria-hidden='false'] {
animation: slideInUp 0.7s;
}
.emoji-list {
display: flex;
.emoji {
margin: 0 5px;
cursor: pointer;
}
}
}
@keyframes slideInUp {
from {
transform: translate3d(0, 100%, 0);
visibility: visible;
}
to {
transform: translate3d(0, 0, 0);
}
}
@keyframes slideOutDown {
from {
transform: translate3d(0, 0, 0);
}
to {
visibility: hidden;
transform: translate3d(0, 100%, 0);
}
}
After creating the emoji-panel
component, the next step is to add it to our chat
component. Update the chat component with the code snippets below.
// chat.component.html
...
<div>
...
<div class="emo-area">
<app-emoji-panel [showEmojis]="showEmojis" [result]="score" (onEmojiSelect)="selectEmoji($event)"></app-emoji-panel>
</div>
<div class="input-area">
<form (submit)="sendMessage()" name="messageForm" #messageForm="ngForm">
<input type="text" name="message" id="message" [(ngModel)]="message">
<button>
<span data-feather="send"></span>
</button>
</form>
</div>
</div>
Update the chat.component.scss
with the following styles:
.main{
...
.chat-box{
...
}
.emo-area{
position: absolute;
bottom: 40px;
left: 0;
width: 100%;
padding: 3px 10px;
}
.input-area{
...
}
}
Open the chat.component.ts
file and update it like so:
// chat.component.ts
...
messages: Array<Message> = [];
message: string = '';
lastMessageId;
showEmojis = false;
score = {
tone: '',
score: 0,
};
sendMessage() {
if (this.message !== '') {
this.lastMessageId = v4();
this.showEmojis = false;
...
}
}
selectEmoji(e) {
const emoji = String.fromCodePoint(e);
this.message += ` ${e}`;
this.showEmojis = false;
}
...
ngOnInit() {
feather.replace();
const channel = this.pusher.init();
channel.bind('message', (data) => {
if (data.id !== this.lastMessageId) {
const message: Message = {
...data,
type: 'incoming',
};
this.showEmojis = true;
this.score = data.sentiment;
this.messages = this.messages.concat(message);
}
});
}
...
selectEmoji
: this method appends the selected emoji to the current message and then hides the emoji panel.
In the Pusher event callback, we set the showEmojis
property to true
. In the same callback, we assign the datas
sentiment property to the score
variable.
By now our application should provide emoji suggestions for received messages.
Note: Both the server(
node server
) and the Angular dev server(ng serve
) must be running to get the application working. Run both command in two separate terminal sessions if you haven’t done so already.
Conclusion
Using sentiment analysis library, we’ve been able to suggest emojis as replies for received messages and with the help of Pusher our application can send messages back and forth in realtime. You can view the source code for the demo here.
9 May 2018
by Christian Nwamba