loop node dynamic height (#3092)

* loop node dynamic height

* fix

* fix
This commit is contained in:
heheer
2024-11-08 12:10:15 +08:00
committed by archer
parent 91645cc420
commit 8ede7add01
11 changed files with 176 additions and 118 deletions

View File

@@ -5,7 +5,7 @@ import { StoreNodeItemType } from '../type/node';
import { StoreEdgeItemType } from '../type/edge';
import { RuntimeEdgeItemType, RuntimeNodeItemType } from './type';
import { VARIABLE_NODE_ID } from '../constants';
import { isReferenceValue, isReferenceValueArray } from '../utils';
import { isReferenceValueFormat } from '../utils';
import { FlowNodeOutputItemType, ReferenceValueType } from '../type/io';
import { ChatItemType, NodeOutputItemType } from '../../../core/chat/type';
import { ChatItemValueTypeEnum, ChatRoleEnum } from '../../../core/chat/constants';
@@ -244,7 +244,7 @@ export const getReferenceVariableValue = ({
const nodeIds = nodes.map((node) => node.nodeId);
// handle single reference value
if (isReferenceValue(value, nodeIds)) {
if (isReferenceValueFormat(value)) {
const sourceNodeId = value[0];
const outputId = value[1];
@@ -261,7 +261,11 @@ export const getReferenceVariableValue = ({
}
// handle reference array
if (isReferenceValueArray(value, nodeIds)) {
if (
Array.isArray(value) &&
value.length > 0 &&
value.every((item) => isReferenceValueFormat(item))
) {
const result = value.map<any>((val) => {
return getReferenceVariableValue({
value: val,
@@ -270,7 +274,7 @@ export const getReferenceVariableValue = ({
});
});
return result.flat();
return result.flat().filter((item) => item !== undefined);
}
return value;

View File

@@ -111,7 +111,7 @@ export const Input_Template_Node_Height: FlowNodeInputItemType = {
renderTypeList: [FlowNodeInputTypeEnum.hidden],
valueType: WorkflowIOValueTypeEnum.number,
label: '',
value: 960
value: 600
};
export const Input_Template_Stream_MODE: FlowNodeInputItemType = {

View File

@@ -306,6 +306,15 @@ export const isReferenceValue = (value: any, nodeIds: string[]): value is [strin
const validIdSet = new Set([VARIABLE_NODE_ID, ...nodeIds]);
return validIdSet.has(value[0]);
};
export const isReferenceValueFormat = (value: any): value is [string, string] => {
return (
Array.isArray(value) &&
value.length === 2 &&
typeof value[0] === 'string' &&
typeof value[1] === 'string'
);
};
export const isReferenceValueArray = (
value: any,
nodeIds: string[]

View File

@@ -501,7 +501,7 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
nodes: runtimeNodes,
variables
});
console.log(value, '=-=-');
// Dynamic input is stored in the dynamic key
if (input.canEdit && dynamicInput && params[dynamicInput.key]) {
params[dynamicInput.key][input.key] = valueTypeFormat(value, input.valueType);

View File

@@ -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;
const isUndo = e.key.toLowerCase() === 'z' && !e.shiftKey;
const isRedo = (e.key.toLowerCase() === 'z' && e.shiftKey) || e.key.toLowerCase() === 'y';
if (isUndo) {
undo();
});
useKeyPress(['ctrl.shift.z', 'meta.shift.z', 'ctrl.y', 'meta.y'], (e) => {
if (!mouseInCanvas) return;
} else if (isRedo) {
redo();
}
});
useKeyPress(['ctrl.add', 'meta.add', 'ctrl.equalsign', 'meta.equalsign'], (e) => {
e.preventDefault();
e.stopPropagation();
if (!mouseInCanvas) return;
zoomIn();
});

View File

@@ -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,7 +353,6 @@ 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;
if (positionChange?.position) {
@@ -372,7 +389,6 @@ export const useWorkflow = () => {
setHelperLineVertical(undefined);
}
});
});
// Check if a node is placed on top of a loop node
const checkNodeOverLoopNode = useMemoizedFn((node: 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
};
};

View File

@@ -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>

View File

@@ -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
}
});
},

View File

@@ -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,6 +269,10 @@ const MultipleReferenceSelector = ({
maxW={'200px'}
className="textEllipsis"
>
{isInvalidItem ? (
<>{t('common:invalid_variable')}</>
) : (
<>
{nodeName}
<MyIcon
name={'common/rightArrowLight'}
@@ -290,13 +281,15 @@ const MultipleReferenceSelector = ({
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;
};

View File

@@ -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]

View File

@@ -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