Skip to main content

Angular chatbot

· 9 min read
Simon Porritt

Yesterday we documented the process of building a chatbot app in 3 hours and 37 minutes. We wrote that app in "vanilla" JS but we know one of the features of the Toolkit that people love is its deep integration with libraries such as Angular, Vue, React and Svelte. In today's post we're going to look at porting the vanilla chatbot to Angular.

Angular chatbot - JsPlumb, leading alternative to GoJS and JointJS

Initialization

ng new angular-chatbot
cd angular-chatbot
npm i @jsplumbtoolkit/browser-ui-angular

This gives us a brand new Angular app, and we've imported the Toolkit's Angular integration package.

Import the Toolkit's Angular module

The first step is to update app-module.ts and import the Toolkit:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { jsPlumbToolkitModule} from "@jsplumbtoolkit/browser-ui-angular"

import { AppComponent } from './app.component';

@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
jsPlumbToolkitModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

Setup the html and css

Next, we're going to clear out app.component.html and copy in the HTML from our vanilla chatbot:

<div class="jtk-demo-main" id="jtk-demo-chatbot">

<!-- main drawing area -->
<div class="jtk-demo-canvas">
<!-- controls -->
<div class="jtk-controls-container"></div>
<!-- miniview -->
<div class="miniview"></div>
</div>
<div class="jtk-demo-rhs">

<!-- the node palette -->
<div class="sidebar node-palette">
<div class="jtk-chatbot-palette-item" data-type="start">
start
</div>
<div class="jtk-chatbot-palette-item" data-type="end">
end
</div>
<div class="jtk-chatbot-palette-item" data-type="message">
message
</div>
<div class="jtk-chatbot-palette-item" data-type="input">
input
</div>
<div class="jtk-chatbot-palette-item" data-type="choice">
choice
</div>
</div>
<!-- node/edge inspector -->
<div class="inspector"></div>
</div>

</div>

We've got placeholders for a controls component, miniview, inspector and node palette in this HTML. We'll be replacing each of those elements in turn, as with the Toolkit's Angular integration there is an Angular component available for each of these things.

CSS

In styles.css we added these few lines:

@import "../node_modules/@jsplumbtoolkit/browser-ui/css/jsplumbtoolkit.css";
@import "../node_modules/@jsplumbtoolkit/browser-ui/css/jsplumbtoolkit-controls.css";

jsplumb-surface, .jtk-surface {
width:100%;
height:100%;
}

The first two import the Toolkit defaults stylesheet and the stylesheet for the controls component. The jsplumb-surface, .jtk-surface rule ensures that the Surface component fills the available width and height.

We then copied everything from the vanilla chatbot's app.css to styles.css.

Copy the dataset

In our final take of the vanilla chatbot we had an example JSON dataset, so we're copying that into /src/assets.

Create node components

When you use the Toolkit's Angular integration, you create a component to represent each node type - or one component representing several, it's up to you. Since it's a straightforward process we're just going to create one component for each type from our vanilla chatbot - START, END, MESSAGE, INPUT and CHOICE.

Here's what the MESSAGE component code looks like:

import {Component} from "@angular/core"
import {BaseNodeComponent} from "@jsplumbtoolkit/browser-ui-angular"

@Component({
template:`<div class="jtk-chatbot-message" data-jtk-target="true">
<div class="jtk-delete" (click)="removeNode()"></div>
{{obj['message']}}
<div class="connect" data-jtk-source="true"></div>
</div>`
})
export class MessageComponent extends BaseNodeComponent { }

The template was ported pretty much directly from the vanilla chatbot's message component template, but note the jtk-delete element: it has a click listener on it, which calls removeNode(), which is a method exposed on BaseNodeComponent in the Toolkit. When you render a node via a component in the Toolkit's Angular integration there are several convenience methods available that can access the underlying model object.

We repeated this process for the other node types.

Configure app component

We made a few updates to AppComponent to get it ready for rendering to a Surface:

  @ViewChild(jsPlumbSurfaceComponent) surfaceComponent!: jsPlumbSurfaceComponent

toolkit!: BrowserUIAngular
surface!: Surface

constructor(public $jsplumb:jsPlumbService) { }

ngAfterViewInit() {

this.surface = this.surfaceComponent.surface
this.toolkit = this.surfaceComponent.toolkit

this.toolkit.load({
url:'/assets/dataset.json'
})
}

}

Specifically:

  • We inject the jsplumb service into the constructor. The jsplumb service has a number of useful "global" methods.
  • We declared a surface component view child, that we will retrieve in our afterViewInit
  • We declared an ngAfterViewInit, which gets a reference to the underlying surface and the toolkit, and then loads our dataset.

Add surface component

The Toolkit's angular integration offers a Surface component that you can embed in your template, which takes care of instantiating a Toolkit instance and rendering it to a Surface. We added this to our app component's html:

<jsplumb-surface surfaceId="surface"
toolkitId="toolkit"
[toolkitParams]="toolkitParams"
[renderParams]="renderParams"
[view]="view"></jsplumb-surface>

We had to pass it a few things:

  • surfaceId A unique ID for this surface. Only a consideration if you have more than one Surface in your app
  • toolkitId A unique ID for this Toolkit instance. Only a consideration if you have more than one Toolkit in your app
  • toolkitParams Options for the Toolkit constructor. In this app we want to tell the Toolkit the name of the property that identifies a node's ports (see below)
  • renderParams These are things such as layout, defaults, whether to zoom to fit after data load, etc. They are declared on AppComponent - we'll show them below.
  • view This is the mapping from node/edge/port types to components, behaviour and appearance.

toolkitParams

portDataProperty tells the Toolkit the name of the property that identifies a node's ports, and its inclusion here allows us to use the Toolkit's addPort and removePort methods.

toolkitParams = {
portDataProperty:"choices"
}

renderParams

We copied these from the vanilla chatbot's render call:

renderParams:AngularRenderOptions = {
zoomToFit:true,
consumeRightClick:false,
defaults:{
endpoint:BlankEndpoint.type,
anchor:AnchorLocations.Continuous
},
layout:{
type:AbsoluteLayout.type
}
}

view

The full view for this app looks like this:

view = {
nodes:{
[SELECTABLE]:{
events:{
[EVENT_TAP]:(p:{obj:Base}) => {
this.toolkit.setSelection(p.obj)
}
}
},
[START]:{
parent:SELECTABLE,
component:StartComponent
},
[END]:{
parent:SELECTABLE,
component:EndComponent
},
[ACTION_MESSAGE]:{
parent:SELECTABLE,
component:MessageComponent
},
[ACTION_INPUT]:{
parent:SELECTABLE,
component:InputComponent
},
[ACTION_CHOICE]:{
parent:SELECTABLE,
component:ChoiceComponent
}
},
edges:{
default:{
overlays:[
{
type:PlainArrowOverlay.type,
options:{
location:1,
width:10,
length:10
}
}
],
label:"{{label}}",
useHTMLLabel:true,
events:{
[EVENT_TAP]:(p:{edge:Edge}) => {
this.toolkit.setSelection(p.edge)
}
}
}
},
ports:{
choice:{
anchor:[AnchorLocations.Left, AnchorLocations.Right ]
}
}
}

The edge and port definitions are identical to those from the vanilla chatbot. The nodes follow the same structure - a parent which responds to a tap event, and a mapping for each type - but instead of supplying a template for each node type in Angular we supply a component.

At this point the app is up and running and it renders a dataset:

But it doesnt have the controls component, and you cannot yet drag new nodes, inspect nodes or add/remove choices.

Controls component

It is straightforward to add a controls component - there's one available in the Angular integration. We just replace this:

<div class="jtk-controls-container"></div>    

with this:

<jsplumb-controls surfaceId="surface"></jsplumb-controls>

Dragging new nodes

This is also easy to configure, via the jsplumb-surface-drop component. We replace this:

<div class="sidebar node-palette">
...
</div>

with:

<div class="sidebar node-palette"
jsplumb-surface-drop
selector=".jtk-chatbot-palette-item"
surfaceId="surface"
[dataGenerator]="dataGenerator">
<div *ngFor="let nodeType of nodeTypes" class="jtk-chatbot-palette-item" [attr.data-type]="nodeType.type" title="Drag to add new">{{nodeType.label}}</div>
</div>

We need to provide a couple of things to this code - an array called nodeTypes, and a dataGenerator, which is responsible for extracting the initial payload from a node that is being dragged. We'll copy that from the vanilla chatbot:

dataGenerator(el:Element) {
const type = el.getAttribute("data-type")
const base = { type }
if (type === ACTION_MESSAGE) {
Object.assign(base, { message:"Send a message"})
} else if (type === ACTION_INPUT) {
Object.assign(base, { message:"Grab some input", prompt:"please enter input"})
} else if (type === ACTION_CHOICE) {
Object.assign(base, {
message:"Make a selection!",
choices:[
{ id:"1", label:"Choice 1"},
{ id:"2", label:"Choice 2"},
]
})
}

return base
}

for nodeTypes we have:

nodeTypes = [
{type:START, label:"Start"},
{type:END, label:"End"},
{type:ACTION_MESSAGE, label:"Message"},
{type:ACTION_INPUT, label:"Input"},
{type:ACTION_CHOICE, label:"Choice"}
]

Add/remove/inspect choices

In the vanilla app we handled the addition or removal of choices via the modelEvents argument we passed to the render call. In Angular we can take advantage of the fact that each node is a component to isolate this behaviour - in this case, to the Choice component. So we'll add a few methods to it:

addChoice() {
this.toolkit.setSelection(this.toolkit.addPort(this.getNode(), {
id:uuid(),
label:"Choice"
}))
}

removeChoice(id:string) {
this.toolkit.removePort(this.getNode(), id)
}

inspectChoice(id:string) {
this.toolkit.setSelection(this.getNode().getPort(id))
}

toolkit is a class member of BaseNodeComponent. We updated the choice node's template with click handlers to invoke these methods:

<div class="jtk-chatbot-choice" data-jtk-target="true">
<div class="jtk-delete" (click)="removeNode()"></div>
{{obj['message']}}
<div class="jtk-choice-add" (click)="addChoice()"></div>
<div class="jtk-chatbot-choice-option" *ngFor="let choice of obj['choices']"
data-jtk-source="true"
data-jtk-port-type="choice"
[attr.data-jtk-port]="choice['id']"
(click)="inspectChoice(choice.id)">
{{choice['label']}}
<div class="jtk-choice-delete" (click)="removeChoice(choice.id)"></div>
</div>
</div>

Inspecting nodes

The final piece we need to port is the node inspector. Again we can copy the templates - for the most part - from the vanilla chatbot. We create an angular component for it:

export class InspectorComponent implements AfterViewInit {

currentType:string = ''

@Input() surfaceId:string

// @ts-ignore
inspector:Inspector

CHOICE_PORT = "choice-port"
EDGE = "edge"

constructor(private $jsplumb:jsPlumbService, private el:ElementRef, private changeDetector:ChangeDetectorRef) { }

ngAfterViewInit(): void {
this.$jsplumb.getSurface(this.surfaceId, (surface) => {
this.inspector = new Inspector({
container:this.el.nativeElement,
surface,
renderEmptyContainer:() => {
this.currentType = ''
},
refresh:(obj:Base, cb:() => void) => {
this.currentType = isNode(obj) ? obj.type : isPort(obj) ? this.CHOICE_PORT : this.EDGE
setTimeout(cb, 0)
this.changeDetector.detectChanges()
}
})
})
}

}

This component maintains a currentType class member, which it sets based upon whatever is passed in to the refresh method we pass to the Inspector. After setting the current type we tell the change detector to detect changes, ie. to repaint the inspector component.

The template for this component writes out different HTML based on currentType :

<div *ngIf="currentType === ''"></div>
<div *ngIf='currentType === "${START}"'></div>
<div *ngIf='currentType === "${END}"'></div>

<div *ngIf='currentType === "${ACTION_MESSAGE}"' class="jtk-chatbot-inspector">
<span>Message:</span>
<input type="text" jtk-att="message" placeholder="message"/>
</div>

<div *ngIf='currentType === "${ACTION_INPUT}"' class="jtk-chatbot-inspector">
<span>Message:</span>
<input type="text" jtk-att="${PROPERTY_MESSAGE}" placeholder="message"/>
<span>Prompt:</span>
<input type="text" jtk-att="${PROPERTY_PROMPT}" placeholder="prompt"/>
</div>

<div *ngIf='currentType === "${ACTION_CHOICE}"' class="jtk-chatbot-inspector">
<span>Message:</span>
<input type="text" jtk-att="${PROPERTY_MESSAGE}" placeholder="message"/>
</div>

<div *ngIf="currentType === CHOICE_PORT" class="jtk-chatbot-inspector">
<span>Label:</span>
<input type="text" jtk-att="${PROPERTY_LABEL}" jtk-focus placeholder="enter label..."/>
</div>

<div *ngIf="currentType === EDGE" class="jtk-chatbot-inspector">
<div>Label</div>
<input type="text" jtk-att="${PROPERTY_LABEL}"/>
</div>

Final thoughts

That's the whole thing ported now. We reused the vast majority of the Toolkit setup code, HTML and CSS from the vanilla chatbot, but we used Angular components to render each node, which enabled us to isolate the code specific to each node - deletion of a node, for all node types, and also adding/removing/inspecting choices. The Toolkit is a powerful library on its own, but when coupled with a library integration it becomes even more so.

You can find the code for this in apps/chatbot/angular at https://github.com/jsplumb/jsplumbtoolkit-applications


Start a free trial

Sending your evaluation request...

Interested in the concepts discussed in this article? Start a free trial and see how jsPlumb can help bring your ideas to market in record time.


Get in touch!

The jsPlumb Toolkit is a very versatile library with a rich feature set. Making these starter applications is a chance for us to demonstrate and illustrate how it might look for your company. If you'd like to discuss any of the ideas/concepts in this article we'd love to hear from you - drop us a line at hello@jsplumbtoolkit.com.