Node Protocol Pattern¶
Problem¶
Node-type-specific behaviors were scattered throughout the codebase with if (node.type === ...) and if (node.imageData) checks in multiple places:
canvas.js:renderNode(),setupNodeEvents(),getNodeTypeLabel(),getNodeTypeIcon(),getNodeSummaryText()app.js:copyNodeContent()
This made it difficult to:
- Add new node types - Required updating multiple files with scattered conditionals
- Understand node behavior - Logic for a single node type was spread across the codebase
- Maintain consistency - Easy to miss a check when adding features
Solution¶
Implemented sklearn-style protocol classes where each node type defines its behaviors in a single class. This centralizes all node-type-specific logic in node-protocols.js.
Protocol Interface¶
Every node wrapper class implements 8 methods:
getTypeLabel() returns string¶
Returns the display label for the node type (e.g., "You", "AI", "Note", "Matrix").
getTypeIcon() returns string¶
Returns the emoji icon for the node type (e.g., "💬", "🤖", "📊").
getSummaryText(canvas) returns string¶
Returns text shown when zoomed out (semantic zoom). Priority: user-set title > LLM summary > generated fallback.
renderContent(canvas) returns string¶
Returns HTML content for the node body. For most nodes, this renders markdown. Matrix nodes return the full HTML structure including header and actions.
getActions() returns Array<{id, label, title}>¶
Returns action buttons for the node action bar (e.g., Reply, Summarize, Copy, Edit).
getHeaderButtons() returns Array<{id, label, title, hidden?}>¶
Returns header buttons (e.g., Stop, Continue, Reset Size, Fit Viewport, Delete). Some buttons can be hidden by default (e.g., Stop/Continue for streaming nodes).
copyToClipboard(canvas, app) returns Promise<void>¶
Handles copying node content to clipboard. For text nodes, copies text. For image nodes, copies image. For matrix nodes, formats as markdown table.
isScrollable() returns boolean¶
Whether this node type has fixed scrollable dimensions (nodes with long streaming content).
Implementation¶
BaseNode Class¶
All node classes extend BaseNode, which provides default implementations for all 8 methods. Node-specific classes override only the methods that differ from defaults.
Node Classes¶
17 node-specific classes:
HumanNode- User messagesAINode- AI responses (includes Stop/Continue buttons)NoteNode- User-created notes (includes Edit action)SummaryNode- LLM-generated summariesReferenceNode- Links to external content (includes Fetch & Summarize action)SearchNode- Web search queriesResearchNode- Deep research with multiple sourcesHighlightNode- Excerpted text or images from other nodesMatrixNode- Cross-product evaluation tables (custom rendering)CellNode- Pinned cells from matrices (can have contextual titles)RowNode- Extracted rows from matricesColumnNode- Extracted columns from matricesFetchResultNode- Fetched content from URLs (includes Edit and Re-summarize actions)PdfNode- Imported PDF documentsOpinionNode- Committee member opinions (includes Stop/Continue buttons)SynthesisNode- Chairman's synthesized answers (includes Stop/Continue buttons)ReviewNode- Committee member reviews (includes Stop/Continue buttons)ImageNode- Uploaded images for analysis
Factory Function¶
wrapNode(node) dispatches to the correct protocol class:
- Checks
node.imageDatafirst (for IMAGE nodes or HIGHLIGHT nodes with images) - Dispatches by
node.typeusing a class map - Falls back to
BaseNodefor unknown types
Usage in Canvas¶
canvas.js uses the protocol in renderNode():
const wrapped = wrapNode(node);
const summaryText = wrapped.getSummaryText(this);
const typeIcon = wrapped.getTypeIcon();
const contentHtml = wrapped.renderContent(this);
const actions = wrapped.getActions();
const headerButtons = wrapped.getHeaderButtons();
Usage in App¶
app.js uses the protocol in copyNodeContent():
Adding a New Node Type¶
To add a new node type:
- Add node type constant in
graph.js:
- Create protocol class in
node-protocols.js:
class NewTypeNode extends BaseNode {
getTypeLabel() { return 'New Type'; }
getTypeIcon() { return '🔷'; }
// Override other methods as needed
getActions() {
return [Actions.REPLY, Actions.COPY];
}
}
- Add to factory in
wrapNode():
That's it! The protocol pattern handles rendering, actions, copying, and all other behaviors automatically.
Design Decisions¶
Flat Inheritance¶
All classes extend BaseNode directly (no deep hierarchy). This keeps the structure simple and makes it easy to understand what each node type does.
Actions as Objects¶
getActions() returns [{id, label, title}] arrays for self-contained rendering. This makes it easy to add new actions without modifying rendering code.
imageData Precedence¶
wrapNode() checks node.imageData first, regardless of node.type. This ensures IMAGE nodes and HIGHLIGHT nodes with images are handled correctly.
CellNode.getTypeLabel()¶
Returns node.title if present, else 'Cell'. This supports contextual labels like "GPT-4 × Accuracy" for pinned matrix cells.
Error Handling¶
Kept as state overlay (not a node class). Errors are transient states that can appear on any node type, so they don't need their own protocol class.
Matrix Node Special Case¶
Matrix nodes return full HTML structure from renderContent() (including header and actions) because they have a complex custom layout. Other nodes return only the content body.
Alternatives Considered¶
Multiple Dispatch¶
Could have used a dispatch table with (nodeType, methodName) keys. Rejected because:
- Less object-oriented
- Harder to see all behaviors for a node type in one place
- More verbose
Dispatch Objects¶
Could have used plain objects with methods instead of classes. Rejected because:
- Classes provide better inheritance
- Easier to validate protocol compliance
- More familiar pattern for JavaScript developers
TypeScript Interfaces¶
Could have used TypeScript interfaces for protocol enforcement. Rejected because:
- Codebase is vanilla JavaScript
- Runtime validation with
validateNodeProtocol()provides similar benefits - No build step required
Benefits¶
- Single source of truth - All node behavior in one file
- Easy to extend - Add new node types in 3 steps
- Type safety - Protocol compliance validated by tests
- Maintainability - Clear separation of concerns
- Testability - Each protocol class can be tested independently