feat(loops): allow user to specify min number of loops, on top of the existing maxIterations functionality

This commit is contained in:
Waleed Latif
2025-03-03 19:47:18 -08:00
parent 76dbc4a52f
commit 0a7684c523
5 changed files with 103 additions and 18 deletions
@@ -13,34 +13,61 @@ export function LoopInput({ id }: NodeProps) {
// Get the max iterations from the store for this loop
const maxIterations = useWorkflowStore((state) => state.loops[loopId]?.maxIterations ?? 5)
const minIterations = useWorkflowStore((state) => state.loops[loopId]?.minIterations ?? 0)
const updateLoopMaxIterations = useWorkflowStore((state) => state.updateLoopMaxIterations)
const updateLoopMinIterations = useWorkflowStore((state) => state.updateLoopMinIterations)
// Local state for input value
const [inputValue, setInputValue] = useState(maxIterations.toString())
// Local state for input values
const [maxInputValue, setMaxInputValue] = useState(maxIterations.toString())
const [minInputValue, setMinInputValue] = useState(minIterations.toString())
const [open, setOpen] = useState(false)
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const handleMaxChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const sanitizedValue = e.target.value.replace(/[^0-9]/g, '')
const numValue = parseInt(sanitizedValue)
// Only update if it's a valid number and <= 50
if (!isNaN(numValue)) {
setInputValue(Math.min(50, numValue).toString())
setMaxInputValue(Math.min(50, numValue).toString())
} else {
setInputValue(sanitizedValue)
setMaxInputValue(sanitizedValue)
}
}
const handleMinChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const sanitizedValue = e.target.value.replace(/[^0-9]/g, '')
const numValue = parseInt(sanitizedValue)
// Only update if it's a valid number and <= max
if (!isNaN(numValue)) {
setMinInputValue(Math.min(parseInt(maxInputValue) || 50, numValue).toString())
} else {
setMinInputValue(sanitizedValue)
}
}
const handleSave = () => {
const value = parseInt(inputValue)
if (!isNaN(value)) {
const newValue = Math.min(50, value)
updateLoopMaxIterations(loopId, newValue)
const maxValue = parseInt(maxInputValue)
const minValue = parseInt(minInputValue)
if (!isNaN(maxValue)) {
const newMaxValue = Math.min(50, Math.max(minValue, maxValue))
updateLoopMaxIterations(loopId, newMaxValue)
// Sync input with store value
setInputValue(newValue.toString())
setMaxInputValue(newMaxValue.toString())
} else {
// Reset to current store value if invalid
setInputValue(maxIterations.toString())
setMaxInputValue(maxIterations.toString())
}
if (!isNaN(minValue)) {
const newMinValue = Math.min(maxValue, Math.max(0, minValue))
updateLoopMinIterations(loopId, newMinValue)
// Sync input with store value
setMinInputValue(newMinValue.toString())
} else {
// Reset to current store value if invalid
setMinInputValue(minIterations.toString())
}
}
@@ -63,24 +90,41 @@ export function LoopInput({ id }: NodeProps) {
'flex items-center gap-1'
)}
>
Max Iterations: {maxIterations}
Iterations: {minIterations}-{maxIterations}
<ChevronDown className="h-3 w-3 text-muted-foreground" />
</Badge>
</PopoverTrigger>
<PopoverContent className="w-48 p-3" align="start" onClick={(e) => e.stopPropagation()}>
<div className="space-y-2">
<div className="text-xs font-medium text-muted-foreground">Max Iterations</div>
<div className="text-xs font-medium text-muted-foreground">Min Iterations</div>
<div className="flex items-center gap-2">
<Input
type="text"
value={inputValue}
onChange={handleChange}
value={minInputValue}
onChange={handleMinChange}
onBlur={handleSave}
onKeyDown={handleKeyDown}
className="h-8 text-sm"
/>
</div>
<div className="text-[10px] text-muted-foreground">Enter a number between 1 and 50</div>
<div className="text-[10px] text-muted-foreground">
Enter a number between 0 and {maxInputValue}
</div>
<div className="mt-3 text-xs font-medium text-muted-foreground">Max Iterations</div>
<div className="flex items-center gap-2">
<Input
type="text"
value={maxInputValue}
onChange={handleMaxChange}
onBlur={handleSave}
onKeyDown={handleKeyDown}
className="h-8 text-sm"
/>
</div>
<div className="text-[10px] text-muted-foreground">
Enter a number between {minInputValue || 1} and 50
</div>
</div>
</PopoverContent>
</Popover>
+17 -2
View File
@@ -7,7 +7,8 @@ import { ExecutionContext } from './types'
export class LoopManager {
constructor(
private loops: Record<string, SerializedLoop>,
private defaultMaxIterations: number = 5
private defaultMaxIterations: number = 5,
private defaultMinIterations: number = 0
) {}
/**
@@ -34,8 +35,12 @@ export class LoopManager {
continue
}
// Check if we need to force iteration due to minimum iterations requirement
const minIterations = loop.minIterations || this.defaultMinIterations
const forceIteration = currentIteration < minIterations
// Check if loop should iterate again
const shouldIterate = this.shouldIterateLoop(loopId, context)
const shouldIterate = forceIteration || this.shouldIterateLoop(loopId, context)
if (shouldIterate) {
// Increment iteration counter
@@ -175,4 +180,14 @@ export class LoopManager {
getMaxIterations(loopId: string): number {
return this.loops[loopId]?.maxIterations || this.defaultMaxIterations
}
/**
* Gets the minimum iterations for a loop.
*
* @param loopId - ID of the loop
* @returns Minimum iterations for the loop
*/
getMinIterations(loopId: string): number {
return this.loops[loopId]?.minIterations || this.defaultMinIterations
}
}
+1
View File
@@ -43,4 +43,5 @@ export interface SerializedLoop {
id: string
nodes: string[]
maxIterations: number
minIterations: number
}
+23
View File
@@ -191,6 +191,7 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
id: loopId,
nodes: path,
maxIterations: 5,
minIterations: 0,
}
processedPaths.add(canonicalPath)
}
@@ -229,6 +230,7 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
id: loopId,
nodes: path,
maxIterations: 5,
minIterations: 0,
}
processedPaths.add(canonicalPath)
}
@@ -467,6 +469,27 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
workflowSync.sync()
},
updateLoopMinIterations: (loopId: string, minIterations: number) => {
const newState = {
blocks: { ...get().blocks },
edges: [...get().edges],
loops: {
...get().loops,
[loopId]: {
...get().loops[loopId],
minIterations: Math.max(
0,
Math.min(get().loops[loopId].maxIterations, minIterations)
), // Clamp between 0 and maxIterations
},
},
}
set(newState)
pushHistory(set, get, newState, 'Update loop min iterations')
get().updateLastSaved()
},
triggerUpdate: () => {
set((state) => ({
...state,
+2
View File
@@ -29,6 +29,7 @@ export interface Loop {
id: string
nodes: string[]
maxIterations: number
minIterations: number
}
export interface WorkflowState {
@@ -56,6 +57,7 @@ export interface WorkflowActions {
toggleBlockWide: (id: string) => void
updateBlockHeight: (id: string, height: number) => void
updateLoopMaxIterations: (loopId: string, maxIterations: number) => void
updateLoopMinIterations: (loopId: string, minIterations: number) => void
triggerUpdate: () => void
setDeploymentStatus: (isDeployed: boolean, deployedAt?: Date) => void
}