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.
139 lines
4.6 KiB
139 lines
4.6 KiB
"use client";
|
|
|
|
import { memo, useMemo, useEffect } from "react";
|
|
import { Handle, Position, useUpdateNodeInternals, useReactFlow, NodeProps } from "@xyflow/react";
|
|
import { BaseNode } from "./BaseNode";
|
|
import { useWorkflowStore } from "@/store/workflowStore";
|
|
import type { WorkflowNode, RouterNodeData } from "@/types";
|
|
|
|
const ALL_HANDLE_TYPES = ["image", "text", "video", "audio", "3d", "easeCurve"] as const;
|
|
|
|
const HANDLE_COLORS: Record<(typeof ALL_HANDLE_TYPES)[number], string> = {
|
|
image: "#10b981", // emerald — matches globals.css
|
|
text: "#3b82f6", // blue — matches globals.css
|
|
video: "#ffffff", // white — default handle style
|
|
audio: "rgb(167, 139, 250)", // violet — matches GenerateAudioNode/OutputNode
|
|
"3d": "#f97316", // orange — matches globals.css
|
|
easeCurve: "#ffffff", // white — default handle style
|
|
};
|
|
|
|
export const RouterNode = memo(({ id, data, selected }: NodeProps<WorkflowNode>) => {
|
|
const nodeData = data as RouterNodeData;
|
|
const edges = useWorkflowStore((state) => state.edges);
|
|
const updateNodeData = useWorkflowStore((state) => state.updateNodeData);
|
|
const updateNodeInternals = useUpdateNodeInternals();
|
|
const { setNodes } = useReactFlow();
|
|
|
|
// Derive active input types from incoming edge connections
|
|
const activeInputTypes = useMemo(() => {
|
|
const typeSet = new Set<(typeof ALL_HANDLE_TYPES)[number]>();
|
|
|
|
edges
|
|
.filter((edge) => edge.target === id)
|
|
.forEach((edge) => {
|
|
const handleType = edge.targetHandle;
|
|
if (handleType && ALL_HANDLE_TYPES.includes(handleType as typeof ALL_HANDLE_TYPES[number])) {
|
|
typeSet.add(handleType as typeof ALL_HANDLE_TYPES[number]);
|
|
}
|
|
});
|
|
|
|
return Array.from(typeSet).sort();
|
|
}, [edges, id]);
|
|
|
|
// Show generic handles when not all types are connected
|
|
const showGenericHandles = activeInputTypes.length < ALL_HANDLE_TYPES.length;
|
|
|
|
// Calculate handle positioning
|
|
const handleSpacing = 24;
|
|
const baseOffset = 38; // Clear the header bar
|
|
|
|
// Dynamic height based on total handle count (active + placeholder)
|
|
const totalHandleSlots = activeInputTypes.length + (showGenericHandles ? 1 : 0);
|
|
const lastHandleTop = baseOffset + (Math.max(totalHandleSlots, 1) - 1) * handleSpacing;
|
|
const minHeight = lastHandleTop + 20;
|
|
|
|
// Resize node and notify React Flow when handle count changes
|
|
useEffect(() => {
|
|
setNodes((nodes) =>
|
|
nodes.map((node) => {
|
|
if (node.id === id) {
|
|
const currentHeight = (node.style?.height as number) || 0;
|
|
if (currentHeight < minHeight) {
|
|
return { ...node, style: { ...node.style, height: minHeight } };
|
|
}
|
|
}
|
|
return node;
|
|
})
|
|
);
|
|
updateNodeInternals(id);
|
|
}, [activeInputTypes.length, id, minHeight, setNodes, updateNodeInternals]);
|
|
|
|
return (
|
|
<BaseNode
|
|
id={id}
|
|
selected={selected}
|
|
minWidth={200}
|
|
minHeight={minHeight}
|
|
className="bg-neutral-800/80 border-neutral-600"
|
|
>
|
|
{/* Input handles (left) */}
|
|
{activeInputTypes.map((type, index) => (
|
|
<Handle
|
|
key={`input-${type}`}
|
|
type="target"
|
|
position={Position.Left}
|
|
id={type}
|
|
data-handletype={type}
|
|
style={{
|
|
top: baseOffset + index * handleSpacing,
|
|
backgroundColor: HANDLE_COLORS[type],
|
|
width: 12,
|
|
height: 12,
|
|
border: "2px solid #1e1e1e",
|
|
}}
|
|
/>
|
|
))}
|
|
{showGenericHandles && (
|
|
<Handle
|
|
type="target"
|
|
position={Position.Left}
|
|
id="generic-input"
|
|
style={{
|
|
top: baseOffset + activeInputTypes.length * handleSpacing,
|
|
backgroundColor: "#6b7280",
|
|
width: 12,
|
|
height: 12,
|
|
border: "2px solid #1e1e1e",
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{/* Output handles (right) */}
|
|
{activeInputTypes.map((type, index) => (
|
|
<Handle
|
|
key={`output-${type}`}
|
|
type="source"
|
|
position={Position.Right}
|
|
id={type}
|
|
data-handletype={type}
|
|
style={{
|
|
top: baseOffset + index * handleSpacing,
|
|
backgroundColor: HANDLE_COLORS[type],
|
|
width: 12,
|
|
height: 12,
|
|
border: "2px solid #1e1e1e",
|
|
}}
|
|
/>
|
|
))}
|
|
|
|
{/* Body content */}
|
|
<div className="text-[10px] text-neutral-500 text-center py-1">
|
|
{activeInputTypes.length > 0
|
|
? `${activeInputTypes.length} type${activeInputTypes.length !== 1 ? "s" : ""} routed`
|
|
: "Drop connections here"}
|
|
</div>
|
|
</BaseNode>
|
|
);
|
|
});
|
|
|
|
RouterNode.displayName = "RouterNode";
|
|
|