4.6.8 supplement (#831)

Co-authored-by: heheer <71265218+newfish-cmyk@users.noreply.github.com>
This commit is contained in:
Archer
2024-02-15 12:26:02 +08:00
committed by GitHub
parent 51bbdf26a3
commit 91bcf8c53e
200 changed files with 4387 additions and 2749 deletions

View File

@@ -248,12 +248,21 @@ const ResponseBox = React.memo(function ResponseBox({
{/* http */}
<>
{activeModule?.body && (
{activeModule?.headers && (
<Row
label={t('core.chat.response.module http body')}
value={`~~~json\n${JSON.stringify(activeModule?.body, null, 2)}`}
label={'Headers'}
value={`~~~json\n${JSON.stringify(activeModule?.headers, null, 2)}`}
/>
)}
{activeModule?.params && (
<Row
label={'Params'}
value={`~~~json\n${JSON.stringify(activeModule?.params, null, 2)}`}
/>
)}
{activeModule?.body && (
<Row label={'Body'} value={`~~~json\n${JSON.stringify(activeModule?.body, null, 2)}`} />
)}
{activeModule?.httpResult && (
<Row
label={t('core.chat.response.module http result')}

View File

@@ -26,25 +26,20 @@ const MdImage = ({ src }: { src?: string }) => {
};
return (
<Skeleton
minH="100px"
isLoaded={!isLoading}
fadeDuration={2}
display={'flex'}
justifyContent={'center'}
my={1}
>
<>
<Image
display={'inline-block'}
borderRadius={'md'}
src={src}
alt={''}
fallbackSrc={'/imgs/errImg.png'}
fallbackStrategy={'onError'}
cursor={succeed ? 'pointer' : 'default'}
loading="eager"
loading="lazy"
objectFit={'contain'}
referrerPolicy="no-referrer"
minW={'120px'}
minH={'120px'}
my={1}
onLoad={() => {
setIsLoading(false);
setSucceed(true);
@@ -74,7 +69,7 @@ const MdImage = ({ src }: { src?: string }) => {
</ModalContent>
<ModalCloseButton bg={'myWhite.500'} zIndex={999999} />
</Modal>
</Skeleton>
</>
);
};

View File

@@ -99,7 +99,7 @@ const Code = React.memo(function Code(e: any) {
{children}
</CodeLight>
);
}, [codeType, className, inline, match, strChildren]);
}, [codeType, className, inline, match, children, strChildren]);
return Component;
});

View File

@@ -0,0 +1,85 @@
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { Flex, Table, Thead, Tbody, Tr, Th, Td, TableContainer } from '@chakra-ui/react';
import {
DatasetSearchModeEnum,
DatasetSearchModeMap
} from '@fastgpt/global/core/dataset/constants';
import { useTranslation } from 'next-i18next';
import React, { useMemo } from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
const SearchParamsTip = ({
searchMode,
similarity = 0,
limit = 1500,
responseEmptyText,
usingReRank = false,
usingQueryExtension = false
}: {
searchMode: `${DatasetSearchModeEnum}`;
similarity?: number;
limit?: number;
responseEmptyText?: string;
usingReRank?: boolean;
usingQueryExtension?: boolean;
}) => {
const { t } = useTranslation();
const { reRankModelList } = useSystemStore();
const hasReRankModel = reRankModelList.length > 0;
const hasEmptyResponseMode = responseEmptyText !== undefined;
const hasSimilarityMode = usingReRank || searchMode === DatasetSearchModeEnum.embedding;
return (
<TableContainer
bg={'primary.50'}
borderRadius={'lg'}
borderWidth={'1px'}
borderColor={'primary.1'}
>
<Table fontSize={'xs'} overflow={'overlay'}>
<Thead>
<Tr color={'myGray.600'}>
<Th>{t('core.dataset.search.search mode')}</Th>
<Th>{t('core.dataset.search.Max Tokens')}</Th>
<Th>{t('core.dataset.search.Min Similarity')}</Th>
{hasReRankModel && <Th>{t('core.dataset.search.ReRank')}</Th>}
<Th>{t('core.module.template.Query extension')}</Th>
{hasEmptyResponseMode && <Th>{t('core.dataset.search.Empty result response')}</Th>}
</Tr>
</Thead>
<Tbody>
<Tr color={'myGray.800'}>
<Td pt={0} pb={1}>
<Flex alignItems={'center'}>
<MyIcon
name={DatasetSearchModeMap[searchMode]?.icon as any}
w={'12px'}
mr={'1px'}
/>
{t(DatasetSearchModeMap[searchMode]?.title)}
</Flex>
</Td>
<Td pt={0} pb={1}>
{limit}
</Td>
<Td pt={0} pb={1}>
{hasSimilarityMode ? similarity : t('core.dataset.search.Nonsupport')}
</Td>
{hasReRankModel && (
<Td pt={0} pb={1}>
{usingReRank ? '✅' : '❌'}
</Td>
)}
<Td pt={0} pb={1}>
{usingQueryExtension ? '✅' : '❌'}
</Td>
{hasEmptyResponseMode && <Th>{responseEmptyText !== '' ? '✅' : '❌'}</Th>}
</Tr>
</Tbody>
</Table>
</TableContainer>
);
};
export default React.memo(SearchParamsTip);

View File

@@ -37,7 +37,7 @@ export type DatasetParamsProps = {
datasetSearchExtensionModel?: string;
datasetSearchExtensionBg?: string;
maxTokens?: number;
maxTokens?: number; // limit max tokens
searchEmptyText?: string;
};
enum SearchSettingTabEnum {

View File

@@ -129,7 +129,7 @@ const NodeDatasetConcat = ({ data, selected }: NodeProps<FlowModuleItemType>) =>
/>
{/* render dataset select */}
{RenderQuoteList}
<Flex position={'absolute'} right={4} top={'50%'} transform={'translate(0,-50%)'}>
<Flex position={'absolute'} right={4} top={'60%'}>
<Box>{t('core.module.Dataset quote.Concat result')}</Box>
<SourceHandle
handleKey={ModuleOutputKeyEnum.datasetQuoteQA}

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useCallback, useMemo, useState, useTransition } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../render/NodeCard';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
@@ -6,15 +6,528 @@ import Divider from '../modules/Divider';
import Container from '../modules/Container';
import RenderInput from '../render/RenderInput';
import RenderOutput from '../render/RenderOutput';
import {
Box,
Flex,
Input,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer
} from '@chakra-ui/react';
import MySelect from '@/components/Select';
import { ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
import { onChangeNode, useFlowProviderStore } from '../../FlowProvider';
import { useTranslation } from 'next-i18next';
import Tabs from '@/components/Tabs';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { FlowNodeInputItemType } from '@fastgpt/global/core/module/node/type';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useForm } from 'react-hook-form';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import JSONEditor from '@fastgpt/web/components/common/Textarea/JsonEditor';
import {
formatEditorVariablePickerIcon,
getGuideModule,
splitGuideModule
} from '@fastgpt/global/core/module/utils';
import { EditorVariablePickerType } from '@fastgpt/web/components/common/Textarea/PromptEditor/type';
enum TabEnum {
params = 'params',
headers = 'headers',
body = 'body'
}
type PropsArrType = {
key: string;
type: string;
value: string;
};
const RenderHttpMethodAndUrl = React.memo(function RenderHttpMethodAndUrl({
moduleId,
inputs
}: {
moduleId: string;
inputs: FlowNodeInputItemType[];
}) {
const { t } = useTranslation();
const { toast } = useToast();
const [_, startSts] = useTransition();
const requestMethods = inputs.find((item) => item.key === ModuleInputKeyEnum.httpMethod);
const requestUrl = inputs.find((item) => item.key === ModuleInputKeyEnum.httpReqUrl);
const onChangeUrl = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: ModuleInputKeyEnum.httpReqUrl,
value: {
...requestUrl,
value: e.target.value
}
});
},
[moduleId, requestUrl]
);
const onBlurUrl = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const val = e.target.value;
// 拆分params和url
const url = val.split('?')[0];
const params = val.split('?')[1];
if (params) {
const paramsArr = params.split('&');
const paramsObj = paramsArr.reduce((acc, cur) => {
const [key, value] = cur.split('=');
return {
...acc,
[key]: value
};
}, {});
const inputParams = inputs.find((item) => item.key === ModuleInputKeyEnum.httpParams);
if (!inputParams || Object.keys(paramsObj).length === 0) return;
const concatParams: PropsArrType[] = inputParams?.value || [];
Object.entries(paramsObj).forEach(([key, value]) => {
if (!concatParams.find((item) => item.key === key)) {
concatParams.push({ key, value: value as string, type: 'string' });
}
});
onChangeNode({
moduleId,
type: 'updateInput',
key: ModuleInputKeyEnum.httpParams,
value: {
...inputParams,
value: concatParams
}
});
onChangeNode({
moduleId,
type: 'updateInput',
key: ModuleInputKeyEnum.httpReqUrl,
value: {
...requestUrl,
value: url
}
});
toast({
status: 'success',
title: t('core.module.http.Url and params have been split')
});
}
},
[inputs, moduleId, requestUrl, t, toast]
);
return (
<Box>
<Box mb={2}>{t('core.module.Http request settings')}</Box>
<Flex alignItems={'center'} className="nodrag">
<MySelect
h={'34px'}
w={'80px'}
bg={'myGray.50'}
width={'100%'}
value={requestMethods?.value}
list={[
{
label: 'GET',
value: 'GET'
},
{
label: 'POST',
value: 'POST'
}
]}
onchange={(e) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: ModuleInputKeyEnum.httpMethod,
value: {
...requestMethods,
value: e
}
});
}}
/>
<Input
ml={2}
h={'34px'}
value={requestUrl?.value}
placeholder={t('core.module.input.label.Http Request Url')}
fontSize={'xs'}
w={'350px'}
onChange={onChangeUrl}
onBlur={onBlurUrl}
/>
</Flex>
</Box>
);
});
const defaultForm = {
key: '',
value: ''
};
function RenderHttpProps({
moduleId,
inputs
}: {
moduleId: string;
inputs: FlowNodeInputItemType[];
}) {
const { t } = useTranslation();
const [selectedTab, setSelectedTab] = useState(TabEnum.params);
const { nodes } = useFlowProviderStore();
const requestMethods = inputs.find((item) => item.key === ModuleInputKeyEnum.httpMethod)?.value;
const params = inputs.find((item) => item.key === ModuleInputKeyEnum.httpParams);
const headers = inputs.find((item) => item.key === ModuleInputKeyEnum.httpHeaders);
const jsonBody = inputs.find((item) => item.key === ModuleInputKeyEnum.httpJsonBody);
const paramsLength = params?.value?.length || 0;
const headersLength = headers?.value?.length || 0;
// get variable
const variables = useMemo(() => {
const globalVariables = formatEditorVariablePickerIcon(
splitGuideModule(getGuideModule(nodes.map((node) => node.data)))?.variableModules || []
);
const systemVariables = [
{
key: 'appId',
label: t('core.module.http.AppId')
},
{
key: 'chatId',
label: t('core.module.http.ChatId')
},
{
key: 'responseChatItemId',
label: t('core.module.http.ResponseChatItemId')
},
{
key: 'variables',
label: t('core.module.http.Variables')
},
{
key: 'histories',
label: t('core.module.http.Histories')
},
{
key: 'cTime',
label: t('core.module.http.Current time')
}
];
const moduleVariables = formatEditorVariablePickerIcon(
inputs
.filter((input) => input.edit)
.map((item) => ({
key: item.key,
label: item.label
}))
);
return [...moduleVariables, ...globalVariables, ...systemVariables];
}, [inputs, nodes, t]);
const variableText = useMemo(() => {
return variables
.map((item) => `${item.key}${item.key !== item.label ? `(${item.label})` : ''}`)
.join('\n');
}, [variables]);
return (
<Box>
<Flex alignItems={'center'} mb={2}>
{t('core.module.Http request props')}
<MyTooltip label={t('core.module.http.Props tip', { variable: variableText })}>
<QuestionOutlineIcon ml={1} />
</MyTooltip>
</Flex>
<Tabs
list={[
{ label: <RenderPropsItem text="Params" num={paramsLength} />, id: TabEnum.params },
...(requestMethods === 'POST'
? [
{
label: (
<Flex alignItems={'center'}>
Body
{jsonBody?.value && <Box ml={1}></Box>}
</Flex>
),
id: TabEnum.body
}
]
: []),
{ label: <RenderPropsItem text="Headers" num={headersLength} />, id: TabEnum.headers }
]}
activeId={selectedTab}
onChange={(e) => setSelectedTab(e as any)}
/>
{params &&
headers &&
jsonBody &&
{
[TabEnum.params]: <RenderForm moduleId={moduleId} input={params} variables={variables} />,
[TabEnum.body]: <RenderJson moduleId={moduleId} variables={variables} input={jsonBody} />,
[TabEnum.headers]: (
<RenderForm moduleId={moduleId} input={headers} variables={variables} />
)
}[selectedTab]}
</Box>
);
}
const RenderForm = ({
moduleId,
input,
variables
}: {
moduleId: string;
input: FlowNodeInputItemType;
variables: EditorVariablePickerType[];
}) => {
const { t } = useTranslation();
const { toast } = useToast();
const [_, startSts] = useTransition();
const { register, reset, handleSubmit } = useForm({
defaultValues: defaultForm
});
const list = useMemo(() => (input.value || []) as PropsArrType[], [input.value]);
const addNewProps = useCallback(
({ key, value }: { key: string; value: string }) => {
const checkExist = list.find((item) => item.key === key);
if (checkExist) {
return toast({
status: 'warning',
title: t('core.module.http.Key already exists')
});
}
if (!key) return;
onChangeNode({
moduleId,
type: 'updateInput',
key: input.key,
value: {
...input,
value: [...list, { key, type: 'string', value }]
}
});
reset(defaultForm);
},
[input, list, moduleId, reset, t, toast]
);
return (
<TableContainer>
<Table>
<Thead>
<Tr>
<Th px={2}>{t('core.module.http.Props name')}</Th>
<Th px={2}>{t('core.module.http.Props value')}</Th>
</Tr>
</Thead>
<Tbody>
{list.map((item, index) => (
<Tr key={`${input.key}${index}`}>
<Td p={0} w={'150px'}>
<Input
w={'150px'}
defaultValue={item.key}
variant={'unstyled'}
paddingLeft={2}
placeholder={t('core.module.http.Props name')}
onBlur={(e) => {
const val = e.target.value;
if (!val) {
return toast({
status: 'warning',
title: t('core.module.http.Key cannot be empty')
});
}
const checkExist = list.find((item, i) => i !== index && item.key == val);
if (checkExist) {
return toast({
status: 'warning',
title: t('core.module.http.Key already exists')
});
}
startSts(() => {
onChangeNode({
moduleId,
type: 'updateInput',
key: input.key,
value: {
...input,
value: list.map((item, i) => (i === index ? { ...item, key: val } : item))
}
});
});
}}
/>
</Td>
<Td p={0} display={'flex'} alignItems={'center'}>
<Input
flex={'1 0 0'}
w={'150px'}
defaultValue={item.value}
variant={'unstyled'}
paddingLeft={2}
placeholder={t('core.module.http.Props value')}
onBlur={(e) => {
const val = e.target.value;
startSts(() => {
onChangeNode({
moduleId,
type: 'updateInput',
key: input.key,
value: {
...input,
value: list.map((item, i) =>
i === index ? { ...item, value: val } : item
)
}
});
});
}}
/>
<MyIcon
name={'delete'}
cursor={'pointer'}
_hover={{ color: 'red.600' }}
w={'14px'}
onClick={(e) => {
e.stopPropagation();
onChangeNode({
moduleId,
type: 'updateInput',
key: input.key,
value: {
...input,
value: list.filter((val) => val.key !== item.key)
}
});
}}
/>
</Td>
</Tr>
))}
<Tr>
<Td p={0} w={'150px'}>
<Input
w={'150px'}
variant={'unstyled'}
paddingLeft={2}
placeholder={t('core.module.http.Add props')}
{...register('key', {
onBlur: handleSubmit(addNewProps)
})}
/>
</Td>
<Td p={0}>
<Input variant={'unstyled'} paddingLeft={2} {...register('value')} />
</Td>
</Tr>
</Tbody>
</Table>
</TableContainer>
);
};
const RenderJson = ({
moduleId,
input,
variables
}: {
moduleId: string;
input: FlowNodeInputItemType;
variables: EditorVariablePickerType[];
}) => {
const [_, startSts] = useTransition();
return (
<Box mt={1}>
<JSONEditor
bg={'myGray.50'}
height={200}
resize
value={input.value}
onChange={(e) => {
startSts(() => {
onChangeNode({
moduleId,
type: 'updateInput',
key: input.key,
value: {
...input,
value: e
}
});
});
}}
variables={variables}
/>
</Box>
);
};
const RenderPropsItem = ({ text, num }: { text: string; num: number }) => {
return (
<Flex alignItems={'center'} fontSize={'xs'} transform={'scale(0.8)'}>
<Box>{text}</Box>
{num > 0 && (
<Box ml={1} borderRadius={'50%'} bg={'myGray.200'} px={2} py={'1px'}>
{num}
</Box>
)}
</Flex>
);
};
const NodeHttp = ({ data, selected }: NodeProps<FlowModuleItemType>) => {
const { moduleId, inputs, outputs } = data;
const CustomComponents = useMemo(
() => ({
[ModuleInputKeyEnum.httpMethod]: () => (
<RenderHttpMethodAndUrl moduleId={moduleId} inputs={inputs} />
),
[ModuleInputKeyEnum.httpHeaders]: () => (
<>
<RenderHttpProps moduleId={moduleId} inputs={inputs} />
<Box mt={2} transform={'translateY(10px)'}>
</Box>
</>
)
}),
[inputs, moduleId]
);
return (
<NodeCard minW={'350px'} selected={selected} {...data}>
<Divider text="Input" />
<Container>
<RenderInput moduleId={moduleId} flowInputList={inputs} />
<RenderInput
moduleId={moduleId}
flowInputList={inputs}
CustomComponent={CustomComponents}
/>
</Container>
<Divider text="Output" />
<Container>

View File

@@ -135,38 +135,38 @@ const NodeCard = (props: Props) => {
boxShadow: '4'
}}
>
<Flex className="custom-drag-handle" px={4} py={3} alignItems={'center'}>
<Avatar src={avatar} borderRadius={'0'} objectFit={'contain'} w={'30px'} h={'30px'} />
<Box ml={3} fontSize={'lg'} color={'myGray.600'}>
{t(name)}
<Box className="custom-drag-handle" px={4} py={3}>
<Flex alignItems={'center'}>
<Avatar src={avatar} borderRadius={'0'} objectFit={'contain'} w={'30px'} h={'30px'} />
<Box ml={3} fontSize={'lg'}>
{t(name)}
</Box>
<Box flex={1} />
{!forbidMenu && (
<MyMenu
offset={[-60, 5]}
width={120}
Button={
<MenuButton
className={'nodrag'}
_hover={{ bg: 'myWhite.600' }}
cursor={'pointer'}
borderRadius={'md'}
onClick={(e) => {
e.stopPropagation();
}}
>
<MyIcon name={'more'} w={'14px'} p={2} />
</MenuButton>
}
menuList={menuList}
/>
)}
</Flex>
<Box fontSize={'xs'} color={'myGray.600'}>
{t(intro)}
</Box>
{intro && (
<MyTooltip label={t(intro)} forceShow>
<QuestionOutlineIcon display={['none', 'inline']} mb={'1px'} ml={1} />
</MyTooltip>
)}
<Box flex={1} />
{!forbidMenu && (
<MyMenu
offset={[-60, 5]}
width={120}
Button={
<MenuButton
className={'nodrag'}
_hover={{ bg: 'myWhite.600' }}
cursor={'pointer'}
borderRadius={'md'}
onClick={(e) => {
e.stopPropagation();
}}
>
<MyIcon name={'more'} w={'14px'} p={2} />
</MenuButton>
}
menuList={menuList}
/>
)}
</Flex>
</Box>
{children}
<EditTitleModal />
<ConfirmModal />

View File

@@ -45,13 +45,19 @@ const JsonEditor = ({ inputs = [], item, moduleId }: RenderInputProps) => {
[item, moduleId]
);
const value = useMemo(() => {
if (typeof item.value === 'string') {
return item.value;
}
return JSON.stringify(item.value, null, 2);
}, [item.value]);
return (
<JSONEditor
title={t(item.label)}
bg={'myWhite.400'}
bg={'myGray.50'}
placeholder={t(item.placeholder || '')}
resize
value={item.value}
value={value}
onChange={(e) => {
update(e);
}}

View File

@@ -1,7 +1,7 @@
import React, { useEffect, useMemo, useState } from 'react';
import type { RenderInputProps } from '../type';
import { onChangeNode, useFlowProviderStore } from '../../../../FlowProvider';
import { Button, useDisclosure } from '@chakra-ui/react';
import { Box, Button, Flex, useDisclosure } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { DatasetSearchModeEnum } from '@fastgpt/global/core/dataset/constants';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
@@ -11,6 +11,7 @@ import DatasetParamsModal, {
DatasetParamsProps
} from '@/components/core/module/DatasetParamsModal';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import SearchParamsTip from '@/components/core/dataset/SearchParamsTip';
const SelectDatasetParam = ({ inputs = [], moduleId }: RenderInputProps) => {
const { nodes } = useFlowProviderStore();
@@ -61,42 +62,60 @@ const SelectDatasetParam = ({ inputs = [], moduleId }: RenderInputProps) => {
const Render = useMemo(() => {
return (
<>
<Button
variant={'whitePrimary'}
leftIcon={<MyIcon name={'common/settingLight'} w={'14px'} />}
onClick={onOpen}
>
{/* label */}
<Flex alignItems={'center'} mb={3}>
{t('core.dataset.search.Params Setting')}
</Button>
{isOpen && (
<DatasetParamsModal
{...data}
maxTokens={tokenLimit}
onClose={onClose}
onSuccess={(e) => {
setData(e);
for (let key in e) {
const item = inputs.find((input) => input.key === key);
if (!item) continue;
onChangeNode({
moduleId,
type: 'updateInput',
key,
value: {
...item,
//@ts-ignore
value: e[key]
}
});
}
<MyIcon
name={'common/settingLight'}
ml={2}
w={'16px'}
cursor={'pointer'}
_hover={{
color: 'primary.600'
}}
onClick={onOpen}
/>
)}
</Flex>
<SearchParamsTip
searchMode={data.searchMode}
similarity={data.similarity}
limit={data.limit}
usingReRank={data.usingReRank}
usingQueryExtension={data.datasetSearchUsingExtensionQuery}
/>
</>
);
}, [data, inputs, isOpen, moduleId, onClose, onOpen, t, tokenLimit]);
}, [data, onOpen, t]);
return Render;
return (
<>
{Render}
{isOpen && (
<DatasetParamsModal
{...data}
maxTokens={tokenLimit}
onClose={onClose}
onSuccess={(e) => {
setData(e);
for (let key in e) {
const item = inputs.find((input) => input.key === key);
if (!item) continue;
onChangeNode({
moduleId,
type: 'updateInput',
key,
value: {
...item,
//@ts-ignore
value: e[key]
}
});
}
}}
/>
)}
</>
);
};
export default React.memo(SelectDatasetParam);

View File

@@ -26,9 +26,15 @@ const TextareaRender = ({ inputs = [], item, moduleId }: RenderInputProps) => {
label: item.label
}))
);
const systemVariables = [
{
key: 'cTime',
label: t('core.module.http.Current time')
}
];
return [...globalVariables, ...moduleVariables];
}, [inputs, nodes]);
return [...globalVariables, ...moduleVariables, ...systemVariables];
}, [inputs, nodes, t]);
const onChange = useCallback(
(e: string) => {

View File

@@ -1,5 +1,5 @@
import React, { useMemo } from 'react';
import ReactFlow, { Background, Controls, ReactFlowProvider } from 'reactflow';
import React, { useCallback, useMemo } from 'react';
import ReactFlow, { Background, Connection, Controls, ReactFlowProvider } from 'reactflow';
import { Box, Flex, IconButton, useDisclosure } from '@chakra-ui/react';
import { SmallCloseIcon } from '@chakra-ui/icons';
import { EDGE_TYPE, FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
@@ -11,6 +11,8 @@ import ModuleTemplateList, { type ModuleTemplateProps } from './ModuleTemplateLi
import { useFlowProviderStore } from './FlowProvider';
import 'reactflow/dist/style.css';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useTranslation } from 'next-i18next';
const NodeSimple = dynamic(() => import('./components/nodes/NodeSimple'));
const nodeTypes: Record<`${FlowNodeTypeEnum}`, any> = {
@@ -25,7 +27,8 @@ const nodeTypes: Record<`${FlowNodeTypeEnum}`, any> = {
[FlowNodeTypeEnum.answerNode]: dynamic(() => import('./components/nodes/NodeAnswer')),
[FlowNodeTypeEnum.classifyQuestion]: dynamic(() => import('./components/nodes/NodeCQNode')),
[FlowNodeTypeEnum.contentExtract]: dynamic(() => import('./components/nodes/NodeExtract')),
[FlowNodeTypeEnum.httpRequest]: dynamic(() => import('./components/nodes/NodeHttp')),
[FlowNodeTypeEnum.httpRequest468]: dynamic(() => import('./components/nodes/NodeHttp')),
[FlowNodeTypeEnum.httpRequest]: NodeSimple,
[FlowNodeTypeEnum.runApp]: NodeSimple,
[FlowNodeTypeEnum.pluginInput]: dynamic(() => import('./components/nodes/NodePluginInput')),
[FlowNodeTypeEnum.pluginOutput]: dynamic(() => import('./components/nodes/NodePluginOutput')),
@@ -37,6 +40,8 @@ const edgeTypes = {
};
const Container = React.memo(function Container() {
const { toast } = useToast();
const { t } = useTranslation();
const { reactFlowWrapper, nodes, onNodesChange, edges, onEdgesChange, onConnect } =
useFlowProviderStore();
@@ -50,6 +55,24 @@ const Container = React.memo(function Container() {
[]
);
const customOnConnect = useCallback(
(connect: Connection) => {
if (!connect.sourceHandle || !connect.targetHandle) {
return;
}
if (connect.source === connect.target) {
return toast({
status: 'warning',
title: t('core.module.Can not connect self')
});
}
onConnect({
connect
});
},
[onConnect, t, toast]
);
return (
<ReactFlow
ref={reactFlowWrapper}
@@ -68,13 +91,7 @@ const Container = React.memo(function Container() {
edgeTypes={edgeTypes}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={(connect) => {
connect.sourceHandle &&
connect.targetHandle &&
onConnect({
connect
});
}}
onConnect={customOnConnect}
>
{memoRenderTools}
</ReactFlow>

View File

@@ -14,7 +14,7 @@ export const flowNode2Modules = ({
const modules: ModuleItemType[] = nodes.map((item) => ({
moduleId: item.data.moduleId,
name: item.data.name,
// avatar: item.data.avatar,
avatar: item.data.avatar,
flowType: item.data.flowType,
showStatus: item.data.showStatus,
position: item.position,