diff --git a/.planning/STATE.md b/.planning/STATE.md
index fc7f8463..9e169fa6 100644
--- a/.planning/STATE.md
+++ b/.planning/STATE.md
@@ -10,18 +10,18 @@ See: .planning/PROJECT.md (updated 2026-01-09)
## Current Position
Phase: 40 of 40 (Node Enhancements)
-Plan: 1 of 4 in current phase
+Plan: 4 of 4 in current phase
Status: In Progress
-Last activity: 2026-02-01 - Completed 40-01-PLAN.md (OutputGallery node)
+Last activity: 2026-02-01 - Completed 40-04-PLAN.md (PromptConstructor node)
-Progress: ███████████░ 95%
+Progress: ████████████ 100%
## Performance Metrics
**Velocity:**
-- Total plans completed: 39
+- Total plans completed: 40
- Average duration: 6.6 min
-- Total execution time: 4.37 hours
+- Total execution time: 4.50 hours
**By Phase:**
@@ -57,11 +57,11 @@ Progress: ███████████░ 95%
| 33. Workflow Edit Safety | 2/2 | 5 min | 5 min |
| 34. Agentic Workflow Editing | 3/3 | 13 min | 4.3 min |
| 35. Large Workflow Handling | 3/3 | 18 min | 6 min |
-| 40. Node Enhancements | 1/4 | 5 min | 5 min |
+| 40. Node Enhancements | 4/4 | 13 min | 3.25 min |
**Recent Trend:**
-- Last 5 plans: 10 min, 3 min, 10 min, 5 min, 2 min, 5 min
-- Trend: Phase 40 in progress - node enhancements (OutputGallery complete)
+- Last 5 plans: 3 min, 10 min, 5 min, 2 min, 5 min, 8 min
+- Trend: Phase 40 complete - node enhancements shipped (OutputGallery, PromptConstructor)
## Accumulated Context
@@ -188,6 +188,12 @@ Recent decisions affecting current work:
- Real-time image collection pattern: useMemo watching edges/nodes for live updates
- nowheel class for scroll isolation inside React Flow nodes
- Pink minimap color (#ec4899) for OutputGallery nodes
+- PromptNode variable naming with optional variableName field (backward compatible)
+- @ icon in PromptNode header indicates variable status (blue when set, dimmed when not)
+- PromptConstructor template resolution uses @variable syntax with alphanumeric + underscore
+- Autocomplete triggers on @ character with keyboard navigation (arrows, enter, tab, escape)
+- Unresolved @variables show warning badge but execution proceeds with literal text
+- Template resolution happens during workflow execution, not on template change
### Deferred Issues
@@ -224,6 +230,6 @@ Recent decisions affecting current work:
## Session Continuity
Last session: 2026-02-01
-Stopped at: Completed 40-01-PLAN.md (OutputGallery node)
+Stopped at: Completed 40-04-PLAN.md (PromptConstructor node)
Resume file: None
-Next action: Continue Phase 40 - 3 more plans remaining (Connection numbering, ImageCompare, PromptConstructor)
+Next action: Phase 40 complete - all 4 plans shipped (OutputGallery, Connection numbering, ImageCompare, PromptConstructor)
diff --git a/package-lock.json b/package-lock.json
index 9a9308e6..f29cb08c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,6 +20,7 @@
"next": "^16.0.7",
"postcss": "^8.5.6",
"react": "^19.2.0",
+ "react-compare-slider": "^3.1.0",
"react-dom": "^19.2.4",
"react-konva": "^19.2.1",
"react-markdown": "^10.1.0",
@@ -5865,6 +5866,19 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-compare-slider": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/react-compare-slider/-/react-compare-slider-3.1.0.tgz",
+ "integrity": "sha512-TQVbZYmYyTIeKRmQciVXCmUwHjTThQTON7GfWfzMAOInRRG9tCiQnVXnCUd5DJ5l3Hngh4IEzOb9TG82gjoEhQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.9.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
"node_modules/react-dom": {
"version": "19.2.4",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
diff --git a/package.json b/package.json
index becfd1d9..1aeefda8 100644
--- a/package.json
+++ b/package.json
@@ -24,6 +24,7 @@
"next": "^16.0.7",
"postcss": "^8.5.6",
"react": "^19.2.0",
+ "react-compare-slider": "^3.1.0",
"react-dom": "^19.2.4",
"react-konva": "^19.2.1",
"react-markdown": "^10.1.0",
diff --git a/src/components/ConnectionDropMenu.tsx b/src/components/ConnectionDropMenu.tsx
index ee6084f1..cb3b5c4a 100644
--- a/src/components/ConnectionDropMenu.tsx
+++ b/src/components/ConnectionDropMenu.tsx
@@ -79,6 +79,15 @@ const IMAGE_TARGET_OPTIONS: MenuOption[] = [
),
},
+ {
+ type: "imageCompare",
+ label: "Image Compare",
+ icon: (
+
+ ),
+ },
];
const TEXT_TARGET_OPTIONS: MenuOption[] = [
diff --git a/src/components/WorkflowCanvas.tsx b/src/components/WorkflowCanvas.tsx
index 73a2b9ee..4ee7fc51 100644
--- a/src/components/WorkflowCanvas.tsx
+++ b/src/components/WorkflowCanvas.tsx
@@ -30,6 +30,7 @@ import {
SplitGridNode,
OutputNode,
OutputGalleryNode,
+ ImageCompareNode,
} from "./nodes";
import { EditableEdge, ReferenceEdge } from "./edges";
import { ConnectionDropMenu, MenuAction } from "./ConnectionDropMenu";
@@ -56,6 +57,7 @@ const nodeTypes: NodeTypes = {
splitGrid: SplitGridNode,
output: OutputNode,
outputGallery: OutputGalleryNode,
+ imageCompare: ImageCompareNode,
};
const edgeTypes: EdgeTypes = {
@@ -104,6 +106,8 @@ const getNodeHandles = (nodeType: string): { inputs: string[]; outputs: string[]
return { inputs: ["image"], outputs: [] };
case "outputGallery":
return { inputs: ["image"], outputs: [] };
+ case "imageCompare":
+ return { inputs: ["image"], outputs: [] };
default:
return { inputs: [], outputs: [] };
}
@@ -897,6 +901,7 @@ export function WorkflowCanvas() {
splitGrid: { width: 300, height: 320 },
output: { width: 320, height: 320 },
outputGallery: { width: 320, height: 360 },
+ imageCompare: { width: 400, height: 360 },
};
const dims = defaultDimensions[nodeType];
addNode(nodeType, { x: centerX - dims.width / 2, y: centerY - dims.height / 2 });
@@ -1388,6 +1393,8 @@ export function WorkflowCanvas() {
return "#ef4444";
case "outputGallery":
return "#ec4899";
+ case "imageCompare":
+ return "#14b8a6";
default:
return "#94a3b8";
}
diff --git a/src/components/nodes/index.ts b/src/components/nodes/index.ts
index cbd13572..0962be21 100644
--- a/src/components/nodes/index.ts
+++ b/src/components/nodes/index.ts
@@ -8,4 +8,5 @@ export { LLMGenerateNode } from "./LLMGenerateNode";
export { SplitGridNode } from "./SplitGridNode";
export { OutputNode } from "./OutputNode";
export { OutputGalleryNode } from "./OutputGalleryNode";
+export { ImageCompareNode } from "./ImageCompareNode";
export { GroupNode } from "./GroupNode";
diff --git a/src/lib/quickstart/validation.ts b/src/lib/quickstart/validation.ts
index 236b16b0..f3bb97c5 100644
--- a/src/lib/quickstart/validation.ts
+++ b/src/lib/quickstart/validation.ts
@@ -36,6 +36,7 @@ const DEFAULT_DIMENSIONS: Record =
splitGrid: { width: 300, height: 320 },
output: { width: 320, height: 320 },
outputGallery: { width: 320, height: 360 },
+ imageCompare: { width: 400, height: 360 },
};
/**
@@ -276,6 +277,11 @@ function createDefaultNodeData(type: NodeType): WorkflowNodeData {
return {
images: [],
};
+ case "imageCompare":
+ return {
+ imageA: null,
+ imageB: null,
+ };
}
}
diff --git a/src/store/utils/nodeDefaults.ts b/src/store/utils/nodeDefaults.ts
index 63ce42f5..de962053 100644
--- a/src/store/utils/nodeDefaults.ts
+++ b/src/store/utils/nodeDefaults.ts
@@ -10,6 +10,7 @@ import {
SplitGridNodeData,
OutputNodeData,
OutputGalleryNodeData,
+ ImageCompareNodeData,
WorkflowNodeData,
GroupColor,
SelectedModel,
@@ -31,6 +32,7 @@ export const defaultNodeDimensions: Record {
return {
images: [],
} as OutputGalleryNodeData;
+ case "imageCompare":
+ return {
+ imageA: null,
+ imageB: null,
+ } as ImageCompareNodeData;
}
};
diff --git a/src/store/workflowStore.ts b/src/store/workflowStore.ts
index 9a3adba6..f52db15f 100644
--- a/src/store/workflowStore.ts
+++ b/src/store/workflowStore.ts
@@ -1832,6 +1832,15 @@ export const useWorkflowStore = create((set, get) => ({
updateNodeData(node.id, { images });
break;
}
+
+ case "imageCompare": {
+ const { images } = getConnectedInputs(node.id);
+ updateNodeData(node.id, {
+ imageA: images[0] || null,
+ imageB: images[1] || null,
+ });
+ break;
+ }
}
}
diff --git a/src/types/nodes.ts b/src/types/nodes.ts
index 8650f77e..d6226f4d 100644
--- a/src/types/nodes.ts
+++ b/src/types/nodes.ts
@@ -32,7 +32,8 @@ export type NodeType =
| "llmGenerate"
| "splitGrid"
| "output"
- | "outputGallery";
+ | "outputGallery"
+ | "imageCompare";
/**
* Node execution status
@@ -184,6 +185,14 @@ export interface OutputGalleryNodeData extends BaseNodeData {
images: string[]; // Array of base64 data URLs from connected nodes
}
+/**
+ * Image Compare node - side-by-side image comparison with draggable slider
+ */
+export interface ImageCompareNodeData extends BaseNodeData {
+ imageA: string | null;
+ imageB: string | null;
+}
+
/**
* Split Grid node - splits image into grid cells for parallel processing
*/
@@ -223,7 +232,8 @@ export type WorkflowNodeData =
| LLMGenerateNodeData
| SplitGridNodeData
| OutputNodeData
- | OutputGalleryNodeData;
+ | OutputGalleryNodeData
+ | ImageCompareNodeData;
/**
* Workflow node with typed data (extended with optional groupId)