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:
Archer
2024-10-15 20:23:18 +08:00
committed by GitHub
parent 00638d6ee7
commit d4e0a43771
58 changed files with 955 additions and 669 deletions

View File

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

View File

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

View File

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

View File

@@ -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)!);

View File

@@ -495,7 +495,8 @@ const ChatBox = (
// 这里,无论是否为交互模式,最后都是 Human 的消息。
const messages = chats2GPTMessages({
messages: newChatList.slice(0, -1),
reserveId: true
reserveId: true,
reserveTool: true
});
const {

View File

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

View File

@@ -228,7 +228,8 @@ const PluginRunContextProvider = ({
value: []
}
],
reserveId: true
reserveId: true,
reserveTool: true
});
try {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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'} />}

View File

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

View File

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

View File

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

View File

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