You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
145 lines
4.6 KiB
145 lines
4.6 KiB
"use client";
|
|
|
|
import { useMemo } from "react";
|
|
import { Handle, Position, NodeProps, Node } from "@xyflow/react";
|
|
import {
|
|
ReactCompareSlider,
|
|
ReactCompareSliderImage,
|
|
} from "react-compare-slider";
|
|
import { BaseNode } from "./BaseNode";
|
|
import { useWorkflowStore } from "@/store/workflowStore";
|
|
import { ImageCompareNodeData } from "@/types";
|
|
|
|
type ImageCompareNodeType = Node<ImageCompareNodeData, "imageCompare">;
|
|
|
|
export function ImageCompareNode({
|
|
id,
|
|
data,
|
|
selected,
|
|
}: NodeProps<ImageCompareNodeType>) {
|
|
const nodeData = data;
|
|
const updateNodeData = useWorkflowStore((state) => state.updateNodeData);
|
|
const edges = useWorkflowStore((state) => state.edges);
|
|
const nodes = useWorkflowStore((state) => state.nodes);
|
|
|
|
// Collect images in real-time from connected nodes (same pattern as OutputGalleryNode)
|
|
const displayImages = useMemo(() => {
|
|
const connectedImages: string[] = [];
|
|
|
|
// Get edges connected to this node, sorted by creation time for stable ordering
|
|
const sortedEdges = edges
|
|
.filter((edge) => edge.target === id)
|
|
.sort((a, b) => {
|
|
const aTime = (a.data?.createdAt as number) || 0;
|
|
const bTime = (b.data?.createdAt as number) || 0;
|
|
return aTime - bTime;
|
|
});
|
|
|
|
sortedEdges.forEach((edge) => {
|
|
const sourceNode = nodes.find((n) => n.id === edge.source);
|
|
if (!sourceNode) return;
|
|
|
|
let image: string | null = null;
|
|
|
|
// Extract image from different node types
|
|
if (sourceNode.type === "imageInput") {
|
|
image = (sourceNode.data as any).image;
|
|
} else if (sourceNode.type === "annotation") {
|
|
image = (sourceNode.data as any).outputImage;
|
|
} else if (sourceNode.type === "nanoBanana") {
|
|
image = (sourceNode.data as any).outputImage;
|
|
}
|
|
|
|
if (image) {
|
|
connectedImages.push(image);
|
|
}
|
|
});
|
|
|
|
return connectedImages;
|
|
}, [edges, nodes, id]);
|
|
|
|
const imageA = displayImages[0] || nodeData.imageA || null;
|
|
const imageB = displayImages[1] || nodeData.imageB || null;
|
|
|
|
return (
|
|
<BaseNode
|
|
id={id}
|
|
selected={selected}
|
|
className="min-w-[200px]"
|
|
>
|
|
{/* Two labeled image input handles */}
|
|
<Handle
|
|
type="target"
|
|
position={Position.Left}
|
|
id="image"
|
|
data-handletype="image"
|
|
style={{ top: "35%" }}
|
|
/>
|
|
<div
|
|
className="absolute left-[-8px] top-[35%] -translate-y-1/2 -translate-x-full mr-1 text-[9px] text-neutral-400 font-medium"
|
|
style={{ pointerEvents: "none" }}
|
|
>
|
|
A
|
|
</div>
|
|
|
|
<Handle
|
|
type="target"
|
|
position={Position.Left}
|
|
id="image-1"
|
|
data-handletype="image"
|
|
style={{ top: "65%" }}
|
|
/>
|
|
<div
|
|
className="absolute left-[-8px] top-[65%] -translate-y-1/2 -translate-x-full mr-1 text-[9px] text-neutral-400 font-medium"
|
|
style={{ pointerEvents: "none" }}
|
|
>
|
|
B
|
|
</div>
|
|
|
|
{/* Comparison view or placeholder */}
|
|
{imageA && imageB ? (
|
|
<div className="flex-1 relative nodrag nopan nowheel">
|
|
<ReactCompareSlider
|
|
itemOne={
|
|
<ReactCompareSliderImage
|
|
src={imageA}
|
|
alt="Image A"
|
|
style={{ objectFit: "contain" }}
|
|
/>
|
|
}
|
|
itemTwo={
|
|
<ReactCompareSliderImage
|
|
src={imageB}
|
|
alt="Image B"
|
|
style={{ objectFit: "contain" }}
|
|
/>
|
|
}
|
|
portrait={false}
|
|
style={{ width: "100%", height: "100%" }}
|
|
/>
|
|
{/* Corner labels */}
|
|
<div className="absolute top-2 left-2 bg-black/50 text-white text-[10px] font-medium px-2 py-1 rounded pointer-events-none">
|
|
A
|
|
</div>
|
|
<div className="absolute top-2 right-2 bg-black/50 text-white text-[10px] font-medium px-2 py-1 rounded pointer-events-none">
|
|
B
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="w-full flex-1 min-h-[200px] border border-dashed border-neutral-600 rounded flex flex-col items-center justify-center gap-2">
|
|
<span className="text-neutral-500 text-[10px] text-center px-4">
|
|
{!imageA && !imageB
|
|
? "Connect 2 images to compare"
|
|
: "Connect another image to compare"}
|
|
</span>
|
|
{imageA && !imageB && (
|
|
<div className="text-[9px] text-neutral-600">Image A connected</div>
|
|
)}
|
|
{!imageA && imageB && (
|
|
<div className="text-[9px] text-neutral-600">Image B connected</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</BaseNode>
|
|
);
|
|
}
|
|
|