diff --git a/sim/app/w/[id]/components/workflow-loop/components/loop-input/loop-input.tsx b/sim/app/w/[id]/components/workflow-loop/components/loop-input/loop-input.tsx index c3c0af9f6c..0f3eee1926 100644 --- a/sim/app/w/[id]/components/workflow-loop/components/loop-input/loop-input.tsx +++ b/sim/app/w/[id]/components/workflow-loop/components/loop-input/loop-input.tsx @@ -8,7 +8,6 @@ import { NodeProps } from 'reactflow' import { Badge } from '@/components/ui/badge' import { Input } from '@/components/ui/input' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' -import { checkTagTrigger, TagDropdown } from '@/components/ui/tag-dropdown' import { cn } from '@/lib/utils' import { useWorkflowStore } from '@/stores/workflows/workflow/store' @@ -29,11 +28,6 @@ export function LoopInput({ id }: NodeProps) { const [open, setOpen] = useState(false) const editorRef = useRef(null) - // State for tag dropdown - const [showTags, setShowTags] = useState(false) - const [cursorPosition, setCursorPosition] = useState(0) - const [activeSourceBlockId, setActiveSourceBlockId] = useState(null) - // Initialize editor value from the store useEffect(() => { if (loopType === 'forEach' && loop?.forEachItems) { @@ -85,27 +79,10 @@ export function LoopInput({ id }: NodeProps) { } } - // Handle tag selection - const handleTagSelect = (newValue: string) => { - setEditorValue(newValue) - updateLoopForEachItems(loopId, newValue) - setShowTags(false) - } - const handleEditorChange = (value: string) => { // Always set the editor value to exactly what the user typed setEditorValue(value) - // Get current cursor position (approximation for editor) - const textArea = editorRef.current?.querySelector('textarea') - if (textArea) { - setCursorPosition(textArea.selectionStart || 0) - - // Check for tag trigger - const tagTrigger = checkTagTrigger(value, textArea.selectionStart || 0) - setShowTags(tagTrigger.show) - } - // Save the items to the store for forEach loops if (loopType === 'forEach') { // Pass the exact string to preserve formatting @@ -113,52 +90,6 @@ export function LoopInput({ id }: NodeProps) { } } - // Handle editor focus - const handleEditorFocus = () => { - // Reset tag dropdown state - setShowTags(false) - setActiveSourceBlockId(null) - } - - // Handle editor blur - const handleEditorBlur = () => { - // We don't immediately hide the tag dropdown to allow clicking on it - setTimeout(() => { - if (!document.activeElement?.closest('.tag-dropdown')) { - setShowTags(false) - } - }, 100) - } - - // Add and remove keyboard event listener for editor - useEffect(() => { - const textArea = editorRef.current?.querySelector('textarea') - if (!textArea) return - - const handleKeyboardEvent = (e: KeyboardEvent) => { - // Update cursor position when key is pressed in editor - setCursorPosition(textArea.selectionStart || 0) - - // Check for tag trigger - const tagTrigger = checkTagTrigger(editorValue, textArea.selectionStart || 0) - - if (e.key === 'Escape') { - setShowTags(false) - } else if (e.key === '<') { - // Show tags dropdown when '<' is typed - setShowTags(true) - } else if (e.key === '>') { - // Hide tags dropdown when '>' is typed - setShowTags(false) - } else { - setShowTags(tagTrigger.show) - } - } - - textArea.addEventListener('keyup', handleKeyboardEvent) - return () => textArea.removeEventListener('keyup', handleKeyboardEvent) - }, [editorValue, editorRef]) - // Determine label based on loop type const getLabel = () => { switch (loopType) { @@ -218,45 +149,27 @@ export function LoopInput({ id }: NodeProps) { /> ) : ( - // Code editor for 'forEach' loops with tag dropdown support -
-
- {editorValue === '' && ( -
- {getPlaceholder()} -
- )} - highlight(code, languages.javascript, 'javascript')} - padding={0} - style={{ - fontFamily: 'monospace', - lineHeight: '21px', - }} - className="focus:outline-none w-full" - textareaClassName="focus:outline-none focus:ring-0 bg-transparent resize-none w-full overflow-hidden whitespace-pre-wrap" - onFocus={handleEditorFocus} - onBlur={handleEditorBlur} - /> -
- { - setShowTags(false) - setActiveSourceBlockId(null) + // Code editor for 'forEach' loops +
+ {editorValue === '' && ( +
+ {getPlaceholder()} +
+ )} + highlight(code, languages.javascript, 'javascript')} + padding={0} + style={{ + fontFamily: 'monospace', + lineHeight: '21px', }} - className="w-[calc(100%)] tag-dropdown" - style={{ top: 'calc(100% + 4px)' }} + className="focus:outline-none w-full" + textareaClassName="focus:outline-none focus:ring-0 bg-transparent resize-none w-full overflow-hidden whitespace-pre-wrap" />
)} diff --git a/sim/executor/index.ts b/sim/executor/index.ts index 4407357df1..e46253808c 100644 --- a/sim/executor/index.ts +++ b/sim/executor/index.ts @@ -45,7 +45,7 @@ export class Executor { this.validateWorkflow() this.workflowInput = workflowInput || {} - this.loopManager = new LoopManager(workflow.loops || {}, 5, workflowVariables) + this.loopManager = new LoopManager(workflow.loops || {}) this.resolver = new InputResolver(workflow, environmentVariables, workflowVariables, this.loopManager) this.pathTracker = new PathTracker(workflow) diff --git a/sim/executor/loops.ts b/sim/executor/loops.ts index ed070ab0f9..1c326c201d 100644 --- a/sim/executor/loops.ts +++ b/sim/executor/loops.ts @@ -5,15 +5,10 @@ import { ExecutionContext } from './types' * Manages loop detection, iteration limits, and state resets. */ export class LoopManager { - private workflowVariables: Record; - constructor( private loops: Record, - private defaultIterations: number = 5, - workflowVariables: Record = {} - ) { - this.workflowVariables = workflowVariables; - } + private defaultIterations: number = 5 + ) {} /** * Processes all loops and checks if any need to be iterated. @@ -252,161 +247,8 @@ export class LoopManager { return []; } - // Import the resolver to handle variable and block references - let InputResolver; - try { - InputResolver = (await import('./resolver')).InputResolver; - } catch (importError) { - console.error(`Failed to import InputResolver: ${importError}`); - return []; - } - - // Create a temporary block for the resolver to use - const dummyBlock = { - id: `temp-${loopId}`, - position: { x: 0, y: 0 }, - config: { - tool: 'loop', - params: { - items: trimmedExpression - } - }, - inputs: {}, - outputs: {}, - enabled: true - }; - - // Create a resolver instance - const resolver = new InputResolver( - context.workflow || { version: '1.0', blocks: [], connections: [], loops: {} }, - context.environmentVariables, - this.workflowVariables, - this - ); - - // Special handling for tag references (single variable tag enclosed in < >) - if (/^<[^>]+>$/.test(trimmedExpression)) { - // For a single tag reference, try to resolve it directly - const resolved = resolver.resolveBlockReferences( - trimmedExpression, - context, - dummyBlock as any - ); - - // If the resolved value has changed, it was successfully resolved - if (resolved !== trimmedExpression) { - try { - // Try to parse as JSON if it looks like JSON - if ((resolved.startsWith('[') && resolved.endsWith(']')) || - (resolved.startsWith('{') && resolved.endsWith('}'))) { - return JSON.parse(resolved); - } else if (resolved.trim() !== '') { - // If it's not JSON but has content, return as a single-item array - return [resolved]; - } - } catch (jsonError) { - // If JSON parsing fails, just return as a single item - return [resolved]; - } - } - } - - // Check if the string contains variables tags (with < and >) - const containsVariableTags = trimmedExpression.includes('<') && trimmedExpression.includes('>'); - - // Handle arrays with variable references: [, , 3, "text"] - if (containsVariableTags && trimmedExpression.startsWith('[') && trimmedExpression.endsWith(']')) { - try { - // Extract the items but preserve the array structure - const itemsStr = trimmedExpression.substring(1, trimmedExpression.length - 1); - - // Use a simple but effective split that respects JSON structure - const items = this.splitArrayItems(itemsStr); - - // Process each item - const result = []; - for (const item of items) { - const trimmedItem = item.trim(); - - // If it's a variable tag, resolve it - if (trimmedItem.includes('<') && trimmedItem.includes('>')) { - // Resolve any variable/block references in this item - const resolvedItem = resolver.resolveBlockReferences( - trimmedItem, - context, - dummyBlock as any - ); - - // If it's still the same as the original, it wasn't resolved - if (resolvedItem === trimmedItem) { - // Keep as is for now - it may be resolvable during iteration - result.push(trimmedItem); - } else { - // Successfully resolved - parse as JSON if possible - try { - if ((resolvedItem.startsWith('[') && resolvedItem.endsWith(']')) || - (resolvedItem.startsWith('{') && resolvedItem.endsWith('}'))) { - result.push(JSON.parse(resolvedItem)); - } else { - // Not JSON, use the primitive value - // Try to convert to appropriate type - if (resolvedItem === 'true') result.push(true); - else if (resolvedItem === 'false') result.push(false); - else if (resolvedItem === 'null') result.push(null); - else if (!isNaN(Number(resolvedItem))) result.push(Number(resolvedItem)); - else result.push(resolvedItem); - } - } catch (jsonError) { - // Not valid JSON, use as is - result.push(resolvedItem); - } - } - } else { - // For non-variable items, parse as JSON - try { - result.push(JSON.parse(trimmedItem)); - } catch (jsonError) { - // If not valid JSON, add as a string - result.push(trimmedItem); - } - } - } - - return result; - } catch (e) { - console.error(`Error processing array with variables for loop ${loopId}:`, e); - // Fall back to treating the whole thing as a string array - return [trimmedExpression]; - } - } - - // Handle objects with variable references: {"key": , "key2": "value"} - if (containsVariableTags && trimmedExpression.startsWith('{') && trimmedExpression.endsWith('}')) { - try { - // For objects, try a different approach - first resolve all variables in the string - const fullyResolvedStr = resolver.resolveBlockReferences( - trimmedExpression, - context, - dummyBlock as any - ); - - // Try to parse the fully resolved string as JSON - try { - return JSON.parse(fullyResolvedStr); - } catch (jsonError) { - console.error(`Error parsing resolved object for loop ${loopId}:`, jsonError); - // If that fails, return as a string - return [fullyResolvedStr]; - } - } catch (e) { - console.error(`Error processing object with variables for loop ${loopId}:`, e); - return [trimmedExpression]; - } - } - - // If it doesn't contain variable tags, or isn't an array/object with tags, - // continue with standard JSON parsing - if (!containsVariableTags && (trimmedExpression.startsWith('[') || trimmedExpression.startsWith('{'))) { + // First check if it's valid JSON (array or object) + if (trimmedExpression.startsWith('[') || trimmedExpression.startsWith('{')) { try { // Try to parse as JSON first return JSON.parse(trimmedExpression); @@ -417,20 +259,16 @@ export class LoopManager { } // If not valid JSON or JSON parsing failed, try to evaluate as an expression - try { - const result = new Function('context', `return ${loop.forEachItems}`)(context); - - // If the result is an array or object, return it - if (Array.isArray(result) || (typeof result === 'object' && result !== null)) { - return result; - } - - // If it's a primitive, wrap it in an array - if (result !== undefined) { - return [result]; - } - } catch (evalError) { - console.error(`Error evaluating expression for loop ${loopId}:`, evalError); + const result = new Function('context', `return ${loop.forEachItems}`)(context); + + // If the result is an array or object, return it + if (Array.isArray(result) || (typeof result === 'object' && result !== null)) { + return result; + } + + // If it's a primitive, wrap it in an array + if (result !== undefined) { + return [result]; } return []; @@ -458,74 +296,6 @@ export class LoopManager { return []; } - /** - * Splits array items respecting nested structures like objects and arrays. - * This is a more robust way to split array items than simply splitting by commas. - * - * @param str - The string containing array items (without the outer brackets) - * @returns Array of item strings - */ - private splitArrayItems(str: string): string[] { - const result: string[] = []; - let currentItem = ''; - let depth = 0; - let inString = false; - let escapeNext = false; - - for (let i = 0; i < str.length; i++) { - const char = str[i]; - - // Handle escape sequences in strings - if (inString && char === '\\' && !escapeNext) { - escapeNext = true; - currentItem += char; - continue; - } - - // If we just processed an escape, reset the flag - if (escapeNext) { - escapeNext = false; - currentItem += char; - continue; - } - - // Handle string boundaries - if (char === '"' || char === "'") { - inString = !inString; - currentItem += char; - continue; - } - - // Skip processing special characters if we're in a string - if (inString) { - currentItem += char; - continue; - } - - // Handle nested structures - if (char === '[' || char === '{') { - depth++; - currentItem += char; - } else if (char === ']' || char === '}') { - depth--; - currentItem += char; - } else if (char === ',' && depth === 0) { - // Only split on commas at the top level - result.push(currentItem); - currentItem = ''; - } else { - currentItem += char; - } - } - - // Add the last item if there is one - if (currentItem.trim()) { - result.push(currentItem); - } - - return result; - } - /** * Finds the entry block for a loop (the one that should be executed first). * Typically the block with the fewest incoming connections. diff --git a/sim/executor/resolver.ts b/sim/executor/resolver.ts index 73e9cb443e..5aad7e4759 100644 --- a/sim/executor/resolver.ts +++ b/sim/executor/resolver.ts @@ -195,26 +195,7 @@ export class InputResolver { // Get the current item directly from the items array at the current index if (Array.isArray(items) && index >= 0 && index < items.length) { - let currentItem = items[index]; - - // Special handling for variable references stored as strings in loop items - if (typeof currentItem === 'string' && currentItem.startsWith('<') && currentItem.endsWith('>') && - currentItem.includes('variable.')) { - // This is a variable reference stored as a string in the array - // Try to resolve it using the same variable resolution logic - const resolvedVar = this.resolveVariableReferences(currentItem); - - // If it was resolved (changed), update the current item - if (resolvedVar !== currentItem) { - currentItem = resolvedVar; - - // Try to convert to appropriate primitive type if possible - if (resolvedVar === 'true') currentItem = true; - else if (resolvedVar === 'false') currentItem = false; - else if (resolvedVar === 'null') currentItem = null; - else if (!isNaN(Number(resolvedVar))) currentItem = Number(resolvedVar); - } - } + const currentItem = items[index]; // Format the value based on type if (currentItem !== undefined) { diff --git a/sim/stores/workflows/workflow/store.ts b/sim/stores/workflows/workflow/store.ts index e064298ec2..9c290de31a 100644 --- a/sim/stores/workflows/workflow/store.ts +++ b/sim/stores/workflows/workflow/store.ts @@ -563,22 +563,19 @@ export const useWorkflowStore = create()( }, updateLoopForEachItems: (loopId: string, items: string) => { - // Don't try to parse JSON if the string contains variable tags <...> - const containsVariableTags = items.includes('<') && items.includes('>') + let parsedItems: any = items; - let parsedItems = items; - - // Only try to parse as JSON if it doesn't contain variable tags - if (!containsVariableTags && - typeof items === 'string' && + // Try to parse the string as JSON if it looks like JSON + if (typeof items === 'string' && ((items.trim().startsWith('[') && items.trim().endsWith(']')) || (items.trim().startsWith('{') && items.trim().endsWith('}'))) ) { try { // First try to parse to validate it's valid JSON - JSON.parse(items); + const parsed = JSON.parse(items); // If parsing succeeds, store the original string to preserve formatting + // This way we keep the user's exact formatting (spacing, line breaks, etc.) parsedItems = items; } catch (e) { // If parsing fails, keep it as a string expression @@ -589,7 +586,7 @@ export const useWorkflowStore = create()( const newState = { blocks: { ...get().blocks }, - edges: [...get().edges ], + edges: [...get().edges], loops: { ...get().loops, [loopId]: {