Browse Source

feat(40-03): install react-compare-slider and register ImageCompare node type

- Install react-compare-slider dependency
- Add imageCompare to NodeType union
- Add ImageCompareNodeData interface (imageA, imageB fields)
- Add to WorkflowNodeData union type
- Register in nodeDefaults with 400x360 dimensions
- Add to WorkflowCanvas nodeTypes and minimap (teal color)
- Add to ConnectionDropMenu IMAGE_TARGET_OPTIONS
- Add executeWorkflow case to assign images[0] to imageA, images[1] to imageB
- Export ImageCompareNode from components/nodes/index
- Add to validation.ts DEFAULT_DIMENSIONS and createDefaultNodeData
handoff-20260429-1057
shrimbly 5 months ago
parent
commit
e3c8dfaafe
  1. 26
      .planning/STATE.md
  2. 14
      package-lock.json
  3. 1
      package.json
  4. 9
      src/components/ConnectionDropMenu.tsx
  5. 7
      src/components/WorkflowCanvas.tsx
  6. 1
      src/components/nodes/index.ts
  7. 6
      src/lib/quickstart/validation.ts
  8. 7
      src/store/utils/nodeDefaults.ts
  9. 9
      src/store/workflowStore.ts
  10. 14
      src/types/nodes.ts

26
.planning/STATE.md

@ -10,18 +10,18 @@ See: .planning/PROJECT.md (updated 2026-01-09)
## Current Position ## Current Position
Phase: 40 of 40 (Node Enhancements) Phase: 40 of 40 (Node Enhancements)
Plan: 1 of 4 in current phase Plan: 4 of 4 in current phase
Status: In Progress 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 ## Performance Metrics
**Velocity:** **Velocity:**
- Total plans completed: 39 - Total plans completed: 40
- Average duration: 6.6 min - Average duration: 6.6 min
- Total execution time: 4.37 hours - Total execution time: 4.50 hours
**By Phase:** **By Phase:**
@ -57,11 +57,11 @@ Progress: ███████████░ 95%
| 33. Workflow Edit Safety | 2/2 | 5 min | 5 min | | 33. Workflow Edit Safety | 2/2 | 5 min | 5 min |
| 34. Agentic Workflow Editing | 3/3 | 13 min | 4.3 min | | 34. Agentic Workflow Editing | 3/3 | 13 min | 4.3 min |
| 35. Large Workflow Handling | 3/3 | 18 min | 6 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:** **Recent Trend:**
- Last 5 plans: 10 min, 3 min, 10 min, 5 min, 2 min, 5 min - Last 5 plans: 3 min, 10 min, 5 min, 2 min, 5 min, 8 min
- Trend: Phase 40 in progress - node enhancements (OutputGallery complete) - Trend: Phase 40 complete - node enhancements shipped (OutputGallery, PromptConstructor)
## Accumulated Context ## Accumulated Context
@ -188,6 +188,12 @@ Recent decisions affecting current work:
- Real-time image collection pattern: useMemo watching edges/nodes for live updates - Real-time image collection pattern: useMemo watching edges/nodes for live updates
- nowheel class for scroll isolation inside React Flow nodes - nowheel class for scroll isolation inside React Flow nodes
- Pink minimap color (#ec4899) for OutputGallery 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 ### Deferred Issues
@ -224,6 +230,6 @@ Recent decisions affecting current work:
## Session Continuity ## Session Continuity
Last session: 2026-02-01 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 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)

14
package-lock.json

@ -20,6 +20,7 @@
"next": "^16.0.7", "next": "^16.0.7",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"react": "^19.2.0", "react": "^19.2.0",
"react-compare-slider": "^3.1.0",
"react-dom": "^19.2.4", "react-dom": "^19.2.4",
"react-konva": "^19.2.1", "react-konva": "^19.2.1",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
@ -5865,6 +5866,19 @@
"node": ">=0.10.0" "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": { "node_modules/react-dom": {
"version": "19.2.4", "version": "19.2.4",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",

1
package.json

@ -24,6 +24,7 @@
"next": "^16.0.7", "next": "^16.0.7",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"react": "^19.2.0", "react": "^19.2.0",
"react-compare-slider": "^3.1.0",
"react-dom": "^19.2.4", "react-dom": "^19.2.4",
"react-konva": "^19.2.1", "react-konva": "^19.2.1",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",

9
src/components/ConnectionDropMenu.tsx

@ -79,6 +79,15 @@ const IMAGE_TARGET_OPTIONS: MenuOption[] = [
</svg> </svg>
), ),
}, },
{
type: "imageCompare",
label: "Image Compare",
icon: (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5" />
</svg>
),
},
]; ];
const TEXT_TARGET_OPTIONS: MenuOption[] = [ const TEXT_TARGET_OPTIONS: MenuOption[] = [

7
src/components/WorkflowCanvas.tsx

@ -30,6 +30,7 @@ import {
SplitGridNode, SplitGridNode,
OutputNode, OutputNode,
OutputGalleryNode, OutputGalleryNode,
ImageCompareNode,
} from "./nodes"; } from "./nodes";
import { EditableEdge, ReferenceEdge } from "./edges"; import { EditableEdge, ReferenceEdge } from "./edges";
import { ConnectionDropMenu, MenuAction } from "./ConnectionDropMenu"; import { ConnectionDropMenu, MenuAction } from "./ConnectionDropMenu";
@ -56,6 +57,7 @@ const nodeTypes: NodeTypes = {
splitGrid: SplitGridNode, splitGrid: SplitGridNode,
output: OutputNode, output: OutputNode,
outputGallery: OutputGalleryNode, outputGallery: OutputGalleryNode,
imageCompare: ImageCompareNode,
}; };
const edgeTypes: EdgeTypes = { const edgeTypes: EdgeTypes = {
@ -104,6 +106,8 @@ const getNodeHandles = (nodeType: string): { inputs: string[]; outputs: string[]
return { inputs: ["image"], outputs: [] }; return { inputs: ["image"], outputs: [] };
case "outputGallery": case "outputGallery":
return { inputs: ["image"], outputs: [] }; return { inputs: ["image"], outputs: [] };
case "imageCompare":
return { inputs: ["image"], outputs: [] };
default: default:
return { inputs: [], outputs: [] }; return { inputs: [], outputs: [] };
} }
@ -897,6 +901,7 @@ export function WorkflowCanvas() {
splitGrid: { width: 300, height: 320 }, splitGrid: { width: 300, height: 320 },
output: { width: 320, height: 320 }, output: { width: 320, height: 320 },
outputGallery: { width: 320, height: 360 }, outputGallery: { width: 320, height: 360 },
imageCompare: { width: 400, height: 360 },
}; };
const dims = defaultDimensions[nodeType]; const dims = defaultDimensions[nodeType];
addNode(nodeType, { x: centerX - dims.width / 2, y: centerY - dims.height / 2 }); addNode(nodeType, { x: centerX - dims.width / 2, y: centerY - dims.height / 2 });
@ -1388,6 +1393,8 @@ export function WorkflowCanvas() {
return "#ef4444"; return "#ef4444";
case "outputGallery": case "outputGallery":
return "#ec4899"; return "#ec4899";
case "imageCompare":
return "#14b8a6";
default: default:
return "#94a3b8"; return "#94a3b8";
} }

1
src/components/nodes/index.ts

@ -8,4 +8,5 @@ export { LLMGenerateNode } from "./LLMGenerateNode";
export { SplitGridNode } from "./SplitGridNode"; export { SplitGridNode } from "./SplitGridNode";
export { OutputNode } from "./OutputNode"; export { OutputNode } from "./OutputNode";
export { OutputGalleryNode } from "./OutputGalleryNode"; export { OutputGalleryNode } from "./OutputGalleryNode";
export { ImageCompareNode } from "./ImageCompareNode";
export { GroupNode } from "./GroupNode"; export { GroupNode } from "./GroupNode";

6
src/lib/quickstart/validation.ts

@ -36,6 +36,7 @@ const DEFAULT_DIMENSIONS: Record<NodeType, { width: number; height: number }> =
splitGrid: { width: 300, height: 320 }, splitGrid: { width: 300, height: 320 },
output: { width: 320, height: 320 }, output: { width: 320, height: 320 },
outputGallery: { width: 320, height: 360 }, outputGallery: { width: 320, height: 360 },
imageCompare: { width: 400, height: 360 },
}; };
/** /**
@ -276,6 +277,11 @@ function createDefaultNodeData(type: NodeType): WorkflowNodeData {
return { return {
images: [], images: [],
}; };
case "imageCompare":
return {
imageA: null,
imageB: null,
};
} }
} }

7
src/store/utils/nodeDefaults.ts

@ -10,6 +10,7 @@ import {
SplitGridNodeData, SplitGridNodeData,
OutputNodeData, OutputNodeData,
OutputGalleryNodeData, OutputGalleryNodeData,
ImageCompareNodeData,
WorkflowNodeData, WorkflowNodeData,
GroupColor, GroupColor,
SelectedModel, SelectedModel,
@ -31,6 +32,7 @@ export const defaultNodeDimensions: Record<NodeType, { width: number; height: nu
splitGrid: { width: 300, height: 320 }, splitGrid: { width: 300, height: 320 },
output: { width: 320, height: 320 }, output: { width: 320, height: 320 },
outputGallery: { width: 320, height: 360 }, outputGallery: { width: 320, height: 360 },
imageCompare: { width: 400, height: 360 },
}; };
/** /**
@ -171,5 +173,10 @@ export const createDefaultNodeData = (type: NodeType): WorkflowNodeData => {
return { return {
images: [], images: [],
} as OutputGalleryNodeData; } as OutputGalleryNodeData;
case "imageCompare":
return {
imageA: null,
imageB: null,
} as ImageCompareNodeData;
} }
}; };

9
src/store/workflowStore.ts

@ -1832,6 +1832,15 @@ export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
updateNodeData(node.id, { images }); updateNodeData(node.id, { images });
break; break;
} }
case "imageCompare": {
const { images } = getConnectedInputs(node.id);
updateNodeData(node.id, {
imageA: images[0] || null,
imageB: images[1] || null,
});
break;
}
} }
} }

14
src/types/nodes.ts

@ -32,7 +32,8 @@ export type NodeType =
| "llmGenerate" | "llmGenerate"
| "splitGrid" | "splitGrid"
| "output" | "output"
| "outputGallery"; | "outputGallery"
| "imageCompare";
/** /**
* Node execution status * Node execution status
@ -184,6 +185,14 @@ export interface OutputGalleryNodeData extends BaseNodeData {
images: string[]; // Array of base64 data URLs from connected nodes 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 * Split Grid node - splits image into grid cells for parallel processing
*/ */
@ -223,7 +232,8 @@ export type WorkflowNodeData =
| LLMGenerateNodeData | LLMGenerateNodeData
| SplitGridNodeData | SplitGridNodeData
| OutputNodeData | OutputNodeData
| OutputGalleryNodeData; | OutputGalleryNodeData
| ImageCompareNodeData;
/** /**
* Workflow node with typed data (extended with optional groupId) * Workflow node with typed data (extended with optional groupId)

Loading…
Cancel
Save