diff --git a/packages/global/core/workflow/constants.ts b/packages/global/core/workflow/constants.ts index 7219f972f..dbeda7060 100644 --- a/packages/global/core/workflow/constants.ts +++ b/packages/global/core/workflow/constants.ts @@ -334,3 +334,14 @@ export enum ContentTypes { xml = 'xml', raw = 'raw-text' } + +export const ArrayTypeMap = { + [WorkflowIOValueTypeEnum.string]: WorkflowIOValueTypeEnum.arrayString, + [WorkflowIOValueTypeEnum.number]: WorkflowIOValueTypeEnum.arrayNumber, + [WorkflowIOValueTypeEnum.boolean]: WorkflowIOValueTypeEnum.arrayBoolean, + [WorkflowIOValueTypeEnum.object]: WorkflowIOValueTypeEnum.arrayObject, + [WorkflowIOValueTypeEnum.arrayString]: WorkflowIOValueTypeEnum.arrayString, + [WorkflowIOValueTypeEnum.arrayNumber]: WorkflowIOValueTypeEnum.arrayNumber, + [WorkflowIOValueTypeEnum.arrayBoolean]: WorkflowIOValueTypeEnum.arrayBoolean, + [WorkflowIOValueTypeEnum.arrayObject]: WorkflowIOValueTypeEnum.arrayObject +}; diff --git a/packages/global/core/workflow/runtime/utils.ts b/packages/global/core/workflow/runtime/utils.ts index 4aadbf57d..32f1021e5 100644 --- a/packages/global/core/workflow/runtime/utils.ts +++ b/packages/global/core/workflow/runtime/utils.ts @@ -235,25 +235,57 @@ export const getReferenceVariableValue = ({ variables: Record; }) => { const nodeIds = nodes.map((node) => node.nodeId); - if (!isReferenceValue(value, nodeIds)) { - return value; - } - const sourceNodeId = value[0]; - const outputId = value[1]; - if (sourceNodeId === VARIABLE_NODE_ID && outputId) { - return variables[outputId]; + // handle single reference value + if (isReferenceValue(value, nodeIds)) { + const sourceNodeId = value[0]; + const outputId = value[1]; + + if (sourceNodeId === VARIABLE_NODE_ID && outputId) { + return variables[outputId]; + } + + const node = nodes.find((node) => node.nodeId === sourceNodeId); + if (!node) { + return undefined; + } + + return node.outputs.find((output) => output.id === outputId)?.value; } - const node = nodes.find((node) => node.nodeId === sourceNodeId); + // handle reference array + if ( + Array.isArray(value) && + value.every( + (val) => val?.length === 2 && typeof val[0] === 'string' && typeof val[1] === 'string' + ) + ) { + const result = value.map((val) => { + if (!isReferenceValue(val, nodeIds)) { + return [val]; + } - if (!node) { - return undefined; + const sourceNodeId = val?.[0]; + const outputId = val?.[1]; + + if (sourceNodeId === VARIABLE_NODE_ID && outputId) { + const variableValue = variables[outputId]; + return Array.isArray(variableValue) ? variableValue : [variableValue]; + } + + const node = nodes.find((node) => node.nodeId === sourceNodeId); + if (!node) { + return undefined; + } + + const outputValue = node.outputs.find((output) => output.id === outputId)?.value; + return Array.isArray(outputValue) ? outputValue : [outputValue]; + }); + + return result.flat(); } - const outputValue = node.outputs.find((output) => output.id === outputId)?.value; - - return outputValue; + return value; }; export const textAdaptGptResponse = ({ diff --git a/packages/global/core/workflow/template/system/datasetConcat.ts b/packages/global/core/workflow/template/system/datasetConcat.ts index 062d86f40..185293b81 100644 --- a/packages/global/core/workflow/template/system/datasetConcat.ts +++ b/packages/global/core/workflow/template/system/datasetConcat.ts @@ -25,7 +25,7 @@ export const getOneQuoteInputTemplate = ({ }): FlowNodeInputItemType => ({ key, renderTypeList: [FlowNodeInputTypeEnum.reference], - label: `${i18nT('workflow:quote_num')},{ num: ${index} }`, + label: `${i18nT('workflow:quote_num')}`, debugLabel: i18nT('workflow:knowledge_base_reference'), canEdit: true, valueType: WorkflowIOValueTypeEnum.datasetQuote diff --git a/packages/service/core/workflow/dispatch/tools/runUpdateVar.ts b/packages/service/core/workflow/dispatch/tools/runUpdateVar.ts index bcb028a33..b380a3911 100644 --- a/packages/service/core/workflow/dispatch/tools/runUpdateVar.ts +++ b/packages/service/core/workflow/dispatch/tools/runUpdateVar.ts @@ -40,11 +40,13 @@ export const dispatchUpdateVariable = async (props: Props): Promise => }) : formatValue; } else { - return getReferenceVariableValue({ + const value = getReferenceVariableValue({ value: item.value, variables, nodes: runtimeNodes }); + + return value; } })(); diff --git a/packages/web/components/common/MySelect/MultipleRowSelect.tsx b/packages/web/components/common/MySelect/MultipleRowSelect.tsx index 68b8d0373..5292de07d 100644 --- a/packages/web/components/common/MySelect/MultipleRowSelect.tsx +++ b/packages/web/components/common/MySelect/MultipleRowSelect.tsx @@ -1,9 +1,10 @@ import React, { useRef, useCallback, useState } from 'react'; -import { Button, useDisclosure, Box, Flex, useOutsideClick } from '@chakra-ui/react'; +import { Button, useDisclosure, Box, Flex, useOutsideClick, Checkbox } from '@chakra-ui/react'; import { MultipleSelectProps } from './type'; import EmptyTip from '../EmptyTip'; import { useTranslation } from 'next-i18next'; import MyIcon from '../../common/Icon'; +import { ChevronDownIcon } from '@chakra-ui/icons'; const MultipleRowSelect = ({ placeholder, @@ -14,12 +15,14 @@ const MultipleRowSelect = ({ maxH = 300, onSelect, popDirection = 'bottom', - styles + styles, + isArray = false }: MultipleSelectProps) => { const { t } = useTranslation(); const ref = useRef(null); const { isOpen, onOpen, onClose } = useDisclosure(); - const [cloneValue, setCloneValue] = useState(value); + + const [navigationPath, setNavigationPath] = useState([]); useOutsideClick({ ref: ref, @@ -28,59 +31,80 @@ const MultipleRowSelect = ({ const RenderList = useCallback( ({ index, list }: { index: number; list: MultipleSelectProps['list'] }) => { - const selectedValue = cloneValue[index]; - const selectedIndex = list.findIndex((item) => item.value === selectedValue); + const currentNav = navigationPath[index]; + const selectedIndex = list.findIndex((item) => item.value === currentNav); const children = list[selectedIndex]?.children || []; const hasChildren = list.some((item) => item.children && item.children?.length > 0); + const handleSelect = (item: any) => { + if (hasChildren) { + // Update parent menu path + const newPath = [...navigationPath]; + newPath[index] = item.value; + // Clear sub paths + newPath.splice(index + 1); + setNavigationPath(newPath); + } else { + if (!isArray) { + onSelect([navigationPath[0], item.value]); + onClose(); + } else { + const parentValue = navigationPath[0]; + const newValues = [...value]; + const newValue = [parentValue, item.value]; + + if (newValues.some((v) => v[0] === parentValue && v[1] === item.value)) { + onSelect(newValues.filter((v) => !(v[0] === parentValue && v[1] === item.value))); + } else { + onSelect([...newValues, newValue]); + } + } + } + }; + return ( <> - {list.map((item) => ( - { - const newValue = [...cloneValue]; + {list.map((item) => { + const isSelected = item.value === currentNav; + const showCheckbox = isArray && index !== 0; + const isChecked = + showCheckbox && + value.some((v) => v[1] === item.value && v[0] === navigationPath[0]); - if (item.value === selectedValue) { - newValue[index] = undefined; - setCloneValue(newValue); - onSelect(newValue); - } else { - newValue[index] = item.value; - setCloneValue(newValue); - if (!hasChildren) { - onSelect(newValue); - onClose(); - } - } - }} - {...(item.value === selectedValue - ? { - color: 'primary.600' - } - : {})} - > - {item.label} - - ))} + return ( + handleSelect(item)} + {...(isSelected ? { color: 'primary.600' } : {})} + > + {showCheckbox && ( + } + mr={1} + /> + )} + {item.label} + + ); + })} {list.length === 0 && ( ); }, - [cloneValue] + [navigationPath, value, isArray, onSelect] ); const onOpenSelect = useCallback(() => { - setCloneValue(value); + setNavigationPath(isArray ? [] : [value[0]?.[0], value[0]?.[1]]); onOpen(); - }, [value, onOpen]); + }, [value, isArray, onOpen]); return ( - + + + + {isOpen && ( = { onSelect: (val: any[]) => void; styles?: ButtonProps; popDirection?: 'top' | 'bottom'; + isArray?: boolean; }; diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/VariableInput.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/VariableInput.tsx index 50c077d88..9eae86119 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/VariableInput.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/VariableInput.tsx @@ -24,6 +24,7 @@ import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; import { useDeepCompareEffect } from 'ahooks'; import { VariableItemType } from '@fastgpt/global/core/app/type'; import MyTextarea from '@/components/common/Textarea/MyTextarea'; +import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput'; export const VariableInputItem = ({ item, @@ -108,23 +109,15 @@ export const VariableInputItem = ({ control={control} name={`variables.${item.key}`} rules={{ required: item.required, min: item.min, max: item.max }} - render={({ field: { ref, value, onChange } }) => ( - ( + onChange(Number(valueString))} - > - - - - - - + onChange={onChange} + /> )} /> )} diff --git a/projects/app/src/components/core/chat/ChatContainer/PluginRunBox/components/RenderInput.tsx b/projects/app/src/components/core/chat/ChatContainer/PluginRunBox/components/RenderInput.tsx index a0907e90e..b0288cd16 100644 --- a/projects/app/src/components/core/chat/ChatContainer/PluginRunBox/components/RenderInput.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/PluginRunBox/components/RenderInput.tsx @@ -80,7 +80,7 @@ const RenderInput = () => { setRestartData(e); onNewChat?.(); }, - [onNewChat] + [onNewChat, setRestartData] ); const formatPluginInputs = useMemo(() => { @@ -101,12 +101,12 @@ const RenderInput = () => { useEffect(() => { // Set config default value if (histories.length === 0) { - // Restart if (restartData) { reset(restartData); setRestartData(undefined); return; } + const defaultFormValues = formatPluginInputs.reduce( (acc, input) => { acc[input.key] = input.defaultValue; @@ -160,7 +160,8 @@ const RenderInput = () => { variables: historyVariables, files: historyFileList }); - }, [histories.length]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [histories]); const [uploading, setUploading] = useState(false); diff --git a/projects/app/src/components/core/chat/ChatContainer/PluginRunBox/components/renderPluginInput.tsx b/projects/app/src/components/core/chat/ChatContainer/PluginRunBox/components/renderPluginInput.tsx index 9673b5bc7..438f2fc54 100644 --- a/projects/app/src/components/core/chat/ChatContainer/PluginRunBox/components/renderPluginInput.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/PluginRunBox/components/renderPluginInput.tsx @@ -1,15 +1,4 @@ -import { - Box, - Button, - Flex, - NumberDecrementStepper, - NumberIncrementStepper, - NumberInput, - NumberInputField, - NumberInputStepper, - Switch, - Textarea -} from '@chakra-ui/react'; +import { Box, Button, Flex, Switch, Textarea } from '@chakra-ui/react'; import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants'; import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io'; @@ -27,17 +16,21 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useEffect, useMemo } from 'react'; import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; import { useFieldArray } from 'react-hook-form'; +import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput'; +import { isEqual } from 'lodash'; const JsonEditor = dynamic(() => import('@fastgpt/web/components/common/Textarea/JsonEditor')); const FileSelector = ({ input, setUploading, - onChange + onChange, + value }: { input: FlowNodeInputItemType; setUploading: React.Dispatch>; onChange: (...event: any[]) => void; + value: any; }) => { const { t } = useTranslation(); const { variablesForm, histories, chatId, outLinkAuthData } = useContextSelector( @@ -56,7 +49,8 @@ const FileSelector = ({ uploadFiles, onOpenSelectFile, onSelectFile, - removeFiles + removeFiles, + replaceFiles } = useFileUpload({ outLinkAuthData, chatId: chatId || '', @@ -68,6 +62,22 @@ const FileSelector = ({ // @ts-ignore fileCtrl }); + + useEffect(() => { + if (!Array.isArray(value)) { + replaceFiles([]); + return; + } + + // compare file names and update if different + const valueFileNames = value.map((item) => item.name); + const currentFileNames = fileList.map((item) => item.name); + if (!isEqual(valueFileNames, currentFileNames)) { + replaceFiles(value); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value]); + const isDisabledInput = histories.length > 0; useRequest2(uploadFiles, { manual: false, @@ -151,7 +161,9 @@ const RenderPluginInput = ({ ); } if (inputType === FlowNodeInputTypeEnum.fileSelect) { - return ; + return ( + + ); } if (input.valueType === WorkflowIOValueTypeEnum.string) { @@ -169,20 +181,17 @@ const RenderPluginInput = ({ } if (input.valueType === WorkflowIOValueTypeEnum.number) { return ( - - - - - - - + value={value} + onChange={onChange} + defaultValue={input.defaultValue} + /> ); } if (input.valueType === WorkflowIOValueTypeEnum.boolean) { diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoop.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoop.tsx index 3c70c8a68..03cab16aa 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoop.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoop.tsx @@ -15,15 +15,26 @@ import RenderInput from '../render/RenderInput'; import { Box } from '@chakra-ui/react'; import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; import RenderOutput from '../render/RenderOutput'; -import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; +import { + ArrayTypeMap, + NodeInputKeyEnum, + VARIABLE_NODE_ID, + WorkflowIOValueTypeEnum +} from '@fastgpt/global/core/workflow/constants'; 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'; const NodeLoop = ({ data, selected }: NodeProps) => { const { t } = useTranslation(); const { nodeId, inputs, outputs, isFolded } = data; const { onChangeNode, nodeList } = useContextSelector(WorkflowContext, (v) => v); + const { appDetail } = useContextSelector(AppContext, (v) => v); + + const arrayValue = inputs.find((input) => input.key === NodeInputKeyEnum.loopInputArray)?.value; const { nodeWidth, nodeHeight } = useMemo(() => { return { @@ -37,6 +48,42 @@ const NodeLoop = ({ data, selected }: NodeProps) => { ); }, [nodeId, nodeList]); + // Detect and update array input type + useEffect(() => { + const inputsClone = cloneDeep(inputs); + const globalVariables = getWorkflowGlobalVariables({ + nodes: nodeList, + chatConfig: appDetail.chatConfig + }); + + let arrayType: WorkflowIOValueTypeEnum | undefined; + if (arrayValue[0]?.[0] === VARIABLE_NODE_ID) { + arrayType = globalVariables.find((item) => item.key === arrayValue[0]?.[1])?.valueType; + } else { + const node = nodeList.find((node) => node.nodeId === arrayValue[0]?.[0]); + const output = node?.outputs.find((output) => output.id === arrayValue[0]?.[1]); + arrayType = output?.valueType; + } + + const arrayInput = inputsClone.find((input) => input.key === NodeInputKeyEnum.loopInputArray); + + if (!arrayInput) { + return; + } + + onChangeNode({ + nodeId, + type: 'updateInput', + key: NodeInputKeyEnum.loopInputArray, + value: { + ...arrayInput, + valueType: arrayType + ? ArrayTypeMap[arrayType as keyof typeof ArrayTypeMap] + : WorkflowIOValueTypeEnum.arrayAny + } + }); + }, [appDetail.chatConfig, arrayValue, inputs, nodeId, nodeList, onChangeNode]); + useEffect(() => { onChangeNode({ nodeId, @@ -47,7 +94,7 @@ const NodeLoop = ({ data, selected }: NodeProps) => { value: JSON.parse(childrenNodeIdList) } }); - }, [childrenNodeIdList]); + }, [childrenNodeIdList, nodeId, onChangeNode]); const Render = useMemo(() => { return ( @@ -80,7 +127,7 @@ const NodeLoop = ({ data, selected }: NodeProps) => { ); - }, [selected, nodeWidth, nodeHeight, data, t, nodeId, inputs, outputs]); + }, [selected, isFolded, nodeWidth, nodeHeight, data, t, nodeId, inputs, outputs]); return Render; }; diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoopStart.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoopStart.tsx index ba2687b1c..f3d4b0f1d 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoopStart.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoopStart.tsx @@ -19,8 +19,7 @@ const typeMap = { [WorkflowIOValueTypeEnum.arrayString]: WorkflowIOValueTypeEnum.string, [WorkflowIOValueTypeEnum.arrayNumber]: WorkflowIOValueTypeEnum.number, [WorkflowIOValueTypeEnum.arrayBoolean]: WorkflowIOValueTypeEnum.boolean, - [WorkflowIOValueTypeEnum.arrayObject]: WorkflowIOValueTypeEnum.object, - [WorkflowIOValueTypeEnum.arrayAny]: WorkflowIOValueTypeEnum.any + [WorkflowIOValueTypeEnum.arrayObject]: WorkflowIOValueTypeEnum.object }; const NodeLoopStart = ({ data, selected }: NodeProps) => { @@ -39,12 +38,7 @@ const NodeLoopStart = ({ data, selected }: NodeProps) => { const parentArrayInput = parentNode?.inputs.find( (input) => input.key === NodeInputKeyEnum.loopInputArray ); - return parentArrayInput?.value - ? (nodeList - .find((node) => node.nodeId === parentArrayInput?.value[0]) - ?.outputs.find((output) => output.id === parentArrayInput?.value[1]) - ?.valueType as keyof typeof typeMap) - : undefined; + return typeMap[parentArrayInput?.valueType as keyof typeof typeMap]; }, [loopStartNode?.parentNodeId, nodeList]); // Auth update loopStartInput output @@ -71,7 +65,7 @@ const NodeLoopStart = ({ data, selected }: NodeProps) => { key: NodeOutputKeyEnum.loopStartInput, label: t('workflow:Array_element'), type: FlowNodeOutputTypeEnum.static, - valueType: typeMap[loopItemInputType as keyof typeof typeMap] + valueType: loopItemInputType } }); } @@ -83,7 +77,7 @@ const NodeLoopStart = ({ data, selected }: NodeProps) => { key: NodeOutputKeyEnum.loopStartInput, value: { ...loopArrayOutput, - valueType: typeMap[loopItemInputType as keyof typeof typeMap] + valueType: loopItemInputType } }); } @@ -128,7 +122,7 @@ const NodeLoopStart = ({ data, selected }: NodeProps) => { {t('workflow:Array_element')} - {typeMap[loopItemInputType]} + {loopItemInputType} diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeDatasetConcat.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeDatasetConcat.tsx index 42dd394a7..be26b3a52 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeDatasetConcat.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeDatasetConcat.tsx @@ -48,7 +48,7 @@ const NodeDatasetConcat = ({ data, selected }: NodeProps) => { }); const onSelect = useCallback( - (e: ReferenceValueProps) => { + (e: ReferenceValueProps | ReferenceValueProps[]) => { const workflowStartNode = nodeList.find( (node) => node.flowNodeType === FlowNodeTypeEnum.workflowStart ); @@ -60,7 +60,7 @@ const NodeDatasetConcat = ({ data, selected }: NodeProps) => { value: { ...inputChildren, value: - e[0] === workflowStartNode?.id && !isWorkflowStartOutput(e[1]) + e[0] === workflowStartNode?.id && !isWorkflowStartOutput(e[1] as string) ? [VARIABLE_NODE_ID, e[1]] : e } diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeIfElse/ListItem.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeIfElse/ListItem.tsx index fb70b3d54..ae41365c8 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeIfElse/ListItem.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeIfElse/ListItem.tsx @@ -324,7 +324,7 @@ const Reference = ({ placeholder={t('common:select_reference_variable')} list={referenceList} value={formatValue} - onSelect={onSelect} + onSelect={onSelect as any} /> ); }; diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodePluginIO/PluginOutput.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodePluginIO/PluginOutput.tsx index b43d52060..1f9f9e8da 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodePluginIO/PluginOutput.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodePluginIO/PluginOutput.tsx @@ -118,13 +118,13 @@ function Reference({ const [editField, setEditField] = useState(); const onSelect = useCallback( - (e: ReferenceValueProps) => { + (e: ReferenceValueProps | ReferenceValueProps[]) => { const workflowStartNode = nodeList.find( (node) => node.flowNodeType === FlowNodeTypeEnum.workflowStart ); const value = - e[0] === workflowStartNode?.id && !isWorkflowStartOutput(e[1]) + e[0] === workflowStartNode?.id && !isWorkflowStartOutput(e[1] as string) ? [VARIABLE_NODE_ID, e[1]] : e; @@ -219,6 +219,7 @@ function Reference({ list={referenceList} value={formatValue} onSelect={onSelect} + isArray={input.valueType?.includes('array')} /> {!!editField && ( diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeVariableUpdate.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeVariableUpdate.tsx index 67bb4de04..39182cc1b 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeVariableUpdate.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeVariableUpdate.tsx @@ -326,7 +326,7 @@ const Reference = ({ placeholder={t('common:select_reference_variable')} list={referenceList} value={formatValue} - onSelect={onSelect} + onSelect={onSelect as any} /> ); }; diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/NodeCard.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/NodeCard.tsx index c9eb769bf..987d76a78 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/NodeCard.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/NodeCard.tsx @@ -365,7 +365,7 @@ const NodeCard = (props: Props) => { > {Header} - + {!isFolded ? children : } {RenderHandle} diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/DynamicInputs/index.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/DynamicInputs/index.tsx index 6eb6bc289..86985bacd 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/DynamicInputs/index.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/DynamicInputs/index.tsx @@ -12,7 +12,7 @@ import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponent import { defaultInput } from '../../FieldEditModal'; import { getInputComponentProps } from '@fastgpt/global/core/workflow/node/io/utils'; import { VARIABLE_NODE_ID } from '@fastgpt/global/core/workflow/constants'; -import { ReferSelector, useReference } from '../Reference'; +import { isReference, ReferSelector, useReference } from '../Reference'; import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; import ValueTypeLabel from '../../../ValueTypeLabel'; import MyIcon from '@fastgpt/web/components/common/Icon'; @@ -126,13 +126,13 @@ function Reference({ const [editField, setEditField] = useState(); const onSelect = useCallback( - (e: ReferenceValueProps) => { + (e: ReferenceValueProps | ReferenceValueProps[]) => { const workflowStartNode = nodeList.find( (node) => node.flowNodeType === FlowNodeTypeEnum.workflowStart ); const newValue = - e[0] === workflowStartNode?.id && !isWorkflowStartOutput(e[1]) + e[0] === workflowStartNode?.id && !isWorkflowStartOutput(e[1] as string) ? [VARIABLE_NODE_ID, e[1]] : e; @@ -155,18 +155,42 @@ function Reference({ value: inputChildren.value }); + // handle array and non-array type conversion + const getValueTypeChange = useCallback( + (data: FlowNodeInputItemType, oldType: string | undefined) => { + const newType = data.valueType; + if (oldType === newType) return data.value; + + if (!oldType?.includes('array') && newType?.includes('array')) { + return Array.isArray(data.value) && data.value.every((item) => isReference(item)) + ? data.value + : [data.value]; + } + if (oldType?.includes('array') && !newType?.includes('array')) { + return Array.isArray(data.value) ? data.value[0] : data.value; + } + return data.value; + }, + [] + ); + const onUpdateField = useCallback( ({ data }: { data: FlowNodeInputItemType }) => { if (!data.key) return; + const updatedValue = getValueTypeChange(data, inputChildren.valueType); + onChangeNode({ nodeId, type: 'replaceInput', key: inputChildren.key, - value: data + value: { + ...data, + value: updatedValue + } }); }, - [inputChildren.key, nodeId, onChangeNode] + [inputChildren, nodeId, onChangeNode, getValueTypeChange] ); const onDel = useCallback(() => { onChangeNode({ @@ -212,6 +236,7 @@ function Reference({ list={referenceList} value={formatValue} onSelect={onSelect} + isArray={inputChildren.valueType?.includes('array')} /> {!!editField && !!item.customInputConfig && ( diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/Reference.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/Reference.tsx index 6c55025e3..372049b1f 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/Reference.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/Reference.tsx @@ -22,7 +22,7 @@ const MultipleRowSelect = dynamic( const Avatar = dynamic(() => import('@fastgpt/web/components/common/Avatar')); type SelectProps = { - value?: ReferenceValueProps; + value?: ReferenceValueProps[]; placeholder?: string; list: { label: string | React.ReactNode; @@ -33,18 +33,25 @@ type SelectProps = { valueType?: WorkflowIOValueTypeEnum; }[]; }[]; - onSelect: (val: ReferenceValueProps) => void; + onSelect: (val: ReferenceValueProps | ReferenceValueProps[]) => void; popDirection?: 'top' | 'bottom'; styles?: ButtonProps; + isArray?: boolean; }; +export const isReference = (val: any) => + Array.isArray(val) && + val.length === 2 && + typeof val[0] === 'string' && + typeof val[1] === 'string'; + const Reference = ({ item, nodeId }: RenderInputProps) => { const { t } = useTranslation(); const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode); const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); const onSelect = useCallback( - (e: ReferenceValueProps) => { + (e: ReferenceValueProps | ReferenceValueProps[]) => { const workflowStartNode = nodeList.find( (node) => node.flowNodeType === FlowNodeTypeEnum.workflowStart ); @@ -92,6 +99,7 @@ const Reference = ({ item, nodeId }: RenderInputProps) => { value={formatValue} onSelect={onSelect} popDirection={popDirection} + isArray={item.valueType?.includes('array')} /> ); }; @@ -111,6 +119,8 @@ export const useReference = ({ const { appDetail } = useContextSelector(AppContext, (v) => v); const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); const edges = useContextSelector(WorkflowContext, (v) => v.edges); + const isArray = valueType?.includes('array'); + const currentType = isArray ? valueType.replace('array', '').toLowerCase() : valueType; const referenceList = useMemo(() => { const sourceNodes = computedNodeInputReference({ @@ -129,8 +139,8 @@ export const useReference = ({ return { label: ( - - {t(node.name as any)} + + {t(node.name as any)} ), value: node.nodeId, @@ -139,10 +149,20 @@ export const useReference = ({ (output) => valueType === WorkflowIOValueTypeEnum.any || output.valueType === WorkflowIOValueTypeEnum.any || + currentType === output.valueType || + // array output.valueType === valueType || - // When valueType is arrayAny, return all array type outputs (valueType === WorkflowIOValueTypeEnum.arrayAny && - output.valueType?.includes('array')) + [ + WorkflowIOValueTypeEnum.arrayString, + WorkflowIOValueTypeEnum.arrayNumber, + WorkflowIOValueTypeEnum.arrayBoolean, + WorkflowIOValueTypeEnum.arrayObject, + WorkflowIOValueTypeEnum.string, + WorkflowIOValueTypeEnum.number, + WorkflowIOValueTypeEnum.boolean, + WorkflowIOValueTypeEnum.object + ].includes(output.valueType as WorkflowIOValueTypeEnum)) ) .filter((output) => output.id !== NodeOutputKeyEnum.addOutputParam) .map((output) => { @@ -157,16 +177,14 @@ export const useReference = ({ .filter((item) => item.children.length > 0); return list; - }, [appDetail.chatConfig, edges, nodeId, nodeList, t, valueType]); + }, [appDetail.chatConfig, currentType, edges, isArray, nodeId, nodeList, t, valueType]); const formatValue = useMemo(() => { - if ( - Array.isArray(value) && - value.length === 2 && - typeof value[0] === 'string' && - typeof value[1] === 'string' - ) { - return value as ReferenceValueProps; + // convert origin reference [variableId, outputId] to new reference [[variableId, outputId], ...] + if (isReference(value)) { + return [value] as ReferenceValueProps[]; + } else if (Array.isArray(value) && value.every((item) => isReference(item))) { + return value as ReferenceValueProps[]; } return undefined; }, [value]); @@ -176,40 +194,115 @@ export const useReference = ({ formatValue }; }; -export const ReferSelector = ({ + +const ReferSelectorComponent = ({ placeholder, value, list = [], onSelect, - popDirection + popDirection, + isArray }: SelectProps) => { - const selectItemLabel = useMemo(() => { - if (!value) { + const { t } = useTranslation(); + + const selectValue = useMemo(() => { + if (!value || value.every((item) => !item || item.every((subItem) => !subItem))) { return; } - const firstColumn = list.find((item) => item.value === value[0]); - if (!firstColumn) { - return; - } - const secondColumn = firstColumn.children.find((item) => item.value === value[1]); - if (!secondColumn) { - return; - } - return [firstColumn, secondColumn]; + return value.map((valueItem) => { + const firstColumn = list.find((item) => item.value === valueItem[0]); + if (!firstColumn) { + return; + } + const secondColumn = firstColumn.children.find((item) => item.value === valueItem[1]); + if (!secondColumn) { + return; + } + return [firstColumn, secondColumn]; + }); }, [list, value]); const Render = useMemo(() => { return ( - {selectItemLabel[0].label} - - {selectItemLabel[1].label} + selectValue && selectValue.length > 0 ? ( + + {isArray ? ( + // [[variableId, outputId], ...] + selectValue.map((item, index) => { + const isInvalidItem = item === undefined; + return ( + + {isInvalidItem ? ( + t('common:invalid_variable') + ) : ( + <> + {item?.[0].label} + + {item?.[1].label} + + )} + { + e.stopPropagation(); + if (isInvalidItem) { + const filteredValue = value?.filter((_, i) => i !== index); + onSelect(filteredValue as any); + return; + } + const filteredValue = value?.filter( + (val) => val[0] !== item?.[0].value || val[1] !== item?.[1].value + ); + filteredValue && onSelect(filteredValue); + }} + /> + + ); + }) + ) : // [variableId, outputId] + selectValue[0] ? ( + + {selectValue[0][0].label} + + {selectValue[0][1].label} + + ) : ( + + {placeholder} + + )} ) : ( - {placeholder} + + {placeholder} + ) } value={value as any[]} @@ -218,9 +311,14 @@ export const ReferSelector = ({ onSelect(e as ReferenceValueProps); }} popDirection={popDirection} + isArray={isArray} /> ); - }, [list, onSelect, placeholder, popDirection, selectItemLabel, value]); + }, [isArray, list, onSelect, placeholder, popDirection, selectValue, t, value]); return Render; }; + +ReferSelectorComponent.displayName = 'ReferSelector'; + +export const ReferSelector = React.memo(ReferSelectorComponent); diff --git a/projects/app/src/web/core/workflow/utils.ts b/projects/app/src/web/core/workflow/utils.ts index b44982da7..7c121433c 100644 --- a/projects/app/src/web/core/workflow/utils.ts +++ b/projects/app/src/web/core/workflow/utils.ts @@ -327,24 +327,37 @@ export const checkWorkflowNodeAndConnection = ({ // check reference invalid const renderType = input.renderTypeList[input.selectedTypeIndex || 0]; if (renderType === FlowNodeInputTypeEnum.reference && input.required) { - if (!input.value || !Array.isArray(input.value) || input.value.length !== 2) { - return true; + const checkReference = (value: [string, string]) => { + if (value[0] === VARIABLE_NODE_ID) { + return false; + } + + const sourceNode = nodes.find((item) => item.data.nodeId === value[0]); + if (!sourceNode) { + return true; + } + + const sourceOutput = sourceNode.data.outputs.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]); } - // variable key not need to check - if (input.value[0] === VARIABLE_NODE_ID) { - return false; - } - - // Can not find key - const sourceNode = nodes.find((item) => item.data.nodeId === input.value[0]); - if (!sourceNode) { - return true; - } - const sourceOutput = sourceNode.data.outputs.find((item) => item.id === input.value[1]); - if (!sourceOutput) { - return true; - } + // New format + return input.value.some((inputItem: ReferenceValueProps) => { + if (!Array.isArray(inputItem) || inputItem.length !== 2) { + return true; + } + return checkReference(inputItem as [string, string]); + }); } return false; })