diff --git a/src/frontend/src/CustomNodes/GenericNode/components/NodeOutputfield/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/NodeOutputfield/index.tsx index b4ffedac2c0d..c30afb965861 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/NodeOutputfield/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/NodeOutputfield/index.tsx @@ -303,6 +303,7 @@ function NodeOutputField({ isToolMode={isToolMode} /> + { + if ((isHovered || openHandle) && !isNullHandle) { + const styleSheet = document.createElement("style"); + styleSheet.id = `pulse-${nodeId}`; + styleSheet.textContent = ` + @keyframes pulseNeon { + 0% { + box-shadow: 0 0 0 2px hsl(var(--node-ring)), + 0 0 2px hsl(var(--datatype-${colorName?.[0]})), + 0 0 4px hsl(var(--datatype-${colorName?.[0]})), + 0 0 6px hsl(var(--datatype-${colorName?.[0]})), + 0 0 8px hsl(var(--datatype-${colorName?.[0]})), + 0 0 10px hsl(var(--datatype-${colorName?.[0]})), + 0 0 15px hsl(var(--datatype-${colorName?.[0]})), + 0 0 20px hsl(var(--datatype-${colorName?.[0]})); + } + 50% { + box-shadow: 0 0 0 2px hsl(var(--node-ring)), + 0 0 4px hsl(var(--datatype-${colorName?.[0]})), + 0 0 8px hsl(var(--datatype-${colorName?.[0]})), + 0 0 12px hsl(var(--datatype-${colorName?.[0]})), + 0 0 16px hsl(var(--datatype-${colorName?.[0]})), + 0 0 20px hsl(var(--datatype-${colorName?.[0]})), + 0 0 25px hsl(var(--datatype-${colorName?.[0]})), + 0 0 30px hsl(var(--datatype-${colorName?.[0]})); + } + 100% { + box-shadow: 0 0 0 2px hsl(var(--node-ring)), + 0 0 2px hsl(var(--datatype-${colorName?.[0]})), + 0 0 4px hsl(var(--datatype-${colorName?.[0]})), + 0 0 6px hsl(var(--datatype-${colorName?.[0]})), + 0 0 8px hsl(var(--datatype-${colorName?.[0]})), + 0 0 10px hsl(var(--datatype-${colorName?.[0]})), + 0 0 15px hsl(var(--datatype-${colorName?.[0]})), + 0 0 20px hsl(var(--datatype-${colorName?.[0]})); + } + } + `; + document.head.appendChild(styleSheet); + + return () => { + const existingStyle = document.getElementById(`pulse-${nodeId}`); + if (existingStyle) { + existingStyle.remove(); + } + }; + } + }, [isHovered, openHandle, isNullHandle, nodeId, colorName]); + + const getNeonShadow = useCallback( + (color: string, isActive: boolean) => { + if (isNullHandle) return "none"; + if (!isActive) return `0 0 0 3px hsl(var(--${color}))`; + return [ + "0 0 0 1px hsl(var(--border))", + `0 0 2px ${color}`, + `0 0 4px ${color}`, + `0 0 6px ${color}`, + `0 0 8px ${color}`, + `0 0 10px ${color}`, + `0 0 15px ${color}`, + `0 0 20px ${color}`, + ].join(", "); + }, + [isNullHandle], + ); + + const contentStyle = useMemo( + () => ({ + background: isNullHandle ? "hsl(var(--border))" : handleColor, + width: "10px", + height: "10px", + transition: "all 0.2s", + boxShadow: getNeonShadow( + accentForegroundColorName, + isHovered || openHandle, + ), + animation: + (isHovered || openHandle) && !isNullHandle + ? "pulseNeon 1.1s ease-in-out infinite" + : "none", + border: isNullHandle ? "2px solid hsl(var(--muted))" : "none", + }), + [ + isNullHandle, + handleColor, + getNeonShadow, + accentForegroundColorName, + isHovered, + openHandle, + ], + ); + + return ( +
+ ); +}); + +const HandleRenderComponent = memo(function HandleRenderComponent({ left, nodes, tooltipTitle = "", @@ -35,236 +175,184 @@ export default function HandleRenderComponent({ edges: any; myData: any; colors: string[]; - setFilterEdge: any; - showNode: any; + setFilterEdge: (edges: any) => void; + showNode: boolean; testIdComplement?: string; nodeId: string; colorName?: string[]; }) { const handleColorName = colorName?.[0] ?? ""; - const accentColorName = `datatype-${handleColorName}`; const accentForegroundColorName = `${accentColorName}-foreground`; - const setHandleDragging = useFlowStore((state) => state.setHandleDragging); - const setFilterType = useFlowStore((state) => state.setFilterType); - const handleDragging = useFlowStore((state) => state.handleDragging); - const filterType = useFlowStore((state) => state.filterType); - const dark = useDarkStore((state) => state.dark); + const [isHovered, setIsHovered] = useState(false); + const [openTooltip, setOpenTooltip] = useState(false); - const onConnect = useFlowStore((state) => state.onConnect); + const { + setHandleDragging, + setFilterType, + handleDragging, + filterType, + onConnect, + } = useFlowStore( + useCallback( + (state) => ({ + setHandleDragging: state.setHandleDragging, + setFilterType: state.setFilterType, + handleDragging: state.handleDragging, + filterType: state.filterType, + onConnect: state.onConnect, + }), + [], + ), + ); - const handleMouseUp = () => { - setHandleDragging(undefined); - document.removeEventListener("mouseup", handleMouseUp); - }; + const dark = useDarkStore((state) => state.dark); const myId = useMemo( () => scapedJSONStringfy(proxy ? { ...id, proxy } : id), [id, proxy], ); - const getConnection = useMemo( - () => - (semiConnection: { - source: string | undefined; - sourceHandle: string | undefined; - target: string | undefined; - targetHandle: string | undefined; - }) => ({ - source: semiConnection.source ?? nodeId, - sourceHandle: semiConnection.sourceHandle ?? myId, - target: semiConnection.target ?? nodeId, - targetHandle: semiConnection.targetHandle ?? myId, - }), + const getConnection = useCallback( + (semiConnection: { + source?: string; + sourceHandle?: string; + target?: string; + targetHandle?: string; + }) => ({ + source: semiConnection.source ?? nodeId, + sourceHandle: semiConnection.sourceHandle ?? myId, + target: semiConnection.target ?? nodeId, + targetHandle: semiConnection.targetHandle ?? myId, + }), [nodeId, myId], ); - const sameDraggingNode = useMemo( - () => (!left ? handleDragging?.target : handleDragging?.source) === nodeId, - [left, handleDragging, nodeId], - ); + const { + sameNode, + ownHandle, + openHandle, + filterOpenHandle, + filterPresent, + currentFilter, + isNullHandle, + handleColor, + } = useMemo(() => { + const sameDraggingNode = + (!left ? handleDragging?.target : handleDragging?.source) === nodeId; + const sameFilterNode = + (!left ? filterType?.target : filterType?.source) === nodeId; - const ownDraggingHandle = useMemo( - () => + const ownDraggingHandle = handleDragging && (left ? handleDragging?.target : handleDragging?.source) && (left ? handleDragging.targetHandle : handleDragging.sourceHandle) === - myId, - [handleDragging, left, myId], - ); - - const sameFilterNode = useMemo( - () => (!left ? filterType?.target : filterType?.source) === nodeId, - [left, filterType, nodeId], - ); + myId; - const ownFilterHandle = useMemo( - () => + const ownFilterHandle = filterType && (left ? filterType?.target : filterType?.source) === nodeId && - (left ? filterType.targetHandle : filterType.sourceHandle) === myId, - [filterType, left, myId], - ); - - const sameNode = useMemo( - () => sameDraggingNode || sameFilterNode, - [sameDraggingNode, sameFilterNode], - ); - const ownHandle = useMemo( - () => ownDraggingHandle || ownFilterHandle, - [ownDraggingHandle, ownFilterHandle], - ); + (left ? filterType.targetHandle : filterType.sourceHandle) === myId; - const draggingOpenHandle = useMemo( - () => + const draggingOpenHandle = handleDragging && (left ? handleDragging.source : handleDragging.target) && !ownDraggingHandle ? isValidConnection(getConnection(handleDragging), nodes, edges) - : false, - [handleDragging, left, ownDraggingHandle, getConnection, nodes, edges], - ); + : false; - const filterOpenHandle = useMemo( - () => + const filterOpenHandle = filterType && (left ? filterType.source : filterType.target) && !ownFilterHandle ? isValidConnection(getConnection(filterType), nodes, edges) - : false, - [filterType, left, ownFilterHandle, getConnection, nodes, edges], - ); - - const openHandle = useMemo( - () => filterOpenHandle || draggingOpenHandle, - [filterOpenHandle, draggingOpenHandle], - ); - - const filterPresent = useMemo( - () => handleDragging || filterType, - [handleDragging, filterType], - ); + : false; - const currentFilter = useMemo( - () => - left - ? { - targetHandle: myId, - target: nodeId, - source: undefined, - sourceHandle: undefined, - type: tooltipTitle, - color: handleColorName, - } - : { - sourceHandle: myId, - source: nodeId, - target: undefined, - targetHandle: undefined, - type: tooltipTitle, - color: handleColorName, - }, - [left, myId, nodeId, tooltipTitle, colors], - ); + const openHandle = filterOpenHandle || draggingOpenHandle; + const filterPresent = handleDragging || filterType; - const isNullHandle = filterPresent && !(openHandle || ownHandle); + const currentFilter = left + ? { + targetHandle: myId, + target: nodeId, + source: undefined, + sourceHandle: undefined, + type: tooltipTitle, + color: handleColorName, + } + : { + sourceHandle: myId, + source: nodeId, + target: undefined, + targetHandle: undefined, + type: tooltipTitle, + color: handleColorName, + }; - const handleColor = useMemo( - () => - isNullHandle - ? dark - ? "conic-gradient(hsl(var(--accent-gray)) 0deg 360deg)" - : "conic-gradient(hsl(var(--accent-gray-foreground)) 0deg 360deg)" - : "conic-gradient(" + - colorName! - .concat(colorName![0]) - .map( - (color, index) => - `hsl(var(--datatype-${color}))` + - " " + - ((360 / colors.length) * index - 360 / (colors.length * 4)) + - "deg " + - ((360 / colors.length) * index + 360 / (colors.length * 4)) + - "deg", - ) - .join(" ,") + - ")", - [filterPresent, openHandle, ownHandle, dark, colors], - ); + const isNullHandle = + filterPresent && !(openHandle || ownDraggingHandle || ownFilterHandle); - const [isHovered, setIsHovered] = useState(false); - const [openTooltip, setOpenTooltip] = useState(false); + const handleColor = isNullHandle + ? dark + ? "conic-gradient(hsl(var(--accent-gray)) 0deg 360deg)" + : "conic-gradient(hsl(var(--accent-gray-foreground)) 0deg 360deg)" + : "conic-gradient(" + + colorName! + .concat(colorName![0]) + .map( + (color, index) => + `hsl(var(--datatype-${color}))` + + " " + + ((360 / colors.length) * index - 360 / (colors.length * 4)) + + "deg " + + ((360 / colors.length) * index + 360 / (colors.length * 4)) + + "deg", + ) + .join(" ,") + + ")"; - useEffect(() => { - if ((isHovered || openHandle) && !isNullHandle) { - const styleSheet = document.createElement("style"); - styleSheet.id = `pulse-${nodeId}`; - styleSheet.textContent = ` - @keyframes pulseNeon { - 0% { - box-shadow: 0 0 0 2px hsl(var(--node-ring)), - 0 0 2px hsl(var(--datatype-${colorName![0]})), - 0 0 4px hsl(var(--datatype-${colorName![0]})), - 0 0 6px hsl(var(--datatype-${colorName![0]})), - 0 0 8px hsl(var(--datatype-${colorName![0]})), - 0 0 10px hsl(var(--datatype-${colorName![0]})), - 0 0 15px hsl(var(--datatype-${colorName![0]})), - 0 0 20px hsl(var(--datatype-${colorName![0]})); - } - 50% { - box-shadow: 0 0 0 2px hsl(var(--node-ring)), - 0 0 4px hsl(var(--datatype-${colorName![0]})), - 0 0 8px hsl(var(--datatype-${colorName![0]})), - 0 0 12px hsl(var(--datatype-${colorName![0]})), - 0 0 16px hsl(var(--datatype-${colorName![0]})), - 0 0 20px hsl(var(--datatype-${colorName![0]})), - 0 0 25px hsl(var(--datatype-${colorName![0]})), - 0 0 30px hsl(var(--datatype-${colorName![0]})); - } - 100% { - box-shadow: 0 0 0 2px hsl(var(--node-ring)), - 0 0 2px hsl(var(--datatype-${colorName![0]})), - 0 0 4px hsl(var(--datatype-${colorName![0]})), - 0 0 6px hsl(var(--datatype-${colorName![0]})), - 0 0 8px hsl(var(--datatype-${colorName![0]})), - 0 0 10px hsl(var(--datatype-${colorName![0]})), - 0 0 15px hsl(var(--datatype-${colorName![0]})), - 0 0 20px hsl(var(--datatype-${colorName![0]})); - } - } - `; - document.head.appendChild(styleSheet); - } - - // Cleanup function should always be returned - return () => { - const existingStyle = document.getElementById(`pulse-${nodeId}`); - if (existingStyle) { - existingStyle.remove(); - } + return { + sameNode: sameDraggingNode || sameFilterNode, + ownHandle: ownDraggingHandle || ownFilterHandle, + openHandle, + filterOpenHandle, + filterPresent, + currentFilter, + isNullHandle, + handleColor, }; - }, [isHovered, openHandle, isNullHandle, colors, nodeId]); - - const getNeonShadow = (color: string, isHovered: boolean) => { - if (isNullHandle) return "none"; - if (!isHovered && !openHandle) return `0 0 0 3px hsl(var(--${color}))`; - return [ - "0 0 0 1px hsl(var(--border))", - `0 0 2px ${color}`, - `0 0 4px ${color}`, - `0 0 6px ${color}`, - `0 0 8px ${color}`, - `0 0 10px ${color}`, - `0 0 15px ${color}`, - `0 0 20px ${color}`, - ].join(", "); - }; + }, [ + left, + handleDragging, + filterType, + nodeId, + myId, + nodes, + edges, + getConnection, + dark, + colors, + colorName, + tooltipTitle, + handleColorName, + ]); - const handleRef = useRef(null); - const invisibleDivRef = useRef(null); + const handleMouseDown = useCallback( + (event: React.MouseEvent) => { + if (event.button === 0) { + setHandleDragging(currentFilter); + const handleMouseUp = () => { + setHandleDragging(undefined); + document.removeEventListener("mouseup", handleMouseUp); + }; + document.addEventListener("mouseup", handleMouseUp); + } + }, + [currentFilter, setHandleDragging], + ); - const handleClick = () => { + const handleClick = useCallback(() => { setFilterEdge(groupByFamily(myData, tooltipTitle!, left, nodes!)); setFilterType(currentFilter); if (filterOpenHandle && filterType) { @@ -272,14 +360,40 @@ export default function HandleRenderComponent({ setFilterType(undefined); setFilterEdge([]); } - }; + }, [ + myData, + tooltipTitle, + left, + nodes, + setFilterEdge, + setFilterType, + currentFilter, + filterOpenHandle, + filterType, + onConnect, + getConnection, + ]); + + const handleMouseEnter = useCallback(() => setIsHovered(true), []); + const handleMouseLeave = useCallback(() => setIsHovered(false), []); + const handleMouseUp = useCallback(() => setOpenTooltip(false), []); + const handleContextMenu = useCallback( + (e: React.MouseEvent) => e.preventDefault(), + [], + ); + + // Memoize the validation function + const validateConnection = useCallback( + (connection: any) => isValidConnection(connection, nodes, edges), + [nodes, edges], + ); return (
- isValidConnection(connection, nodes, edges) - } + isValidConnection={validateConnection} className={cn( `group/handle z-50 transition-all`, !showNode && "no-show", )} + style={BASE_HANDLE_STYLES} onClick={handleClick} - onMouseUp={() => { - setOpenTooltip(false); - }} - onContextMenu={(event) => { - event.preventDefault(); - }} - onMouseDown={(event) => { - if (event.button === 0) { - setHandleDragging(currentFilter); - document.addEventListener("mouseup", handleMouseUp); - } - }} - style={{ - width: "32px", - height: "32px", - top: "50%", - position: "absolute", - zIndex: 30, - background: "transparent", - border: "none", - }} - onMouseEnter={() => setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} + onMouseUp={handleMouseUp} + onContextMenu={handleContextMenu} + onMouseDown={handleMouseDown} + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} > -
setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - onContextMenu={(event) => { - event.preventDefault(); - }} +
); -} +}); + +export default HandleRenderComponent;