| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254 |
- 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, string> = {
- 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, string> = {
- 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 (
- <div>
- <div
- className={`flex items-center gap-1 py-1.5 px-2 hover:bg-gray-50 dark:hover:bg-slate-700 rounded cursor-pointer group ${
- depth > 0 ? 'ml-' + Math.min(depth * 4, 12) : ''
- }`}
- style={{ marginLeft: depth * 16 }}
- draggable
- onDragStart={handleDragStart}
- >
- {/* Expand/collapse toggle */}
- {hasChildren ? (
- <button
- onClick={() => setIsExpanded(!isExpanded)}
- className="p-0.5 hover:bg-gray-200 dark:hover:bg-slate-600 rounded"
- >
- {isExpanded ? (
- <ChevronDown className="w-3 h-3 text-gray-500 dark:text-gray-400" />
- ) : (
- <ChevronRight className="w-3 h-3 text-gray-500 dark:text-gray-400" />
- )}
- </button>
- ) : (
- <span className="w-4" />
- )}
- {/* Drag handle */}
- <GripVertical className="w-3 h-3 text-gray-300 dark:text-gray-600 group-hover:text-gray-500 dark:group-hover:text-gray-400 cursor-grab" />
- {/* Type badge */}
- <span
- className={`px-1 py-0.5 rounded text-[9px] font-mono border ${
- TYPE_COLORS[field.type] || TYPE_COLORS.any
- }`}
- title={field.type}
- >
- {TYPE_ICONS[field.type] || TYPE_ICONS.any}
- </span>
- {/* Field name */}
- <span
- className="flex-1 text-sm text-gray-800 dark:text-gray-200 truncate"
- onClick={handleInsert}
- title={`Click to insert: ${expression}`}
- >
- {field.displayName || field.path.split('.').pop()}
- </span>
- {/* Copy button */}
- <button
- onClick={handleCopy}
- className="p-1 opacity-0 group-hover:opacity-100 hover:bg-gray-200 dark:hover:bg-slate-600 rounded transition-opacity"
- title="Copy expression"
- >
- {copied ? (
- <Check className="w-3 h-3 text-green-500" />
- ) : (
- <Copy className="w-3 h-3 text-gray-400 dark:text-gray-500" />
- )}
- </button>
- </div>
- {/* Children */}
- {hasChildren && isExpanded && (
- <div>
- {field.children!.map((child, index) => (
- <FieldItem
- key={`${child.path}-${index}`}
- field={child}
- depth={depth + 1}
- onInsertExpression={onInsertExpression}
- />
- ))}
- </div>
- )}
- </div>
- )
- }
- interface NodeGroupProps {
- nodeName: string
- fields: FieldInfo[]
- onInsertExpression?: (expression: string) => void
- }
- function NodeGroup({ nodeName, fields, onInsertExpression }: NodeGroupProps) {
- const [isExpanded, setIsExpanded] = useState(true)
- return (
- <div className="border border-gray-200 dark:border-slate-700 rounded-lg overflow-hidden mb-2">
- <button
- onClick={() => setIsExpanded(!isExpanded)}
- className="w-full flex items-center gap-2 px-3 py-2 bg-gray-50 dark:bg-slate-800 hover:bg-gray-100 dark:hover:bg-slate-700 text-left"
- >
- {isExpanded ? (
- <ChevronDown className="w-4 h-4 text-gray-500 dark:text-gray-400" />
- ) : (
- <ChevronRight className="w-4 h-4 text-gray-500 dark:text-gray-400" />
- )}
- <span className="text-sm font-medium text-gray-700 dark:text-gray-200">{nodeName}</span>
- <span className="text-xs text-gray-400 dark:text-gray-500 ml-auto">{fields.length} fields</span>
- </button>
- {isExpanded && (
- <div className="py-1 bg-white dark:bg-slate-900">
- {fields.map((field, index) => (
- <FieldItem
- key={`${field.path}-${index}`}
- field={field}
- depth={0}
- onInsertExpression={onInsertExpression}
- />
- ))}
- </div>
- )}
- </div>
- )
- }
- 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<string, { nodeName: string; nodeId: string; fields: FieldInfo[] }>)
- const nodeGroups = Object.values(groupedByNode)
- if (fields.length === 0) {
- return (
- <div className="p-4 text-center text-sm text-gray-500 dark:text-gray-400">
- <div className="mb-2">No upstream data available</div>
- <div className="text-xs text-gray-400 dark:text-gray-500">
- Connect nodes to see their output fields here
- </div>
- </div>
- )
- }
- return (
- <div className="h-full flex flex-col">
- <div className="px-3 py-2 border-b border-gray-200 dark:border-slate-700 bg-gray-50 dark:bg-slate-800">
- <h3 className="text-sm font-medium text-gray-700 dark:text-gray-200">{title}</h3>
- <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
- Drag or click to insert expressions
- </p>
- </div>
- <div className="flex-1 overflow-auto p-2 bg-white dark:bg-slate-900">
- {nodeGroups.map((group) => (
- <NodeGroup
- key={group.nodeId}
- nodeName={group.nodeName}
- fields={group.fields}
- onInsertExpression={onInsertExpression}
- />
- ))}
- </div>
- {/* Quick reference */}
- <div className="px-3 py-2 border-t border-gray-200 dark:border-slate-700 bg-gray-50 dark:bg-slate-800 text-xs text-gray-500 dark:text-gray-400">
- <div className="font-medium mb-1">Expression Syntax:</div>
- <code className="text-[10px] bg-gray-100 dark:bg-slate-700 px-1 rounded">
- {'{{$node["Name"].path}}'}
- </code>
- </div>
- </div>
- )
- }
|