Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.
"use client";
import "@xyflow/react/dist/style.css";
import {
addEdge,
applyEdgeChanges,
applyNodeChanges,
Background,
type Connection,
type EdgeChange,
type Node,
type NodeChange,
type NodeProps,
type NodeTypes,
ReactFlow,
ReactFlowProvider,
} from "@xyflow/react";
import { nanoid } from "nanoid";
import { useCallback, useState } from "react";
import { PromptCrafterNode } from "@/components/ui/flow/prompt-crafter-node";
const PromptCrafterNodeController = ({
id,
data,
...props
}: NodeProps<Node>) => {
const [template, setTemplate] = useState("Hello {{name}}");
const [dynamicHandles, setDynamicHandles] = useState({
"template-tags": [
{
id: "name",
name: "name",
},
],
});
const handleCreateInput = useCallback(
(name: string) => {
setDynamicHandles({
...dynamicHandles,
"template-tags": [
...dynamicHandles["template-tags"],
{ id: nanoid(), name },
],
});
return true;
},
[dynamicHandles],
);
const handleRemoveInput = useCallback(() => {
setDynamicHandles({
...dynamicHandles,
"template-tags": dynamicHandles["template-tags"].filter(
(input) => input.id !== "name",
),
});
return true;
}, [dynamicHandles]);
const handleUpdateInputName = useCallback(
(handleId: string, newLabel: string) => {
setDynamicHandles({
...dynamicHandles,
"template-tags": dynamicHandles["template-tags"].map((input) =>
input.id === handleId
? { ...input, name: newLabel }
: input,
),
});
return true;
},
[dynamicHandles],
);
return (
<PromptCrafterNode
id={id}
data={{
status: "success",
config: {
template,
},
dynamicHandles,
}}
{...props}
type="prompt-crafter"
onPromptTextChange={setTemplate}
onCreateInput={handleCreateInput}
onRemoveInput={handleRemoveInput}
onUpdateInputName={handleUpdateInputName}
onDeleteNode={() => {}}
/>
);
};
const nodeTypes: NodeTypes = {
"prompt-crafter-node": PromptCrafterNodeController,
};
const initialNodes = [
{
id: "node-1",
type: "prompt-crafter-node",
position: { x: 0, y: -50 },
data: {},
},
];
export function ResizableNodeDemo() {
const [nodes, setNodes] = useState<Node[]>(initialNodes);
const [edges, setEdges] = useState([]);
// Add default viewport configuration
const defaultViewport = { x: 100, y: 100, zoom: 0.9 };
const onNodesChange = useCallback(
(changes: NodeChange<Node>[]) =>
setNodes((nds) => applyNodeChanges(changes, nds)),
[],
);
const onEdgesChange = useCallback(
(changes: EdgeChange<never>[]) =>
setEdges((eds) => applyEdgeChanges(changes, eds)),
[],
);
const onConnect = useCallback(
(connection: Connection) => setEdges((eds) => addEdge(connection, eds)),
[],
);
return (
<div className="w-full h-full">
<ReactFlowProvider>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
nodeTypes={nodeTypes}
defaultViewport={defaultViewport}
/* fitView */
>
<Background />
</ReactFlow>
</ReactFlowProvider>
</div>
);
}
Overview
The Prompt Crafter Node is a React Flow component that allows you to build dynamic prompts by combining multiple inputs using a template-based approach. It provides:
- A template editor where you can write your prompt with placeholders
- Dynamic input handles that can receive text from other nodes
- Template syntax highlighting that validates input references
- A single output that combines all inputs according to your template
Components Used
This node is built using several React Flow Components:
- Base Node - For the core node structure and styling
- Node Header - For the node's header section
- Labeled Handle - For the input and output connection points
Installation
pnpm dlx shadcn@latest add @simple-ai/prompt-crafter-node
Usage
The Prompt Crafter Node requires a controller component to manage its state and handle input-related operations. Here's how to implement it in your React Flow application:
// Controller component to manage the Prompt Crafter Node
const PromptCrafterNodeController = ({
id,
data,
...props
}: NodeProps<Node>) => {
const [template, setTemplate] = useState("Hello {{name}}");
const [dynamicHandles, setDynamicHandles] = useState({
"template-tags": [{ id: "name", name: "name" }],
});
// Handle input creation
const handleCreateInput = useCallback((name: string) => {
setDynamicHandles({
...dynamicHandles,
"template-tags": [
...dynamicHandles["template-tags"],
{ id: nanoid(), name },
],
});
return true;
}, [dynamicHandles]);
// Handle input removal
const handleRemoveInput = useCallback((handleId: string) => {
setDynamicHandles({
...dynamicHandles,
"template-tags": dynamicHandles["template-tags"].filter(
(input) => input.id !== handleId
),
});
}, [dynamicHandles]);
return (
<PromptCrafterNode
id={id}
data={{
status: "idle",
config: { template },
dynamicHandles,
}}
{...props}
onPromptTextChange={setTemplate}
onCreateInput={handleCreateInput}
onRemoveInput={handleRemoveInput}
onUpdateInputName={handleUpdateInputName}
/>
);
};
// Register the node type
const nodeTypes = {
"prompt-crafter": PromptCrafterNodeController,
};The node provides the following connection points:
- Inputs:
- Dynamic inputs that can be added/removed as needed
- Each input can be referenced in the template using
{{input-name}}
- Output:
result: The final prompt with all inputs combined according to the template
The node's features include:
- Template editor with syntax highlighting
- Dynamic input management (add, remove, rename)
- Input validation in the template
- Visual feedback for valid/invalid input references
- Quick input insertion through a command palette