Skip to main content

React chatbot

· 11 min read
Simon Porritt
JsPlumb core team

Recently we documented the process of building a chatbot app in 3 hours and 37 minutes. A couple of days later we documented porting that to make an Angular Chatbot, and now in today's post we're going to look at porting the vanilla chatbot to React.

React chatbot JsPlumb, leading alternative to GoJS, JointJS and ReactFlow - JsPlumb - JavaScript and Typescript diagramming library that fuels exceptional UIs

Initialization

npm create vite@latest
- make a React app -
npm i @jsplumbtoolkit/browser-ui-react

This gives us a brand new React app, and we've imported JsPlumb's React integration package.

Setup the html and css

Next, we're going to clear out App.jsx :

import './App.css';

import ChatbotComponent from './ChatbotComponent'

function App() {
return (
<div className="App">
<ChatbotComponent/>
</div>
);
}

export default App;

We'll put the bulk of our markup inside a ChatbotComponent.jsx:

export default function ChatbotComponent() {

const surfaceComponent = useRef(null)

const toolkit = newInstance({
// the name of the property in each node's data that is the key for the data for the ports for that node.
// for more complex setups you can use `portExtractor` and `portUpdater` functions - see the documentation for examples.
portDataProperty:CHOICES
})

const view = {... mappings for nodes - see below ...}
const renderParams = { ... params that control various aspects of the Surface behaviour ... }

return <div style={{width:"100%",height:"100%",display:"flex"}}>
<SurfaceProvider>
<div className="jtk-demo-canvas">

<SurfaceComponent renderOptions={renderParams} toolkit={toolkit}
url="/public/dataset.json"
viewOptions={view} ref={ surfaceComponent }/>

<ControlsComponent/>
<MiniviewComponent/>
</div>
<div className="jtk-demo-rhs">
<div className="sidebar node-palette">
<Palette/>
<Inspector/>
<div className="description"/>
</div>
</div>
</SurfaceProvider>
</div>

The first thing to note here is the SurfaceProvider - this is a JsPlumb React provider that provides a shared context for a surface and a whole bunch of associated components such as the drag/drop palette, miniview, controls and inspector.

Inside of the SurfaceProvider we've declared a SurfaceComponent, which is the component that is the canvas on which we render. We also declared a ControlsComponent and MiniviewComponent; these components will discover the surface to attach to via the SurfaceProvider.

We have a right hand side bar containing a Palette and Inspector. These are wrappers around JsPlumb React's PaletteComponent and InspectorComponent - more details below.

CSS

In index.css we added these few lines:

@import '../node_modules/@jsplumbtoolkit/browser-ui/css/jsplumbtoolkit.css';

.jtk-surface {
width:100%;
}

The first line imports the JsPlumb defaults stylesheet. The .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 chatbot.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 JSX and map it

When you use JsPlumb's React integration, you specify some JSX to represent each node type (or several, it's up to you). In our React port, we're going to create one functional component for each type from our vanilla chatbot - START, END, MESSAGE, INPUT and CHOICE.

Here's what the MESSAGE component code looks like:

import React from "react"

export default function MessageComponent({ctx}) {

const { vertex, surface, toolkit } = ctx

return <div className="jtk-chatbot-message" data-jtk-target="true">
<div className="jtk-delete" onClick={() => toolkit.removeNode(vertex)}></div>
{vertex.data.message}
<div className="connect" data-jtk-source="true"></div>
</div>
}

The template was ported pretty much directly from the vanilla chatbot's message component template. JsPlumb React passes in a ctx object to each node it wants to render, so the first line of our component extracts the things it needs from there:

const { vertex, surface, toolkit } = ctx

We can then access each of those things in our JSX. For instance, note the jtk-delete element: it has a click listener on it, which calls toolkit.removeNode(vertex).

We repeated this process for the other node types.

Mapping components

We map each of these components inside the view, for which we only provided a stub in the code snippet above. To map the message component, for instance, we updated our view to be this:

const view = {
nodes:{
[SELECTABLE]:{
events:{
[EVENT_TAP]:(p) => {
toolkit.setSelection(p.obj)
}
}
},
[ACTION_MESSAGE]:{
parent:SELECTABLE,
jsx: (ctx) => <MessageComponent ctx={ctx}/>
}
}
}

The SELECTABLE mapping is what we call an "abstract" mapping - it does not provide any jsx, but it provides base functionality, and we reference it as the parent for a message component.

Note here that for our message JSX we have <MessageComponent ctx={ctx}/> - we're passing in the component we have made as our JSX, but that is in no way a requirement. You can pass in any valid JSX:

[ACTION_MESSAGE]:{
jsx: (ctx) => <div><h1>HELLO!</h1></div>
}

Add SurfaceComponent

JsPlumb's React integration offers a SurfaceComponent that you can embed in your template. We added this to our ChatbotComponent's jsx:

<SurfaceComponent renderOptions={renderParams} 
toolkit={toolkit}
url="/public/dataset.json"
viewOptions={view} ref={ surfaceComponent }/>

We passed it a few things:

  • toolkit The JsPlumb Toolkit instance to render
  • renderOptions 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.
  • viewOptions This is the mapping from node/edge/port types to components, behaviour and appearance.
  • url This is optional, and it allows us to instruct JsPlumb to load some dataset at create time.

JsPlumb Toolkit instance

We create an instance of JsPlumb Toolkit at the top of the ChatbotComponent:

const toolkit = newInstance({
portDataProperty:CHOICES

})

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

renderOptions

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

renderParams:AngularRenderOptions = {
zoomToFit:true,
consumeRightClick:false
}

zoomToFit instructs JsPlumb to zoom the canvas after data load so that all the elements are visible. consumeRightClick is mostly a development flag - it stops JsPlumb from consuming right clicks.

view

The full view for this app looks like this:

const view = {
nodes:{
[SELECTABLE]:{
events:{
[EVENT_TAP]:(p) => {
toolkit.setSelection(p.obj)
}
}
},
[START]:{
parent:SELECTABLE,
jsx: (ctx) => <StartComponent ctx={ctx}/>
},
[END]:{
parent:SELECTABLE,
jsx: (ctx) => <EndComponent ctx={ctx}/>
},
[ACTION_MESSAGE]:{
parent:SELECTABLE,
jsx: (ctx) => <MessageComponent ctx={ctx}/>
},
[ACTION_INPUT]:{
parent:SELECTABLE,
jsx: (ctx) => <InputComponent ctx={ctx}/>
},
[ACTION_CHOICE]:{
parent:SELECTABLE,
jsx: (ctx) => <ChoiceComponent ctx={ctx}/>
},
[ACTION_TEST]:{
parent:SELECTABLE,
jsx: (ctx) => <TestComponent ctx={ctx}/>
}
},
edges:{
default:{
overlays:[
{
type:PlainArrowOverlay.type,
options:{
location:1,
width:10,
length:10
}
}
],
label:"{{label}}",
events:{
[EVENT_TAP]:(p) => {
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 React we supply some JSX.

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 React integration. We just replaced this from the Vanilla Chatbot:

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

with this:

<ControlsComponent/>

Dragging new nodes

This is also easy to configure, via the PaletteComponent. This a SurfaceProvider aware component that you wrap with your own component, which in our case we've called Palette. From the vanilla Chatbot we replaced:

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

with:

<Palette/>

The Palette component's code looks like this:

import React from 'react';

import {ACTION_CHOICE, ACTION_INPUT, ACTION_MESSAGE, ACTION_TEST, nodeTypes} from "./constants";

import { PaletteComponent } from "@jsplumbtoolkit/browser-ui-react";

export default function Palette() {

function 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:"Please choose:",
choices:[
{ id:"1", label:"Choice 1"},
{ id:"2", label:"Choice 2"},
]
})
} else if (type === ACTION_TEST) {
Object.assign(base, {
message:"Test",
choices:[
{ id:"1", label:"Result 1"},
{ id:"2", label:"Result 2"},
]
})
}

return base
}

return <PaletteComponent dataGenerator={dataGenerator}
selector="[data-type]" className="sidebar node-palette">
{ nodeTypes.map(nt => <div key={nt.type} className="jtk-chatbot-palette-item" data-type={nt.type}>{nt.label}</div>) }
</PaletteComponent>
}

The bulk of the code here is in the dataGenerator, which is responsible for creating an appropriate payload whenever a new node is dragged onto the canvas.

The JSX for our Palette uses a PaletteComponent, and passes it:

  • dataGenerator The function used to generate payloads for new vertices
  • selector CSS selector identifying child elements of the palette that represent draggable elements
  • className Optional classes to set on the PaletteComponent DOM element

We write out nodeTypes as the child elements for the palette. Note how each one has a data-type attribute set; this matches the selector prop.

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 React we can take advantage of the fact that each node is a component to isolate this behaviour - in this case, to the ChoiceComponent. We have a few helper methods:

export default function ChoiceComponent({ctx}) {

const { vertex, surface, toolkit } = ctx

function addChoice(){
toolkit.addPort(vertex, {
id:uuid()
})
}

function removeChoice(id) {
toolkit.removePort(vertex, id)
}

function inspectChoice(id) {
toolkit.setSelection(vertex.getPort(id))
}

}

Our JSX uses these helper methods on a few click handlers:

return <div className="jtk-chatbot-choice" data-jtk-target="true">
<div className="jtk-delete" onClick={() => toolkit.removeNode(vertex)}></div>
{vertex.data.message}
<div className="jtk-choice-add" onClick={() => addChoice()}></div>
{vertex.data.choices.map(c =>
<div key={c.id} className="jtk-chatbot-choice-option" data-jtk-source="true" data-jtk-port-type="choice" data-jtk-port={c.id} onClick={() => inspectChoice(c.id)}>
{c.label}
<div className="jtk-choice-delete" onClick={() => removeChoice(c.id)}></div>
</div>
)}
</div>

Inspecting nodes

The final piece we need to port is the node inspector. We create a React component for this, which uses JsPlumb's InspectorComponent:

import React, {useState} from "react";

import { isNode, isPort} from "@jsplumbtoolkit/browser-ui"

import { InspectorComponent } from "@jsplumbtoolkit/browser-ui-react"

import {
ACTION_TEST, ACTION_MESSAGE, ACTION_CHOICE, ACTION_INPUT, START, END
} from "./constants";

const CHOICE_PORT="choicePort"
const EDGE = "edge"

export default function ChatbotInspector() {

const [currentType, setCurrentType] = useState('')

const renderEmptyContainer= () => setCurrentType('')
const refresh = (obj) => {
const ct = isNode(obj) ? obj.data.type : isPort(obj) ? CHOICE_PORT : EDGE
setCurrentType(ct)
}

function baseActionTemplate() {
return <div className="jtk-chatbot-inspector">
<span>Message:</span>
<input type="text" jtk-att="message" placeholder="message"/>
</div>
}

return <InspectorComponent refresh={refresh} renderEmptyContainer={renderEmptyContainer}>

{ currentType === '' && <div/>}
{ currentType === START && <div/>}
{ currentType === END && <div/>}

{ currentType === ACTION_MESSAGE && baseActionTemplate()}
{ currentType === ACTION_CHOICE && baseActionTemplate()}
{ currentType === ACTION_TEST && baseActionTemplate()}

{ currentType === ACTION_INPUT &&
<div className="jtk-chatbot-inspector">
<span>Message:</span>
<input type="text" jtk-att="message" placeholder="message"/>
<span>Prompt:</span>
<input type="text" jtk-att="prompt" placeholder="prompt"/>
</div>
}

{ currentType === CHOICE_PORT &&
<div className="jtk-chatbot-inspector">
<span>Label:</span>
<input type="text" jtk-att="label" jtk-focus placeholder="enter label..."/>
</div>

}

{ currentType === EDGE &&
<div className="jtk-chatbot-inspector">
<div>Label</div>
<input type="text" jtk-att="value"/>
</div>
}


</InspectorComponent>

}

This component maintains a currentType state member, which it uses inside the JSX to determine what to render. We ported the HTML snippets from the vanilla Chatbot over to the various branches inside the JSX

Final thoughts

That's the whole thing ported now. We reused the vast majority of the JsPlumb setup code, HTML and CSS from the vanilla chatbot, but we used React 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. JsPlumb 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 at https://github.com/jsplumb-demonstrations/chatbot


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!

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.