Browse Source
- Create RouterNode.tsx with dynamic handle arrays derived from edges - Implement color-coded typed handles (blue, emerald, purple, amber, cyan, red) - Add gray dashed generic handles when not all 6 types are connected - Use useUpdateNodeInternals to sync handle positions on type changes - Implement compact strip BaseNode layout with dynamic height adjustment - Add activeInputTypes useMemo watching edges for handle derivation - Display type count in node body (or 'Drop connections here' hint) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>handoff-20260429-1057
1 changed files with 143 additions and 0 deletions
@ -0,0 +1,143 @@ |
|||
"use client"; |
|||
|
|||
import { memo, useMemo, useEffect } from "react"; |
|||
import { Handle, Position, useUpdateNodeInternals, 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: "#3b82f6", // blue-500
|
|||
text: "#10b981", // emerald-500
|
|||
video: "#a855f7", // purple-500
|
|||
audio: "#f59e0b", // amber-500
|
|||
"3d": "#06b6d4", // cyan-500
|
|||
easeCurve: "#ef4444", // red-500
|
|||
}; |
|||
|
|||
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(); |
|||
|
|||
// 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]); |
|||
|
|||
// Notify React Flow when handle count changes so it can recalculate positions
|
|||
useEffect(() => { |
|||
updateNodeInternals(id); |
|||
}, [activeInputTypes.length, id, updateNodeInternals]); |
|||
|
|||
// 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 = 20; |
|||
|
|||
// Dynamic height based on active handle count
|
|||
const minHeight = 60 + activeInputTypes.length * handleSpacing; |
|||
|
|||
return ( |
|||
<BaseNode |
|||
title="Router" |
|||
customTitle={nodeData.customTitle} |
|||
comment={nodeData.comment} |
|||
onCustomTitleChange={(customTitle) => updateNodeData(id, { customTitle })} |
|||
onCommentChange={(comment) => updateNodeData(id, { comment })} |
|||
selected={selected} |
|||
minWidth={200} |
|||
minHeight={minHeight} |
|||
className="bg-neutral-800/50 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 dashed #9ca3af", |
|||
}} |
|||
/> |
|||
)} |
|||
|
|||
{/* 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", |
|||
}} |
|||
/> |
|||
))} |
|||
{showGenericHandles && ( |
|||
<Handle |
|||
type="source" |
|||
position={Position.Right} |
|||
id="generic-output" |
|||
style={{ |
|||
top: baseOffset + activeInputTypes.length * handleSpacing, |
|||
backgroundColor: "#6b7280", |
|||
width: 12, |
|||
height: 12, |
|||
border: "2px dashed #9ca3af", |
|||
}} |
|||
/> |
|||
)} |
|||
|
|||
{/* 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"; |
|||
Loading…
Reference in new issue