11// ========================================
22// Node Palette Component
33// ========================================
4- // Draggable node palette for creating new nodes
4+ // Draggable node palette with quick templates for creating nodes
55
66import { DragEvent , useState } from 'react' ;
77import { useIntl } from 'react-intl' ;
8- import { MessageSquare , ChevronDown , ChevronRight , GripVertical } from 'lucide-react' ;
8+ import {
9+ MessageSquare , ChevronDown , ChevronRight , GripVertical ,
10+ Search , Code , FileOutput , GitBranch , GitFork , GitMerge , Plus , Terminal
11+ } from 'lucide-react' ;
912import { cn } from '@/lib/utils' ;
1013import { Button } from '@/components/ui/Button' ;
1114import { useFlowStore } from '@/stores' ;
12- import { NODE_TYPE_CONFIGS } from '@/types/flow' ;
15+ import { NODE_TYPE_CONFIGS , QUICK_TEMPLATES } from '@/types/flow' ;
1316
1417interface NodePaletteProps {
1518 className ?: string ;
1619}
1720
1821/**
19- * Draggable card for the unified Prompt Template node type
22+ * Icon mapping for quick templates
2023 */
21- function PromptTemplateCard ( ) {
24+ const TEMPLATE_ICONS : Record < string , React . ElementType > = {
25+ 'slash-command-main' : Terminal ,
26+ 'slash-command-async' : Terminal ,
27+ analysis : Search ,
28+ implementation : Code ,
29+ 'file-operation' : FileOutput ,
30+ conditional : GitBranch ,
31+ parallel : GitFork ,
32+ merge : GitMerge ,
33+ } ;
34+
35+ /**
36+ * Draggable card for a quick template
37+ */
38+ function QuickTemplateCard ( {
39+ template,
40+ } : {
41+ template : typeof QUICK_TEMPLATES [ number ] ;
42+ } ) {
43+ const Icon = TEMPLATE_ICONS [ template . id ] || MessageSquare ;
44+
45+ // Handle drag start - store template ID
46+ const onDragStart = ( event : DragEvent < HTMLDivElement > ) => {
47+ event . dataTransfer . setData ( 'application/reactflow-node-type' , 'prompt-template' ) ;
48+ event . dataTransfer . setData ( 'application/reactflow-template-id' , template . id ) ;
49+ event . dataTransfer . effectAllowed = 'move' ;
50+ } ;
51+
52+ // Handle double-click to add node at default position
53+ const onDoubleClick = ( ) => {
54+ const position = { x : 100 + Math . random ( ) * 200 , y : 100 + Math . random ( ) * 200 } ;
55+ useFlowStore . getState ( ) . addNodeFromTemplate ( template . id , position ) ;
56+ } ;
57+
58+ return (
59+ < div
60+ draggable
61+ onDragStart = { onDragStart }
62+ onDoubleClick = { onDoubleClick }
63+ className = { cn (
64+ 'group flex items-center gap-3 p-3 rounded-lg border-2 bg-card cursor-grab transition-all' ,
65+ 'hover:shadow-md hover:scale-[1.02] active:cursor-grabbing active:scale-[0.98]' ,
66+ `border-${ template . color . replace ( 'bg-' , '' ) } `
67+ ) }
68+ >
69+ < div className = { cn ( 'p-2 rounded-md text-white' , template . color , `hover:${ template . color } ` ) } >
70+ < Icon className = "w-4 h-4" />
71+ </ div >
72+ < div className = "flex-1 min-w-0" >
73+ < div className = "text-sm font-medium text-foreground" > { template . label } </ div >
74+ < div className = "text-xs text-muted-foreground truncate" > { template . description } </ div >
75+ </ div >
76+ < GripVertical className = "w-4 h-4 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity" />
77+ </ div >
78+ ) ;
79+ }
80+
81+ /**
82+ * Basic empty prompt template card
83+ */
84+ function BasicTemplateCard ( ) {
2285 const config = NODE_TYPE_CONFIGS [ 'prompt-template' ] ;
2386
24- // Handle drag start
2587 const onDragStart = ( event : DragEvent < HTMLDivElement > ) => {
2688 event . dataTransfer . setData ( 'application/reactflow-node-type' , 'prompt-template' ) ;
2789 event . dataTransfer . effectAllowed = 'move' ;
2890 } ;
2991
92+ const onDoubleClick = ( ) => {
93+ const position = { x : 100 + Math . random ( ) * 200 , y : 100 + Math . random ( ) * 200 } ;
94+ useFlowStore . getState ( ) . addNode ( position ) ;
95+ } ;
96+
3097 return (
3198 < div
3299 draggable
33100 onDragStart = { onDragStart }
101+ onDoubleClick = { onDoubleClick }
34102 className = { cn (
35103 'group flex items-center gap-3 p-3 rounded-lg border-2 bg-card cursor-grab transition-all' ,
36104 'hover:shadow-md hover:scale-[1.02] active:cursor-grabbing active:scale-[0.98]' ,
105+ 'border-dashed border-muted-foreground/50 hover:border-primary' ,
37106 'border-blue-500'
38107 ) }
39108 >
40109 < div className = "p-2 rounded-md text-white bg-blue-500 hover:bg-blue-600" >
41- < MessageSquare className = "w-4 h-4" />
110+ < Plus className = "w-4 h-4" />
42111 </ div >
43112 < div className = "flex-1 min-w-0" >
44113 < div className = "text-sm font-medium text-foreground" > { config . label } </ div >
@@ -49,9 +118,41 @@ function PromptTemplateCard() {
49118 ) ;
50119}
51120
121+ /**
122+ * Category section with expand/collapse
123+ */
124+ function TemplateCategory ( {
125+ title,
126+ children,
127+ defaultExpanded = true ,
128+ } : {
129+ title : string ;
130+ children : React . ReactNode ;
131+ defaultExpanded ?: boolean ;
132+ } ) {
133+ const [ isExpanded , setIsExpanded ] = useState ( defaultExpanded ) ;
134+
135+ return (
136+ < div >
137+ < button
138+ onClick = { ( ) => setIsExpanded ( ! isExpanded ) }
139+ className = "flex items-center gap-2 w-full text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2 hover:text-foreground transition-colors"
140+ >
141+ { isExpanded ? (
142+ < ChevronDown className = "w-3 h-3" />
143+ ) : (
144+ < ChevronRight className = "w-3 h-3" />
145+ ) }
146+ { title }
147+ </ button >
148+
149+ { isExpanded && < div className = "space-y-2" > { children } </ div > }
150+ </ div >
151+ ) ;
152+ }
153+
52154export function NodePalette ( { className } : NodePaletteProps ) {
53155 const { formatMessage } = useIntl ( ) ;
54- const [ isExpanded , setIsExpanded ] = useState ( true ) ;
55156 const isPaletteOpen = useFlowStore ( ( state ) => state . isPaletteOpen ) ;
56157 const setIsPaletteOpen = useFlowStore ( ( state ) => state . setIsPaletteOpen ) ;
57158
@@ -91,34 +192,39 @@ export function NodePalette({ className }: NodePaletteProps) {
91192 { formatMessage ( { id : 'orchestrator.palette.instructions' } ) }
92193 </ div >
93194
94- { /* Node Type Categories */ }
195+ { /* Template Categories */ }
95196 < div className = "flex-1 overflow-y-auto p-4 space-y-4" >
96- { /* Execution Nodes */ }
97- < div >
98- < button
99- onClick = { ( ) => setIsExpanded ( ! isExpanded ) }
100- className = "flex items-center gap-2 w-full text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2"
101- >
102- { isExpanded ? (
103- < ChevronDown className = "w-3 h-3" />
104- ) : (
105- < ChevronRight className = "w-3 h-3" />
106- ) }
107- { formatMessage ( { id : 'orchestrator.palette.nodeTypes' } ) }
108- </ button >
109-
110- { isExpanded && (
111- < div className = "space-y-2" >
112- < PromptTemplateCard />
113- </ div >
114- ) }
115- </ div >
197+ { /* Basic / Empty Template */ }
198+ < TemplateCategory title = "Basic" defaultExpanded = { false } >
199+ < BasicTemplateCard />
200+ </ TemplateCategory >
201+
202+ { /* Slash Commands */ }
203+ < TemplateCategory title = "Slash Commands" defaultExpanded = { true } >
204+ { QUICK_TEMPLATES . filter ( t => t . id . startsWith ( 'slash-command' ) ) . map ( ( template ) => (
205+ < QuickTemplateCard key = { template . id } template = { template } />
206+ ) ) }
207+ </ TemplateCategory >
208+
209+ { /* CLI Tools */ }
210+ < TemplateCategory title = "CLI Tools" defaultExpanded = { true } >
211+ { QUICK_TEMPLATES . filter ( t => [ 'analysis' , 'implementation' ] . includes ( t . id ) ) . map ( ( template ) => (
212+ < QuickTemplateCard key = { template . id } template = { template } />
213+ ) ) }
214+ </ TemplateCategory >
215+
216+ { /* Flow Control */ }
217+ < TemplateCategory title = "Flow Control" defaultExpanded = { true } >
218+ { QUICK_TEMPLATES . filter ( t => [ 'file-operation' , 'conditional' , 'parallel' , 'merge' ] . includes ( t . id ) ) . map ( ( template ) => (
219+ < QuickTemplateCard key = { template . id } template = { template } />
220+ ) ) }
221+ </ TemplateCategory >
116222 </ div >
117223
118224 { /* Footer */ }
119225 < div className = "px-4 py-3 border-t border-border bg-muted/30" >
120226 < div className = "text-xs text-muted-foreground" >
121- < span className = "font-medium" > { formatMessage ( { id : 'orchestrator.palette.tipLabel' } ) } </ span > { formatMessage ( { id : 'orchestrator.palette.tip' } ) }
227+ < span className = "font-medium" > Tip: </ span > Drag to canvas or double-click to add
122228 </ div >
123229 </ div >
124230 </ div >
0 commit comments