Build a group chat app with Angular and Dialogflow
You will need Node and npm installed on your machine. A basic understanding of JavaScript will be helpful. This tutorial requires Angular 6+.
Introduction
Over the past few years, advances in machine learning, especially natural language processing (NLP), have made it easy for computers to analyse and derive meaning from human language in a smart way. With this, developers can now create smaller, simpler and more intuitive natural language processing software.
In this tutorial, we’ll demonstrate how to build a group chat using Pusher with an Angular application. We will also build a basic chatbot using the Dialogflow conversation platform formerly know as api.ai by Google. This bot will engage in a simple conversation with any user within the group chat.
Prerequisites
We are going to make use of the following dependencies:
-
Angular 6+: a platform that makes it easy to build applications with the web. Angular combines declarative templates, dependency injection, end to end tooling, and integrated best practices to solve development challenges.
-
Pusher: a hosted service that makes it super-easy to add realtime data and functionality to web and mobile applications. It has different products based on the need of your application.
-
Bootstrap 4: an open source toolkit for developing with HTML and CSS. It includes HTML and CSS based design templates for typography, forms, buttons, tables, navigation, modals, image carousels and many other, as well as optional JavaScript plugins.
-
Dialogflow: an engine for building conversational experiences. It leverages Google’s machine learning and natural language processing technology, thereby giving developers the ability to utilize a user-friendly interface to build chatbots and deploy them to any platform.
Please ensure you have Node and npm installed before starting the tutorial.
No knowledge of Angular is required, but a basic understanding of Javascript (not necessarily Typescript) may be helpful.
Let’s build our group chat
Bootstrapping with Angular-cli:
npm install -g @angular/cli
ng new botAndPusher
cd botAndPusher
We installed a command line interface to scaffold and build Angular apps globally. It exposes ng
in our terminal for us to make use of the commands available. To confirm everything went well, run the below command on your terminal within the newly created app folder /botAndPusher
.
ng serve
You should see this:
Installing dependencies:
npm install bootstrap pusher-js
Open angular.json
within our application root folder and update the projects.botAndPusher.architect.build.styles
field to look like so:
"styles": [
"../node_modules/bootstrap/dist/css/bootstrap.css",
"styles.css"
]
Open src/styles.css
and add the below CSS to it:
/* You can add global styles to this file, and also import other style files */
.box-shadow { box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); }
We added a utility style for adding shadows to our div.
Next, we need to create two services to manage our Pusher instance (we don’t have to always create a new instance of Pusher anytime we want to use it, we can use just one instance of it throughout the entire app) and our chat (interaction with the remote server and vice versa for chat messages):
ng g s services/pusher --spec false
ng g s services/chat --spec false
ng g i interfaces/ichat
We used another command of Angular CLI, which in full is ng generate service path/name
. This creates a folder if it doesn’t already exist called services/
and place our service files there. We also used --spec false
to let the Angular CLI know we don’t need to generate a test file for the service file. The last comand generates an employee interface to help give an idea of what an employee object should have.
Open src/app/services/pusher.service.ts
and update it to look like the one below:
import { Injectable } from '@angular/core';
import * as Pusher from 'pusher-js';
// this is here to discourage the instantiating of pusher any where its
// needed, better to reference it from one place
@Injectable()
export class PusherService {
private _pusher: any;
constructor() {
this._pusher = new Pusher('API_KEY', {
cluster: 'CLUSTER',
encrypted: true
});
}
// any time it is needed we simply call this method
getPusher() {
return this._pusher;
}
}
We insert the free API_KEY
and CLUSTER
we get after signing up and creating a channel app with Pusher.
After creating an app from Pusher’s dashboard, navigate to App Keys tab to see your app credentials
To ensure that connection traffic is encrypted, we set encrypted
to the Boolean true
in our app. Read more about client configuration here.
Open src/app/interfaces/ichat.ts
and update it to look like so:
export interface IChat {
id: string;
displayName: string;
email: string;
type: 'human' | 'joined';
message: string;
createdAt: string;
isMe: boolean;
}
From the above interface, every chat message must contain an ID, display name (used to display who sent the message), and a type (whether not the speaker is human).
Let’s create our chat service to help send messages to others, connect to Pusher’s channel and to join the group.
// src/app/services/chat.service.ts
import { Injectable } from '@angular/core';
import { PusherService } from './pusher.service';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import {tap} from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class ChatService {
user: {displayName: string, email: string};
private _endPoint = 'http://localhost:2000'; // normally you use environment.ts
private _channel: any;
constructor(private _pusherService: PusherService, private _http: HttpClient) {
this._channel = this._pusherService.getPusher().subscribe('chat-group');
}
join(param): Observable<any> {
return this._http.post(`${this._endPoint}/join`, param)
.pipe(tap(data => {
this.user = param;
}));
}
sendMessage(message: string): Observable<any> {
const param = {
message,
type: 'human',
...this.user
};
return this._http.post(`${this._endPoint}/message`, param);
}
getChannel() {
return this._channel;
}
}
We’ve created three different methods to help us interact with others within our service file.
join
: this is like registration of new users to get their display name and email address which we are going to be using to send messages to others within the group. If you observe, anytime we join, we automatically use RxJs tap (formerly called do) to pipe the response and do something with the response before sending it to the caller (save our user information for later usage).sendMessage
: this attaches the user object we saved after joining to the message before sending it to our server.getChannel
: is returning our Pusher’s channel so we can easily subscribe to any message coming fromchat-group
channel
Let’s make our services available in the app. Add them to app.module.ts
, along with the [HTTPClientModule](https://angular.io/guide/http)
since it was used within chat.service.ts
for http requests. ReactiveFormsModule and FormsModule are going to be used later to build our forms :
// src/app/app.module.ts
import { ChatService } from './services/chat.service';
import { PusherService } from './services/pusher.service';
import { HttpClientModule } from '@angular/common/http';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
...
imports: [
...,
HttpClientModule,
FormsModule,
ReactiveFormsModule
],
providers:[ChatService, PusherService]
Angular components to join and chat with others
We’ll now create two more components to join and chat with others in the group chat:
ng g c myDetails -is --spec false
ng g c chat --spec false
Using the Angular CLI, we generated a component with no test attached as explained earlier. It will create a folder for each containing a .ts
and a .html
. The -is
command means don’t create a .css
file. We will use inline styling within our .ts
.
Let’s begin with my details component, it should look like our gif above. Open my-details/my-details.component.html
and update it to look like so:
<!-- src/app/my-details/my-details.component.html -->
<h6 class="pb-2 mb-0">My Details</h6>
<form [formGroup]="detailsForm" (ngSubmit)="onSubmit()" novalidate>
<div class="form-group">
<label for="name">Display Name</label>
<input formControlName="displayName" type="text" class="form-control" id="name" placeholder="Alien">
<small *ngIf="detailsForm.get('displayName').hasError('required')" class="form-text text-danger">Display name is required.</small>
</div>
<div class="form-group">
<label for="email">Email</label>
<input formControlName="email" type="email" class="form-control" id="email" placeholder="test@example.com">
<small *ngIf="detailsForm.get('email').hasError('required')" class="form-text text-danger">Email is required.</small>
<small *ngIf="detailsForm.get('email').hasError('email')" class="form-text text-danger">Email is invalid.</small>
</div>
<button type="submit" [disabled]="loader || detailsForm.invalid" class="btn btn-primary">{{loader?'Joining':'Join Group'}}</button>
</form>
We have created our template using Bootrap’s forms with Angular’s reactive form directives such as formControlName
and [formGroup]
which allows us to easily validate the form.
Let’s wire the template above to be functional, open my-details/my-details.component.ts
// src/app/my-details/my-details.component.ts
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { ChatService } from '../services/chat.service';
@Component({
selector: 'app-my-details',
templateUrl: './my-details.component.html',
styles: []
})
export class MyDetailsComponent implements OnInit {
detailsForm: FormGroup;
loader;
constructor(private _fb: FormBuilder, private _chatService: ChatService) { }
ngOnInit() {
this._createForm();
}
/**
* create our reactive form here
*/
private _createForm() {
this.detailsForm = this._fb.group({
displayName: ['', Validators.required],
email: ['', [Validators.required, Validators.email]]
});
}
/**
* Join the fun
*/
onSubmit() {
const param = this.detailsForm.value;
this._chatService.join(param)
.subscribe((resp) => {
this.loader = false;
},
(error) => {
console.error(error);
this.loader = false;
});
}
}
We used the _createForm
method to instantiate our reactive form and set the validations needed. On submit of our form we call the onSubmit
method, which was used ((ngSubmit)="onSubmit()"
) within our my-details.component.html
.
Let’s make use of our just created component within our app.component.html
.
<!-- src/app/app.component.html -->
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<a class="navbar-brand" href="#">
<img src="assets/angular.svg" width="30" height="30" class="d-inline-block align-top" alt="">
Angular Group Chat with Pusher And Dialogflow Chat Bot
</a>
</nav>
<div class="container">
<p align="center">
<img src="assets/pusher.svg" width="30" height="100" width="60" alt="Pusher">
</p>
<div class="row justify-content-center">
<div class="col-md-4">
<div class="my-3 p-3 bg-white rounded box-shadow">
<app-my-details></app-my-details>
</div>
</div>
</div>
</div>
Run ng serve
on your terminal to see our component in action, we should get something like so:
You can join the group, but we need a screen to see who joined and also to chat with others within the group.
Open chat.component.html
and update it to look like so:
<!-- src/app/chat/chat.component.html -->
<h6 class="pb-2 mb-0">Group Chat <button class="btn btn-sm btn-primary">invite others</button></h6>
<div class="chats">
<div class="chatbox">
<div *ngFor="let chat of chats">
<div class="message sent" *ngIf="chat.type!=='joined' && chat.isMe">
{{chat.message}}
<span class="metadata">
<span class="time">{{chat.createdAt | date: 'HH:mm aaa'}}</span>
</span>
</div>
<div class="message received" *ngIf="chat.type!=='joined' && !chat.isMe">
<strong>{{chat.displayName}}</strong> <br>
{{chat.message}}
<span class="metadata">
<span class="time">{{chat.createdAt | date: 'HH:mm aaa'}}</span>
</span>
</div>
<p align="center" class="joined" *ngIf="chat.type==='joined'">
<span class="rounded bg-primary text-white">{{chat.displayName}} Joined</span>
</p>
</div>
</div>
<div class="d-flex flex-row">
<input [(ngModel)]="message" type="text" class="form-control" placeholder="Enter message" style="margin-right: 10px">
<button [disabled]="!message || sending" (click)="sendMessage(message)" class="btn btn-primary"> {{sending ? 'Sending' : 'Send'}}</button>
</div>
</div>
From the above template, we are making use of the *ngFor
directive to loop through our chats and display them based on the type of chat message. We have three types of chat messages:
- Joined: once you join a chat from my details component, we receive a chat message of type
joined
. Doing this gives us the privilege to style it or handle how to display who just joined the chat to others. - From me: if the chat message is from me which means my email address from my details matches with the email address within the chat message. Here, you observe we didn’t show the name of the person sending the message since it’s me, and the styling we used
message sent
class. - Not me: if the chat message is not from me and it’s not joined, which means another user from the group sent a message. Here, we added the user display name, so we can easily identify who sent the message.
Now, let’s add styling to our chat component, to differentiate the three types of chat messages. Open chat.component.css
and update it to look like so:
/* src/app/chat/chat.component.css */
.chats {
height: calc(100% - 12px);
position: relative;
z-index: 0;
}
.chats .chatbox {
height: calc(100% - 68px);
overflow-x: hidden;
padding: 0 16px;
margin-bottom: 5px;
}
.joined {
clear: both;
line-height: 18px;
font-size: 15px;
margin: 8px 0;
padding: 8px;
}
.joined span {
padding: 5px
}
/* Messages*/
.message {
color: #000;
clear: both;
line-height: 18px;
font-size: 15px;
padding: 8px;
position: relative;
margin: 8px 0;
max-width: 85%;
word-wrap: break-word;
z-index: -1;
}
.message:after {
position: absolute;
content: "";
width: 0;
height: 0;
border-style: solid;
}
.metadata {
display: inline-block;
float: right;
padding: 0 0 0 7px;
position: relative;
bottom: -4px;
}
.metadata .time {
color: rgba(0, 0, 0, .45);
font-size: 11px;
display: inline-block;
}
.message:first-child {
margin: 16px 0 8px;
}
.message.received {
background: #ccc;
border-radius: 0px 5px 5px 5px;
float: left;
}
.message.received:after {
border-width: 0px 10px 10px 0;
border-color: transparent #ccc transparent transparent;
top: 0;
left: -10px;
}
.message.sent {
background: #e1ffc7;
border-radius: 5px 0px 5px 5px;
float: right;
}
.message.sent:after {
border-width: 0px 0 10px 10px;
border-color: transparent transparent transparent #e1ffc7;
top: 0;
right: -10px;
}
.metadata {
display: inline-block;
float: right;
padding: 0 0 0 7px;
position: relative;
bottom: -4px;
}
.metadata .time {
color: rgba(0, 0, 0, .45);
font-size: 11px;
display: inline-block;
}
Before we try out our chat component, let’s incorporate Pusher and connect to our server to send messages to others.
Integrating Pusher and message sending to server
Pusher is going to listen to the channel we created above for chat and give us realtime updates of any triggered event related to our chat group.
Open chat.component.ts
and update it to look like so:
// src/app/chat/chat.component.ts
import { Component, OnInit } from '@angular/core';
import { IChat } from '../interfaces/ichat';
import { ChatService } from '../services/chat.service';
@Component({
selector: 'app-chat',
templateUrl: './chat.component.html',
styleUrls: ['./chat.component.css']
})
export class ChatComponent implements OnInit {
chats: IChat[] = [];
message: string;
sending: boolean;
constructor(private _chatService: ChatService) { }
ngOnInit() {
// subscribe to pusher's event
this._chatService.getChannel().bind('chat', data => {
if (data.email === this._chatService.user.email) {
data.isMe = true;
}
this.chats.push(data);
});
}
sendMessage(message: string) {
this.sending = true;
this._chatService.sendMessage(message)
.subscribe(resp => {
this.message = undefined;
this.sending = false;
}, err => {
this.sending = false;
} );
}
}
From the above code, we subscribed to our Pusher channel for chat-group
when the component is being called by Angular (ngOnInit). We also check if the message that is coming from Pusher contains the same email as the one we used in joining the chat group. If so we add an extra key called isMe:boolean
, which is going to be true when it matches.
We used sendMessage
method to send messages to our server and toggling a variable called sending
to notify our template to show what is going on to the users. Then we reset the message to undefined to clear the message input field on our chat.component.html
.
Finalizing our Angular application
We need to toggle the chat component only when the user joined our group. So, initially, any new user sees my details component, which is going to get the display name and email address before entering the chat component to chat with others.
Open app.component.ts
and update it to look like so:
import { Component } from '@angular/core';
import { ChatService } from './services/chat.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
constructor(public chatService: ChatService) {
}
}
What we simply did above, is just to make use of our chat service, which contains the user
object. This user
object is gotten anytime a user joins our chat group. If it is undefined
it means the current user trying to use our group chat is not registered or hasn’t joined. Let’s make use of it to toggle which view/component a user is going to see.
Open app.component.html
and update it to look like so:
<!-- src/app/app.component.html -->
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<a class="navbar-brand" href="#">
<img src="assets/angular.svg" width="30" height="30" class="d-inline-block align-top" alt="">
Angular Group Chat with Pusher And Dialogflow Chat Bot
</a>
</nav>
<div class="container">
<p align="center">
<img src="assets/pusher.svg" width="30" height="100" width="60" alt="Pusher">
</p>
<div class="row justify-content-center">
<div class="col-md-8" [hidden]="!chatService.user">
<div class="my-3 p-3 bg-white rounded box-shadow">
<app-chat></app-chat>
</div>
</div>
<div class="col-md-4" [hidden]="chatService.user">
<div class="my-3 p-3 bg-white rounded box-shadow">
<app-my-details></app-my-details>
</div>
</div>
</div>
</div>
We’ve used the user
within our chatService
to handle what the user sees using angular’s hidden
directive. If the user is defined we hide our my-details
component. Else, we show it to get details of the new user. If there is no user, we hide our chat
component and wait for registration through the my-details component.
Run ng serve
. You should get something like this:
Observe our above result, we were unable to join our chat group because the server specified in our chatService._endPoint
does not exist. Let’s go over to setting up our server.
Setting up our Node server with Pusher
Our Node server is going to handle when a new message is sent from our frontend Angular application and also when a new user is joining the chat group.
Open your terminal and run the below commands:
npm install express body-parser cors pusher dotenv shortid
touch server.js
touch .env
What we have done above is to install our node dependencies, creating our server file which is going to hold our APIs using Express and .env
to help manage our environment variables using the dotenv module.
Open server.js
and update it to look like so:
// server.js
const express = require('express')
const bodyParser = require('body-parser')
const Pusher = require('pusher')
const cors = require('cors')
require('dotenv').config()
const shortId = require('shortid')
const app = express()
app.use(cors())
app.use(bodyParser.urlencoded({ extended: false }))
app.use(bodyParser.json())
const pusher = new Pusher({
appId: process.env.PUSHER_APP_ID,
key: process.env.PUSHER_APP_KEY,
secret: process.env.PUSHER_APP_SECRET,
cluster: 'eu',
encrypted: true
})
app.post('/message', async (req, res) => {
// simulate actual db save with id and createdAt added
const chat = {
...req.body,
id: shortId.generate(),
createdAt: new Date().toISOString()
}
// trigger this update to our pushers listeners
pusher.trigger('chat-group', 'chat', chat)
res.send(chat)
})
app.post('/join', (req, res) => {
const chat = {
...req.body,
id: shortId.generate(),
type: 'joined',
createdAt: new Date().toISOString()
}
// trigger this update to our pushers listeners
pusher.trigger('chat-group', 'chat', chat)
res.send(chat)
})
app.listen(process.env.PORT || 2000, () => console.log('Listening at 2000'))
We have created two APIs:
POST /messsage
: send a message to others in the group by triggering an event with Pusher so our chat component (which subscribed to it earlier on) can receive the message.POST /join
: anytime a new user wants to join our chat we send a trigger to the others that a user just joined the chat with atype:'joined``'
and thereq.body
which containsemail
anddisplayName
.
Before running our server to test our group chat, we need to set our .env
variables used in our server.js
. Open .env
and make it look like so:
PUSHER_APP_ID="APP_ID"
PUSHER_APP_KEY="APP_KEY"
PUSHER_APP_SECRET="APP_SECRET"
You can get your own .env
config within your Pusher’s channel application dashboard. Navigate to the Getting Started tab, under Add this to your server column, select .env
to copy, then paste in your .env
file.
Let’s run our server. Start the frontend if it is not already running. Open your terminal and run this:
node server.js
Now, let’s go over to our application to test it out:
Our application is working as it should 😊, but we are not done yet. The next section is going to show how to add a chatbot user to our group.
Integrating Dialogflow with our server
We are going to add a chatbot to our group chat to help with getting live scores of football matches. We can easily trigger our bot or call out our bot with the below command:
/bot message-here
For example: /bot who are you ?
Let’s head over to Dialogflow to setup our chatbot. Open your Dialogflow dashboard after creating a free account:
Click on Create Agent button to create our chatbot. We are going to name our agent Pusher-chatbot
Creating small talk
For a quick start, we can easily program our chatbot for common chat style questions in the small talk panel. This will give our bot a basic level of interactivity with the users. By default, there are responses with predefined phrases within the small talk panel. Go ahead and customize the response as you deem fit. For now, we are going to keep it simple and respond to few questions:
Enable and save it. You can use the Try it now by your right to test our chatbot responses. Let’s get our access token for connecting to our chatbot from our Node server:
Click on the icon within the red circle to view our agent settings where we can locate our API keys. Open our .env
and add our client access token not the developer token, as our use case we are going to be doing more of querying of our chatbot via APIs. Read more.
PUSHER_APP_ID="APP_ID"
PUSHER_APP_KEY="APP_KEY"
PUSHER_APP_SECRET="APP_SECRET"
DIALOG_ACCESS_TOKEN="CLIENT_ACCESS_TOKEN"
Let’s install Axios for sending HTTP request from our Node server to Dialogflow endpoints:
npm install axios
touch dialogFlow.js
Open dialogFlow.js
and update it to look like so:
// dialogFlow.js
const axios = require('axios')
const accessToken = process.env.DIALOG_ACCESS_TOKEN
const baseURL = 'https://api.dialogflow.com/v1/query?v=20150910'
module.exports = {
send (message) {
const data = {
query: message,
lang: 'en',
sessionId: '123456789!@#$%'
}
return axios.post(baseURL, data, {
headers: { Authorization: `Bearer ${accessToken}` }
})
}
}
We used Axios to send a post request to Dialogflow, passing our message to the bot as query
. Let’s make use of our utility function above to communicate with out chatbot. Open server.js
and update it to look like so:
const express = require('express')
const bodyParser = require('body-parser')
const Pusher = require('pusher')
const cors = require('cors')
require('dotenv').config()
const shortId = require('shortid')
const dialogFlow = require('./dialogFlow')
const app = express()
app.use(cors())
app.use(bodyParser.urlencoded({ extended: false }))
app.use(bodyParser.json())
const pusher = new Pusher({
appId: process.env.PUSHER_APP_ID,
key: process.env.PUSHER_APP_KEY,
secret: process.env.PUSHER_APP_SECRET,
cluster: 'eu',
encrypted: true
})
app.post('/message', async (req, res) => {
// simulate actual db save with id and createdAt added
const chat = {
...req.body,
id: shortId.generate(),
createdAt: new Date().toISOString()
}
// trigger this update to our pushers listeners
pusher.trigger('chat-group', 'chat', chat)
// check if this message was invoking our bot, /bot
if (chat.message.startsWith('/bot')) {
const message = chat.message.split('/bot')[1]
const response = await dialogFlow.send(message)
pusher.trigger('chat-group', 'chat', {
message: `@${chat.displayName} ${
response.data.result.fulfillment.speech
}`,
displayName: 'Bot User',
email: 'bot@we.com',
createdAt: new Date().toISOString(),
id: shortId.generate()
})
}
res.send(chat)
})
app.post('/join', (req, res) => {
const chat = {
...req.body,
id: shortId.generate(),
type: 'joined',
createdAt: new Date().toISOString()
}
// trigger this update to our pushers listeners
pusher.trigger('chat-group', 'chat', chat)
res.send(chat)
})
app.listen(process.env.PORT || 2000, () => console.log('Listening at 2000'))
What we have done is to update the POST /message
endpoint to handle any message sent from our angular application. The goal is to check where /bot
is used to begin a message, then intercept those type of messages after sending a Pusher event to others that a user just called a bot. The code within our POST /message
checks if the message starts with /bot
. If so we take the second part of the message as what the user is trying to ask the bot to do. After getting the main message we send it to Dialogflow through our utility function. Dialogflow returns a response containing what our bot processed from the message. Then we triggered Pusher’s event to send a message to our group as a bot user using the response from Dialogflow.
Let’s re-run our server like so:
node server.js
Open our angular app and try sending this message:
/bot who are you?
Conclusion
Here, we have been able to build a group chat with a chatbot user. The intention was to give you a general building block that can be built on, explored, and improved to build an amazing chatbot that can do much more with group chats.
I hope this tutorial was helpful and gave you enough information required to start building bots tailored for other use cases, as you deem fit in your organization.
The source code for this tutorial can be found on GitHub. Feel free to explore and add more features.
22 May 2018
by Christian Nwamba