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:
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:
- create a template that will render start nodes
- create a template that will render end nodes
- map these templates in our view so that the Toolkit knows when to use them
- 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>`
}
}
}
})
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:
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:
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:
- The Surface to attach to
- The element in which the draggable nodes will be found
- A CSS selector that identifies draggable nodes in that element
- 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:
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 }
]
}
})
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:
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:
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;
}
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 simple 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:
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 theContinuous
anchor, so let's set that up, and we'll also setup theBlank
endpoint. We do this in the render parameters, in thedefaults
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.
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
}
}
]
}
}
}
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:
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:
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:
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:
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;
}
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:
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 your nodes, allowing for real complexity and reactiveness. 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.
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.
Not a user of the jsPlumb Toolkit but thinking of checking it out? Head over to https://jsplumbtoolkit.com/trial. It's a good time to get started with jsPlumb.