How to Create Custom Node Types with Plugins¶
This guide shows you how to extend Canvas-Chat with custom node types using the plugin system.
Overview¶
Canvas-Chat's plugin system allows you to create custom node types with:
- Custom rendering (HTML/CSS)
- Custom actions (buttons in the node toolbar)
- Custom event handlers (click, hover, etc.)
- Automatic registration and loading
Plugins are JavaScript ES modules that import from Canvas-Chat's core APIs.
Prerequisites¶
- Canvas-Chat with a
config.yamlfile - Basic knowledge of JavaScript and HTML/CSS
Note: Plugins work with or without --admin-mode. Use whichever mode fits your deployment.
Step 1: Create a Plugin File¶
Create a new JavaScript file for your plugin (e.g., my-custom-node.js):
/**
* Custom Node Plugin Example
*/
import { BaseNode, Actions } from '/static/js/node-protocols.js';
import { NodeRegistry } from '/static/js/node-registry.js';
class MyCustomNode extends BaseNode {
/**
* Display label shown in node header
*/
getTypeLabel() {
return 'My Custom Node';
}
/**
* Emoji icon for the node type
*/
getTypeIcon() {
return '🎯';
}
/**
* Render the HTML content for the node
*/
renderContent(canvas) {
const content = this.node.content || 'No content';
return `
<div class="my-custom-content">
<p>${canvas.escapeHtml(content)}</p>
<button class="my-button">Click Me</button>
</div>
`;
}
/**
* Action buttons for the toolbar
*/
getActions() {
return [{ id: 'custom-action', label: '✨ Custom', title: 'Do something custom' }, Actions.REPLY, Actions.COPY];
}
/**
* Event bindings for interactive elements
*/
getEventBindings() {
return [
{
selector: '.my-button',
handler: (nodeId, e, canvas) => {
alert('Button clicked!');
// Emit custom events to the canvas
canvas.emit('myCustomEvent', nodeId);
},
},
];
}
}
// Register the node type
NodeRegistry.register({
type: 'my-custom',
protocol: MyCustomNode,
defaultSize: { width: 400, height: 300 },
css: `
.my-custom-content {
padding: 16px;
}
.my-button {
padding: 8px 16px;
background: var(--primary-color);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.my-button:hover {
opacity: 0.8;
}
`,
});
console.log('My custom node plugin loaded');
Step 2: Add Plugin to Config¶
Edit your config.yaml to include the plugin:
models:
- id: 'openai/gpt-4o'
name: 'GPT-4o'
apiKeyEnvVar: 'OPENAI_API_KEY'
plugins:
# Relative path (from config file location)
- path: ./plugins/my-custom-node.js
# Or absolute path
# - path: /Users/you/projects/my-custom-node.js
Step 3: Run Canvas-Chat¶
Start canvas-chat with your config file:
# Normal mode (users provide their own API keys)
uvx canvas-chat launch --config config.yaml
# OR admin mode (server-side keys)
uvx canvas-chat launch --config config.yaml --admin-mode
You should see in the logs:
Step 4: Create Nodes of Your Type¶
In the browser console or via the API, create nodes:
// In browser console
const node = createNode('my-custom', 'Hello from my custom node!');
app.graph.addNode(node);
app.canvas.renderNode(node);
Node Protocol API Reference¶
Required Methods¶
Every custom node class must extend BaseNode and can override these methods:
getTypeLabel(): string¶
Display name shown in the node header.
getTypeIcon(): string¶
Emoji or text icon shown in the node header.
renderContent(canvas): string¶
Returns HTML for the node's content area.
renderContent(canvas) {
// Access node data
const text = this.node.content;
// Use canvas utilities
const escaped = canvas.escapeHtml(text);
const truncated = canvas.truncate(text, 100);
return `<div class="content">${escaped}</div>`;
}
Optional Methods¶
getSummaryText(canvas): string¶
Text shown when zoomed out. Defaults to truncated content.
getActions(): Array<Action>¶
Action buttons in the toolbar.
getActions() {
return [
{ id: 'custom', label: '✨ Custom', title: 'Custom action' },
Actions.REPLY,
Actions.COPY,
];
}
getHeaderButtons(): Array<HeaderButton>¶
Buttons in the node header (next to delete).
getHeaderButtons() {
const base = super.getHeaderButtons(); // Gets standard buttons
return [
...base,
{
id: 'custom-header',
icon: '⚙️',
title: 'Settings',
class: 'custom-header-btn',
},
];
}
getEventBindings(): Array<EventBinding>¶
Declarative event handlers for interactive elements.
getEventBindings() {
return [
// Single element
{
selector: '.my-button',
handler: (nodeId, e, canvas) => {
console.log('Button clicked', nodeId);
},
},
// Multiple elements
{
selector: '.list-item',
multiple: true, // Handle all matching elements
handler: (nodeId, e, canvas) => {
const index = e.currentTarget.dataset.index;
console.log('Item clicked', index);
},
},
// Named event (emitted by canvas)
{
selector: '.vote-btn',
handler: 'pollVote', // String = event name
},
];
}
isScrollable(): boolean¶
Whether the node content should scroll. Defaults to true.
NodeRegistry.register() Options¶
NodeRegistry.register({
// Required
type: 'my-node', // Unique type identifier
protocol: MyNodeClass, // Class extending BaseNode
// Optional
defaultSize: {
// Default dimensions
width: 400,
height: 300,
},
css: `...`, // CSS rules for this node type
cssVariables: {
// CSS custom properties
'--node-my-node-bg': '#f0f0f0',
},
});
Available Utilities¶
Canvas Utilities (passed to renderContent)¶
canvas.escapeHtml(text); // Escape HTML entities
canvas.truncate(text, maxLength); // Truncate with ellipsis
canvas.emit(eventName, ...args); // Emit custom events
canvas.showCopyFeedback(nodeId); // Show "Copied!" feedback
Built-in Actions¶
import { Actions } from '/static/js/node-protocols.js';
Actions.REPLY; // Reply to node
Actions.COPY; // Copy content
Actions.SUMMARIZE; // Generate summary
Actions.EDIT_CONTENT; // Edit in modal
Actions.FETCH_SUMMARIZE; // Fetch and summarize URL
Actions.RESUMMARIZE; // Regenerate summary
Actions.EDIT_CODE; // Edit code in modal
Actions.GENERATE; // Generate code
Actions.RUN_CODE; // Run Python code
Actions.ANALYZE; // Analyze CSV data
Best Practices¶
- Use ES module imports: Always import from
/static/js/paths - Escape user content: Use
canvas.escapeHtml()for any user-provided text - Handle errors gracefully: Wrap async operations in try-catch
- Keep nodes responsive: Avoid blocking the main thread
- Test in different zoom levels: Nodes should work at all zoom scales
- Use CSS variables: Follow Canvas-Chat's theme system
- Log plugin loading: Add
console.log()at the end for debugging
Example: Complete Poll Node¶
See /static/js/plugins/example-poll-node.js for a complete example with:
- Custom rendering
- Interactive voting
- Event bindings
- CSS styling
- Copy to clipboard
Troubleshooting¶
Plugin not loading¶
Check the server logs for:
- "Plugin file not found" - Check the path in config.yaml
- "Invalid plugin entry" - Check YAML syntax
- Browser console for import errors
Events not firing¶
- Ensure
getEventBindings()returns the correct selector - Check that the element exists in
renderContent()output - Use browser DevTools to inspect the DOM
Styling issues¶
- Use browser DevTools to inspect CSS cascade
- Check that CSS variables are defined
- Ensure CSS is scoped to your node type (
.node.my-node {})
Next Steps¶
- Read the Node Protocol Reference
- See example-poll-node.js for a complete implementation
- Learn about Admin Mode Configuration