Browse Source

feat(45-02): add ConditionalSwitch execution and match-aware text routing

- Import executeConditionalSwitch in workflowStore
- Add evaluateRuleMatch helper function for rule evaluation
- Add conditionalSwitch case to executeWorkflow with fresh match evaluation
- Add conditionalSwitch case to regenerateNode with same pattern
- Import ConditionalSwitchNodeData and MatchMode in connectedInputs
- Add conditionalSwitch passthrough in getConnectedInputsPure
- Block non-active outputs (isMatched=false) from data flow
- Default output active when no rules match
- Active outputs recursively get upstream text
handoff-20260429-1057
shrimbly 5 months ago
parent
commit
f41c09a6c1
  1. 29
      src/store/utils/connectedInputs.ts
  2. 81
      src/store/workflowStore.ts

29
src/store/utils/connectedInputs.ts

@ -25,6 +25,8 @@ import {
LLMGenerateNodeData, LLMGenerateNodeData,
GLBViewerNodeData, GLBViewerNodeData,
SwitchNodeData, SwitchNodeData,
ConditionalSwitchNodeData,
MatchMode,
} from "@/types"; } from "@/types";
/** /**
@ -227,6 +229,33 @@ export function getConnectedInputsPure(
return; // Skip normal getSourceOutput processing return; // Skip normal getSourceOutput processing
} }
// 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;
// Active output: recursively get upstream text
const condInputs = getConnectedInputsPure(sourceNode.id, nodes, edges, _visited, dimmedNodeIds);
if (condInputs.text) text = condInputs.text;
return; // Skip normal getSourceOutput processing
}
const handleId = edge.targetHandle; const handleId = edge.targetHandle;
const { type, value } = getSourceOutput( const { type, value } = getSourceOutput(
sourceNode, sourceNode,

81
src/store/workflowStore.ts

@ -80,11 +80,48 @@ import {
executeGlbViewer, executeGlbViewer,
executeRouter, executeRouter,
executeSwitch, executeSwitch,
executeConditionalSwitch,
} from "./execution"; } from "./execution";
import type { NodeExecutionContext } from "./execution"; import type { NodeExecutionContext } from "./execution";
export type { LevelGroup } from "./utils/executionUtils"; export type { LevelGroup } from "./utils/executionUtils";
export { CONCURRENCY_SETTINGS_KEY } from "./utils/executionUtils"; export { CONCURRENCY_SETTINGS_KEY } from "./utils/executionUtils";
/**
* Evaluates a rule against incoming text with the specified match mode.
* Returns true if any comma-separated value matches using OR logic.
*/
function evaluateRuleMatch(text: string | null, ruleValue: string, mode: "exact" | "contains" | "starts-with" | "ends-with"): boolean {
// Guard: no match if text or ruleValue is empty
if (!text || !ruleValue) return false;
// Normalize text
const normalizedText = text.toLowerCase().trim();
// Split and normalize rule values (comma-separated)
const values = ruleValue
.split(',')
.map(v => v.toLowerCase().trim())
.filter(v => v.length > 0);
if (values.length === 0) return false;
// OR logic: match if ANY value matches
return values.some(value => {
switch (mode) {
case "exact":
return normalizedText === value;
case "contains":
return normalizedText.includes(value);
case "starts-with":
return normalizedText.startsWith(value);
case "ends-with":
return normalizedText.endsWith(value);
default:
return false;
}
});
}
function saveLogSession(): void { function saveLogSession(): void {
const session = logger.getCurrentSession(); const session = logger.getCurrentSession();
if (session) { if (session) {
@ -1050,6 +1087,28 @@ const workflowStoreImpl: StateCreator<WorkflowStore> = (set, get) => ({
case "switch": case "switch":
await executeSwitch(executionCtx); await executeSwitch(executionCtx);
break; break;
case "conditionalSwitch": {
// Before executing, compute fresh match status from incoming text
const condInputs = get().getConnectedInputs(node.id);
const incomingText = condInputs.text;
const nodeData = node.data as { rules: Array<{ id: string; value: string; mode: string; label: string; isMatched: boolean }> };
// Evaluate each rule against incoming text
const updatedRules = nodeData.rules.map(rule => {
const isMatched = evaluateRuleMatch(incomingText, rule.value, rule.mode as "exact" | "contains" | "starts-with" | "ends-with");
return { ...rule, isMatched };
});
// Update node data with fresh match status
get().updateNodeData(node.id, {
incomingText,
rules: updatedRules,
});
// Then execute the node (status flash)
await executeConditionalSwitch(executionCtx);
break;
}
} }
}; // End of executeSingleNode helper }; // End of executeSingleNode helper
@ -1374,6 +1433,28 @@ const workflowStoreImpl: StateCreator<WorkflowStore> = (set, get) => ({
case "switch": case "switch":
await executeSwitch(executionCtx); await executeSwitch(executionCtx);
break; break;
case "conditionalSwitch": {
// Before executing, compute fresh match status from incoming text
const condInputs = get().getConnectedInputs(node.id);
const incomingText = condInputs.text;
const nodeData = node.data as { rules: Array<{ id: string; value: string; mode: string; label: string; isMatched: boolean }> };
// Evaluate each rule against incoming text
const updatedRules = nodeData.rules.map(rule => {
const isMatched = evaluateRuleMatch(incomingText, rule.value, rule.mode as "exact" | "contains" | "starts-with" | "ends-with");
return { ...rule, isMatched };
});
// Update node data with fresh match status
get().updateNodeData(node.id, {
incomingText,
rules: updatedRules,
});
// Then execute the node (status flash)
await executeConditionalSwitch(executionCtx);
break;
}
} }
}; };

Loading…
Cancel
Save