feat: support array reference multi-select (#3041)

* feat: support array reference multi-select

* fix build

* fix

* fix loop multi-select

* adjust condition

* fix get value

* array and non-array conversion

* fix plugin input

* merge func
This commit is contained in:
heheer 2024-11-05 13:02:09 +08:00 committed by archer
parent f4dbe7c021
commit 0db0cbf376
No known key found for this signature in database
GPG Key ID: 4446499B846D4A9E
19 changed files with 442 additions and 176 deletions

View File

@ -334,3 +334,14 @@ export enum ContentTypes {
xml = 'xml', xml = 'xml',
raw = 'raw-text' 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
};

View File

@ -235,9 +235,9 @@ export const getReferenceVariableValue = ({
variables: Record<string, any>; variables: Record<string, any>;
}) => { }) => {
const nodeIds = nodes.map((node) => node.nodeId); const nodeIds = nodes.map((node) => node.nodeId);
if (!isReferenceValue(value, nodeIds)) {
return value; // handle single reference value
} if (isReferenceValue(value, nodeIds)) {
const sourceNodeId = value[0]; const sourceNodeId = value[0];
const outputId = value[1]; const outputId = value[1];
@ -246,14 +246,46 @@ export const getReferenceVariableValue = ({
} }
const node = nodes.find((node) => node.nodeId === sourceNodeId); const node = nodes.find((node) => node.nodeId === sourceNodeId);
if (!node) {
return undefined;
}
return node.outputs.find((output) => output.id === outputId)?.value;
}
// 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];
}
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) { if (!node) {
return undefined; return undefined;
} }
const outputValue = node.outputs.find((output) => output.id === outputId)?.value; const outputValue = node.outputs.find((output) => output.id === outputId)?.value;
return Array.isArray(outputValue) ? outputValue : [outputValue];
});
return outputValue; return result.flat();
}
return value;
}; };
export const textAdaptGptResponse = ({ export const textAdaptGptResponse = ({

View File

@ -25,7 +25,7 @@ export const getOneQuoteInputTemplate = ({
}): FlowNodeInputItemType => ({ }): FlowNodeInputItemType => ({
key, key,
renderTypeList: [FlowNodeInputTypeEnum.reference], renderTypeList: [FlowNodeInputTypeEnum.reference],
label: `${i18nT('workflow:quote_num')},{ num: ${index} }`, label: `${i18nT('workflow:quote_num')}`,
debugLabel: i18nT('workflow:knowledge_base_reference'), debugLabel: i18nT('workflow:knowledge_base_reference'),
canEdit: true, canEdit: true,
valueType: WorkflowIOValueTypeEnum.datasetQuote valueType: WorkflowIOValueTypeEnum.datasetQuote

View File

@ -40,11 +40,13 @@ export const dispatchUpdateVariable = async (props: Props): Promise<Response> =>
}) })
: formatValue; : formatValue;
} else { } else {
return getReferenceVariableValue({ const value = getReferenceVariableValue({
value: item.value, value: item.value,
variables, variables,
nodes: runtimeNodes nodes: runtimeNodes
}); });
return value;
} }
})(); })();

View File

@ -1,9 +1,10 @@
import React, { useRef, useCallback, useState } from 'react'; 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 { MultipleSelectProps } from './type';
import EmptyTip from '../EmptyTip'; import EmptyTip from '../EmptyTip';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import MyIcon from '../../common/Icon'; import MyIcon from '../../common/Icon';
import { ChevronDownIcon } from '@chakra-ui/icons';
const MultipleRowSelect = ({ const MultipleRowSelect = ({
placeholder, placeholder,
@ -14,12 +15,14 @@ const MultipleRowSelect = ({
maxH = 300, maxH = 300,
onSelect, onSelect,
popDirection = 'bottom', popDirection = 'bottom',
styles styles,
isArray = false
}: MultipleSelectProps) => { }: MultipleSelectProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const [cloneValue, setCloneValue] = useState(value);
const [navigationPath, setNavigationPath] = useState<string[]>([]);
useOutsideClick({ useOutsideClick({
ref: ref, ref: ref,
@ -28,24 +31,56 @@ const MultipleRowSelect = ({
const RenderList = useCallback( const RenderList = useCallback(
({ index, list }: { index: number; list: MultipleSelectProps['list'] }) => { ({ index, list }: { index: number; list: MultipleSelectProps['list'] }) => {
const selectedValue = cloneValue[index]; const currentNav = navigationPath[index];
const selectedIndex = list.findIndex((item) => item.value === selectedValue); const selectedIndex = list.findIndex((item) => item.value === currentNav);
const children = list[selectedIndex]?.children || []; const children = list[selectedIndex]?.children || [];
const hasChildren = list.some((item) => item.children && item.children?.length > 0); 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 ( return (
<> <>
<Box <Box
className="nowheel" className="nowheel"
flex={'1 0 auto'} flex={'1 0 auto'}
// width={0}
px={2} px={2}
borderLeft={index !== 0 ? 'base' : 'none'} borderLeft={index !== 0 ? 'base' : 'none'}
maxH={`${maxH}px`} maxH={`${maxH}px`}
overflowY={'auto'} overflowY={'auto'}
whiteSpace={'nowrap'} whiteSpace={'nowrap'}
> >
{list.map((item) => ( {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]);
return (
<Flex <Flex
key={item.value} key={item.value}
py={2} py={2}
@ -56,31 +91,20 @@ const MultipleRowSelect = ({
bg: 'primary.50', bg: 'primary.50',
color: 'primary.600' color: 'primary.600'
}} }}
onClick={() => { onClick={() => handleSelect(item)}
const newValue = [...cloneValue]; {...(isSelected ? { color: 'primary.600' } : {})}
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'
}
: {})}
> >
{showCheckbox && (
<Checkbox
isChecked={isChecked}
icon={<MyIcon name={'common/check'} w={'12px'} />}
mr={1}
/>
)}
{item.label} {item.label}
</Flex> </Flex>
))} );
})}
{list.length === 0 && ( {list.length === 0 && (
<EmptyTip <EmptyTip
text={emptyTip ?? t('common:common.MultipleRowSelect.No data')} text={emptyTip ?? t('common:common.MultipleRowSelect.No data')}
@ -93,28 +117,39 @@ const MultipleRowSelect = ({
</> </>
); );
}, },
[cloneValue] [navigationPath, value, isArray, onSelect]
); );
const onOpenSelect = useCallback(() => { const onOpenSelect = useCallback(() => {
setCloneValue(value); setNavigationPath(isArray ? [] : [value[0]?.[0], value[0]?.[1]]);
onOpen(); onOpen();
}, [value, onOpen]); }, [value, isArray, onOpen]);
return ( return (
<Box ref={ref} position={'relative'}> <Box ref={ref} position={'relative'}>
<Button <Flex
justifyContent={'space-between'} justifyContent={'space-between'}
alignItems={'center'}
overflow={'auto'}
width={'100%'} width={'100%'}
variant={'whitePrimaryOutline'} variant={'whitePrimaryOutline'}
size={'lg'} size={'lg'}
fontSize={'sm'} fontSize={'sm'}
px={3} px={3}
py={1}
minH={10}
maxH={24}
outline={'none'} outline={'none'}
rightIcon={<MyIcon name={'core/chat/chevronDown'} w={4} color={'myGray.500'} />} rightIcon={<MyIcon name={'core/chat/chevronDown'} w={4} color={'myGray.500'} />}
border={'1px solid'}
borderRadius={'md'}
bg={'white'}
_active={{ _active={{
transform: 'none' transform: 'none'
}} }}
_hover={{
borderColor: 'primary.500'
}}
{...(isOpen {...(isOpen
? { ? {
borderColor: 'primary.600', borderColor: 'primary.600',
@ -127,9 +162,13 @@ const MultipleRowSelect = ({
})} })}
{...styles} {...styles}
onClick={() => (isOpen ? onClose() : onOpenSelect())} onClick={() => (isOpen ? onClose() : onOpenSelect())}
className="nowheel"
> >
<Box>{label ?? placeholder}</Box> <Box>{label ?? placeholder}</Box>
</Button> <Flex alignItems={'center'} ml={1}>
<ChevronDownIcon />
</Flex>
</Flex>
{isOpen && ( {isOpen && (
<Box <Box
position={'absolute'} position={'absolute'}

View File

@ -14,4 +14,5 @@ export type MultipleSelectProps<T = any> = {
onSelect: (val: any[]) => void; onSelect: (val: any[]) => void;
styles?: ButtonProps; styles?: ButtonProps;
popDirection?: 'top' | 'bottom'; popDirection?: 'top' | 'bottom';
isArray?: boolean;
}; };

View File

@ -24,6 +24,7 @@ import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { useDeepCompareEffect } from 'ahooks'; import { useDeepCompareEffect } from 'ahooks';
import { VariableItemType } from '@fastgpt/global/core/app/type'; import { VariableItemType } from '@fastgpt/global/core/app/type';
import MyTextarea from '@/components/common/Textarea/MyTextarea'; import MyTextarea from '@/components/common/Textarea/MyTextarea';
import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput';
export const VariableInputItem = ({ export const VariableInputItem = ({
item, item,
@ -108,23 +109,15 @@ export const VariableInputItem = ({
control={control} control={control}
name={`variables.${item.key}`} name={`variables.${item.key}`}
rules={{ required: item.required, min: item.min, max: item.max }} rules={{ required: item.required, min: item.min, max: item.max }}
render={({ field: { ref, value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<NumberInput <MyNumberInput
step={1} step={1}
min={item.min} min={item.min}
max={item.max} max={item.max}
bg={'white'} bg={'white'}
rounded={'md'}
clampValueOnBlur={false}
value={value} value={value}
onChange={(valueString) => onChange(Number(valueString))} onChange={onChange}
> />
<NumberInputField ref={ref} bg={'white'} />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
)} )}
/> />
)} )}

View File

@ -80,7 +80,7 @@ const RenderInput = () => {
setRestartData(e); setRestartData(e);
onNewChat?.(); onNewChat?.();
}, },
[onNewChat] [onNewChat, setRestartData]
); );
const formatPluginInputs = useMemo(() => { const formatPluginInputs = useMemo(() => {
@ -101,12 +101,12 @@ const RenderInput = () => {
useEffect(() => { useEffect(() => {
// Set config default value // Set config default value
if (histories.length === 0) { if (histories.length === 0) {
// Restart
if (restartData) { if (restartData) {
reset(restartData); reset(restartData);
setRestartData(undefined); setRestartData(undefined);
return; return;
} }
const defaultFormValues = formatPluginInputs.reduce( const defaultFormValues = formatPluginInputs.reduce(
(acc, input) => { (acc, input) => {
acc[input.key] = input.defaultValue; acc[input.key] = input.defaultValue;
@ -160,7 +160,8 @@ const RenderInput = () => {
variables: historyVariables, variables: historyVariables,
files: historyFileList files: historyFileList
}); });
}, [histories.length]); // eslint-disable-next-line react-hooks/exhaustive-deps
}, [histories]);
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);

View File

@ -1,15 +1,4 @@
import { import { Box, Button, Flex, Switch, Textarea } from '@chakra-ui/react';
Box,
Button,
Flex,
NumberDecrementStepper,
NumberIncrementStepper,
NumberInput,
NumberInputField,
NumberInputStepper,
Switch,
Textarea
} from '@chakra-ui/react';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants'; import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io'; 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 { useEffect, useMemo } from 'react';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { useFieldArray } from 'react-hook-form'; 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 JsonEditor = dynamic(() => import('@fastgpt/web/components/common/Textarea/JsonEditor'));
const FileSelector = ({ const FileSelector = ({
input, input,
setUploading, setUploading,
onChange onChange,
value
}: { }: {
input: FlowNodeInputItemType; input: FlowNodeInputItemType;
setUploading: React.Dispatch<React.SetStateAction<boolean>>; setUploading: React.Dispatch<React.SetStateAction<boolean>>;
onChange: (...event: any[]) => void; onChange: (...event: any[]) => void;
value: any;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { variablesForm, histories, chatId, outLinkAuthData } = useContextSelector( const { variablesForm, histories, chatId, outLinkAuthData } = useContextSelector(
@ -56,7 +49,8 @@ const FileSelector = ({
uploadFiles, uploadFiles,
onOpenSelectFile, onOpenSelectFile,
onSelectFile, onSelectFile,
removeFiles removeFiles,
replaceFiles
} = useFileUpload({ } = useFileUpload({
outLinkAuthData, outLinkAuthData,
chatId: chatId || '', chatId: chatId || '',
@ -68,6 +62,22 @@ const FileSelector = ({
// @ts-ignore // @ts-ignore
fileCtrl 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; const isDisabledInput = histories.length > 0;
useRequest2(uploadFiles, { useRequest2(uploadFiles, {
manual: false, manual: false,
@ -151,7 +161,9 @@ const RenderPluginInput = ({
); );
} }
if (inputType === FlowNodeInputTypeEnum.fileSelect) { if (inputType === FlowNodeInputTypeEnum.fileSelect) {
return <FileSelector onChange={onChange} input={input} setUploading={setUploading} />; return (
<FileSelector onChange={onChange} input={input} setUploading={setUploading} value={value} />
);
} }
if (input.valueType === WorkflowIOValueTypeEnum.string) { if (input.valueType === WorkflowIOValueTypeEnum.string) {
@ -169,20 +181,17 @@ const RenderPluginInput = ({
} }
if (input.valueType === WorkflowIOValueTypeEnum.number) { if (input.valueType === WorkflowIOValueTypeEnum.number) {
return ( return (
<NumberInput <MyNumberInput
step={1} step={1}
min={input.min} min={input.min}
max={input.max} max={input.max}
bg={'myGray.50'} bg={'myGray.50'}
isDisabled={isDisabled} isDisabled={isDisabled}
isInvalid={isInvalid} isInvalid={isInvalid}
> value={value}
<NumberInputField value={value} onChange={onChange} defaultValue={input.defaultValue} /> onChange={onChange}
<NumberInputStepper> defaultValue={input.defaultValue}
<NumberIncrementStepper /> />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
); );
} }
if (input.valueType === WorkflowIOValueTypeEnum.boolean) { if (input.valueType === WorkflowIOValueTypeEnum.boolean) {

View File

@ -15,15 +15,26 @@ import RenderInput from '../render/RenderInput';
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import RenderOutput from '../render/RenderOutput'; 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 { Input_Template_Children_Node_List } from '@fastgpt/global/core/workflow/template/input';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../../context'; import { WorkflowContext } from '../../../context';
import { cloneDeep } from 'lodash';
import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils';
import { AppContext } from '../../../../context';
const NodeLoop = ({ data, selected }: NodeProps<FlowNodeItemType>) => { const NodeLoop = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { nodeId, inputs, outputs, isFolded } = data; const { nodeId, inputs, outputs, isFolded } = data;
const { onChangeNode, nodeList } = useContextSelector(WorkflowContext, (v) => v); 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(() => { const { nodeWidth, nodeHeight } = useMemo(() => {
return { return {
@ -37,6 +48,42 @@ const NodeLoop = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
); );
}, [nodeId, nodeList]); }, [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(() => { useEffect(() => {
onChangeNode({ onChangeNode({
nodeId, nodeId,
@ -47,7 +94,7 @@ const NodeLoop = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
value: JSON.parse(childrenNodeIdList) value: JSON.parse(childrenNodeIdList)
} }
}); });
}, [childrenNodeIdList]); }, [childrenNodeIdList, nodeId, onChangeNode]);
const Render = useMemo(() => { const Render = useMemo(() => {
return ( return (
@ -80,7 +127,7 @@ const NodeLoop = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
</Container> </Container>
</NodeCard> </NodeCard>
); );
}, [selected, nodeWidth, nodeHeight, data, t, nodeId, inputs, outputs]); }, [selected, isFolded, nodeWidth, nodeHeight, data, t, nodeId, inputs, outputs]);
return Render; return Render;
}; };

View File

@ -19,8 +19,7 @@ const typeMap = {
[WorkflowIOValueTypeEnum.arrayString]: WorkflowIOValueTypeEnum.string, [WorkflowIOValueTypeEnum.arrayString]: WorkflowIOValueTypeEnum.string,
[WorkflowIOValueTypeEnum.arrayNumber]: WorkflowIOValueTypeEnum.number, [WorkflowIOValueTypeEnum.arrayNumber]: WorkflowIOValueTypeEnum.number,
[WorkflowIOValueTypeEnum.arrayBoolean]: WorkflowIOValueTypeEnum.boolean, [WorkflowIOValueTypeEnum.arrayBoolean]: WorkflowIOValueTypeEnum.boolean,
[WorkflowIOValueTypeEnum.arrayObject]: WorkflowIOValueTypeEnum.object, [WorkflowIOValueTypeEnum.arrayObject]: WorkflowIOValueTypeEnum.object
[WorkflowIOValueTypeEnum.arrayAny]: WorkflowIOValueTypeEnum.any
}; };
const NodeLoopStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => { const NodeLoopStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
@ -39,12 +38,7 @@ const NodeLoopStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const parentArrayInput = parentNode?.inputs.find( const parentArrayInput = parentNode?.inputs.find(
(input) => input.key === NodeInputKeyEnum.loopInputArray (input) => input.key === NodeInputKeyEnum.loopInputArray
); );
return parentArrayInput?.value return typeMap[parentArrayInput?.valueType as keyof typeof typeMap];
? (nodeList
.find((node) => node.nodeId === parentArrayInput?.value[0])
?.outputs.find((output) => output.id === parentArrayInput?.value[1])
?.valueType as keyof typeof typeMap)
: undefined;
}, [loopStartNode?.parentNodeId, nodeList]); }, [loopStartNode?.parentNodeId, nodeList]);
// Auth update loopStartInput output // Auth update loopStartInput output
@ -71,7 +65,7 @@ const NodeLoopStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
key: NodeOutputKeyEnum.loopStartInput, key: NodeOutputKeyEnum.loopStartInput,
label: t('workflow:Array_element'), label: t('workflow:Array_element'),
type: FlowNodeOutputTypeEnum.static, type: FlowNodeOutputTypeEnum.static,
valueType: typeMap[loopItemInputType as keyof typeof typeMap] valueType: loopItemInputType
} }
}); });
} }
@ -83,7 +77,7 @@ const NodeLoopStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
key: NodeOutputKeyEnum.loopStartInput, key: NodeOutputKeyEnum.loopStartInput,
value: { value: {
...loopArrayOutput, ...loopArrayOutput,
valueType: typeMap[loopItemInputType as keyof typeof typeMap] valueType: loopItemInputType
} }
}); });
} }
@ -128,7 +122,7 @@ const NodeLoopStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
{t('workflow:Array_element')} {t('workflow:Array_element')}
</Flex> </Flex>
</Td> </Td>
<Td>{typeMap[loopItemInputType]}</Td> <Td>{loopItemInputType}</Td>
</Tr> </Tr>
</Tbody> </Tbody>
</Table> </Table>

View File

@ -48,7 +48,7 @@ const NodeDatasetConcat = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
}); });
const onSelect = useCallback( const onSelect = useCallback(
(e: ReferenceValueProps) => { (e: ReferenceValueProps | ReferenceValueProps[]) => {
const workflowStartNode = nodeList.find( const workflowStartNode = nodeList.find(
(node) => node.flowNodeType === FlowNodeTypeEnum.workflowStart (node) => node.flowNodeType === FlowNodeTypeEnum.workflowStart
); );
@ -60,7 +60,7 @@ const NodeDatasetConcat = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
value: { value: {
...inputChildren, ...inputChildren,
value: value:
e[0] === workflowStartNode?.id && !isWorkflowStartOutput(e[1]) e[0] === workflowStartNode?.id && !isWorkflowStartOutput(e[1] as string)
? [VARIABLE_NODE_ID, e[1]] ? [VARIABLE_NODE_ID, e[1]]
: e : e
} }

View File

@ -324,7 +324,7 @@ const Reference = ({
placeholder={t('common:select_reference_variable')} placeholder={t('common:select_reference_variable')}
list={referenceList} list={referenceList}
value={formatValue} value={formatValue}
onSelect={onSelect} onSelect={onSelect as any}
/> />
); );
}; };

View File

@ -118,13 +118,13 @@ function Reference({
const [editField, setEditField] = useState<FlowNodeInputItemType>(); const [editField, setEditField] = useState<FlowNodeInputItemType>();
const onSelect = useCallback( const onSelect = useCallback(
(e: ReferenceValueProps) => { (e: ReferenceValueProps | ReferenceValueProps[]) => {
const workflowStartNode = nodeList.find( const workflowStartNode = nodeList.find(
(node) => node.flowNodeType === FlowNodeTypeEnum.workflowStart (node) => node.flowNodeType === FlowNodeTypeEnum.workflowStart
); );
const value = const value =
e[0] === workflowStartNode?.id && !isWorkflowStartOutput(e[1]) e[0] === workflowStartNode?.id && !isWorkflowStartOutput(e[1] as string)
? [VARIABLE_NODE_ID, e[1]] ? [VARIABLE_NODE_ID, e[1]]
: e; : e;
@ -219,6 +219,7 @@ function Reference({
list={referenceList} list={referenceList}
value={formatValue} value={formatValue}
onSelect={onSelect} onSelect={onSelect}
isArray={input.valueType?.includes('array')}
/> />
{!!editField && ( {!!editField && (

View File

@ -326,7 +326,7 @@ const Reference = ({
placeholder={t('common:select_reference_variable')} placeholder={t('common:select_reference_variable')}
list={referenceList} list={referenceList}
value={formatValue} value={formatValue}
onSelect={onSelect} onSelect={onSelect as any}
/> />
); );
}; };

View File

@ -365,7 +365,7 @@ const NodeCard = (props: Props) => {
> >
<NodeDebugResponse nodeId={nodeId} debugResult={debugResult} /> <NodeDebugResponse nodeId={nodeId} debugResult={debugResult} />
{Header} {Header}
<Flex flexDirection={'column'} flex={1} my={4} gap={2}> <Flex flexDirection={'column'} flex={1} my={!isFolded ? 4 : 0} gap={2}>
{!isFolded ? children : <Box h={4} />} {!isFolded ? children : <Box h={4} />}
</Flex> </Flex>
{RenderHandle} {RenderHandle}

View File

@ -12,7 +12,7 @@ import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponent
import { defaultInput } from '../../FieldEditModal'; import { defaultInput } from '../../FieldEditModal';
import { getInputComponentProps } from '@fastgpt/global/core/workflow/node/io/utils'; import { getInputComponentProps } from '@fastgpt/global/core/workflow/node/io/utils';
import { VARIABLE_NODE_ID } from '@fastgpt/global/core/workflow/constants'; 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 FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import ValueTypeLabel from '../../../ValueTypeLabel'; import ValueTypeLabel from '../../../ValueTypeLabel';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
@ -126,13 +126,13 @@ function Reference({
const [editField, setEditField] = useState<FlowNodeInputItemType>(); const [editField, setEditField] = useState<FlowNodeInputItemType>();
const onSelect = useCallback( const onSelect = useCallback(
(e: ReferenceValueProps) => { (e: ReferenceValueProps | ReferenceValueProps[]) => {
const workflowStartNode = nodeList.find( const workflowStartNode = nodeList.find(
(node) => node.flowNodeType === FlowNodeTypeEnum.workflowStart (node) => node.flowNodeType === FlowNodeTypeEnum.workflowStart
); );
const newValue = const newValue =
e[0] === workflowStartNode?.id && !isWorkflowStartOutput(e[1]) e[0] === workflowStartNode?.id && !isWorkflowStartOutput(e[1] as string)
? [VARIABLE_NODE_ID, e[1]] ? [VARIABLE_NODE_ID, e[1]]
: e; : e;
@ -155,18 +155,42 @@ function Reference({
value: inputChildren.value 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( const onUpdateField = useCallback(
({ data }: { data: FlowNodeInputItemType }) => { ({ data }: { data: FlowNodeInputItemType }) => {
if (!data.key) return; if (!data.key) return;
const updatedValue = getValueTypeChange(data, inputChildren.valueType);
onChangeNode({ onChangeNode({
nodeId, nodeId,
type: 'replaceInput', type: 'replaceInput',
key: inputChildren.key, key: inputChildren.key,
value: data value: {
...data,
value: updatedValue
}
}); });
}, },
[inputChildren.key, nodeId, onChangeNode] [inputChildren, nodeId, onChangeNode, getValueTypeChange]
); );
const onDel = useCallback(() => { const onDel = useCallback(() => {
onChangeNode({ onChangeNode({
@ -212,6 +236,7 @@ function Reference({
list={referenceList} list={referenceList}
value={formatValue} value={formatValue}
onSelect={onSelect} onSelect={onSelect}
isArray={inputChildren.valueType?.includes('array')}
/> />
{!!editField && !!item.customInputConfig && ( {!!editField && !!item.customInputConfig && (

View File

@ -22,7 +22,7 @@ const MultipleRowSelect = dynamic(
const Avatar = dynamic(() => import('@fastgpt/web/components/common/Avatar')); const Avatar = dynamic(() => import('@fastgpt/web/components/common/Avatar'));
type SelectProps = { type SelectProps = {
value?: ReferenceValueProps; value?: ReferenceValueProps[];
placeholder?: string; placeholder?: string;
list: { list: {
label: string | React.ReactNode; label: string | React.ReactNode;
@ -33,18 +33,25 @@ type SelectProps = {
valueType?: WorkflowIOValueTypeEnum; valueType?: WorkflowIOValueTypeEnum;
}[]; }[];
}[]; }[];
onSelect: (val: ReferenceValueProps) => void; onSelect: (val: ReferenceValueProps | ReferenceValueProps[]) => void;
popDirection?: 'top' | 'bottom'; popDirection?: 'top' | 'bottom';
styles?: ButtonProps; 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 Reference = ({ item, nodeId }: RenderInputProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode); const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const onSelect = useCallback( const onSelect = useCallback(
(e: ReferenceValueProps) => { (e: ReferenceValueProps | ReferenceValueProps[]) => {
const workflowStartNode = nodeList.find( const workflowStartNode = nodeList.find(
(node) => node.flowNodeType === FlowNodeTypeEnum.workflowStart (node) => node.flowNodeType === FlowNodeTypeEnum.workflowStart
); );
@ -92,6 +99,7 @@ const Reference = ({ item, nodeId }: RenderInputProps) => {
value={formatValue} value={formatValue}
onSelect={onSelect} onSelect={onSelect}
popDirection={popDirection} popDirection={popDirection}
isArray={item.valueType?.includes('array')}
/> />
); );
}; };
@ -111,6 +119,8 @@ export const useReference = ({
const { appDetail } = useContextSelector(AppContext, (v) => v); const { appDetail } = useContextSelector(AppContext, (v) => v);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const edges = useContextSelector(WorkflowContext, (v) => v.edges); const edges = useContextSelector(WorkflowContext, (v) => v.edges);
const isArray = valueType?.includes('array');
const currentType = isArray ? valueType.replace('array', '').toLowerCase() : valueType;
const referenceList = useMemo(() => { const referenceList = useMemo(() => {
const sourceNodes = computedNodeInputReference({ const sourceNodes = computedNodeInputReference({
@ -129,8 +139,8 @@ export const useReference = ({
return { return {
label: ( label: (
<Flex alignItems={'center'}> <Flex alignItems={'center'}>
<Avatar src={node.avatar} w={'1.25rem'} borderRadius={'xs'} /> <Avatar src={node.avatar} w={isArray ? '1rem' : '1.25rem'} borderRadius={'xs'} />
<Box ml={2}>{t(node.name as any)}</Box> <Box ml={1}>{t(node.name as any)}</Box>
</Flex> </Flex>
), ),
value: node.nodeId, value: node.nodeId,
@ -139,10 +149,20 @@ export const useReference = ({
(output) => (output) =>
valueType === WorkflowIOValueTypeEnum.any || valueType === WorkflowIOValueTypeEnum.any ||
output.valueType === WorkflowIOValueTypeEnum.any || output.valueType === WorkflowIOValueTypeEnum.any ||
currentType === output.valueType ||
// array
output.valueType === valueType || output.valueType === valueType ||
// When valueType is arrayAny, return all array type outputs
(valueType === WorkflowIOValueTypeEnum.arrayAny && (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) .filter((output) => output.id !== NodeOutputKeyEnum.addOutputParam)
.map((output) => { .map((output) => {
@ -157,16 +177,14 @@ export const useReference = ({
.filter((item) => item.children.length > 0); .filter((item) => item.children.length > 0);
return list; return list;
}, [appDetail.chatConfig, edges, nodeId, nodeList, t, valueType]); }, [appDetail.chatConfig, currentType, edges, isArray, nodeId, nodeList, t, valueType]);
const formatValue = useMemo(() => { const formatValue = useMemo(() => {
if ( // convert origin reference [variableId, outputId] to new reference [[variableId, outputId], ...]
Array.isArray(value) && if (isReference(value)) {
value.length === 2 && return [value] as ReferenceValueProps[];
typeof value[0] === 'string' && } else if (Array.isArray(value) && value.every((item) => isReference(item))) {
typeof value[1] === 'string' return value as ReferenceValueProps[];
) {
return value as ReferenceValueProps;
} }
return undefined; return undefined;
}, [value]); }, [value]);
@ -176,40 +194,115 @@ export const useReference = ({
formatValue formatValue
}; };
}; };
export const ReferSelector = ({
const ReferSelectorComponent = ({
placeholder, placeholder,
value, value,
list = [], list = [],
onSelect, onSelect,
popDirection popDirection,
isArray
}: SelectProps) => { }: SelectProps) => {
const selectItemLabel = useMemo(() => { const { t } = useTranslation();
if (!value) {
const selectValue = useMemo(() => {
if (!value || value.every((item) => !item || item.every((subItem) => !subItem))) {
return; return;
} }
const firstColumn = list.find((item) => item.value === value[0]); return value.map((valueItem) => {
const firstColumn = list.find((item) => item.value === valueItem[0]);
if (!firstColumn) { if (!firstColumn) {
return; return;
} }
const secondColumn = firstColumn.children.find((item) => item.value === value[1]); const secondColumn = firstColumn.children.find((item) => item.value === valueItem[1]);
if (!secondColumn) { if (!secondColumn) {
return; return;
} }
return [firstColumn, secondColumn]; return [firstColumn, secondColumn];
});
}, [list, value]); }, [list, value]);
const Render = useMemo(() => { const Render = useMemo(() => {
return ( return (
<MultipleRowSelect <MultipleRowSelect
label={ label={
selectItemLabel ? ( selectValue && selectValue.length > 0 ? (
<Flex alignItems={'center'}> <Flex
{selectItemLabel[0].label} gap={2}
<MyIcon name={'core/chat/chevronRight'} mx={1} w={4} /> flexWrap={isArray ? 'wrap' : undefined}
{selectItemLabel[1].label} alignItems={'center'}
fontSize={'14px'}
>
{isArray ? (
// [[variableId, outputId], ...]
selectValue.map((item, index) => {
const isInvalidItem = item === undefined;
return (
<Flex
alignItems={'center'}
key={index}
bg={isInvalidItem ? 'red.50' : 'primary.50'}
color={isInvalidItem ? 'red.600' : 'myGray.900'}
py={1}
px={1.5}
rounded={'sm'}
>
{isInvalidItem ? (
t('common:invalid_variable')
) : (
<>
{item?.[0].label}
<MyIcon
name={'common/rightArrowLight'}
mx={1}
w={'12px'}
color={'myGray.500'}
/>
{item?.[1].label}
</>
)}
<MyIcon
name={'common/closeLight'}
w={'16px'}
ml={1}
cursor={'pointer'}
color={'myGray.500'}
_hover={{
color: 'primary.600'
}}
onClick={(e) => {
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);
}}
/>
</Flex>
);
})
) : // [variableId, outputId]
selectValue[0] ? (
<Flex py={1} pl={1}>
{selectValue[0][0].label}
<MyIcon name={'common/rightArrowLight'} mx={1} w={'12px'} color={'myGray.500'} />
{selectValue[0][1].label}
</Flex> </Flex>
) : ( ) : (
<Box>{placeholder}</Box> <Box pl={2} py={1} fontSize={'14px'}>
{placeholder}
</Box>
)}
</Flex>
) : (
<Box pl={2} py={1} fontSize={'14px'}>
{placeholder}
</Box>
) )
} }
value={value as any[]} value={value as any[]}
@ -218,9 +311,14 @@ export const ReferSelector = ({
onSelect(e as ReferenceValueProps); onSelect(e as ReferenceValueProps);
}} }}
popDirection={popDirection} popDirection={popDirection}
isArray={isArray}
/> />
); );
}, [list, onSelect, placeholder, popDirection, selectItemLabel, value]); }, [isArray, list, onSelect, placeholder, popDirection, selectValue, t, value]);
return Render; return Render;
}; };
ReferSelectorComponent.displayName = 'ReferSelector';
export const ReferSelector = React.memo(ReferSelectorComponent);

View File

@ -327,24 +327,37 @@ export const checkWorkflowNodeAndConnection = ({
// check reference invalid // check reference invalid
const renderType = input.renderTypeList[input.selectedTypeIndex || 0]; const renderType = input.renderTypeList[input.selectedTypeIndex || 0];
if (renderType === FlowNodeInputTypeEnum.reference && input.required) { if (renderType === FlowNodeInputTypeEnum.reference && input.required) {
if (!input.value || !Array.isArray(input.value) || input.value.length !== 2) { const checkReference = (value: [string, string]) => {
return true; if (value[0] === VARIABLE_NODE_ID) {
}
// variable key not need to check
if (input.value[0] === VARIABLE_NODE_ID) {
return false; return false;
} }
// Can not find key const sourceNode = nodes.find((item) => item.data.nodeId === value[0]);
const sourceNode = nodes.find((item) => item.data.nodeId === input.value[0]);
if (!sourceNode) { if (!sourceNode) {
return true; return true;
} }
const sourceOutput = sourceNode.data.outputs.find((item) => item.id === input.value[1]);
if (!sourceOutput) { 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]);
}
// New format
return input.value.some((inputItem: ReferenceValueProps) => {
if (!Array.isArray(inputItem) || inputItem.length !== 2) {
return true; return true;
} }
return checkReference(inputItem as [string, string]);
});
} }
return false; return false;
}) })