node-palette.tsx
1 import { useState, type DragEvent } from 'react' 2 import { cn } from '@/lib/utils' 3 import type { ProtocolStepKind } from '@/types' 4 5 interface PaletteCategory { 6 label: string 7 items: Array<{ kind: ProtocolStepKind; label: string; description: string }> 8 } 9 10 const CATEGORIES: PaletteCategory[] = [ 11 { 12 label: 'Phases', 13 items: [ 14 { kind: 'present', label: 'Present', description: 'Show info to participants' }, 15 { kind: 'collect_independent_inputs', label: 'Collect Inputs', description: 'Gather independent responses' }, 16 { kind: 'round_robin', label: 'Round Robin', description: 'Turn-based discussion' }, 17 { kind: 'compare', label: 'Compare', description: 'Compare agent outputs' }, 18 { kind: 'decide', label: 'Decide', description: 'Make a decision' }, 19 { kind: 'summarize', label: 'Summarize', description: 'Synthesize results' }, 20 ], 21 }, 22 { 23 label: 'Actions', 24 items: [ 25 { kind: 'emit_tasks', label: 'Emit Tasks', description: 'Create tasks from context' }, 26 { kind: 'dispatch_task', label: 'Dispatch Task', description: 'Assign a specific task' }, 27 { kind: 'dispatch_delegation', label: 'Delegate', description: 'Delegate to an agent' }, 28 ], 29 }, 30 { 31 label: 'Control Flow', 32 items: [ 33 { kind: 'branch', label: 'Branch', description: 'Conditional path' }, 34 { kind: 'repeat', label: 'Repeat', description: 'Loop with exit condition' }, 35 { kind: 'parallel', label: 'Parallel', description: 'Fork into parallel branches' }, 36 { kind: 'join', label: 'Join', description: 'Merge parallel branches' }, 37 { kind: 'for_each', label: 'For Each', description: 'Iterate over items' }, 38 ], 39 }, 40 { 41 label: 'Advanced', 42 items: [ 43 { kind: 'subflow', label: 'Subflow', description: 'Nested protocol template' }, 44 { kind: 'swarm_claim', label: 'Swarm Claim', description: 'Competitive task claiming' }, 45 { kind: 'wait', label: 'Wait', description: 'Pause until external input' }, 46 { kind: 'complete', label: 'Complete', description: 'End the protocol' }, 47 ], 48 }, 49 ] 50 51 export function NodePalette() { 52 const [expandedCategory, setExpandedCategory] = useState<string | null>('Phases') 53 54 const onDragStart = (e: DragEvent, kind: ProtocolStepKind, label: string) => { 55 e.dataTransfer.effectAllowed = 'move' 56 e.dataTransfer.setData('application/x-protocol-node-kind', kind) 57 e.dataTransfer.setData('application/x-protocol-node-label', label) 58 } 59 60 return ( 61 <div className="flex w-52 flex-col overflow-y-auto rounded-lg border bg-card p-3 shadow-sm"> 62 <h3 className="mb-3 text-xs font-bold uppercase tracking-wider text-muted-foreground"> 63 Drag to canvas 64 </h3> 65 66 {CATEGORIES.map((cat) => ( 67 <div key={cat.label} className="mb-2"> 68 <button 69 onClick={() => setExpandedCategory(expandedCategory === cat.label ? null : cat.label)} 70 className="mb-1 flex w-full items-center gap-1 text-xs font-semibold text-muted-foreground hover:text-foreground" 71 > 72 <span className="text-[10px]">{expandedCategory === cat.label ? '\u25BC' : '\u25B6'}</span> 73 {cat.label} 74 </button> 75 {expandedCategory === cat.label && ( 76 <div className="space-y-1"> 77 {cat.items.map(({ kind, label, description }) => ( 78 <div 79 key={kind} 80 draggable 81 onDragStart={(e) => onDragStart(e, kind, label)} 82 className={cn( 83 'cursor-grab rounded-md border bg-background px-3 py-2 text-sm', 84 'transition-shadow hover:shadow-md active:cursor-grabbing', 85 )} 86 title={description} 87 > 88 {label} 89 </div> 90 ))} 91 </div> 92 )} 93 </div> 94 ))} 95 </div> 96 ) 97 }