import { useState } from 'react' import { ChevronRight, ChevronDown, GripVertical, Copy, Check } from 'lucide-react' export interface FieldInfo { path: string type: string nodeName: string nodeId: string displayName?: string children?: FieldInfo[] isDirectUpstream?: boolean // true if this is an immediate predecessor node targetInput?: string // the connection's target_input name (for direct upstream) } interface AvailableDataPanelProps { fields: FieldInfo[] onInsertExpression?: (expression: string) => void title?: string } const TYPE_COLORS: Record = { string: 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 border-green-200 dark:border-green-800', number: 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400 border-blue-200 dark:border-blue-800', boolean: 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-400 border-purple-200 dark:border-purple-800', object: 'bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-400 border-orange-200 dark:border-orange-800', array: 'bg-pink-100 dark:bg-pink-900/30 text-pink-700 dark:text-pink-400 border-pink-200 dark:border-pink-800', any: 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-400 border-gray-200 dark:border-gray-700', } const TYPE_ICONS: Record = { string: '"A"', number: '123', boolean: '?', object: '{ }', array: '[ ]', any: '*', } interface FieldItemProps { field: FieldInfo depth: number onInsertExpression?: (expression: string) => void } function FieldItem({ field, depth, onInsertExpression }: FieldItemProps) { const [isExpanded, setIsExpanded] = useState(false) const [copied, setCopied] = useState(false) const hasChildren = field.children && field.children.length > 0 // Both direct and non-direct upstream use {{}} braces for evaluation // Direct upstream: path prefixed with targetInput (evaluated against input) // Non-direct upstream: $node accessor to reference specific node output const expression = field.isDirectUpstream ? field.targetInput ? `{{${field.targetInput}.${field.path}}}` // Direct upstream with targetInput prefix : `{{${field.path}}}` // Fallback (rare case) : `{{$node["${field.nodeName}"].${field.path}}}` const handleCopy = () => { navigator.clipboard.writeText(expression) setCopied(true) setTimeout(() => setCopied(false), 2000) } const handleInsert = () => { onInsertExpression?.(expression) } const handleDragStart = (e: React.DragEvent) => { e.dataTransfer.setData('text/plain', expression) e.dataTransfer.effectAllowed = 'copy' } return (
0 ? 'ml-' + Math.min(depth * 4, 12) : '' }`} style={{ marginLeft: depth * 16 }} draggable onDragStart={handleDragStart} > {/* Expand/collapse toggle */} {hasChildren ? ( ) : ( )} {/* Drag handle */} {/* Type badge */} {TYPE_ICONS[field.type] || TYPE_ICONS.any} {/* Field name */} {field.displayName || field.path.split('.').pop()} {/* Copy button */}
{/* Children */} {hasChildren && isExpanded && (
{field.children!.map((child, index) => ( ))}
)}
) } interface NodeGroupProps { nodeName: string fields: FieldInfo[] onInsertExpression?: (expression: string) => void } function NodeGroup({ nodeName, fields, onInsertExpression }: NodeGroupProps) { const [isExpanded, setIsExpanded] = useState(true) return (
{isExpanded && (
{fields.map((field, index) => ( ))}
)}
) } export function AvailableDataPanel({ fields, onInsertExpression, title = 'Available Data', }: AvailableDataPanelProps) { // Group fields by node const groupedByNode = fields.reduce((acc, field) => { const key = field.nodeId if (!acc[key]) { acc[key] = { nodeName: field.nodeName, nodeId: field.nodeId, fields: [], } } acc[key].fields.push(field) return acc }, {} as Record) const nodeGroups = Object.values(groupedByNode) if (fields.length === 0) { return (
No upstream data available
Connect nodes to see their output fields here
) } return (

{title}

Drag or click to insert expressions

{nodeGroups.map((group) => ( ))}
{/* Quick reference */}
Expression Syntax:
{'{{$node["Name"].path}}'}
) }