mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
Revert "improvement(loops): added vars to loop-input"
This reverts commit 89ba7fa3c2.
This commit is contained in:
@@ -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<HTMLDivElement | null>(null)
|
||||
|
||||
// State for tag dropdown
|
||||
const [showTags, setShowTags] = useState(false)
|
||||
const [cursorPosition, setCursorPosition] = useState(0)
|
||||
const [activeSourceBlockId, setActiveSourceBlockId] = useState<string | null>(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) {
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
// Code editor for 'forEach' loops with tag dropdown support
|
||||
<div className="relative">
|
||||
<div
|
||||
className="relative min-h-[80px] rounded-md bg-background font-mono text-sm px-3 pt-2 pb-3 border border-input"
|
||||
ref={editorRef}
|
||||
>
|
||||
{editorValue === '' && (
|
||||
<div className="absolute top-[8.5px] left-3 text-muted-foreground/50 pointer-events-none select-none">
|
||||
{getPlaceholder()}
|
||||
</div>
|
||||
)}
|
||||
<Editor
|
||||
value={editorValue}
|
||||
onValueChange={handleEditorChange}
|
||||
highlight={(code) => 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}
|
||||
/>
|
||||
</div>
|
||||
<TagDropdown
|
||||
visible={showTags}
|
||||
onSelect={handleTagSelect}
|
||||
blockId={id}
|
||||
activeSourceBlockId={activeSourceBlockId}
|
||||
inputValue={editorValue}
|
||||
cursorPosition={cursorPosition}
|
||||
onClose={() => {
|
||||
setShowTags(false)
|
||||
setActiveSourceBlockId(null)
|
||||
// Code editor for 'forEach' loops
|
||||
<div
|
||||
className="relative min-h-[80px] rounded-md bg-background font-mono text-sm px-3 pt-2 pb-3 border border-input"
|
||||
ref={editorRef}
|
||||
>
|
||||
{editorValue === '' && (
|
||||
<div className="absolute top-[8.5px] left-3 text-muted-foreground/50 pointer-events-none select-none">
|
||||
{getPlaceholder()}
|
||||
</div>
|
||||
)}
|
||||
<Editor
|
||||
value={editorValue}
|
||||
onValueChange={handleEditorChange}
|
||||
highlight={(code) => 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"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
+14
-244
@@ -5,15 +5,10 @@ import { ExecutionContext } from './types'
|
||||
* Manages loop detection, iteration limits, and state resets.
|
||||
*/
|
||||
export class LoopManager {
|
||||
private workflowVariables: Record<string, any>;
|
||||
|
||||
constructor(
|
||||
private loops: Record<string, SerializedLoop>,
|
||||
private defaultIterations: number = 5,
|
||||
workflowVariables: Record<string, any> = {}
|
||||
) {
|
||||
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: [<var1>, <var2>, 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": <var>, "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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -563,22 +563,19 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
},
|
||||
|
||||
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<WorkflowStoreWithHistory>()(
|
||||
|
||||
const newState = {
|
||||
blocks: { ...get().blocks },
|
||||
edges: [...get().edges ],
|
||||
edges: [...get().edges],
|
||||
loops: {
|
||||
...get().loops,
|
||||
[loopId]: {
|
||||
|
||||
Reference in New Issue
Block a user