Skip to main content

2 posts tagged with "decision tree"

View All Tags

· 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 - JavaScript and Typescript diagramming library that fuels exceptional UIs

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

Not a user of jsPlumb but thinking of checking it out? There's a whole lot more to discover and it's a great time to get started!


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.

· 24 min read
Simon Porritt

Recently we had the idea that a good addition to our list of starter applications might be a chatbot demo. In this post we're going to track the process in real time, or real-ish time, at least. We're going to write this in ES6, and we'll use esbuild to create our bundle.

The source code of this demo is available on Github. If you're not a licensee of the Toolkit, you can start your 30 day free trial of jsPlumb here.

So - here goes.

8:10am - initialisation

Create a new folder for this app and initialise npm

mkdir chatbot
cd chatbot
npm init

Install esbuild, Babel and jsPlumb Toolkit

npm i --save-dev esbuild babel-cli babel-preset-env

npm i @jsplumbtoolkit/browser-ui

Add a few build tasks

We're building to ES6, both minified and unminified, and also to ES5 (via Babel)

"scripts": {
"build-es6": "./node_modules/.bin/esbuild demo.js --target=es2016 --bundle --format=iife --outfile=bundle.js",
"build-es6-min": "./node_modules/.bin/esbuild demo.js --target=es2016 --bundle --minify --format=iife --outfile=bundle-min.js",
"transpile-es5": "./node_modules/.bin/babel bundle.js -o bundle-es5.js",
"transpile-es5-min": "./node_modules/.bin/babel bundle-min.js -o bundle-min-es5.js",
"build": "npm run build-es6;npm run transpile-es5",
"build-min": "npm run build-es6-min;npm run transpile-es5-min",
"serve": "./node_modules/.bin/http-server ."
}

8:15am - create structure

Create index.html, demo.js and app.css

The head of our index.html will include the Toolkit's default CSS file, as well the CSS for the controls component - more on which later.

<head>
<title>jsPlumbToolkit - Chatbot builder</title>
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1" />
<meta http-equiv="content-type" content="text/html;charset=utf-8" />
<link rel="stylesheet" href="node_modules/@jsplumbtoolkit/browser-ui/css/jsplumbtoolkit.css">
<link rel="stylesheet" href="node_modules/@jsplumbtoolkit/browser-ui/css/jsplumbtoolkit-controls.css">
<link rel="stylesheet" href="./app.css">
</head>

In the body we've got a basic structure of a main canvas, plus placeholders for our miniview and controls component, and then a sidebar from which we will drag new nodes onto the canvas.

<body>

<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>
<!-- node/edge inspector -->
<div class="inspector"></div>
</div>


</div>

<!-- the demo code -->
<script src="bundle.js"></script>

</body>

8:25am - create demo.js and run an initial build

import {
newInstance,
ready,
AbsoluteLayout
} from "@jsplumbtoolkit/browser-ui"


ready(() => {

const canvas = document.querySelector(".jtk-demo-canvas")
const palette = document.querySelector(".node-palette")
const inspector = document.querySelector(".inspector")

const toolkit = newInstance()

const surface = toolkit.render(canvas, {
layout:{
type:AbsoluteLayout.type
}
})

})
npm run build

...and, we have this:

Chatbot initial render - JsPlumb, build diagrams and rich visual UIs fast

A blank screen! Excellent. That makes sense - we haven't configured anything to render and we havent loaded any data. So let's do that.


8:35am - add start/end nodes

Our chatbot builder is going to allow our users to create a flow from a start node, through several actions, to an end node. We'll start by adding support for start and end nodes. To do that, we need to:

  1. create a template that will render start nodes
  2. create a template that will render end nodes
  3. map these templates in our view so that the Toolkit knows when to use them
  4. create a node palette that can render these and allow a user to drag them on to the canvas.

First we'll create a couple of constants to identify these node types:

const START = "start"
const END = "end"

Then we'll create a template for each one in the view:


const START = "start"
const END = "end"

const surface = toolkit.render(canvas, {
layout:{
type:AbsoluteLayout.type
},
view:{
nodes:{
[START]:{
template:`<div class="jtk-chatbot-start">START</div>`
},
[END]:{
template:`<div class="jtk-chatbot-end">END</div>`
}
}
}
})
note

You don't have to provide node templates directly in your view like this. If you have a more complex template you can embed it in your HTML page or include it in a separate HTML page.

Let's also just add one of each of these to the Toolkit when we start up the app so we can have a look at them:

toolkit.load({
data:{
nodes:[
{ id:"1", type:"start", left:0, top:0 },
{ id:"2", type:"end", left:0, top:250 }
]
}
})

Let's have a look now:

Chatbot start and end node render - JsPlumb - Angular, React, Vue, Svelte diagramming library

A little plain. I think we'll make these nodes circles with a solid color and white text. We can change it up later easily enough as it's all just handled in the CSS:

.jtk-chatbot-start, .jtk-chatbot-end {
width:80px;
height:80px;
border-radius:50%;
display:flex;
color:white;
justify-content: center;
align-items: center;
text-transform: uppercase;
}

.jtk-chatbot-start {
background-color:navy;
}

.jtk-chatbot-end {
background-color:#ff667b;
}

.jtk-chatbot-start::after {
content:'START';
}

.jtk-chatbot-end::after {
content:'END';
}

and also, let's have the Toolkit center the content after it loads it, via the zoomToFit render option:

const surface = toolkit.render(canvas, {
layout:{
type:AbsoluteLayout.type
},
view:{
nodes:{
[START]:{
template:`<div class="jtk-chatbot-start"></div>`
},
[END]:{
template:`<div class="jtk-chatbot-end"></div>`
}
}
},
zoomToFit:true
})

Not bad:

Chatbot start and end node render - JsPlumb, leading alternative to GoJS and JointJS

We've just done steps 1,2 and 3 outlined above. Now to add the node palette. We're going to use a SurfaceDropManager for this - a component that hooks up to a Surface and lets you render a list of items you'd like to drop onto the Surface. The code to initialise our drop manager looks like this:

const dropManager = new SurfaceDropManager({
surface,
source: palette,
selector: ".jtk-chatbot-palette-item",
dataGenerator:(el) => {
return {
type:el.getAttribute("data-type")
}
}
})

We've given it four pieces of information:

  1. The Surface to attach to
  2. The element in which the draggable nodes will be found
  3. A CSS selector that identifies draggable nodes in that element
  4. A function that it can use to generate an initial payload for any node that is dragged

We've also updated our HTML to include a draggable item for start and end nodes:

<div class="sidebar node-palette">
<div class="jtk-chatbot-palette-item" data-type="start">
<div class="jtk-chatbot-start"></div>
</div>
<div class="jtk-chatbot-palette-item" data-type="end">
<div class="jtk-chatbot-start"></div>
</div>
</div>

And now our UI looks like this:

Chatbot node palette render - JsPlumb - JavaScript and Typescript diagramming library that fuels exceptional UIs

Getting there!


9:03am - adding action nodes

What we want to do with this app is provide the ability for the chatbot to say things to our users, and to perhaps gather responses from the user before moving on to the next step. We're going to support, for now, 3 different types of actions:

  • Chatbot tells the user something and does not wait for a response, moving on to the next step
  • Chatbot tells the user something and offers a set of responses from which the user needs to select one
  • Chatbot tells the user something and waits for a free-form response from the user

The Data Model

We always recommend to people that a clear concept of the data model behind an application is key to its success. This application consists of a series of steps, and we're representing each step as a Node. But how can we represent the transitions to some other step in our data model? When we send a message and don't wait for a response, or when the user enters some free-form text, the transition is simple: go from this step to the next step - no decision about the next step is required. But if we want to provide the user with some choices then our data model is a little more complex, and in the Toolkit we generally use ports for this. As our documentation says:

Ports are points on your Nodes/Groups that are the endpoint of some relationship with another Node/Group, or with a Port on another Node/Group.

Let's put the cart slightly ahead of the horse and sketch out the action nodes. First we'll add constants to represent their data types:

const ACTION_MESSAGE = "message"
const ACTION_INPUT = "input"
const ACTION_CHOICE = "choice"

Then we'll create initial templates for them and map them in our view:

view:{
nodes:{
[START]:{
template:`<div class="jtk-chatbot-start"></div>`
},
[END]:{
template:`<div class="jtk-chatbot-end"></div>`
},
[ACTION_MESSAGE]:{
template:`<div class="jtk-chatbot-message">{{message}}</div>`
},
[ACTION_INPUT]:{
template:`<div class="jtk-chatbot-input">
{{message}}
<textarea rows="5" cols="10" placeholder="{{prompt}}"/>
</div>`
},
[ACTION_CHOICE]:{
template:`<div class="jtk-chatbot-choice">
{{message}}
<r-each in="choices" key="id">
<span>{{label}}</span>
</r-each>
</div>`
}
}
}

Now let's add them to our initial dataset so we can take a look:

toolkit.load({
data:{
nodes:[
{ id:"1", type:"start", left:0, top:0 },
{ id:"3", type:"message", left:-150, top:250, message:"Hi! I am a chatbot" },
{ id:"4", type:"input", left:0, top:250, message:"Please enter your name:", prompt:"your name, please!"},
{
id:"5",
type:"choice",
left:250, top:250,
message:"Make a choice",
choices:[
{ id:"choice1", label:"Choice 1" },
{ id:"choice2", label:"Choice 2" },
]
},
{ id:"2", type:"end", left:0, top:650 }
]
}
})

Chatbot action node render - JsPlumb, leading alternative to GoJS and JointJS

Hmmm, not bad. Clearly it's time for some more CSS!

.jtk-chatbot-message, .jtk-chatbot-input, .jtk-chatbot-choice {
color:white;
display:flex;
padding:1rem;
border-radius:5px;
}

.jtk-chatbot-message {
width:80px;
background-color: mediumseagreen;
justify-content: center;
align-items: center;
text-transform: uppercase;
}

.jtk-chatbot-input {
min-width:150px;
background-color: coral;
flex-direction: column;
}

.jtk-chatbot-input textarea {
margin-top:5px;
resize: none;
}

.jtk-chatbot-choice {
min-width:150px;
background-color: rebeccapurple;
flex-direction: column;
}

.jtk-chatbot-choice-option {
display:flex;
align-items: center;
margin:3px 0;
background-color: palevioletred;
padding:3px;
border-radius:3px;
}

And we have this:

Chatbot action node render with css - JsPlumb, build diagrams and rich visual UIs fast

It's starting to come together now. We'd like to be able to drag those action items from the node palette, though, so we'll update the HTML and add them:

<div class="sidebar node-palette">
<div class="jtk-chatbot-palette-item" data-type="start">
<div class="jtk-chatbot-start"></div>
</div>
<div class="jtk-chatbot-palette-item" data-type="end">
<div class="jtk-chatbot-end"></div>
</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>

We also need to update the code that generates a payload for each of these new action types:

dataGenerator:(el) => {
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
}

The result:

Chatbot action node palette with actions - JsPlumb, build diagrams and rich visual UIs fast

I've helpfully dragged out one of each action node type onto the canvas for your enjoyment. Obviously the styles there might need to be revisited, but that's fine, it'll just be a little bit of CSS. And maybe a little bit of HTML.


9:45am - establishing connectivity

So far we have the ability to drag start, end and action nodes onto a canvas and to render them. We can also load up a dataset. But we can't yet connect anything. As I mentioned above, we're representing each step as a node, and on the CHOICE node type we're representing each choice as a port.

To setup visual connectivity, we need to decide on two main things:

What are the rules governing how steps can be connected?

  • We can only drag from START nodes
  • We can only drag to END nodes
  • We can drag to and from MESSAGE nodes
  • We can drag to and from INPUT nodes
  • We can drag to a CHOICE node, but not to the individual choices
  • We can drag from the choices in a CHOICE node, but not from the node itself

How exactly do we want the user to interact with our UI to establish edges?

There's a key tradeoff with this sort of application: how can we allow a node to be draggable but also allow a user to drag from it? The Toolkit allows us to specify particular parts of some node's element that can act as edge sources and/or targets. Let's take the START node as an example - we could, theoretically, make it non-draggable and then use the whole node as the edge source. But we don't want to do that in this app, so we're going to add an element to its markup and designate it as an edge source:

[START]:{
template:`<div class="jtk-chatbot-start">
<div class="connect" data-jtk-source="true"/>
</div>`
}

The data-jtk-source attribute is how we tell the Toolkit that the given element is somewhere from which the user can drag edges. We'll style that a little in our CSS:

.connect {
width: 15px;
height: 15px;
background-color: #e8e8b2;
border-radius: 50%;
cursor: pointer;
position: absolute;
bottom: -0.25rem;
left: 50%;
margin-left: -7.5px;
}

Chatbot start and connect - JsPlumb, leading alternative to GoJS and JointJS

Great! So let's add that to the MESSAGE and INPUT nodes now. We don't need to add it to END nodes, because they can only accept incoming connections, meaning we can use the whole element. And we're not adding it to CHOICE nodes, because only their individual choices can be the source of connections.

[ACTION_MESSAGE]:{
template:`<div class="jtk-chatbot-message">
{{message}}
<div class="connect" data-jtk-source="true"/>
</div>`
},
[ACTION_INPUT]:{
template:`<div class="jtk-chatbot-input">
{{message}}
<textarea rows="5" cols="10" placeholder="{{prompt}}"/>
<div class="connect" data-jtk-source="true"/>
</div>`
}

Configuring choices as edge sources

To setup each choice as an edge source, we alter the markup as follows:

<r-each in="choices" key="id">
<div class="jtk-chatbot-choice-option"
data-jtk-source="true"
data-jtk-port="{{id}}">{{label}}</div>
</r-each>

data-jtk-source tells the Toolkit that the whole element is an edge source. data-jtk-port tells the Toolkit the ID of the port that the edge is coming from. So imagine we had this CHOICE node, like from our initial dataset:

{
id:"5",
type:"choice",
left:250, top:250,
message:"Make a choice",
choices:[
{ id:"choice1", label:"Choice 1" },
{ id:"choice2", label:"Choice 2" },
]
}

When you drag an edge from Choice 1, the Toolkit uses the ID 5.choice1, meaning "the port with ID 'choice1' on the node with ID '5'". If we dragged an edge from Choice 1 to the END node now, in the model we'd have:

{
source:"5.choice1", target:"2"
}

because "2" is the ID of the END node above.

Configuring edge targets

We've got all our edge sources configured, now we'll setup the targets, based on the rules given above. In a nutshell, every node except the START node can be an edge target. This is straightforward to setup in the Toolkit - we simply add a data-jtk-target attribute to the template for each node that should be a target. For instance, here's the END node template now:

[END]:{
template:`<div class="jtk-chatbot-end" data-jtk-target="true"></div>`
}

We've done the same thing on ACTION, INPUT and CHOICE nodes, but not START of course.

At this point, we can drag some edges:

Chatbot edges take 1 - JsPlumb, leading alternative to GoJS and JointJS

it doesn't look very pretty, though - what's going on? Three things:

  • We need to decide if we want an endpoint on each edge or not. I don't want an endpoint. I'm going to use a Blank endpoint - see below.
  • We need to come up with a nice appearance for our edges. We can do this programmatically, by supplying a style object to the Surface, or we can do it with CSS. We'll use CSS - it's simple and easy to change.
  • We need to think about where on each node we want connections to be attached. We call these anchors in the Toolkit. Configuring your anchors can make a significant difference to the appearance of an app using the Toolkit. So let's think about our options. Basically we're probably OK with every node being able to support connections on any side of the element. For this we can use the Continuous anchor, so let's set that up, and we'll also setup the Blank endpoint. We do this in the render parameters, in the defaults section:
toolkit.render(canvas, {
view:{ ... },
zoomToFit:true,
defaults:{
endpoint:BlankEndpoint.type,
anchor:AnchorLocations.Continuous
}
})

In the absence of any more specific instructions, the Toolkit will use a Blank endpoint and a Continuous anchor throughout the UI.

Chatbot edges take 2 - JsPlumb, leading alternative to GoJS and JointJS

That's not bad, although it's hard to see the connector - we need to do what we mentioned above, make a style for the connector. I think we'll just use black, with a stroke width of 2 pixels. That should pop. But if you don't like it, it's easily changed!

.jtk-connector path {
stroke:black;
stroke-width: 2px;
}

Also it's a little difficult to see the flow. Let's add an arrow to the end of the edge. There are various different arrows you can use. I think here we'll go for a PlainArrow. To do this we're going to introduce a default edge definition into our view, and declare an overlay on it:

view:{
nodes: { ... },
edges:{
default:{
overlays:[
{
type:PlainArrowOverlay.type,
options:{
location:1,
width:10,
height:10
}
}
]
}
}
}

Chatbot edge with overlay - JsPlumb, industry standard diagramming and rich visual UI Javascript library

Looks pretty good. I'm a little bothered by the way the arrow goes from Choice 1 to END, though - it doesn't feel very intuitive. It would be better if it only went from the right or left hand side of the choice element, but since we're using the Continuous anchor it can choose any face - and it chooses the closest face to its target. The solution? We can define a port type for the choice elements, and map an anchor to that specific type.

First of all we update the template for CHOICE nodes to make each choice element declare a port type:

<div class="jtk-chatbot-choice-option" 
data-jtk-source="true"
data-jtk-port-type="choice"
data-jtk-port="{{id}}">{{label}}</div>

We're calling it "choice". Then we add a mapping for this port type to our view, and declare an anchor for it:

view:{
nodes:{ ... },
edges:{ ... },
ports:{
choice:{
anchor:[AnchorLocations.Left, AnchorLocations.Right ]
}
}
}

Now when we drag an edge from a choice, it is always anchored on the left or right hand side of the element:

Chatbot edge with custom port - JsPlumb, flowcharts, chatbots, bar charts, decision trees, mindmaps, org charts and more

This is known as a dynamic anchor in the Toolkit - an anchor whose position can be selected from one of a number of points. The Toolkit picks the point that is closest to the center of the element at the other end of the edge.


10:44am - stocktake.

So, what have we got so far?

  • we can drag various node types onto the canvas
  • we can connect nodes to each other based upon a set of connectivity rules
  • we can support multiple outputs from a choice node

What else would we like to do? We'd probably like to inspect our nodes and update their values.


10:45am - adding an inspector

We declared an inspector div from the outset in our HTML. The Toolkit has support for Inspectors - components that connect to an underlying Toolkit instance and allow you to edit the values of nodes and edges.

To setup an inspector we need to decide which node types can be edited. In this app we'll support editing MESSAGE, INPUT and CHOICE nodes. So we need to create a UI for each of these, exposing the fields that are editable for each type. To keep the code easy to read, let's do this in a new file, inspector.js. The code looks like this:

import {
VanillaInspector
} from "@jsplumbtoolkit/browser-ui"

import { ACTION_INPUT, ACTION_MESSAGE, ACTION_CHOICE } from "./demo";

const PROPERTY_MESSAGE = "message"
const PROPERTY_PROMPT = "prompt"

/**
* Inspector for chatbot nodes.
*/
export class ChatbotInspector extends VanillaInspector {

inspectors = {
[ACTION_MESSAGE]:`
<div class="jtk-chatbot-inspector">
<input type="text" jtk-att="${PROPERTY_MESSAGE}" placeholder="message"/>
</div>`,
[ACTION_INPUT]:`
<div class="jtk-chatbot-inspector">
<input type="text" jtk-att="${PROPERTY_MESSAGE}" placeholder="message"/>
<input type="text" jtk-att="${PROPERTY_PROMPT}" placeholder="prompt"/>
</div>`,
[ACTION_CHOICE]:`
<div class="jtk-chatbot-inspector">
<input type="text" jtk-att="${PROPERTY_MESSAGE}" placeholder="message"/>
</div>`
}

constructor(options) {
super(Object.assign(options, {
templateResolver:(obj) => {
return this.inspectors[obj.type]
}
}))

}
}

We provide a template for each of the three node types we support. This class extends VanillaInspector, which ships with the Toolkit, and which expects a templateResolver as a constructor argument.

With this inspector available, we now need to create one, in demo.js:

new ChatbotInspector({
toolkit,
container:inspector,
surface
})

And we need to somehow trigger the inspector. Inspectors listen to changes in the underlying Toolkit's current selection, so here we add a new node type which has a tap event listener on it:

view:{
nodes:{
[SELECTABLE]:{
events:{
[EVENT_TAP]:(p) => {
toolkit.setSelection(p.obj)
}
}
},
[ACTION_MESSAGE]:{
parent:SELECTABLE,
template:`<div class="jtk-chatbot-message" data-jtk-target="true">
{{message}}
<div class="connect" data-jtk-source="true"/>
</div>`
},
...
}
}

The tap event listener sets the tapped node to be the Toolkit's current selection, which the inspector detects. Note here in the ACTION_MESSAGE node definition, we added parent:SELECTABLE. Node, edge, port and group definitions in the Toolkit can declare a parent definition, which allow you to setup common behaviour. This is all discussed in our documentation on views.

And this is the result:

Chatbot inspector - JsPlumb, leading alternative to GoJS and JointJS

I added a few styles to app.css for the inspector, but I wanted to call out this style that I also added, which you can see in the screenshot above:

.jtk-surface-selected-element {
outline:2px dotted cornflowerblue;
outline-offset: 0.5rem;
}

When an element is in the Toolkit's current selection, it has the class jtk-surface-selected-element applied to it in the UI. You can use this to provide visual cues to your users.


11:13am - managing choices

We're obviously going to want to manage the list of choices on each element. This will require 3 things:

  • the ability to delete a choice
  • the ability to add a choice
  • the ability to edit a choice

Adding a choice

Since choices are stored in the choices array on a CHOICE node, adding a choice is a case of instructing the Toolkit update the choices array. We'll add a + button to the header of the CHOICE template, and an X button to each choice:

[ACTION_CHOICE]:{
parent:SELECTABLE,
template:`<div class="jtk-chatbot-choice" data-jtk-target="true">
{{message}}
<div class="jtk-choice-add"></div>
<r-each in="choices" key="id">
<div class="jtk-chatbot-choice-option"
data-jtk-source="true"
data-jtk-port-type="choice"
data-jtk-port="{{id}}">
{{label}}
<div class="jtk-edge-delete"></div>
</div>
</r-each>
</div>`
}

and some CSS for these buttons:

.jtk-choice-add {
position: absolute;
top: 0.5rem;
right: 0.5rem;
color: white;
cursor: pointer;
}

.jtk-choice-add::after {
content:'+'
}

.jtk-choice-delete {
color:white;
margin-left:auto;
cursor: pointer;
}

.jtk-choice-delete::after {
content:'x';
}

To wire these up, we add a listener for each of them to the modelEvents parameter we pass to the render call:

toolkit.render(canvas, {

...,
modelEvents:[
{
event:EVENT_TAP,
selector:".jtk-choice-add",
callback:(event, eventTarget, modelObject) => {
toolkit.addPort(modelObject.obj, { id:uuid(), label:"Choice"})
}
},
{
event:EVENT_TAP,
selector:".jtk-choice-delete",
callback:(event, eventTarget, modelObject) => {
toolkit.removePort(modelObject.obj)
}
}
]

})

In order for addPort to know where to put the new port's data, we also had to update the way we create the Toolkit instance:

const toolkit = newInstance({portDataProperty:"choices"})

You can read about that concept in detail in our documentation.

Editing a choice

We're almost done now, but there's just one thing missing - we can't edit each choice. To do this, we'll first update our inspector so that it can handle being given either a node or a choice. We'll add a couple of constants:

const PROPERTY_LABEL = "label"
const CHOICE = "choice"

and we'll add a template for the choice type in the inspector:

[CHOICE_PORT]:`<div class="jtk-chatbot-inspector">
<input type="text" jtk-att="${PROPERTY_LABEL}" jtk-focus placeholder="enter label..."/>
</div>`

Notice the jtk-focus attribute on that template? It instructs the Toolkit to set it as the focus when the inspection event starts.

We'll update our template resolver so it can pick the appropriate template. If the incoming object is a node then we just do what we did before - look it up by its type. Otherwise we return the CHOICE template:

templateResolver:(obj) => {
if (isNode(obj)) {
return this.inspectors[obj.type]
} else {
return this.inspectors[CHOICE_PORT]
}
}

Now we want to trigger the choice inspector in two places - first, when a new choice is added, so we update the model event from before:

{
event:EVENT_TAP,
selector:".jtk-choice-add",
callback:(event, eventTarget, modelObject) => {
toolkit.setSelection(toolkit.addPort(modelObject.obj, { id:uuid(), label:"Choice"}))
}
}

and we also add a new model event to listen for a tap on a choice:

modelEvents:{

...,
{
event:EVENT_TAP,
selector:".jtk-chatbot-choice-option",
callback:(event, eventTarget, modelObject) => {
toolkit.setSelection(modelObject.obj)
}
}

}

The choice inspector in all its glory:

Chatbot choice inspector - JsPlumb, leading alternative to GoJS and JointJS


11:33am - undo/redo

The chatbot app is looking quite complete now. But we can make just one more change and still bring this in in record time - we'll use the Toolkit's ControlsComponent to add support for undo/redo and zooming. This is a straightforward process:

And this is what we get:

Chatbot controls component - JsPlumb - Angular, React, Vue, Svelte diagramming library


11:35am - final touches

When is a computer program finished? For most computer programmers the answer is, of course, never - we like to tinker. But for now I'm just going to do a last couple of things and call this "done".

Deleting nodes

A neat way to do this is to add a delete button to each node's template that, via CSS, is only visible when the node is selected. For example, here's the START node template now:

[START]:{
parent:SELECTABLE,
template:`<div class="jtk-chatbot-start">
<div class="jtk-delete"></div>
<div class="connect" data-jtk-source="true"/>
</div>`
}

The .jtk-delete button's visibility is managed via css:

.jtk-delete {
position:absolute;
...
display:none;
}

.jtk-delete::after {
content:'x';
}

.jtk-surface-selected-element .jtk-delete {
display:flex;
}
note

As you can see above, I have now declared the START node to have SELECTABLE as its parent, so that the user can click it, so that it can show a delete button. I did the same for the END node. The inspector will not choke if there is no template available to inspect a given node - it will just do nothing.

Palette styles

I reworked the styles for the palette to make it look neater:

Chatbot palette styles final - JsPlumb, flowcharts, chatbots, bar charts, decision trees, mindmaps, org charts and more

info

One trick to keep in mind when styling items in a palette is that when they are being dragged to the canvas, their parent element is the document object. So if you style palette items with reference to some container, eg

.node-palette > div {
width:80px;
color:red;
}

you will lose that style when the element is dragged. Better to use a unique class name on the elements and then scope them via that in your CSS:

.my-palette-item {
width:80px;
color:red
}

The final styles for the palette in this app are:

.jtk-chatbot-palette-item {
margin:0.25rem 0;
color:white;
padding:0.5rem;
border-radius:5px;
width:90px;
text-align: center;
}

.jtk-chatbot-palette-item[data-type='choice'] {
background-color: var(--bg-choice);
}
.jtk-chatbot-palette-item[data-type='message'] {
background-color: var(--bg-message);
}
.jtk-chatbot-palette-item[data-type='input'] {
background-color: var(--bg-input);
}
.jtk-chatbot-palette-item[data-type='start'] {
background-color: var(--bg-start);
}
.jtk-chatbot-palette-item[data-type='end'] {
background-color: var(--bg-end);
}

11:47am - coffee

So that's 3 hours and 37 minutes to build a chatbot application from scratch. I hope this has given you a good insight into the power and flexibility of the Toolkit, and how it can assist you to quickly bring your own ideas to life. If you want to now see the app in action, you can find it here.

I need a coffee.


Will this work with Angular/React/Vue/Svelte?

As our friends at JointJS like to say - because they do not offer a deep integration with any frameworks - this app is compatible with Angular, Vue, React and Svelte. Being compatible is basically like you can put a bowl of soup on a table. The physics holds. You can take the HTML, CSS and JS from this demo and render it inside a component you rendered with some framework and it will work. But the jsPlumb Toolkit offers a level of integration well beyond mere compatibility - with jsPlumb you can use components from your framework of choice to render the nodes and groups in your UI, allowing for real complexity and reactivity. We will be writing a version of this application for all of our library integrations in the near future, and we'll document the process of porting from this vanilla JS app.


Start a free trial

Not a user of jsPlumb but thinking of checking it out? There's a whole lot more to discover and it's a great time to get started!


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.