mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-23 13:03:50 +00:00
4.8.12 dev (#2928)
* perf: optimize global variables (#2863) * feat: add global variable types * add global variables to debug * fix select dnd * unify InputTypeConfig params * feat: http node url support variables (#2891) * feat: http node url support variables * change to prompt editor * fix: global variables (#2892) * fix global variables * fix type * perf: global variables * perf: workflow delete node error (#2905) * update lock * update 4812 doc * feat: add node course url config (#2897) * feat: add node course url config * change plugin course url * change default doc url * change url store * delete unused code * fix: global variable (#2915) * fix: global variable * add comment * fix: interactive check * locj * perf: debug switch to global tab when click run & global var default reset (#2925) * fix: tool course url * fix: global var default value & wrap variable form (#2926) * fix: add dataset tags not update render (#2927) * feat: tool will save histories * perf: global variables code * perf: FE_DOMAIN config --------- Co-authored-by: heheer <heheer@sealos.io>
This commit is contained in:
@@ -2,17 +2,7 @@ import React, { useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
ModalFooter,
|
||||
ModalBody,
|
||||
NumberInput,
|
||||
NumberInputField,
|
||||
NumberInputStepper,
|
||||
NumberIncrementStepper,
|
||||
NumberDecrementStepper,
|
||||
Flex,
|
||||
Switch,
|
||||
Input,
|
||||
FormControl,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
@@ -20,7 +10,7 @@ import {
|
||||
Th,
|
||||
Td,
|
||||
TableContainer,
|
||||
useDisclosure
|
||||
Stack
|
||||
} from '@chakra-ui/react';
|
||||
import { SmallAddIcon } from '@chakra-ui/icons';
|
||||
import {
|
||||
@@ -31,38 +21,34 @@ import {
|
||||
import type { VariableItemType } from '@fastgpt/global/core/app/type.d';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useFieldArray } from 'react-hook-form';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import MyRadio from '@/components/common/MyRadio';
|
||||
import { formatEditorVariablePickerIcon } from '@fastgpt/global/core/workflow/utils';
|
||||
import ChatFunctionTip from './Tip';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
import { FlowValueTypeMap } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import InputTypeConfig from '@/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodePluginIO/InputTypeConfig';
|
||||
|
||||
export const defaultVariable: VariableItemType = {
|
||||
id: nanoid(),
|
||||
key: 'key',
|
||||
label: 'label',
|
||||
key: '',
|
||||
label: '',
|
||||
type: VariableInputEnum.input,
|
||||
description: '',
|
||||
required: true,
|
||||
maxLen: 50,
|
||||
enums: [{ value: '' }],
|
||||
valueType: WorkflowIOValueTypeEnum.string
|
||||
};
|
||||
export const addVariable = () => {
|
||||
const newVariable = { ...defaultVariable, key: '', id: '' };
|
||||
return newVariable;
|
||||
|
||||
type InputItemType = VariableItemType & {
|
||||
list: { label: string; value: string }[];
|
||||
};
|
||||
const valueTypeMap = {
|
||||
[VariableInputEnum.input]: WorkflowIOValueTypeEnum.string,
|
||||
[VariableInputEnum.select]: WorkflowIOValueTypeEnum.string,
|
||||
[VariableInputEnum.textarea]: WorkflowIOValueTypeEnum.string,
|
||||
[VariableInputEnum.custom]: WorkflowIOValueTypeEnum.any
|
||||
|
||||
export const addVariable = () => {
|
||||
const newVariable = { ...defaultVariable, key: '', id: '', list: [{ value: '', label: '' }] };
|
||||
return newVariable;
|
||||
};
|
||||
|
||||
const VariableEdit = ({
|
||||
@@ -74,46 +60,37 @@ const VariableEdit = ({
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
|
||||
const VariableTypeList = useMemo(
|
||||
const form = useForm<VariableItemType>();
|
||||
const { setValue, reset, watch, getValues } = form;
|
||||
const value = getValues();
|
||||
const type = watch('type');
|
||||
const valueType = watch('valueType');
|
||||
const max = watch('max');
|
||||
const min = watch('min');
|
||||
const defaultValue = watch('defaultValue');
|
||||
|
||||
const inputTypeList = useMemo(
|
||||
() =>
|
||||
Object.entries(variableMap).map(([key, value]) => ({
|
||||
title: t(value.title as any),
|
||||
icon: value.icon,
|
||||
value: key
|
||||
Object.values(variableMap).map((item) => ({
|
||||
icon: item.icon,
|
||||
label: t(item.label as any),
|
||||
value: item.value,
|
||||
defaultValueType: item.defaultValueType,
|
||||
description: item.description ? t(item.description as any) : ''
|
||||
})),
|
||||
[t]
|
||||
);
|
||||
|
||||
const { isOpen: isOpenEdit, onOpen: onOpenEdit, onClose: onCloseEdit } = useDisclosure();
|
||||
const {
|
||||
setValue,
|
||||
reset: resetEdit,
|
||||
register: registerEdit,
|
||||
getValues: getValuesEdit,
|
||||
setValue: setValuesEdit,
|
||||
control: editVariableController,
|
||||
handleSubmit: handleSubmitEdit,
|
||||
watch
|
||||
} = useForm<{ variable: VariableItemType }>();
|
||||
|
||||
const variableType = watch('variable.type');
|
||||
const valueType = watch('variable.valueType');
|
||||
|
||||
const {
|
||||
fields: selectEnums,
|
||||
append: appendEnums,
|
||||
remove: removeEnums
|
||||
} = useFieldArray({
|
||||
control: editVariableController,
|
||||
name: 'variable.enums'
|
||||
});
|
||||
const defaultValueType = useMemo(() => {
|
||||
const item = inputTypeList.find((item) => item.value === type);
|
||||
return item?.defaultValueType;
|
||||
}, [inputTypeList, type]);
|
||||
|
||||
const formatVariables = useMemo(() => {
|
||||
const results = formatEditorVariablePickerIcon(variables);
|
||||
return results.map((item) => {
|
||||
const variable = variables.find((variable) => variable.key === item.key);
|
||||
return results.map<VariableItemType & { icon?: string }>((item) => {
|
||||
const variable = variables.find((variable) => variable.key === item.key)!;
|
||||
return {
|
||||
...variable,
|
||||
icon: item.icon
|
||||
@@ -121,45 +98,12 @@ const VariableEdit = ({
|
||||
});
|
||||
}, [variables]);
|
||||
|
||||
const valueTypeSelectList = useMemo(
|
||||
() =>
|
||||
Object.values(FlowValueTypeMap)
|
||||
.map((item) => ({
|
||||
label: t(item.label as any),
|
||||
value: item.value
|
||||
}))
|
||||
.filter(
|
||||
(item) =>
|
||||
![
|
||||
WorkflowIOValueTypeEnum.arrayAny,
|
||||
WorkflowIOValueTypeEnum.selectApp,
|
||||
WorkflowIOValueTypeEnum.selectDataset,
|
||||
WorkflowIOValueTypeEnum.dynamic
|
||||
].includes(item.value)
|
||||
),
|
||||
[t]
|
||||
);
|
||||
const showValueTypeSelect = variableType === VariableInputEnum.custom;
|
||||
const onSubmitSuccess = useCallback(
|
||||
(data: InputItemType, action: 'confirm' | 'continue') => {
|
||||
data.label = data?.label?.trim();
|
||||
|
||||
const onSubmit = useCallback(
|
||||
({ variable }: { variable: VariableItemType }) => {
|
||||
variable.key = variable.key.trim();
|
||||
|
||||
// check select
|
||||
if (variable.type === VariableInputEnum.select) {
|
||||
const enums = variable.enums.filter((item) => item.value);
|
||||
if (enums.length === 0) {
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: t('common:core.module.variable.variable option is required')
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// check repeat key
|
||||
const existingVariable = variables.find(
|
||||
(item) => item.key === variable.key && item.id !== variable.id
|
||||
(item) => item.label === data.label && item.id !== data.id
|
||||
);
|
||||
if (existingVariable) {
|
||||
toast({
|
||||
@@ -169,32 +113,59 @@ const VariableEdit = ({
|
||||
return;
|
||||
}
|
||||
|
||||
// set valuetype based on variable.type
|
||||
variable.valueType =
|
||||
variable.type === VariableInputEnum.custom
|
||||
? variable.valueType
|
||||
: valueTypeMap[variable.type];
|
||||
data.key = data.label;
|
||||
data.enums = data.list;
|
||||
|
||||
// set default required value based on variableType
|
||||
if (variable.type === VariableInputEnum.custom) {
|
||||
variable.required = false;
|
||||
if (data.type === VariableInputEnum.custom) {
|
||||
data.required = false;
|
||||
}
|
||||
|
||||
if (data.type === VariableInputEnum.numberInput) {
|
||||
data.valueType = WorkflowIOValueTypeEnum.number;
|
||||
}
|
||||
|
||||
const onChangeVariable = [...variables];
|
||||
// update
|
||||
if (variable.id) {
|
||||
const index = variables.findIndex((item) => item.id === variable.id);
|
||||
onChangeVariable[index] = variable;
|
||||
if (data.id) {
|
||||
const index = variables.findIndex((item) => item.id === data.id);
|
||||
onChangeVariable[index] = data;
|
||||
} else {
|
||||
onChangeVariable.push({
|
||||
...variable,
|
||||
...data,
|
||||
id: nanoid()
|
||||
});
|
||||
}
|
||||
onChange(onChangeVariable);
|
||||
onCloseEdit();
|
||||
|
||||
if (action === 'confirm') {
|
||||
onChange(onChangeVariable);
|
||||
reset({});
|
||||
} else if (action === 'continue') {
|
||||
onChange(onChangeVariable);
|
||||
toast({
|
||||
status: 'success',
|
||||
title: t('common:common.Add Success')
|
||||
});
|
||||
reset({
|
||||
...addVariable(),
|
||||
defaultValue: ''
|
||||
});
|
||||
}
|
||||
},
|
||||
[onChange, onCloseEdit, t, toast, variables]
|
||||
[variables, toast, t, onChange, reset]
|
||||
);
|
||||
|
||||
const onSubmitError = useCallback(
|
||||
(e: Object) => {
|
||||
for (const item of Object.values(e)) {
|
||||
if (item.message) {
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: item.message
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
[toast]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -212,8 +183,7 @@ const VariableEdit = ({
|
||||
size={'sm'}
|
||||
mr={'-5px'}
|
||||
onClick={() => {
|
||||
resetEdit({ variable: addVariable() });
|
||||
onOpenEdit();
|
||||
reset(addVariable());
|
||||
}}
|
||||
>
|
||||
{t('common:common.Add New')}
|
||||
@@ -232,7 +202,7 @@ const VariableEdit = ({
|
||||
w={'18px !important'}
|
||||
p={0}
|
||||
/>
|
||||
<Th fontSize={'mini'}>{t('common:core.module.variable.variable name')}</Th>
|
||||
<Th fontSize={'mini'}>{t('workflow:Variable_name')}</Th>
|
||||
<Th fontSize={'mini'}>{t('common:core.module.variable.key')}</Th>
|
||||
<Th fontSize={'mini'}>{t('common:common.Require Input')}</Th>
|
||||
<Th fontSize={'mini'} borderRadius={'none !important'}></Th>
|
||||
@@ -241,8 +211,8 @@ const VariableEdit = ({
|
||||
<Tbody>
|
||||
{formatVariables.map((item) => (
|
||||
<Tr key={item.id}>
|
||||
<Td textAlign={'center'} p={0} pl={3}>
|
||||
<MyIcon name={item.icon as any} w={'14px'} color={'myGray.500'} />
|
||||
<Td p={0} pl={3}>
|
||||
<MyIcon name={item.icon as any} w={'16px'} color={'myGray.500'} />
|
||||
</Td>
|
||||
<Td>{item.label}</Td>
|
||||
<Td>{item.key}</Td>
|
||||
@@ -254,8 +224,11 @@ const VariableEdit = ({
|
||||
w={'16px'}
|
||||
cursor={'pointer'}
|
||||
onClick={() => {
|
||||
resetEdit({ variable: item });
|
||||
onOpenEdit();
|
||||
const formattedItem = {
|
||||
...item,
|
||||
list: item.enums || []
|
||||
};
|
||||
reset(formattedItem);
|
||||
}}
|
||||
/>
|
||||
<MyIcon
|
||||
@@ -274,160 +247,100 @@ const VariableEdit = ({
|
||||
</TableContainer>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Edit modal */}
|
||||
<MyModal
|
||||
iconSrc="core/app/simpleMode/variable"
|
||||
title={t('common:core.module.Variable Setting')}
|
||||
isOpen={isOpenEdit}
|
||||
onClose={onCloseEdit}
|
||||
maxW={['90vw', '500px']}
|
||||
>
|
||||
<ModalBody>
|
||||
{variableType !== VariableInputEnum.custom && (
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel w={'70px'}>{t('common:common.Require Input')}</FormLabel>
|
||||
<Switch {...registerEdit('variable.required')} />
|
||||
</Flex>
|
||||
)}
|
||||
<Flex mt={5} alignItems={'center'}>
|
||||
<FormLabel w={'80px'}>{t('common:core.module.variable.variable name')}</FormLabel>
|
||||
<Input
|
||||
{...registerEdit('variable.label', {
|
||||
required: t('common:core.module.variable.variable name is required')
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex mt={5} alignItems={'center'}>
|
||||
<FormLabel w={'80px'}>{t('common:core.module.variable.key')}</FormLabel>
|
||||
<Input
|
||||
{...registerEdit('variable.key', {
|
||||
required: t('common:core.module.variable.key is required')
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex mt={5} alignItems={'center'}>
|
||||
<FormLabel w={'80px'}>{t('workflow:value_type')}</FormLabel>
|
||||
{showValueTypeSelect ? (
|
||||
<Box flex={1}>
|
||||
<MySelect<WorkflowIOValueTypeEnum>
|
||||
list={valueTypeSelectList.filter(
|
||||
(item) => item.value !== WorkflowIOValueTypeEnum.arrayAny
|
||||
)}
|
||||
value={valueType}
|
||||
onchange={(e) => {
|
||||
setValue('variable.valueType', e);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<Box fontSize={'14px'}>{valueTypeMap[variableType]}</Box>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<FormLabel mt={5} mb={2}>
|
||||
{t('common:core.workflow.Variable.Variable type')}
|
||||
</FormLabel>
|
||||
<MyRadio
|
||||
gridGap={4}
|
||||
gridTemplateColumns={'repeat(2,1fr)'}
|
||||
value={variableType}
|
||||
list={VariableTypeList}
|
||||
color={'myGray.600'}
|
||||
hiddenCircle
|
||||
onChange={(e) => {
|
||||
setValuesEdit('variable.type', e as any);
|
||||
setRefresh(!refresh);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* desc */}
|
||||
{variableMap[variableType]?.desc && (
|
||||
<Box mt={2} fontSize={'sm'} color={'myGray.500'} whiteSpace={'pre-wrap'}>
|
||||
{t(variableMap[variableType].desc as any)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{variableType === VariableInputEnum.input && (
|
||||
<>
|
||||
<FormLabel mt={5} mb={2}>
|
||||
{t('common:core.module.variable.text max length')}
|
||||
{!!Object.keys(value).length && (
|
||||
<MyModal
|
||||
iconSrc="core/app/simpleMode/variable"
|
||||
title={t('common:core.module.Variable Setting')}
|
||||
isOpen={true}
|
||||
onClose={() => reset({})}
|
||||
maxW={['90vw', '928px']}
|
||||
w={'100%'}
|
||||
isCentered
|
||||
>
|
||||
<Flex h={'560px'}>
|
||||
<Stack gap={4} p={8}>
|
||||
<FormLabel color={'myGray.600'} fontWeight={'medium'}>
|
||||
{t('workflow:Variable.Variable type')}
|
||||
</FormLabel>
|
||||
<Box>
|
||||
<NumberInput max={500} min={1} step={1} position={'relative'}>
|
||||
<NumberInputField
|
||||
{...registerEdit('variable.maxLen', {
|
||||
min: 1,
|
||||
max: 500,
|
||||
valueAsNumber: true
|
||||
})}
|
||||
max={500}
|
||||
/>
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberInputStepper>
|
||||
</NumberInput>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
{variableType === VariableInputEnum.select && (
|
||||
<>
|
||||
<Box mt={5} mb={2}>
|
||||
{t('common:core.module.variable.variable options')}
|
||||
</Box>
|
||||
<Box>
|
||||
{selectEnums.map((item, i) => (
|
||||
<Flex key={item.id} mb={2} alignItems={'center'}>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...registerEdit(`variable.enums.${i}.value`, {
|
||||
required: t(
|
||||
'common:core.module.variable.variable option is value is required'
|
||||
)
|
||||
})}
|
||||
/>
|
||||
</FormControl>
|
||||
{selectEnums.length > 1 && (
|
||||
<Flex flexDirection={'column'} gap={4}></Flex>
|
||||
<Box display={'grid'} gridTemplateColumns={'repeat(2, 1fr)'} gap={4}>
|
||||
{inputTypeList.map((item) => {
|
||||
const isSelected = type === item.value;
|
||||
return (
|
||||
<Box
|
||||
display={'flex'}
|
||||
key={item.label}
|
||||
border={isSelected ? '1px solid #3370FF' : '1px solid #DFE2EA'}
|
||||
p={3}
|
||||
rounded={'6px'}
|
||||
fontWeight={'medium'}
|
||||
fontSize={'14px'}
|
||||
alignItems={'center'}
|
||||
cursor={'pointer'}
|
||||
boxShadow={isSelected ? '0px 0px 0px 2.4px rgba(51, 112, 255, 0.15)' : 'none'}
|
||||
_hover={{
|
||||
'& > svg': {
|
||||
color: 'primary.600'
|
||||
},
|
||||
'& > span': {
|
||||
color: 'myGray.900'
|
||||
},
|
||||
border: '1px solid #3370FF',
|
||||
boxShadow: '0px 0px 0px 2.4px rgba(51, 112, 255, 0.15)'
|
||||
}}
|
||||
onClick={() => {
|
||||
const defaultValIsNumber = !isNaN(Number(value.defaultValue));
|
||||
// 如果切换到 numberInput,不是数字,则清空
|
||||
if (
|
||||
item.value === VariableInputEnum.select ||
|
||||
(item.value === VariableInputEnum.numberInput && !defaultValIsNumber)
|
||||
) {
|
||||
setValue('defaultValue', '');
|
||||
}
|
||||
setValue('type', item.value);
|
||||
}}
|
||||
>
|
||||
<MyIcon
|
||||
ml={3}
|
||||
name={'delete'}
|
||||
w={'16px'}
|
||||
cursor={'pointer'}
|
||||
p={2}
|
||||
borderRadius={'md'}
|
||||
_hover={{ bg: 'red.100' }}
|
||||
onClick={() => removeEnums(i)}
|
||||
name={item.icon as any}
|
||||
w={'20px'}
|
||||
mr={1.5}
|
||||
color={isSelected ? 'primary.600' : 'myGray.400'}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
))}
|
||||
<Box
|
||||
as="span"
|
||||
color={isSelected ? 'myGray.900' : 'inherit'}
|
||||
pr={4}
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
{item.label}
|
||||
</Box>
|
||||
{item.description && (
|
||||
<QuestionTip label={item.description as string} ml={1} />
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
<Button
|
||||
variant={'solid'}
|
||||
w={'100%'}
|
||||
textAlign={'left'}
|
||||
leftIcon={<SmallAddIcon />}
|
||||
bg={'myGray.100 !important'}
|
||||
onClick={() => appendEnums({ value: '' })}
|
||||
>
|
||||
{t('common:core.module.variable add option')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button variant={'whiteBase'} mr={3} onClick={onCloseEdit}>
|
||||
{t('common:common.Close')}
|
||||
</Button>
|
||||
<Button onClick={handleSubmitEdit(onSubmit)}>
|
||||
{getValuesEdit('variable.id')
|
||||
? t('common:common.Confirm Update')
|
||||
: t('common:common.Add New')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
</Stack>
|
||||
<InputTypeConfig
|
||||
form={form}
|
||||
type={'variable'}
|
||||
isEdit={!!value.key}
|
||||
inputType={type}
|
||||
valueType={valueType}
|
||||
defaultValue={defaultValue}
|
||||
defaultValueType={defaultValueType}
|
||||
max={max}
|
||||
min={min}
|
||||
onClose={() => reset({})}
|
||||
onSubmitSuccess={onSubmitSuccess}
|
||||
onSubmitError={onSubmitError}
|
||||
/>
|
||||
</Flex>
|
||||
</MyModal>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
@@ -81,16 +81,11 @@ const ChatInput = ({
|
||||
const canSendMessage = havInput && !hasFileUploading;
|
||||
|
||||
// Upload files
|
||||
useRequest2(
|
||||
async () => {
|
||||
uploadFiles();
|
||||
},
|
||||
{
|
||||
manual: false,
|
||||
errorToast: t('common:upload_file_error'),
|
||||
refreshDeps: [fileList, outLinkAuthData, chatId]
|
||||
}
|
||||
);
|
||||
useRequest2(uploadFiles, {
|
||||
manual: false,
|
||||
errorToast: t('common:upload_file_error'),
|
||||
refreshDeps: [fileList, outLinkAuthData, chatId]
|
||||
});
|
||||
|
||||
/* on send */
|
||||
const handleSend = useCallback(
|
||||
|
@@ -1,7 +1,18 @@
|
||||
import React from 'react';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import { Controller, UseFormReturn } from 'react-hook-form';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { Box, Button, Card, FormControl, Input, Textarea } from '@chakra-ui/react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Input,
|
||||
NumberDecrementStepper,
|
||||
NumberIncrementStepper,
|
||||
NumberInput,
|
||||
NumberInputField,
|
||||
NumberInputStepper,
|
||||
Textarea
|
||||
} from '@chakra-ui/react';
|
||||
import ChatAvatar from './ChatAvatar';
|
||||
import { MessageCardStyle } from '../constants';
|
||||
import { VariableInputEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
@@ -10,6 +21,113 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { ChatBoxInputFormType } from '../type.d';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { ChatBoxContext } from '../Provider';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import { useDeepCompareEffect } from 'ahooks';
|
||||
import { VariableItemType } from '@fastgpt/global/core/app/type';
|
||||
|
||||
export const VariableInputItem = ({
|
||||
item,
|
||||
variablesForm
|
||||
}: {
|
||||
item: VariableItemType;
|
||||
variablesForm: UseFormReturn<any>;
|
||||
}) => {
|
||||
const { register, control, setValue } = variablesForm;
|
||||
|
||||
return (
|
||||
<Box key={item.id} mb={4} pl={1}>
|
||||
<Box
|
||||
as={'label'}
|
||||
display={'flex'}
|
||||
position={'relative'}
|
||||
mb={1}
|
||||
alignItems={'center'}
|
||||
w={'full'}
|
||||
>
|
||||
{item.label}
|
||||
{item.required && (
|
||||
<Box
|
||||
position={'absolute'}
|
||||
top={'-2px'}
|
||||
left={'-8px'}
|
||||
color={'red.500'}
|
||||
fontWeight={'bold'}
|
||||
>
|
||||
*
|
||||
</Box>
|
||||
)}
|
||||
{item.description && <QuestionTip ml={1} label={item.description} />}
|
||||
</Box>
|
||||
{item.type === VariableInputEnum.input && (
|
||||
<Input
|
||||
maxLength={item.maxLength || 4000}
|
||||
bg={'myGray.50'}
|
||||
{...register(item.key, {
|
||||
required: item.required
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{item.type === VariableInputEnum.textarea && (
|
||||
<Textarea
|
||||
{...register(item.key, {
|
||||
required: item.required
|
||||
})}
|
||||
rows={5}
|
||||
bg={'myGray.50'}
|
||||
maxLength={item.maxLength || 4000}
|
||||
/>
|
||||
)}
|
||||
{item.type === VariableInputEnum.select && (
|
||||
<Controller
|
||||
key={item.key}
|
||||
control={control}
|
||||
name={item.key}
|
||||
rules={{ required: item.required }}
|
||||
render={({ field: { ref, value } }) => {
|
||||
return (
|
||||
<MySelect
|
||||
ref={ref}
|
||||
width={'100%'}
|
||||
list={(item.enums || []).map((item: { value: any }) => ({
|
||||
label: item.value,
|
||||
value: item.value
|
||||
}))}
|
||||
value={value}
|
||||
onchange={(e) => setValue(item.key, e)}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{item.type === VariableInputEnum.numberInput && (
|
||||
<Controller
|
||||
key={item.key}
|
||||
control={control}
|
||||
name={item.key}
|
||||
rules={{ required: item.required, min: item.min, max: item.max }}
|
||||
render={({ field: { ref, value, onChange } }) => (
|
||||
<NumberInput
|
||||
step={1}
|
||||
min={item.min}
|
||||
max={item.max}
|
||||
bg={'white'}
|
||||
rounded={'md'}
|
||||
clampValueOnBlur={false}
|
||||
value={value}
|
||||
onChange={(valueString) => onChange(Number(valueString))}
|
||||
>
|
||||
<NumberInputField ref={ref} bg={'white'} />
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberInputStepper>
|
||||
</NumberInput>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const VariableInput = ({
|
||||
chatForm,
|
||||
@@ -21,13 +139,22 @@ const VariableInput = ({
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { appAvatar, variableList, variablesForm } = useContextSelector(ChatBoxContext, (v) => v);
|
||||
const { register, setValue, handleSubmit: handleSubmitChat, control } = variablesForm;
|
||||
const { reset, handleSubmit: handleSubmitChat } = variablesForm;
|
||||
|
||||
const defaultValues = useMemo(() => {
|
||||
return variableList.reduce((acc: Record<string, any>, item) => {
|
||||
acc[item.key] = item.defaultValue;
|
||||
return acc;
|
||||
}, {});
|
||||
}, [variableList]);
|
||||
|
||||
useDeepCompareEffect(() => {
|
||||
reset(defaultValues);
|
||||
}, [defaultValues]);
|
||||
|
||||
return (
|
||||
<Box py={3}>
|
||||
{/* avatar */}
|
||||
<ChatAvatar src={appAvatar} type={'AI'} />
|
||||
{/* message */}
|
||||
<Box textAlign={'left'}>
|
||||
<Card
|
||||
order={2}
|
||||
@@ -38,74 +165,21 @@ const VariableInput = ({
|
||||
boxShadow={'0 0 8px rgba(0,0,0,0.15)'}
|
||||
>
|
||||
{variableList.map((item) => (
|
||||
<Box key={item.id} mb={4}>
|
||||
<Box as={'label'} display={'inline-block'} position={'relative'} mb={1}>
|
||||
{item.label}
|
||||
{item.required && (
|
||||
<Box
|
||||
position={'absolute'}
|
||||
top={'-2px'}
|
||||
right={'-10px'}
|
||||
color={'red.500'}
|
||||
fontWeight={'bold'}
|
||||
>
|
||||
*
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
{item.type === VariableInputEnum.input && (
|
||||
<Input
|
||||
bg={'myWhite.400'}
|
||||
{...register(item.key, {
|
||||
required: item.required
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{item.type === VariableInputEnum.textarea && (
|
||||
<Textarea
|
||||
bg={'myWhite.400'}
|
||||
{...register(item.key, {
|
||||
required: item.required
|
||||
})}
|
||||
rows={5}
|
||||
maxLength={4000}
|
||||
/>
|
||||
)}
|
||||
{item.type === VariableInputEnum.select && (
|
||||
<Controller
|
||||
key={item.key}
|
||||
control={control}
|
||||
name={item.key}
|
||||
rules={{ required: item.required }}
|
||||
render={({ field: { ref, value } }) => {
|
||||
return (
|
||||
<MySelect
|
||||
ref={ref}
|
||||
width={'100%'}
|
||||
list={(item.enums || []).map((item) => ({
|
||||
label: item.value,
|
||||
value: item.value
|
||||
}))}
|
||||
value={value}
|
||||
onchange={(e) => setValue(item.key, e)}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<VariableInputItem key={item.id} item={item} variablesForm={variablesForm} />
|
||||
))}
|
||||
{!chatStarted && (
|
||||
<Button
|
||||
leftIcon={<MyIcon name={'core/chat/chatFill'} w={'16px'} />}
|
||||
size={'sm'}
|
||||
maxW={'100px'}
|
||||
onClick={handleSubmitChat(() => {
|
||||
chatForm.setValue('chatStarted', true);
|
||||
})}
|
||||
>
|
||||
{t('common:core.chat.Start Chat')}
|
||||
</Button>
|
||||
<Box>
|
||||
<Button
|
||||
leftIcon={<MyIcon name={'core/chat/chatFill'} w={'16px'} />}
|
||||
size={'sm'}
|
||||
maxW={'100px'}
|
||||
onClick={handleSubmitChat(() => {
|
||||
chatForm.setValue('chatStarted', true);
|
||||
})}
|
||||
>
|
||||
{t('common:core.chat.Start Chat')}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Card>
|
||||
</Box>
|
||||
|
@@ -181,7 +181,7 @@ export const useFileUpload = (props: UseFileUploadOptions) => {
|
||||
});
|
||||
|
||||
// Update file url
|
||||
copyFile.url = `${location.origin}${previewUrl}`;
|
||||
copyFile.url = previewUrl;
|
||||
updateFiles(fileIndex, copyFile);
|
||||
} catch (error) {
|
||||
errorFileIndex.push(fileList.findIndex((item) => item.id === file.id)!);
|
||||
|
@@ -495,7 +495,8 @@ const ChatBox = (
|
||||
// 这里,无论是否为交互模式,最后都是 Human 的消息。
|
||||
const messages = chats2GPTMessages({
|
||||
messages: newChatList.slice(0, -1),
|
||||
reserveId: true
|
||||
reserveId: true,
|
||||
reserveTool: true
|
||||
});
|
||||
|
||||
const {
|
||||
|
@@ -57,9 +57,9 @@ export const checkIsInteractiveByHistories = (chatHistories: ChatSiteItemType[])
|
||||
) {
|
||||
const params = lastMessageValue.interactive.params;
|
||||
// 如果用户选择了,则不认为是交互模式(可能是上一轮以交互结尾,发起的新的一轮对话)
|
||||
if ('userSelectOptions' in params && 'userSelectedVal' in params) {
|
||||
if ('userSelectOptions' in params) {
|
||||
return !params.userSelectedVal;
|
||||
} else if ('inputForm' in params && 'submitted' in params) {
|
||||
} else if ('inputForm' in params) {
|
||||
return !params.submitted;
|
||||
}
|
||||
}
|
||||
|
@@ -228,7 +228,8 @@ const PluginRunContextProvider = ({
|
||||
value: []
|
||||
}
|
||||
],
|
||||
reserveId: true
|
||||
reserveId: true,
|
||||
reserveTool: true
|
||||
});
|
||||
|
||||
try {
|
||||
|
@@ -354,11 +354,11 @@ const RenderList = React.memo(function RenderList({
|
||||
<HStack mb={4} spacing={1} fontSize={'sm'}>
|
||||
<MyIcon name={'common/info'} w={'1.25rem'} />
|
||||
<Box flex={1}>{t('app:tool_input_param_tip')}</Box>
|
||||
{configTool.inputExplanationUrl && (
|
||||
{configTool.courseUrl && (
|
||||
<Box
|
||||
cursor={'pointer'}
|
||||
color={'primary.500'}
|
||||
onClick={() => window.open(configTool.inputExplanationUrl, '_blank')}
|
||||
onClick={() => window.open(configTool.courseUrl, '_blank')}
|
||||
>
|
||||
{t('app:workflow.Input guide')}
|
||||
</Box>
|
||||
|
@@ -1,29 +1,12 @@
|
||||
import React from 'react';
|
||||
import { Box, StackProps, HStack } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
const IOTitle = ({
|
||||
text,
|
||||
inputExplanationUrl,
|
||||
...props
|
||||
}: { text?: 'Input' | 'Output' | string; inputExplanationUrl?: string } & StackProps) => {
|
||||
const { t } = useTranslation();
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
|
||||
const IOTitle = ({ text, ...props }: { text?: 'Input' | 'Output' | string } & StackProps) => {
|
||||
return (
|
||||
<HStack fontSize={'md'} alignItems={'center'} fontWeight={'medium'} mb={3} {...props}>
|
||||
<Box w={'3px'} h={'14px'} borderRadius={'13px'} bg={'primary.600'} />
|
||||
<Box color={'myGray.900'}>{text}</Box>
|
||||
<Box flex={1} />
|
||||
|
||||
{inputExplanationUrl && (
|
||||
<Box
|
||||
cursor={'pointer'}
|
||||
color={'primary.500'}
|
||||
onClick={() => window.open(inputExplanationUrl, '_blank')}
|
||||
>
|
||||
{t('app:workflow.Input guide')}
|
||||
</Box>
|
||||
)}
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { storeNodes2RuntimeNodes } from '@fastgpt/global/core/workflow/runtime/utils';
|
||||
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
|
||||
import { RuntimeEdgeItemType, StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCallback, useState, useMemo, useEffect } from 'react';
|
||||
import { checkWorkflowNodeAndConnection } from '@/web/core/workflow/utils';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
@@ -21,19 +21,30 @@ import {
|
||||
NumberInputStepper,
|
||||
Switch
|
||||
} from '@chakra-ui/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { FieldErrors, useForm } from 'react-hook-form';
|
||||
import {
|
||||
VariableInputEnum,
|
||||
WorkflowIOValueTypeEnum
|
||||
} from '@fastgpt/global/core/workflow/constants';
|
||||
import { checkInputIsReference } from '@fastgpt/global/core/workflow/utils';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext, getWorkflowStore } from '../../context';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { AppContext } from '../../../context';
|
||||
import { VariableInputItem } from '@/components/core/chat/ChatContainer/ChatBox/components/VariableInput';
|
||||
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
|
||||
|
||||
const MyRightDrawer = dynamic(
|
||||
() => import('@fastgpt/web/components/common/MyDrawer/MyRightDrawer')
|
||||
);
|
||||
const JsonEditor = dynamic(() => import('@fastgpt/web/components/common/Textarea/JsonEditor'));
|
||||
|
||||
enum TabEnum {
|
||||
global = 'global',
|
||||
node = 'node'
|
||||
}
|
||||
|
||||
export const useDebug = () => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
@@ -43,6 +54,23 @@ export const useDebug = () => {
|
||||
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
|
||||
const onStartNodeDebug = useContextSelector(WorkflowContext, (v) => v.onStartNodeDebug);
|
||||
|
||||
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
|
||||
|
||||
const filteredVar = useMemo(() => {
|
||||
const variables = appDetail.chatConfig.variables;
|
||||
return variables?.filter((item) => item.type !== VariableInputEnum.custom) || [];
|
||||
}, [appDetail.chatConfig.variables]);
|
||||
|
||||
const [defaultGlobalVariables, setDefaultGlobalVariables] = useState<Record<string, any>>(
|
||||
filteredVar.reduce(
|
||||
(acc, item) => {
|
||||
acc[item.key] = item.defaultValue;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, any>
|
||||
)
|
||||
);
|
||||
|
||||
const [runtimeNodeId, setRuntimeNodeId] = useState<string>();
|
||||
const [runtimeNodes, setRuntimeNodes] = useState<RuntimeNodeItemType[]>();
|
||||
const [runtimeEdges, setRuntimeEdges] = useState<RuntimeEdgeItemType[]>();
|
||||
@@ -108,6 +136,8 @@ export const useDebug = () => {
|
||||
const DebugInputModal = useCallback(() => {
|
||||
if (!runtimeNodes || !runtimeEdges) return <></>;
|
||||
|
||||
const [currentTab, setCurrentTab] = useState<TabEnum>(TabEnum.node);
|
||||
|
||||
const runtimeNode = runtimeNodes.find((node) => node.nodeId === runtimeNodeId);
|
||||
|
||||
if (!runtimeNode) return <></>;
|
||||
@@ -117,20 +147,24 @@ export const useDebug = () => {
|
||||
if (input.required && !input.value) return true;
|
||||
});
|
||||
|
||||
const { register, getValues, setValue, handleSubmit } = useForm<Record<string, any>>({
|
||||
defaultValues: renderInputs.reduce((acc: Record<string, any>, input) => {
|
||||
const isReference = checkInputIsReference(input);
|
||||
if (isReference) {
|
||||
acc[input.key] = undefined;
|
||||
} else if (typeof input.value === 'object') {
|
||||
acc[input.key] = JSON.stringify(input.value, null, 2);
|
||||
} else {
|
||||
acc[input.key] = input.value;
|
||||
}
|
||||
const variablesForm = useForm<Record<string, any>>({
|
||||
defaultValues: {
|
||||
nodeVariables: renderInputs.reduce((acc: Record<string, any>, input) => {
|
||||
const isReference = checkInputIsReference(input);
|
||||
if (isReference) {
|
||||
acc[input.key] = undefined;
|
||||
} else if (typeof input.value === 'object') {
|
||||
acc[input.key] = JSON.stringify(input.value, null, 2);
|
||||
} else {
|
||||
acc[input.key] = input.value;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {})
|
||||
return acc;
|
||||
}, {}),
|
||||
globalVariables: defaultGlobalVariables
|
||||
}
|
||||
});
|
||||
const { register, getValues, setValue, handleSubmit } = variablesForm;
|
||||
|
||||
const onClose = () => {
|
||||
setRuntimeNodeId(undefined);
|
||||
@@ -152,12 +186,13 @@ export const useDebug = () => {
|
||||
input.valueType === WorkflowIOValueTypeEnum.string ||
|
||||
input.valueType === WorkflowIOValueTypeEnum.number ||
|
||||
input.valueType === WorkflowIOValueTypeEnum.boolean
|
||||
)
|
||||
return data[input.key];
|
||||
) {
|
||||
return data.nodeVariables[input.key];
|
||||
}
|
||||
|
||||
return JSON.parse(data[input.key]);
|
||||
return JSON.parse(data.nodeVariables[input.key]);
|
||||
} catch (e) {
|
||||
return data[input.key];
|
||||
return data.nodeVariables[input.key];
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -169,11 +204,33 @@ export const useDebug = () => {
|
||||
}
|
||||
: node
|
||||
),
|
||||
runtimeEdges: runtimeEdges
|
||||
runtimeEdges: runtimeEdges,
|
||||
variables: data.globalVariables
|
||||
});
|
||||
|
||||
// Filter global variables and set them as default global variable values
|
||||
setDefaultGlobalVariables(data.globalVariables);
|
||||
|
||||
onClose();
|
||||
};
|
||||
|
||||
const onCheckRunError = useCallback((e: FieldErrors<Record<string, any>>) => {
|
||||
const hasRequiredNodeVar =
|
||||
e.nodeVariables && Object.values(e.nodeVariables).some((item) => item.type === 'required');
|
||||
|
||||
if (hasRequiredNodeVar) {
|
||||
return setCurrentTab(TabEnum.node);
|
||||
}
|
||||
|
||||
const hasRequiredGlobalVar =
|
||||
e.globalVariables &&
|
||||
Object.values(e.globalVariables).some((item) => item.type === 'required');
|
||||
|
||||
if (hasRequiredGlobalVar) {
|
||||
setCurrentTab(TabEnum.global);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<MyRightDrawer
|
||||
onClose={onClose}
|
||||
@@ -183,89 +240,124 @@ export const useDebug = () => {
|
||||
px={0}
|
||||
>
|
||||
<Box flex={'1 0 0'} overflow={'auto'} px={6}>
|
||||
{renderInputs.map((input) => {
|
||||
const required = input.required || false;
|
||||
{filteredVar.length > 0 && (
|
||||
<LightRowTabs<TabEnum>
|
||||
gap={3}
|
||||
ml={-2}
|
||||
mb={5}
|
||||
inlineStyles={{}}
|
||||
list={[
|
||||
{ label: t('workflow:Node_variables'), value: TabEnum.node },
|
||||
{ label: t('common:core.module.Variable'), value: TabEnum.global }
|
||||
]}
|
||||
value={currentTab}
|
||||
onChange={setCurrentTab}
|
||||
/>
|
||||
)}
|
||||
<Box display={currentTab === TabEnum.global ? 'block' : 'none'}>
|
||||
{filteredVar.map((item) => (
|
||||
<VariableInputItem
|
||||
key={item.id}
|
||||
item={{ ...item, key: `globalVariables.${item.key}` }}
|
||||
variablesForm={variablesForm}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
<Box display={currentTab === TabEnum.node ? 'block' : 'none'}>
|
||||
{renderInputs.map((input) => {
|
||||
const required = input.required || false;
|
||||
|
||||
const RenderInput = (() => {
|
||||
if (input.valueType === WorkflowIOValueTypeEnum.string) {
|
||||
return (
|
||||
<Textarea
|
||||
{...register(`nodeVariables.${input.key}`, {
|
||||
required
|
||||
})}
|
||||
placeholder={t(input.placeholder || ('' as any))}
|
||||
bg={'myGray.50'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (input.valueType === WorkflowIOValueTypeEnum.number) {
|
||||
return (
|
||||
<NumberInput step={input.step} min={input.min} max={input.max} bg={'myGray.50'}>
|
||||
<NumberInputField
|
||||
{...register(`nodeVariables.${input.key}`, {
|
||||
required: input.required,
|
||||
min: input.min,
|
||||
max: input.max,
|
||||
valueAsNumber: true
|
||||
})}
|
||||
/>
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberInputStepper>
|
||||
</NumberInput>
|
||||
);
|
||||
}
|
||||
if (input.valueType === WorkflowIOValueTypeEnum.boolean) {
|
||||
return (
|
||||
<Box>
|
||||
<Switch {...register(`nodeVariables.${input.key}`)} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
let value = getValues(input.key) || '';
|
||||
if (typeof value !== 'string') {
|
||||
value = JSON.stringify(value, null, 2);
|
||||
}
|
||||
|
||||
const RenderInput = (() => {
|
||||
if (input.valueType === WorkflowIOValueTypeEnum.string) {
|
||||
return (
|
||||
<Textarea
|
||||
{...register(input.key, {
|
||||
required
|
||||
})}
|
||||
placeholder={t(input.placeholder || ('' as any))}
|
||||
<JsonEditor
|
||||
bg={'myGray.50'}
|
||||
placeholder={t(input.placeholder || ('' as any))}
|
||||
resize
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
setValue(`nodeVariables.${input.key}`, e);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (input.valueType === WorkflowIOValueTypeEnum.number) {
|
||||
return (
|
||||
<NumberInput step={input.step} min={input.min} max={input.max} bg={'myGray.50'}>
|
||||
<NumberInputField
|
||||
{...register(input.key, {
|
||||
required: input.required,
|
||||
min: input.min,
|
||||
max: input.max,
|
||||
valueAsNumber: true
|
||||
})}
|
||||
/>
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberInputStepper>
|
||||
</NumberInput>
|
||||
);
|
||||
}
|
||||
if (input.valueType === WorkflowIOValueTypeEnum.boolean) {
|
||||
return (
|
||||
<Box>
|
||||
<Switch {...register(input.key)} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
})();
|
||||
|
||||
let value = getValues(input.key) || '';
|
||||
if (typeof value !== 'string') {
|
||||
value = JSON.stringify(value, null, 2);
|
||||
}
|
||||
|
||||
return (
|
||||
<JsonEditor
|
||||
bg={'myGray.50'}
|
||||
placeholder={t(input.placeholder || ('' as any))}
|
||||
resize
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
setValue(input.key, e);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})();
|
||||
|
||||
return !!RenderInput ? (
|
||||
<Box key={input.key} _notLast={{ mb: 4 }} px={1}>
|
||||
<Flex alignItems={'center'} mb={1}>
|
||||
<Box position={'relative'}>
|
||||
{required && (
|
||||
<Box position={'absolute'} right={-2} top={'-1px'} color={'red.600'}>
|
||||
*
|
||||
</Box>
|
||||
)}
|
||||
{t(input.debugLabel || (input.label as any))}
|
||||
</Box>
|
||||
{input.description && <QuestionTip ml={2} label={input.description} />}
|
||||
</Flex>
|
||||
{RenderInput}
|
||||
</Box>
|
||||
) : null;
|
||||
})}
|
||||
return !!RenderInput ? (
|
||||
<Box key={input.key} _notLast={{ mb: 4 }} px={1}>
|
||||
<Flex alignItems={'center'} mb={1}>
|
||||
<Box position={'relative'}>
|
||||
{required && (
|
||||
<Box position={'absolute'} right={-2} top={'-1px'} color={'red.600'}>
|
||||
*
|
||||
</Box>
|
||||
)}
|
||||
{t(input.debugLabel || (input.label as any))}
|
||||
</Box>
|
||||
{input.description && <QuestionTip ml={2} label={input.description} />}
|
||||
</Flex>
|
||||
{RenderInput}
|
||||
</Box>
|
||||
) : null;
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
<Flex py={2} justifyContent={'flex-end'} px={6}>
|
||||
<Button onClick={handleSubmit(onClickRun)}>{t('common:common.Run')}</Button>
|
||||
<Button onClick={handleSubmit(onClickRun, onCheckRunError)}>
|
||||
{t('common:common.Run')}
|
||||
</Button>
|
||||
</Flex>
|
||||
</MyRightDrawer>
|
||||
);
|
||||
}, [onStartNodeDebug, runtimeEdges, runtimeNodeId, runtimeNodes, t]);
|
||||
}, [
|
||||
defaultGlobalVariables,
|
||||
filteredVar,
|
||||
onStartNodeDebug,
|
||||
runtimeEdges,
|
||||
runtimeNodeId,
|
||||
runtimeNodes,
|
||||
t
|
||||
]);
|
||||
|
||||
return {
|
||||
DebugInputModal,
|
||||
|
@@ -412,41 +412,37 @@ export const useWorkflow = () => {
|
||||
});
|
||||
|
||||
/* node */
|
||||
// Remove change node and its child nodes and edges
|
||||
const handleRemoveNode = useMemoizedFn((change: NodeRemoveChange, nodeId: string) => {
|
||||
// If the node has child nodes, remove the child nodes
|
||||
const deletedNodeIdList = [nodeId];
|
||||
const deletedEdgeIdList = edges
|
||||
.filter((edge) => edge.source === nodeId || edge.target === nodeId)
|
||||
.map((edge) => edge.id);
|
||||
|
||||
const childNodes = nodes.filter((n) => n.data.parentNodeId === nodeId);
|
||||
if (childNodes.length > 0) {
|
||||
const childNodeIds = childNodes.map((node) => node.id);
|
||||
deletedNodeIdList.push(...childNodeIds);
|
||||
|
||||
const childEdges = edges.filter(
|
||||
(edge) => childNodeIds.includes(edge.source) || childNodeIds.includes(edge.target)
|
||||
);
|
||||
|
||||
onNodesChange(
|
||||
childNodes.map<NodeRemoveChange>((node) => ({
|
||||
type: 'remove',
|
||||
id: node.id
|
||||
}))
|
||||
);
|
||||
onEdgesChange(
|
||||
childEdges.map<EdgeRemoveChange>((edge) => ({
|
||||
type: 'remove',
|
||||
id: edge.id
|
||||
}))
|
||||
);
|
||||
deletedEdgeIdList.push(...childEdges.map((edge) => edge.id));
|
||||
}
|
||||
|
||||
onNodesChange([change]);
|
||||
|
||||
// Remove the edges connected to the node
|
||||
const nodeEdges = edges.filter((edge) => edge.source === nodeId || edge.target === nodeId);
|
||||
onEdgesChange(
|
||||
nodeEdges.map<EdgeRemoveChange>((edge) => ({
|
||||
onNodesChange(
|
||||
deletedNodeIdList.map<NodeRemoveChange>((id) => ({
|
||||
type: 'remove',
|
||||
id: edge.id
|
||||
id
|
||||
}))
|
||||
);
|
||||
onEdgesChange(
|
||||
deletedEdgeIdList.map<EdgeRemoveChange>((id) => ({
|
||||
type: 'remove',
|
||||
id
|
||||
}))
|
||||
);
|
||||
|
||||
return;
|
||||
});
|
||||
const handleSelectNode = useMemoizedFn((change: NodeSelectionChange) => {
|
||||
// If the node is not selected and the Ctrl key is pressed, select the node
|
||||
|
@@ -110,7 +110,7 @@ const NodeLoopStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th borderBottomLeftRadius={'none !important'}>
|
||||
{t('common:core.module.variable.variable name')}
|
||||
{t('workflow:Variable_name')}
|
||||
</Th>
|
||||
<Th>{t('common:core.workflow.Value type')}</Th>
|
||||
</Tr>
|
||||
|
@@ -80,12 +80,13 @@ const NodeComment = ({ data }: NodeProps<FlowNodeItemType>) => {
|
||||
menuForbid={{
|
||||
debug: true
|
||||
}}
|
||||
border={'none'}
|
||||
rounded={'none'}
|
||||
bg={'#D8E9FF'}
|
||||
boxShadow={
|
||||
'0px 4px 10px 0px rgba(19, 51, 107, 0.10), 0px 0px 1px 0px rgba(19, 51, 107, 0.10)'
|
||||
}
|
||||
customStyle={{
|
||||
border: 'none',
|
||||
rounded: 'none',
|
||||
bg: '#D8E9FF',
|
||||
boxShadow:
|
||||
'0px 4px 10px 0px rgba(19, 51, 107, 0.10), 0px 0px 1px 0px rgba(19, 51, 107, 0.10)'
|
||||
}}
|
||||
>
|
||||
<Box w={'full'} h={'full'} position={'relative'}>
|
||||
<Box
|
||||
|
@@ -19,7 +19,8 @@ export const defaultFormInput: UserInputFormItemType = {
|
||||
maxLength: undefined,
|
||||
defaultValue: '',
|
||||
valueType: WorkflowIOValueTypeEnum.string,
|
||||
required: false
|
||||
required: false,
|
||||
list: [{ label: '', value: '' }]
|
||||
};
|
||||
|
||||
// Modal for add or edit user input form items
|
||||
@@ -39,10 +40,7 @@ const InputFormEditModal = ({
|
||||
const { toast } = useToast();
|
||||
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
...defaultValue,
|
||||
list: defaultValue.list?.length ? defaultValue.list : [{ label: '', value: '' }]
|
||||
}
|
||||
defaultValues: defaultValue
|
||||
});
|
||||
const { setValue, watch, reset } = form;
|
||||
|
||||
@@ -51,6 +49,7 @@ const InputFormEditModal = ({
|
||||
const maxLength = watch('maxLength');
|
||||
const max = watch('max');
|
||||
const min = watch('min');
|
||||
const defaultInputValue = watch('defaultValue');
|
||||
|
||||
const inputTypeList = [
|
||||
{
|
||||
@@ -111,7 +110,7 @@ const InputFormEditModal = ({
|
||||
reset(defaultFormInput);
|
||||
}
|
||||
},
|
||||
[toast, t, reset, onSubmit, onClose, defaultFormInput, defaultValueType]
|
||||
[defaultValue.key, keys, defaultValueType, isEdit, toast, t, onSubmit, onClose, reset]
|
||||
);
|
||||
|
||||
const onSubmitError = useCallback(
|
||||
@@ -197,6 +196,7 @@ const InputFormEditModal = ({
|
||||
maxLength={maxLength}
|
||||
max={max}
|
||||
min={min}
|
||||
defaultValue={defaultInputValue}
|
||||
onClose={onClose}
|
||||
onSubmitSuccess={onSubmitSuccess}
|
||||
onSubmitError={onSubmitError}
|
||||
|
@@ -81,6 +81,9 @@ const RenderHttpMethodAndUrl = React.memo(function RenderHttpMethodAndUrl({
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
|
||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||
|
||||
const { isOpen: isOpenCurl, onOpen: onOpenCurl, onClose: onCloseCurl } = useDisclosure();
|
||||
|
||||
@@ -91,19 +94,18 @@ const RenderHttpMethodAndUrl = React.memo(function RenderHttpMethodAndUrl({
|
||||
(item) => item.key === NodeInputKeyEnum.httpReqUrl
|
||||
) as FlowNodeInputItemType;
|
||||
|
||||
const onChangeUrl = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const onChangeUrl = (value: string) => {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: NodeInputKeyEnum.httpReqUrl,
|
||||
value: {
|
||||
...requestUrl,
|
||||
value: e.target.value
|
||||
value
|
||||
}
|
||||
});
|
||||
};
|
||||
const onBlurUrl = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const val = e.target.value;
|
||||
const onBlurUrl = (val: string) => {
|
||||
// 拆分params和url
|
||||
const url = val.split('?')[0];
|
||||
const params = val.split('?')[1];
|
||||
@@ -154,6 +156,16 @@ const RenderHttpMethodAndUrl = React.memo(function RenderHttpMethodAndUrl({
|
||||
}
|
||||
};
|
||||
|
||||
const variables = useCreation(() => {
|
||||
return getEditorVariables({
|
||||
nodeId,
|
||||
nodeList,
|
||||
edges,
|
||||
appDetail,
|
||||
t
|
||||
});
|
||||
}, [nodeId, nodeList, edges, appDetail, t]);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={2} display={'flex'} justifyContent={'space-between'}>
|
||||
@@ -166,7 +178,7 @@ const RenderHttpMethodAndUrl = React.memo(function RenderHttpMethodAndUrl({
|
||||
</Box>
|
||||
<Flex alignItems={'center'} className="nodrag">
|
||||
<MySelect
|
||||
h={'34px'}
|
||||
h={'40px'}
|
||||
w={'88px'}
|
||||
bg={'white'}
|
||||
width={'100%'}
|
||||
@@ -205,17 +217,29 @@ const RenderHttpMethodAndUrl = React.memo(function RenderHttpMethodAndUrl({
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
flex={'1 0 0'}
|
||||
ml={2}
|
||||
h={'34px'}
|
||||
<Box
|
||||
w={'full'}
|
||||
border={'1px solid'}
|
||||
borderColor={'myGray.200'}
|
||||
rounded={'md'}
|
||||
bg={'white'}
|
||||
value={requestUrl?.value || ''}
|
||||
placeholder={t('common:core.module.input.label.Http Request Url')}
|
||||
fontSize={'xs'}
|
||||
onChange={onChangeUrl}
|
||||
onBlur={onBlurUrl}
|
||||
/>
|
||||
ml={2}
|
||||
>
|
||||
<PromptEditor
|
||||
placeholder={
|
||||
t('common:core.module.input.label.Http Request Url') +
|
||||
', ' +
|
||||
t('common:textarea_variable_picker_tip')
|
||||
}
|
||||
value={requestUrl?.value || ''}
|
||||
variableLabels={variables}
|
||||
variables={variables}
|
||||
onBlur={onBlurUrl}
|
||||
onChange={onChangeUrl}
|
||||
minH={40}
|
||||
showOpenModal={false}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
{isOpenCurl && <CurlImportModal nodeId={nodeId} inputs={inputs} onClose={onCloseCurl} />}
|
||||
|
@@ -22,7 +22,8 @@ export const defaultInput: FlowNodeInputItemType = {
|
||||
key: '',
|
||||
label: '',
|
||||
description: '',
|
||||
defaultValue: ''
|
||||
defaultValue: '',
|
||||
list: [{ label: '', value: '' }]
|
||||
};
|
||||
|
||||
const FieldEditModal = ({
|
||||
@@ -133,10 +134,7 @@ const FieldEditModal = ({
|
||||
|
||||
const isEdit = !!defaultValue.key;
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
...defaultValue,
|
||||
list: defaultValue.list?.length ? defaultValue.list : [{ label: '', value: '' }]
|
||||
}
|
||||
defaultValues: defaultValue
|
||||
});
|
||||
const { getValues, setValue, watch, reset } = form;
|
||||
|
||||
@@ -149,7 +147,7 @@ const FieldEditModal = ({
|
||||
const max = watch('max');
|
||||
const min = watch('min');
|
||||
const selectValueTypeList = watch('customInputConfig.selectValueTypeList');
|
||||
const defaultJsonValue = watch('defaultValue');
|
||||
const defaultInputValue = watch('defaultValue');
|
||||
|
||||
const defaultValueType =
|
||||
inputTypeList.flat().find((item) => item.value === inputType)?.defaultValueType ||
|
||||
@@ -157,9 +155,9 @@ const FieldEditModal = ({
|
||||
|
||||
const onSubmitSuccess = useCallback(
|
||||
(data: FlowNodeInputItemType, action: 'confirm' | 'continue') => {
|
||||
data.key = data?.key?.trim();
|
||||
data.label = data?.label?.trim();
|
||||
|
||||
if (!data.key) {
|
||||
if (!data.label) {
|
||||
return toast({
|
||||
status: 'warning',
|
||||
title: t('common:core.module.edit.Field Name Cannot Be Empty')
|
||||
@@ -199,7 +197,7 @@ const FieldEditModal = ({
|
||||
data.toolDescription = undefined;
|
||||
}
|
||||
|
||||
data.label = data.key;
|
||||
data.key = data.label;
|
||||
|
||||
if (action === 'confirm') {
|
||||
onSubmit(data);
|
||||
@@ -327,7 +325,7 @@ const FieldEditModal = ({
|
||||
max={max}
|
||||
min={min}
|
||||
selectValueTypeList={selectValueTypeList}
|
||||
defaultJsonValue={defaultJsonValue}
|
||||
defaultValue={defaultInputValue}
|
||||
isToolInput={isToolInput}
|
||||
setIsToolInput={setIsToolInput}
|
||||
valueType={valueType}
|
||||
|
@@ -6,11 +6,19 @@ import {
|
||||
FormLabel,
|
||||
HStack,
|
||||
Input,
|
||||
NumberDecrementStepper,
|
||||
NumberIncrementStepper,
|
||||
NumberInput,
|
||||
NumberInputField,
|
||||
NumberInputStepper,
|
||||
Stack,
|
||||
Switch,
|
||||
Textarea
|
||||
} from '@chakra-ui/react';
|
||||
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import {
|
||||
VariableInputEnum,
|
||||
WorkflowIOValueTypeEnum
|
||||
} from '@fastgpt/global/core/workflow/constants';
|
||||
import {
|
||||
FlowNodeInputTypeEnum,
|
||||
FlowValueTypeMap
|
||||
@@ -25,6 +33,9 @@ import React, { useMemo } from 'react';
|
||||
import { useFieldArray, UseFormReturn } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import DndDrag, { Draggable } from '@fastgpt/web/components/common/DndDrag';
|
||||
|
||||
type ListValueType = { id: string; value: string; label: string }[];
|
||||
|
||||
const InputTypeConfig = ({
|
||||
form,
|
||||
@@ -36,7 +47,7 @@ const InputTypeConfig = ({
|
||||
max,
|
||||
min,
|
||||
selectValueTypeList,
|
||||
defaultJsonValue,
|
||||
defaultValue,
|
||||
isToolInput,
|
||||
setIsToolInput,
|
||||
valueType,
|
||||
@@ -48,15 +59,15 @@ const InputTypeConfig = ({
|
||||
form: UseFormReturn<any>;
|
||||
isEdit: boolean;
|
||||
onClose: () => void;
|
||||
type: 'plugin' | 'formInput';
|
||||
inputType: FlowNodeInputTypeEnum;
|
||||
type: 'plugin' | 'formInput' | 'variable';
|
||||
inputType: FlowNodeInputTypeEnum | VariableInputEnum;
|
||||
|
||||
maxLength?: number;
|
||||
max?: number;
|
||||
min?: number;
|
||||
|
||||
selectValueTypeList?: WorkflowIOValueTypeEnum[];
|
||||
defaultJsonValue?: string;
|
||||
defaultValue?: string;
|
||||
|
||||
// Plugin-specific fields
|
||||
isToolInput?: boolean;
|
||||
@@ -69,8 +80,23 @@ const InputTypeConfig = ({
|
||||
onSubmitError: (e: Object) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const defaultListValue = { label: t('common:None'), value: '' };
|
||||
|
||||
const { register, setValue, handleSubmit, control } = form;
|
||||
const { register, setValue, handleSubmit, control, watch } = form;
|
||||
const listValue: ListValueType = watch('list');
|
||||
|
||||
const typeLabels = {
|
||||
name: {
|
||||
formInput: t('common:core.module.input_name'),
|
||||
plugin: t('common:core.module.Field Name'),
|
||||
variable: t('workflow:Variable_name')
|
||||
},
|
||||
description: {
|
||||
formInput: t('common:core.module.input_description'),
|
||||
plugin: t('workflow:field_description'),
|
||||
variable: t('workflow:variable_description')
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
fields: selectEnums,
|
||||
@@ -81,6 +107,11 @@ const InputTypeConfig = ({
|
||||
name: 'list'
|
||||
});
|
||||
|
||||
const mergedSelectEnums = selectEnums.map((field, index) => ({
|
||||
...field,
|
||||
...listValue[index]
|
||||
}));
|
||||
|
||||
const valueTypeSelectList = Object.values(FlowValueTypeMap).map((item) => ({
|
||||
label: t(item.label as any),
|
||||
value: item.value
|
||||
@@ -88,21 +119,26 @@ const InputTypeConfig = ({
|
||||
|
||||
const showValueTypeSelect =
|
||||
inputType === FlowNodeInputTypeEnum.reference ||
|
||||
inputType === FlowNodeInputTypeEnum.customVariable;
|
||||
inputType === FlowNodeInputTypeEnum.customVariable ||
|
||||
inputType === VariableInputEnum.custom;
|
||||
|
||||
const showRequired = useMemo(() => {
|
||||
const list = [FlowNodeInputTypeEnum.addInputParam, FlowNodeInputTypeEnum.customVariable];
|
||||
const list = [
|
||||
FlowNodeInputTypeEnum.addInputParam,
|
||||
FlowNodeInputTypeEnum.customVariable,
|
||||
VariableInputEnum.custom
|
||||
];
|
||||
return !list.includes(inputType);
|
||||
}, [inputType]);
|
||||
|
||||
const showMaxLenInput = useMemo(() => {
|
||||
const list = [FlowNodeInputTypeEnum.input, FlowNodeInputTypeEnum.textarea];
|
||||
return list.includes(inputType);
|
||||
return list.includes(inputType as FlowNodeInputTypeEnum);
|
||||
}, [inputType]);
|
||||
|
||||
const showMinMaxInput = useMemo(() => {
|
||||
const list = [FlowNodeInputTypeEnum.numberInput];
|
||||
return list.includes(inputType);
|
||||
return list.includes(inputType as FlowNodeInputTypeEnum);
|
||||
}, [inputType]);
|
||||
|
||||
const showDefaultValue = useMemo(() => {
|
||||
@@ -111,34 +147,31 @@ const InputTypeConfig = ({
|
||||
FlowNodeInputTypeEnum.textarea,
|
||||
FlowNodeInputTypeEnum.JSONEditor,
|
||||
FlowNodeInputTypeEnum.numberInput,
|
||||
FlowNodeInputTypeEnum.switch
|
||||
FlowNodeInputTypeEnum.switch,
|
||||
FlowNodeInputTypeEnum.select
|
||||
];
|
||||
|
||||
return list.includes(inputType);
|
||||
return list.includes(inputType as FlowNodeInputTypeEnum);
|
||||
}, [inputType]);
|
||||
|
||||
return (
|
||||
<Stack flex={1} borderLeft={'1px solid #F0F1F6'} justifyContent={'space-between'}>
|
||||
<Flex flexDirection={'column'} p={8} gap={4} flex={'1 0 0'} overflow={'auto'}>
|
||||
<Flex flexDirection={'column'} p={8} pb={2} gap={4} flex={'1 0 0'} overflow={'auto'}>
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 100px'} fontWeight={'medium'}>
|
||||
{type === 'formInput'
|
||||
? t('common:core.module.input_name')
|
||||
: t('common:core.module.Field Name')}
|
||||
{typeLabels.name[type] || typeLabels.name.formInput}
|
||||
</FormLabel>
|
||||
<Input
|
||||
bg={'myGray.50'}
|
||||
placeholder="appointment/sql"
|
||||
{...register(type === 'formInput' ? 'label' : 'key', {
|
||||
{...register('label', {
|
||||
required: true
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex alignItems={'flex-start'}>
|
||||
<FormLabel flex={'0 0 100px'} fontWeight={'medium'}>
|
||||
{type === 'formInput'
|
||||
? t('common:core.module.input_description')
|
||||
: t('workflow:field_description')}
|
||||
{typeLabels.description[type] || typeLabels.description.plugin}
|
||||
</FormLabel>
|
||||
<Textarea
|
||||
bg={'myGray.50'}
|
||||
@@ -149,7 +182,7 @@ const InputTypeConfig = ({
|
||||
</Flex>
|
||||
|
||||
{/* value type */}
|
||||
{type === 'plugin' && (
|
||||
{type !== 'formInput' && (
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 100px'} fontWeight={'medium'}>
|
||||
{t('common:core.module.Data Type')}
|
||||
@@ -167,13 +200,15 @@ const InputTypeConfig = ({
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<Box fontSize={'14px'}>{defaultValueType}</Box>
|
||||
<Box fontSize={'14px'} mb={2}>
|
||||
{defaultValueType}
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
{showRequired && (
|
||||
<Flex alignItems={'center'} minH={'40px'}>
|
||||
<FormLabel flex={'1'} fontWeight={'medium'}>
|
||||
<FormLabel flex={'0 0 100px'} fontWeight={'medium'}>
|
||||
{t('workflow:field_required')}
|
||||
</FormLabel>
|
||||
<Switch {...register('required')} />
|
||||
@@ -191,7 +226,6 @@ const InputTypeConfig = ({
|
||||
isChecked={isToolInput}
|
||||
onChange={(e) => {
|
||||
setIsToolInput && setIsToolInput();
|
||||
console.log(isToolInput);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
@@ -208,6 +242,7 @@ const InputTypeConfig = ({
|
||||
bg={'myGray.50'}
|
||||
placeholder={t('common:core.module.Max Length placeholder')}
|
||||
value={maxLength}
|
||||
max={50000}
|
||||
onChange={(e) => {
|
||||
// @ts-ignore
|
||||
setValue('maxLength', e || '');
|
||||
@@ -258,13 +293,18 @@ const InputTypeConfig = ({
|
||||
{t('common:core.module.Default Value')}
|
||||
</FormLabel>
|
||||
{inputType === FlowNodeInputTypeEnum.numberInput && (
|
||||
<Input
|
||||
bg={'myGray.50'}
|
||||
max={max}
|
||||
min={min}
|
||||
type={'number'}
|
||||
{...register('defaultValue')}
|
||||
/>
|
||||
<NumberInput flex={1} step={1} min={min} max={max} position={'relative'}>
|
||||
<NumberInputField
|
||||
{...register('defaultValue', {
|
||||
min: min,
|
||||
max: max
|
||||
})}
|
||||
/>
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberInputStepper>
|
||||
</NumberInput>
|
||||
)}
|
||||
{inputType === FlowNodeInputTypeEnum.input && (
|
||||
<Input bg={'myGray.50'} maxLength={maxLength} {...register('defaultValue')} />
|
||||
@@ -280,10 +320,29 @@ const InputTypeConfig = ({
|
||||
onChange={(e) => {
|
||||
setValue('defaultValue', e);
|
||||
}}
|
||||
defaultValue={String(defaultJsonValue)}
|
||||
defaultValue={defaultValue}
|
||||
/>
|
||||
)}
|
||||
{inputType === FlowNodeInputTypeEnum.switch && <Switch {...register('defaultValue')} />}
|
||||
{inputType === FlowNodeInputTypeEnum.select && (
|
||||
<MySelect<string>
|
||||
list={[defaultListValue, ...listValue]
|
||||
.filter((item) => item.label !== '')
|
||||
.map((item) => ({
|
||||
label: item.label,
|
||||
value: item.value
|
||||
}))}
|
||||
value={
|
||||
defaultValue && listValue.map((item) => item.value).includes(defaultValue)
|
||||
? defaultValue
|
||||
: ''
|
||||
}
|
||||
onchange={(e) => {
|
||||
setValue('defaultValue', e);
|
||||
}}
|
||||
w={'200px'}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
@@ -314,40 +373,116 @@ const InputTypeConfig = ({
|
||||
|
||||
{inputType === FlowNodeInputTypeEnum.select && (
|
||||
<>
|
||||
<Flex flexDirection={'column'} gap={4}>
|
||||
{selectEnums.map((item, i) => (
|
||||
<Flex key={item.id} alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 100px'} fontWeight={'medium'}>
|
||||
{`${t('common:core.module.variable.variable options')} ${i + 1}`}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
fontSize={'12px'}
|
||||
bg={'myGray.50'}
|
||||
placeholder={`${t('common:core.module.variable.variable options')} ${i + 1}`}
|
||||
{...register(`list.${i}.label`, {
|
||||
required: true,
|
||||
onChange: (e: any) => {
|
||||
setValue(`list.${i}.value`, e.target.value);
|
||||
}
|
||||
})}
|
||||
/>
|
||||
</FormControl>
|
||||
{selectEnums.length > 1 && (
|
||||
<MyIcon
|
||||
ml={3}
|
||||
name={'delete'}
|
||||
w={'16px'}
|
||||
cursor={'pointer'}
|
||||
p={2}
|
||||
borderRadius={'md'}
|
||||
_hover={{ bg: 'red.100' }}
|
||||
onClick={() => removeEnums(i)}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
<DndDrag<{ id: string; value: string }>
|
||||
onDragEndCb={(list) => {
|
||||
const newOrder = list.map((item) => item.id);
|
||||
const newSelectEnums = newOrder
|
||||
.map((id) => mergedSelectEnums.find((item) => item.id === id))
|
||||
.filter(Boolean) as { id: string; value: string }[];
|
||||
removeEnums();
|
||||
newSelectEnums.forEach((item) => appendEnums(item));
|
||||
|
||||
// 防止最后一个元素被focus
|
||||
setTimeout(() => {
|
||||
if (document.activeElement instanceof HTMLElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
}, 0);
|
||||
}}
|
||||
dataList={mergedSelectEnums}
|
||||
renderClone={(provided, snapshot, rubric) => {
|
||||
return (
|
||||
<Box
|
||||
bg={'myGray.50'}
|
||||
border={'1px solid'}
|
||||
borderColor={'myGray.200'}
|
||||
p={2}
|
||||
borderRadius="md"
|
||||
boxShadow="md"
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
>
|
||||
{mergedSelectEnums[rubric.source.index].value}
|
||||
</Box>
|
||||
);
|
||||
}}
|
||||
>
|
||||
{(provided) => (
|
||||
<Box
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
display={'flex'}
|
||||
flexDirection={'column'}
|
||||
gap={4}
|
||||
>
|
||||
{mergedSelectEnums.map((item, i) => (
|
||||
<Draggable key={i} draggableId={i.toString()} index={i}>
|
||||
{(provided, snapshot) => (
|
||||
<Box
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
style={{
|
||||
...provided.draggableProps.style,
|
||||
opacity: snapshot.isDragging ? 0.8 : 1
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
position={'relative'}
|
||||
transform={snapshot.isDragging ? `scale(0.5)` : ''}
|
||||
transformOrigin={'top left'}
|
||||
>
|
||||
<FormLabel flex={'0 0 100px'} fontWeight={'medium'}>
|
||||
{`${t('common:core.module.variable.variable options')} ${i + 1}`}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
fontSize={'12px'}
|
||||
bg={'myGray.50'}
|
||||
placeholder={`${t('common:core.module.variable.variable options')} ${i + 1}`}
|
||||
{...register(`list.${i}.label`, {
|
||||
required: true,
|
||||
onChange: (e: any) => {
|
||||
setValue(`list.${i}.value`, e.target.value);
|
||||
}
|
||||
})}
|
||||
/>
|
||||
</FormControl>
|
||||
{selectEnums.length > 1 && (
|
||||
<Flex>
|
||||
<MyIcon
|
||||
ml={3}
|
||||
name={'delete'}
|
||||
w={'16px'}
|
||||
cursor={'pointer'}
|
||||
p={2}
|
||||
borderRadius={'md'}
|
||||
_hover={{ bg: 'red.100' }}
|
||||
onClick={() => removeEnums(i)}
|
||||
/>
|
||||
<Box {...provided.dragHandleProps}>
|
||||
<MyIcon
|
||||
name={'drag'}
|
||||
cursor={'pointer'}
|
||||
p={2}
|
||||
borderRadius={'md'}
|
||||
_hover={{ color: 'primary.600' }}
|
||||
w={'16px'}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
<Box h="0" w="0">
|
||||
{provided.placeholder}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</DndDrag>
|
||||
<Button
|
||||
variant={'whiteBase'}
|
||||
leftIcon={<MyIcon name={'common/addLight'} w={'16px'} />}
|
||||
|
@@ -21,9 +21,7 @@ const VariableTable = ({
|
||||
<Table bg={'white'}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th borderBottomLeftRadius={'none !important'}>
|
||||
{t('common:core.module.variable.variable name')}
|
||||
</Th>
|
||||
<Th borderBottomLeftRadius={'none !important'}>{t('workflow:Variable_name')}</Th>
|
||||
<Th>{t('common:core.workflow.Value type')}</Th>
|
||||
{showToolColumn && <Th>{t('workflow:tool_input')}</Th>}
|
||||
<Th borderBottomRightRadius={'none !important'}></Th>
|
||||
|
@@ -38,10 +38,7 @@ const NodeSimple = ({
|
||||
{filterHiddenInputs.length > 0 && (
|
||||
<>
|
||||
<Container>
|
||||
<IOTitle
|
||||
text={t('common:common.Input')}
|
||||
inputExplanationUrl={data.inputExplanationUrl}
|
||||
/>
|
||||
<IOTitle text={t('common:common.Input')} />
|
||||
<RenderInput nodeId={nodeId} flowInputList={commonInputs} />
|
||||
</Container>
|
||||
</>
|
||||
|
@@ -25,6 +25,8 @@ import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useWorkflowUtils } from '../../hooks/useUtils';
|
||||
import { WholeResponseContent } from '@/components/core/chat/components/WholeResponseModal';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { getDocPath } from '@/web/common/system/doc';
|
||||
|
||||
type Props = FlowNodeItemType & {
|
||||
children?: React.ReactNode | React.ReactNode[] | string;
|
||||
@@ -39,7 +41,8 @@ type Props = FlowNodeItemType & {
|
||||
copy?: boolean;
|
||||
delete?: boolean;
|
||||
};
|
||||
} & Omit<FlexProps, 'children'>;
|
||||
customStyle?: FlexProps;
|
||||
};
|
||||
|
||||
const NodeCard = (props: Props) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -63,9 +66,8 @@ const NodeCard = (props: Props) => {
|
||||
isError = false,
|
||||
debugResult,
|
||||
isFolded,
|
||||
...customStyle
|
||||
customStyle
|
||||
} = props;
|
||||
|
||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||
const setHoverNodeId = useContextSelector(WorkflowContext, (v) => v.setHoverNodeId);
|
||||
const onUpdateNodeError = useContextSelector(WorkflowContext, (v) => v.onUpdateNodeError);
|
||||
@@ -102,14 +104,6 @@ const NodeCard = (props: Props) => {
|
||||
if (!node?.pluginId) return;
|
||||
const template = await getPreviewPluginNode({ appId: node.pluginId });
|
||||
|
||||
// Focus update plugin latest inputExplanationUrl
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'attr',
|
||||
key: 'inputExplanationUrl',
|
||||
value: template.inputExplanationUrl
|
||||
});
|
||||
|
||||
return template;
|
||||
} else {
|
||||
const template = moduleTemplatesFlat.find(
|
||||
@@ -275,6 +269,24 @@ const NodeCard = (props: Props) => {
|
||||
</Box>
|
||||
</MyTooltip>
|
||||
)}
|
||||
{!!nodeTemplate?.diagram && node?.courseUrl && (
|
||||
<Box bg={'myGray.300'} w={'1px'} h={'12px'} mx={1} />
|
||||
)}
|
||||
{node?.courseUrl && !hasNewVersion && (
|
||||
<MyTooltip label={t('workflow:Node.Open_Node_Course')}>
|
||||
<MyIcon
|
||||
cursor={'pointer'}
|
||||
name="book"
|
||||
color={'primary.600'}
|
||||
w={'18px'}
|
||||
ml={1}
|
||||
_hover={{
|
||||
color: 'primary.800'
|
||||
}}
|
||||
onClick={() => window.open(getDocPath(node.courseUrl || ''), '_blank')}
|
||||
/>
|
||||
</MyTooltip>
|
||||
)}
|
||||
</Flex>
|
||||
<NodeIntro nodeId={nodeId} intro={intro} />
|
||||
</Box>
|
||||
@@ -295,6 +307,7 @@ const NodeCard = (props: Props) => {
|
||||
onOpenConfirmSync,
|
||||
onClickSyncVersion,
|
||||
nodeTemplate?.diagram,
|
||||
node?.courseUrl,
|
||||
intro,
|
||||
menuForbid,
|
||||
nodeList,
|
||||
|
@@ -148,11 +148,13 @@ type WorkflowContextType = {
|
||||
onStartNodeDebug: ({
|
||||
entryNodeId,
|
||||
runtimeNodes,
|
||||
runtimeEdges
|
||||
runtimeEdges,
|
||||
variables
|
||||
}: {
|
||||
entryNodeId: string;
|
||||
runtimeNodes: RuntimeNodeItemType[];
|
||||
runtimeEdges: RuntimeEdgeItemType[];
|
||||
variables: Record<string, any>;
|
||||
}) => Promise<void>;
|
||||
onStopNodeDebug: () => void;
|
||||
|
||||
@@ -749,17 +751,19 @@ const WorkflowContextProvider = ({
|
||||
async ({
|
||||
entryNodeId,
|
||||
runtimeNodes,
|
||||
runtimeEdges
|
||||
runtimeEdges,
|
||||
variables
|
||||
}: {
|
||||
entryNodeId: string;
|
||||
runtimeNodes: RuntimeNodeItemType[];
|
||||
runtimeEdges: RuntimeEdgeItemType[];
|
||||
variables: Record<string, any>;
|
||||
}) => {
|
||||
const data = {
|
||||
runtimeNodes,
|
||||
runtimeEdges,
|
||||
nextRunNodes: runtimeNodes.filter((node) => node.nodeId === entryNodeId),
|
||||
variables: {}
|
||||
variables
|
||||
};
|
||||
onStopNodeDebug();
|
||||
setWorkflowDebugData(data);
|
||||
|
Reference in New Issue
Block a user