loop node dynamic height (#3092)
* loop node dynamic height * fix * fix
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
Background,
|
||||
ControlButton,
|
||||
@@ -46,17 +46,24 @@ const FlowController = React.memo(function FlowController() {
|
||||
|
||||
const isMac = !window ? false : window.navigator.userAgent.toLocaleLowerCase().includes('mac');
|
||||
|
||||
// Controller shortcut key
|
||||
useKeyPress(['ctrl.z', 'meta.z'], (e) => {
|
||||
useKeyPress(['ctrl.z', 'meta.z', 'ctrl.shift.z', 'meta.shift.z', 'ctrl.y', 'meta.y'], (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!mouseInCanvas) return;
|
||||
undo();
|
||||
});
|
||||
useKeyPress(['ctrl.shift.z', 'meta.shift.z', 'ctrl.y', 'meta.y'], (e) => {
|
||||
if (!mouseInCanvas) return;
|
||||
redo();
|
||||
|
||||
const isUndo = e.key.toLowerCase() === 'z' && !e.shiftKey;
|
||||
const isRedo = (e.key.toLowerCase() === 'z' && e.shiftKey) || e.key.toLowerCase() === 'y';
|
||||
|
||||
if (isUndo) {
|
||||
undo();
|
||||
} else if (isRedo) {
|
||||
redo();
|
||||
}
|
||||
});
|
||||
|
||||
useKeyPress(['ctrl.add', 'meta.add', 'ctrl.equalsign', 'meta.equalsign'], (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!mouseInCanvas) return;
|
||||
zoomIn();
|
||||
});
|
||||
|
||||
@@ -293,9 +293,24 @@ export const useWorkflow = () => {
|
||||
const { isDowningCtrl } = useKeyboard();
|
||||
|
||||
// Loop node size and position
|
||||
const resetParentNodeSizeAndPosition = useMemoizedFn((rect: Rect, parentId: string) => {
|
||||
const width = rect.width + 110 > 900 ? rect.width + 110 : 900;
|
||||
const height = rect.height + 420 > 900 ? rect.height + 420 : 900;
|
||||
const resetParentNodeSizeAndPosition = useMemoizedFn((parentId: string) => {
|
||||
const { childNodes, loopNode } = nodes.reduce(
|
||||
(acc, node) => {
|
||||
if (node.data.parentNodeId === parentId) {
|
||||
acc.childNodes.push(node);
|
||||
}
|
||||
if (node.id === parentId) {
|
||||
acc.loopNode = node;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{ childNodes: [] as Node[], loopNode: undefined as Node | undefined }
|
||||
);
|
||||
const rect = getNodesBounds(childNodes);
|
||||
|
||||
// Calculate parent node size with minimum width/height constraints
|
||||
const width = Math.max(rect.width + 80, 840);
|
||||
const height = Math.max(rect.height + 80, 600);
|
||||
|
||||
// Update parentNode size and position
|
||||
onChangeNode({
|
||||
@@ -317,14 +332,17 @@ export const useWorkflow = () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate position offset
|
||||
const offsetHeight = loopNode?.height ? loopNode.height - height - 380 : 0;
|
||||
|
||||
// Update parentNode position
|
||||
onNodesChange([
|
||||
{
|
||||
id: parentId,
|
||||
type: 'position',
|
||||
position: {
|
||||
x: rect.x - 50,
|
||||
y: rect.y - 300
|
||||
x: rect.x - 70,
|
||||
y: rect.y - (320 + offsetHeight)
|
||||
}
|
||||
}
|
||||
]);
|
||||
@@ -335,43 +353,41 @@ export const useWorkflow = () => {
|
||||
const [helperLineVertical, setHelperLineVertical] = useState<THelperLine>();
|
||||
|
||||
const checkNodeHelpLine = useMemoizedFn((change: NodeChange, nodes: Node[]) => {
|
||||
requestAnimationFrame(() => {
|
||||
const positionChange = change.type === 'position' && change.dragging ? change : undefined;
|
||||
const positionChange = change.type === 'position' && change.dragging ? change : undefined;
|
||||
|
||||
if (positionChange?.position) {
|
||||
// 只判断,3000px 内的 nodes,并按从近到远的顺序排序
|
||||
const filterNodes = nodes
|
||||
.filter((node) => {
|
||||
if (!positionChange.position) return false;
|
||||
if (positionChange?.position) {
|
||||
// 只判断,3000px 内的 nodes,并按从近到远的顺序排序
|
||||
const filterNodes = nodes
|
||||
.filter((node) => {
|
||||
if (!positionChange.position) return false;
|
||||
|
||||
return (
|
||||
Math.abs(node.position.x - positionChange.position.x) <= 3000 &&
|
||||
Math.abs(node.position.y - positionChange.position.y) <= 3000
|
||||
);
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (!positionChange.position) return 0;
|
||||
return (
|
||||
Math.abs(a.position.x - positionChange.position.x) +
|
||||
Math.abs(a.position.y - positionChange.position.y) -
|
||||
Math.abs(b.position.x - positionChange.position.x) -
|
||||
Math.abs(b.position.y - positionChange.position.y)
|
||||
);
|
||||
})
|
||||
.slice(0, 15);
|
||||
return (
|
||||
Math.abs(node.position.x - positionChange.position.x) <= 3000 &&
|
||||
Math.abs(node.position.y - positionChange.position.y) <= 3000
|
||||
);
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (!positionChange.position) return 0;
|
||||
return (
|
||||
Math.abs(a.position.x - positionChange.position.x) +
|
||||
Math.abs(a.position.y - positionChange.position.y) -
|
||||
Math.abs(b.position.x - positionChange.position.x) -
|
||||
Math.abs(b.position.y - positionChange.position.y)
|
||||
);
|
||||
})
|
||||
.slice(0, 15);
|
||||
|
||||
const helperLines = computeHelperLines(positionChange, filterNodes);
|
||||
const helperLines = computeHelperLines(positionChange, filterNodes);
|
||||
|
||||
positionChange.position.x = helperLines.snapPosition.x ?? positionChange.position.x;
|
||||
positionChange.position.y = helperLines.snapPosition.y ?? positionChange.position.y;
|
||||
positionChange.position.x = helperLines.snapPosition.x ?? positionChange.position.x;
|
||||
positionChange.position.y = helperLines.snapPosition.y ?? positionChange.position.y;
|
||||
|
||||
setHelperLineHorizontal(helperLines.horizontal);
|
||||
setHelperLineVertical(helperLines.vertical);
|
||||
} else {
|
||||
setHelperLineHorizontal(undefined);
|
||||
setHelperLineVertical(undefined);
|
||||
}
|
||||
});
|
||||
setHelperLineHorizontal(helperLines.horizontal);
|
||||
setHelperLineVertical(helperLines.vertical);
|
||||
} else {
|
||||
setHelperLineHorizontal(undefined);
|
||||
setHelperLineVertical(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
// Check if a node is placed on top of a loop node
|
||||
@@ -412,9 +428,7 @@ export const useWorkflow = () => {
|
||||
state.filter((edge) => edge.source !== node.id && edge.target !== node.id)
|
||||
);
|
||||
|
||||
const childNodes = [...nodes.filter((n) => n.data.parentNodeId === parentNode.id), node];
|
||||
const rect = getNodesBounds(childNodes);
|
||||
resetParentNodeSizeAndPosition(rect, parentNode.id);
|
||||
resetParentNodeSizeAndPosition(parentNode.id);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -469,7 +483,7 @@ export const useWorkflow = () => {
|
||||
const childNodes = nodes.filter((n) => n.data.parentNodeId === parentId);
|
||||
checkNodeHelpLine(change, childNodes);
|
||||
|
||||
resetParentNodeSizeAndPosition(getNodesBounds(childNodes), parentId);
|
||||
resetParentNodeSizeAndPosition(parentId);
|
||||
}
|
||||
// If node is parent node, move parent node and child nodes
|
||||
else if (parentNode[node.data.flowNodeType]) {
|
||||
@@ -679,7 +693,8 @@ export const useWorkflow = () => {
|
||||
helperLineVertical,
|
||||
onNodeDragStop,
|
||||
onPaneContextMenu,
|
||||
onPaneClick
|
||||
onPaneClick,
|
||||
resetParentNodeSizeAndPosition
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import React, { useEffect, useMemo, useRef, useCallback } from 'react';
|
||||
import { Background, NodeProps } from 'reactflow';
|
||||
import NodeCard from '../render/NodeCard';
|
||||
import Container from '../../components/Container';
|
||||
@@ -24,17 +24,18 @@ import {
|
||||
import { Input_Template_Children_Node_List } from '@fastgpt/global/core/workflow/template/input';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../../../context';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils';
|
||||
import { AppContext } from '../../../../context';
|
||||
import { isReferenceValue, isReferenceValueArray } from '@fastgpt/global/core/workflow/utils';
|
||||
import { ReferenceItemValueType } from '@fastgpt/global/core/workflow/type/io';
|
||||
import { useWorkflow } from '../../hooks/useWorkflow';
|
||||
|
||||
const NodeLoop = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
const { t } = useTranslation();
|
||||
const { nodeId, inputs, outputs, isFolded } = data;
|
||||
const { onChangeNode, nodeList } = useContextSelector(WorkflowContext, (v) => v);
|
||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||
const { resetParentNodeSizeAndPosition } = useWorkflow();
|
||||
|
||||
const loopInputArray = inputs.find((input) => input.key === NodeInputKeyEnum.loopInputArray);
|
||||
|
||||
@@ -44,6 +45,7 @@ const NodeLoop = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
nodeHeight: inputs.find((input) => input.key === NodeInputKeyEnum.nodeHeight)?.value
|
||||
};
|
||||
}, [inputs]);
|
||||
|
||||
const childrenNodeIdList = useMemo(() => {
|
||||
return JSON.stringify(
|
||||
nodeList.filter((node) => node.parentNodeId === nodeId).map((node) => node.nodeId)
|
||||
@@ -84,6 +86,7 @@ const NodeLoop = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
|
||||
return type ?? WorkflowIOValueTypeEnum.arrayAny;
|
||||
}, [appDetail.chatConfig, loopInputArray, nodeList]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loopInputArray) return;
|
||||
onChangeNode({
|
||||
@@ -110,20 +113,15 @@ const NodeLoop = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
});
|
||||
}, [childrenNodeIdList, nodeId, onChangeNode]);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
resetParentNodeSizeAndPosition(nodeId);
|
||||
}, 0);
|
||||
}, [loopInputArray, nodeId, resetParentNodeSizeAndPosition]);
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
<NodeCard
|
||||
selected={selected}
|
||||
maxW="full"
|
||||
{...(!isFolded && {
|
||||
minW: '900px',
|
||||
minH: '900px',
|
||||
w: nodeWidth,
|
||||
h: nodeHeight
|
||||
})}
|
||||
menuForbid={{ copy: true }}
|
||||
{...data}
|
||||
>
|
||||
<NodeCard selected={selected} maxW="full" menuForbid={{ copy: true }} {...data}>
|
||||
<Container position={'relative'} flex={1}>
|
||||
<IOTitle text={t('common:common.Input')} />
|
||||
<Box mb={6} maxW={'500px'}>
|
||||
@@ -132,7 +130,17 @@ const NodeLoop = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
<FormLabel required fontWeight={'medium'} mb={3} color={'myGray.600'}>
|
||||
{t('workflow:loop_body')}
|
||||
</FormLabel>
|
||||
<Box flex={1} position={'relative'} border={'base'} bg={'myGray.100'} rounded={'8px'}>
|
||||
<Box
|
||||
flex={1}
|
||||
position={'relative'}
|
||||
border={'base'}
|
||||
bg={'myGray.100'}
|
||||
rounded={'8px'}
|
||||
{...(!isFolded && {
|
||||
minW: nodeWidth,
|
||||
minH: nodeHeight
|
||||
})}
|
||||
>
|
||||
<Background />
|
||||
</Box>
|
||||
</Container>
|
||||
|
||||
@@ -145,14 +145,25 @@ function Reference({
|
||||
const onUpdateField = useCallback(
|
||||
({ data }: { data: FlowNodeInputItemType }) => {
|
||||
if (!data.key) return;
|
||||
const oldType = inputChildren.valueType;
|
||||
const newType = data.valueType;
|
||||
let newValue = data.value;
|
||||
if (oldType?.includes('array') && !newType?.includes('array')) {
|
||||
newValue = data.value[0];
|
||||
} else if (!oldType?.includes('array') && newType?.includes('array')) {
|
||||
newValue = [data.value];
|
||||
}
|
||||
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'replaceInput',
|
||||
key: inputChildren.key,
|
||||
value: {
|
||||
...data,
|
||||
value: data
|
||||
...inputChildren,
|
||||
value: newValue,
|
||||
key: data.key,
|
||||
label: data.label,
|
||||
valueType: data.valueType
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -2,7 +2,10 @@ import React, { useCallback, useMemo } from 'react';
|
||||
import type { RenderInputProps } from '../type';
|
||||
import { Flex, Box, ButtonProps, Grid } from '@chakra-ui/react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { computedNodeInputReference } from '@/web/core/workflow/utils';
|
||||
import {
|
||||
computedNodeInputReference,
|
||||
filterWorkflowNodeOutputsByType
|
||||
} from '@/web/core/workflow/utils';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import {
|
||||
NodeOutputKeyEnum,
|
||||
@@ -77,7 +80,6 @@ export const useReference = ({
|
||||
if (!sourceNodes) return [];
|
||||
|
||||
const isArray = valueType?.includes('array');
|
||||
const arrayItemType = isArray ? valueType.replace('array', '').toLowerCase() : valueType;
|
||||
|
||||
// 转换为 select 的数据结构
|
||||
const list: CommonSelectProps['list'] = sourceNodes
|
||||
@@ -90,16 +92,7 @@ export const useReference = ({
|
||||
</Flex>
|
||||
),
|
||||
value: node.nodeId,
|
||||
children: node.outputs
|
||||
.filter(
|
||||
(output) =>
|
||||
valueType === WorkflowIOValueTypeEnum.any ||
|
||||
valueType === WorkflowIOValueTypeEnum.arrayAny ||
|
||||
output.valueType === WorkflowIOValueTypeEnum.any ||
|
||||
output.valueType === valueType ||
|
||||
// Array<String> can select string
|
||||
arrayItemType === output.valueType
|
||||
)
|
||||
children: filterWorkflowNodeOutputsByType(node.outputs, valueType)
|
||||
.filter((output) => output.id !== NodeOutputKeyEnum.addOutputParam)
|
||||
.map((output) => {
|
||||
return {
|
||||
@@ -199,7 +192,7 @@ const SingleReferenceSelector = ({
|
||||
label={
|
||||
isValidSelect ? (
|
||||
<Flex gap={2} alignItems={'center'} fontSize={'sm'}>
|
||||
<Flex py={1} pl={1}>
|
||||
<Flex py={1} pl={1} alignItems={'center'}>
|
||||
{nodeName}
|
||||
<MyIcon name={'common/rightArrowLight'} mx={1} w={'12px'} color={'myGray.500'} />
|
||||
{outputName}
|
||||
@@ -249,20 +242,14 @@ const MultipleReferenceSelector = ({
|
||||
|
||||
const ArraySelector = useMemo(() => {
|
||||
const selectorVal = value as ReferenceItemValueType[];
|
||||
const notValidItem =
|
||||
!selectorVal ||
|
||||
selectorVal.length === 0 ||
|
||||
selectorVal.every((item) => {
|
||||
const [nodeName, outputName] = getSelectValue(item);
|
||||
return !nodeName || !outputName;
|
||||
});
|
||||
const validSelectValue = selectorVal && selectorVal.length > 0;
|
||||
|
||||
return (
|
||||
<MultipleRowArraySelect
|
||||
label={
|
||||
!notValidItem ? (
|
||||
validSelectValue ? (
|
||||
<Grid py={3} gridTemplateColumns={'1fr 1fr'} gap={2} fontSize={'sm'}>
|
||||
{selectorVal.map((item, index) => {
|
||||
{selectorVal?.map((item, index) => {
|
||||
const [nodeName, outputName] = getSelectValue(item);
|
||||
const isInvalidItem = !nodeName || !outputName;
|
||||
|
||||
@@ -270,8 +257,8 @@ const MultipleReferenceSelector = ({
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
key={index}
|
||||
bg={'primary.50'}
|
||||
color={'myGray.900'}
|
||||
bg={isInvalidItem ? 'red.50' : 'primary.50'}
|
||||
color={isInvalidItem ? 'red.500' : 'myGray.900'}
|
||||
py={1}
|
||||
px={1.5}
|
||||
rounded={'sm'}
|
||||
@@ -282,21 +269,27 @@ const MultipleReferenceSelector = ({
|
||||
maxW={'200px'}
|
||||
className="textEllipsis"
|
||||
>
|
||||
{nodeName}
|
||||
<MyIcon
|
||||
name={'common/rightArrowLight'}
|
||||
mx={1}
|
||||
w={'12px'}
|
||||
color={'myGray.500'}
|
||||
/>
|
||||
{outputName}
|
||||
{isInvalidItem ? (
|
||||
<>{t('common:invalid_variable')}</>
|
||||
) : (
|
||||
<>
|
||||
{nodeName}
|
||||
<MyIcon
|
||||
name={'common/rightArrowLight'}
|
||||
mx={1}
|
||||
w={'12px'}
|
||||
color={'myGray.500'}
|
||||
/>
|
||||
{outputName}
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
<MyIcon
|
||||
name={'common/closeLight'}
|
||||
w={'1rem'}
|
||||
ml={1}
|
||||
cursor={'pointer'}
|
||||
color={'myGray.500'}
|
||||
color={isInvalidItem ? 'red.500' : 'myGray.500'}
|
||||
_hover={{
|
||||
color: 'red.600'
|
||||
}}
|
||||
@@ -321,7 +314,7 @@ const MultipleReferenceSelector = ({
|
||||
popDirection={popDirection}
|
||||
/>
|
||||
);
|
||||
}, [getSelectValue, list, onSelect, placeholder, popDirection, value]);
|
||||
}, [getSelectValue, list, onSelect, placeholder, popDirection, t, value]);
|
||||
|
||||
return ArraySelector;
|
||||
};
|
||||
|
||||
@@ -355,7 +355,6 @@ const WorkflowContextProvider = ({
|
||||
const getNodes = useContextSelector(WorkflowActionContext, (state) => state.getNodes);
|
||||
const nodeListString = useContextSelector(WorkflowActionContext, (state) => state.nodeListString);
|
||||
|
||||
console.log(121211111111);
|
||||
const nodeList = useMemo(
|
||||
() => JSON.parse(nodeListString) as FlowNodeItemType[],
|
||||
[nodeListString]
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
getAppChatConfig,
|
||||
getGuideModule,
|
||||
isReferenceValue,
|
||||
isReferenceValueArray
|
||||
isReferenceValueFormat
|
||||
} from '@fastgpt/global/core/workflow/utils';
|
||||
import { TFunction } from 'next-i18next';
|
||||
import {
|
||||
@@ -263,6 +263,20 @@ export const getRefData = ({
|
||||
};
|
||||
};
|
||||
|
||||
export const filterWorkflowNodeOutputsByType = (
|
||||
outputs: FlowNodeOutputItemType[],
|
||||
valueType: WorkflowIOValueTypeEnum
|
||||
): FlowNodeOutputItemType[] => {
|
||||
return outputs.filter(
|
||||
(output) =>
|
||||
valueType === WorkflowIOValueTypeEnum.any ||
|
||||
valueType === WorkflowIOValueTypeEnum.arrayAny ||
|
||||
output.valueType === WorkflowIOValueTypeEnum.any ||
|
||||
output.valueType === valueType ||
|
||||
valueType?.replace('array', '').toLowerCase() === output.valueType
|
||||
);
|
||||
};
|
||||
|
||||
/* Connection rules */
|
||||
export const checkWorkflowNodeAndConnection = ({
|
||||
nodes,
|
||||
@@ -337,7 +351,7 @@ export const checkWorkflowNodeAndConnection = ({
|
||||
|
||||
// check reference invalid
|
||||
const renderType = input.renderTypeList[input.selectedTypeIndex || 0];
|
||||
if (renderType === FlowNodeInputTypeEnum.reference && input.required) {
|
||||
if (renderType === FlowNodeInputTypeEnum.reference) {
|
||||
const checkReference = (value: [string, string]) => {
|
||||
if (value[0] === VARIABLE_NODE_ID) {
|
||||
return false;
|
||||
@@ -348,18 +362,16 @@ export const checkWorkflowNodeAndConnection = ({
|
||||
return true;
|
||||
}
|
||||
|
||||
const sourceOutput = sourceNode.data.outputs.find((item) => item.id === value[1]);
|
||||
const sourceOutput = filterWorkflowNodeOutputsByType(
|
||||
sourceNode.data.outputs,
|
||||
input.valueType as WorkflowIOValueTypeEnum
|
||||
).find((item) => item.id === value[1]);
|
||||
return !sourceOutput;
|
||||
};
|
||||
|
||||
// Old format
|
||||
if (
|
||||
Array.isArray(input.value) &&
|
||||
input.value.length === 2 &&
|
||||
typeof input.value[0] === 'string' &&
|
||||
typeof input.value[1] === 'string'
|
||||
) {
|
||||
return checkReference(input.value as [string, string]);
|
||||
if (isReferenceValueFormat(input.value)) {
|
||||
return input.required && checkReference(input.value);
|
||||
}
|
||||
|
||||
// New format
|
||||
|
||||
Reference in New Issue
Block a user