From fe6a2e548001585a4483d2b46a0b1780f5c192a6 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Sat, 28 Feb 2026 20:47:16 +1300 Subject: [PATCH] feat: add Clear button to ConditionalSwitch node Adds an evaluationPaused flag that lets users reset evaluation state and un-dim all downstream paths. The flag auto-resets on workflow execution or manual rule edits. Co-Authored-By: Claude Opus 4.6 --- .../nodes/ConditionalSwitchNode.tsx | 53 +++++++++++++--- src/store/utils/connectedInputs.ts | 38 ++++++------ src/store/utils/dimmingUtils.ts | 61 +++++++++++-------- src/store/workflowStore.ts | 3 +- src/types/nodes.ts | 1 + 5 files changed, 104 insertions(+), 52 deletions(-) diff --git a/src/components/nodes/ConditionalSwitchNode.tsx b/src/components/nodes/ConditionalSwitchNode.tsx index ed71f3f8..1557d1c1 100644 --- a/src/components/nodes/ConditionalSwitchNode.tsx +++ b/src/components/nodes/ConditionalSwitchNode.tsx @@ -25,6 +25,9 @@ export const ConditionalSwitchNode = memo(({ id, data, selected }: NodeProps { + // Skip re-evaluation when paused — prevents upstream text from re-setting isMatched + if (nodeData.evaluationPaused) return; + const updatedRules = nodeData.rules.map(rule => ({ ...rule, isMatched: evaluateRule(incomingText, rule.value, rule.mode) @@ -44,7 +47,7 @@ export const ConditionalSwitchNode = memo(({ id, data, selected }: NodeProps>({}); @@ -98,24 +101,24 @@ export const ConditionalSwitchNode = memo(({ id, data, selected }: NodeProps { const updatedRules = nodeData.rules.map((rule) => rule.id === ruleId ? { ...rule, value: newValue } : rule ); - updateNodeData(id, { rules: updatedRules }); + updateNodeData(id, { rules: updatedRules, evaluationPaused: false }); }, [id, nodeData.rules, updateNodeData] ); - // Handle mode change + // Handle mode change (auto-unpauses evaluation) const handleModeChange = useCallback( (ruleId: string, newMode: MatchMode) => { const updatedRules = nodeData.rules.map((rule) => rule.id === ruleId ? { ...rule, mode: newMode } : rule ); - updateNodeData(id, { rules: updatedRules }); + updateNodeData(id, { rules: updatedRules, evaluationPaused: false }); }, [id, nodeData.rules, updateNodeData] ); @@ -159,6 +162,22 @@ export const ConditionalSwitchNode = memo(({ id, data, selected }: NodeProps { + const clearedRules = nodeData.rules.map(rule => ({ + ...rule, + isMatched: false, + })); + updateNodeData(id, { + evaluationPaused: true, + incomingText: null, + rules: clearedRules, + }); + }, [id, nodeData.rules, updateNodeData]); + + // Show clear button when there's evaluation state to clear and not already paused + const showClearButton = !nodeData.evaluationPaused && nodeData.incomingText !== null; + // Handle reorder (move up) const handleMoveUp = useCallback( (index: number) => { @@ -181,8 +200,8 @@ export const ConditionalSwitchNode = memo(({ id, data, selected }: NodeProps r.isMatched); + // Check if default is matched (no rules matched) — suppress when paused + const defaultMatched = !nodeData.evaluationPaused && !nodeData.rules.some(r => r.isMatched); return ( + + + ) : undefined} > {/* Input handle (left) - text only, aligned with header */} {/* Text preview — fixed height, above the handle-aligned area */}
- {incomingText ? ( + {nodeData.evaluationPaused ? ( + Evaluation paused + ) : incomingText ? ( <>Input: "{incomingText.slice(0, 50)}{incomingText.length > 50 ? "..." : ""}" ) : ( "No input connected" diff --git a/src/store/utils/connectedInputs.ts b/src/store/utils/connectedInputs.ts index 42a0c03a..2a852087 100644 --- a/src/store/utils/connectedInputs.ts +++ b/src/store/utils/connectedInputs.ts @@ -232,25 +232,29 @@ export function getConnectedInputsPure( // Conditional Switch passthrough — traverse upstream if output is active (matched or default) if (sourceNode.type === "conditionalSwitch") { const condData = sourceNode.data as ConditionalSwitchNodeData; - const sourceHandle = edge.sourceHandle; - - // Find matching rule or check if default - const rule = condData.rules.find(r => r.id === sourceHandle); - const isDefaultHandle = sourceHandle === "default"; - - // Determine if this output is active - let isActive = false; - if (rule) { - isActive = rule.isMatched; - } else if (isDefaultHandle) { - // Default is active when NO rules match - isActive = !condData.rules.some(r => r.isMatched); - } - // Block non-active outputs (data does not flow through non-matching rules) - if (!isActive) return; + // When evaluation is paused, all outputs are active (gate is open) + if (!condData.evaluationPaused) { + const sourceHandle = edge.sourceHandle; + + // Find matching rule or check if default + const rule = condData.rules.find(r => r.id === sourceHandle); + const isDefaultHandle = sourceHandle === "default"; + + // Determine if this output is active + let isActive = false; + if (rule) { + isActive = rule.isMatched; + } else if (isDefaultHandle) { + // Default is active when NO rules match + isActive = !condData.rules.some(r => r.isMatched); + } + + // Block non-active outputs (data does not flow through non-matching rules) + if (!isActive) return; + } - // Active output: ConditionalSwitch is a gate — trigger downstream but don't pass data through + // Active output (or paused): ConditionalSwitch is a gate — trigger downstream but don't pass data through return; } diff --git a/src/store/utils/dimmingUtils.ts b/src/store/utils/dimmingUtils.ts index 0860faf4..b36e09a4 100644 --- a/src/store/utils/dimmingUtils.ts +++ b/src/store/utils/dimmingUtils.ts @@ -36,6 +36,9 @@ export function computeDimmedNodes( const condData = node.data as ConditionalSwitchNodeData; if (!condData.rules) return; + // When evaluation is paused, skip dimming entirely — all paths stay active + if (condData.evaluationPaused) return; + // Non-matching rules: their downstream should be dimmed condData.rules.forEach(rule => { if (rule.isMatched) return; // Only process non-matching rules @@ -83,20 +86,24 @@ export function computeDimmedNodes( } } else if (sourceNode?.type === "conditionalSwitch") { const condData = sourceNode.data as ConditionalSwitchNodeData; - const rule = condData.rules?.find(r => r.id === edge.sourceHandle); - const isDefaultHandle = edge.sourceHandle === "default"; - - // Check if this output is inactive - let isInactive = false; - if (rule) { - isInactive = !rule.isMatched; - } else if (isDefaultHandle) { - // Default is inactive when any rule matches - isInactive = condData.rules?.some(r => r.isMatched) ?? false; - } - if (isInactive && edge.targetHandle) { - blockedTypes.add(edge.targetHandle); + // When evaluation is paused, outputs are not inactive + if (!condData.evaluationPaused) { + const rule = condData.rules?.find(r => r.id === edge.sourceHandle); + const isDefaultHandle = edge.sourceHandle === "default"; + + // Check if this output is inactive + let isInactive = false; + if (rule) { + isInactive = !rule.isMatched; + } else if (isDefaultHandle) { + // Default is inactive when any rule matches + isInactive = condData.rules?.some(r => r.isMatched) ?? false; + } + + if (isInactive && edge.targetHandle) { + blockedTypes.add(edge.targetHandle); + } } } else if (potentiallyDimmed.has(edge.source) && edge.targetHandle) { blockedTypes.add(edge.targetHandle); @@ -114,22 +121,24 @@ export function computeDimmedNodes( const switchEntry = switchData.switches?.find(s => s.id === edge.sourceHandle); if (switchEntry && !switchEntry.enabled) return false; } - // Skip non-matching ConditionalSwitch outputs + // Skip non-matching ConditionalSwitch outputs (but not when paused — all outputs active) if (sourceNode?.type === "conditionalSwitch") { const condData = sourceNode.data as ConditionalSwitchNodeData; - const rule = condData.rules?.find(r => r.id === edge.sourceHandle); - const isDefaultHandle = edge.sourceHandle === "default"; - - // Check if this output is inactive - let isInactive = false; - if (rule) { - isInactive = !rule.isMatched; - } else if (isDefaultHandle) { - // Default is inactive when any rule matches - isInactive = condData.rules?.some(r => r.isMatched) ?? false; + if (!condData.evaluationPaused) { + const rule = condData.rules?.find(r => r.id === edge.sourceHandle); + const isDefaultHandle = edge.sourceHandle === "default"; + + // Check if this output is inactive + let isInactive = false; + if (rule) { + isInactive = !rule.isMatched; + } else if (isDefaultHandle) { + // Default is inactive when any rule matches + isInactive = condData.rules?.some(r => r.isMatched) ?? false; + } + + if (isInactive) return false; } - - if (isInactive) return false; } // Active input — only counts if it provides a blocked type return edge.targetHandle ? blockedTypes.has(edge.targetHandle) : false; diff --git a/src/store/workflowStore.ts b/src/store/workflowStore.ts index 8688d627..2b956728 100644 --- a/src/store/workflowStore.ts +++ b/src/store/workflowStore.ts @@ -110,6 +110,7 @@ async function evaluateAndExecuteConditionalSwitch( updateNodeData(node.id, { incomingText, rules: updatedRules, + evaluationPaused: false, }); await executeConditionalSwitch(executionCtx); @@ -533,7 +534,7 @@ const workflowStoreImpl: StateCreator = (set, get) => ({ if (node?.type === "switch" && "switches" in data) { get().recomputeDimmedNodes(); } - if (node?.type === "conditionalSwitch" && "rules" in data) { + if (node?.type === "conditionalSwitch" && ("rules" in data || "evaluationPaused" in data)) { get().recomputeDimmedNodes(); } }, diff --git a/src/types/nodes.ts b/src/types/nodes.ts index 83b1f9e8..65725dbd 100644 --- a/src/types/nodes.ts +++ b/src/types/nodes.ts @@ -394,6 +394,7 @@ export interface ConditionalSwitchRule { export interface ConditionalSwitchNodeData extends BaseNodeData { incomingText: string | null; // Upstream text for evaluation and display rules: ConditionalSwitchRule[]; // User-defined rules + evaluationPaused?: boolean; // When true, skips rule evaluation and downstream dimming } /**