v4.5.2 (#439)
This commit is contained in:
@@ -77,13 +77,7 @@ const QuoteModal = ({
|
||||
</>
|
||||
}
|
||||
>
|
||||
<ModalBody
|
||||
pt={0}
|
||||
whiteSpace={'pre-wrap'}
|
||||
textAlign={'justify'}
|
||||
wordBreak={'break-all'}
|
||||
fontSize={'sm'}
|
||||
>
|
||||
<ModalBody pt={0} whiteSpace={'pre-wrap'} textAlign={'justify'} wordBreak={'break-all'}>
|
||||
{rawSearch.map((item, i) => (
|
||||
<Box
|
||||
key={i}
|
||||
@@ -95,11 +89,18 @@ const QuoteModal = ({
|
||||
position={'relative'}
|
||||
overflow={'hidden'}
|
||||
_hover={{ '& .hover-data': { display: 'flex' } }}
|
||||
bg={i % 2 === 0 ? 'white' : 'myWhite.500'}
|
||||
>
|
||||
{!isShare && (
|
||||
<Flex alignItems={'flex-end'} mb={3} color={'myGray.500'}>
|
||||
<RawSourceText sourceName={item.sourceName} sourceId={item.sourceId} />
|
||||
<Box flex={1} />
|
||||
<Flex alignItems={'flex-end'} mb={3} fontSize={'sm'}>
|
||||
<RawSourceText
|
||||
fontWeight={'bold'}
|
||||
color={'black'}
|
||||
sourceName={item.sourceName}
|
||||
sourceId={item.sourceId}
|
||||
addr={!isShare}
|
||||
/>
|
||||
<Box flex={1} />
|
||||
{!isShare && (
|
||||
<Link
|
||||
as={NextLink}
|
||||
className="hover-data"
|
||||
@@ -111,13 +112,13 @@ const QuoteModal = ({
|
||||
{t('core.dataset.Go Dataset')}
|
||||
<MyIcon name={'rightArrowLight'} w={'10px'} />
|
||||
</Link>
|
||||
</Flex>
|
||||
)}
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<Box color={'black'}>{item.q}</Box>
|
||||
<Box color={'black'}>{item.a}</Box>
|
||||
<Box color={'myGray.600'}>{item.a}</Box>
|
||||
{!isShare && (
|
||||
<Flex alignItems={'center'} mt={3} gap={4} color={'myGray.500'}>
|
||||
<Flex alignItems={'center'} fontSize={'sm'} mt={3} gap={4} color={'myGray.500'}>
|
||||
{isPc && (
|
||||
<MyTooltip label={t('core.dataset.data.id')}>
|
||||
<Flex border={theme.borders.base} px={3} borderRadius={'md'}>
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { ChatHistoryItemResType, ChatItemType } from '@/types/chat';
|
||||
import { Flex, BoxProps, useDisclosure } from '@chakra-ui/react';
|
||||
import { Flex, BoxProps, useDisclosure, Image, useTheme } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import type { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
|
||||
import dynamic from 'next/dynamic';
|
||||
import Tag from '../Tag';
|
||||
import MyTooltip from '../MyTooltip';
|
||||
import { FlowModuleTypeEnum } from '@/constants/flow';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
|
||||
import { getSourceNameIcon } from '@fastgpt/global/core/dataset/utils';
|
||||
import ChatBoxDivider from '@/components/core/chat/Divider';
|
||||
|
||||
const QuoteModal = dynamic(() => import('./QuoteModal'), { ssr: false });
|
||||
const ContextModal = dynamic(() => import('./ContextModal'), { ssr: false });
|
||||
const WholeResponseModal = dynamic(() => import('./WholeResponseModal'), { ssr: false });
|
||||
|
||||
const ResponseTags = ({ responseData = [] }: { responseData?: ChatHistoryItemResType[] }) => {
|
||||
const theme = useTheme();
|
||||
const { isPc } = useSystemStore();
|
||||
const { t } = useTranslation();
|
||||
const [quoteModalData, setQuoteModalData] = useState<SearchDataResponseItemType[]>();
|
||||
@@ -27,18 +30,36 @@ const ResponseTags = ({ responseData = [] }: { responseData?: ChatHistoryItemRes
|
||||
const {
|
||||
chatAccount,
|
||||
quoteList = [],
|
||||
sourceList = [],
|
||||
historyPreview = [],
|
||||
runningTime = 0
|
||||
} = useMemo(() => {
|
||||
const chatData = responseData.find((item) => item.moduleType === FlowModuleTypeEnum.chatNode);
|
||||
const chatData = responseData.find((item) => item.moduleType === FlowNodeTypeEnum.chatNode);
|
||||
const quoteList = responseData
|
||||
.filter((item) => item.moduleType === FlowNodeTypeEnum.chatNode)
|
||||
.map((item) => item.quoteList)
|
||||
.flat()
|
||||
.filter((item) => item) as SearchDataResponseItemType[];
|
||||
const sourceList = quoteList.reduce(
|
||||
(acc: Record<string, SearchDataResponseItemType[]>, cur) => {
|
||||
if (!acc[cur.sourceName]) {
|
||||
acc[cur.sourceName] = [cur];
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
return {
|
||||
chatAccount: responseData.filter((item) => item.moduleType === FlowModuleTypeEnum.chatNode)
|
||||
chatAccount: responseData.filter((item) => item.moduleType === FlowNodeTypeEnum.chatNode)
|
||||
.length,
|
||||
quoteList: responseData
|
||||
.filter((item) => item.moduleType === FlowModuleTypeEnum.chatNode)
|
||||
.map((item) => item.quoteList)
|
||||
quoteList,
|
||||
sourceList: Object.values(sourceList)
|
||||
.flat()
|
||||
.filter((item) => item) as SearchDataResponseItemType[],
|
||||
.map((item) => ({
|
||||
sourceName: item.sourceName,
|
||||
icon: getSourceNameIcon({ sourceId: item.sourceId, sourceName: item.sourceName })
|
||||
})),
|
||||
historyPreview: chatData?.historyPreview,
|
||||
runningTime: +responseData.reduce((sum, item) => sum + (item.runningTime || 0), 0).toFixed(2)
|
||||
};
|
||||
@@ -50,64 +71,93 @@ const ResponseTags = ({ responseData = [] }: { responseData?: ChatHistoryItemRes
|
||||
};
|
||||
|
||||
return responseData.length === 0 ? null : (
|
||||
<Flex alignItems={'center'} mt={2} flexWrap={'wrap'}>
|
||||
{quoteList.length > 0 && (
|
||||
<MyTooltip label="查看引用">
|
||||
<Tag
|
||||
colorSchema="blue"
|
||||
cursor={'pointer'}
|
||||
{...TagStyles}
|
||||
onClick={() => setQuoteModalData(quoteList)}
|
||||
>
|
||||
{quoteList.length}条引用
|
||||
</Tag>
|
||||
</MyTooltip>
|
||||
)}
|
||||
{chatAccount === 1 && (
|
||||
<>
|
||||
{sourceList.length > 0 && (
|
||||
<>
|
||||
{historyPreview.length > 0 && (
|
||||
<MyTooltip label={'点击查看完整对话记录'}>
|
||||
<Tag
|
||||
colorSchema="green"
|
||||
<ChatBoxDivider icon="core/chat/quoteFill" text={t('chat.Quote')} />
|
||||
<Flex alignItems={'center'} flexWrap={'wrap'} gap={2}>
|
||||
{sourceList.map((item) => (
|
||||
<Flex
|
||||
key={item.sourceName}
|
||||
alignItems={'center'}
|
||||
flexWrap={'wrap'}
|
||||
fontSize={'sm'}
|
||||
cursor={'pointer'}
|
||||
{...TagStyles}
|
||||
onClick={() => setContextModalData(historyPreview)}
|
||||
border={theme.borders.sm}
|
||||
py={1}
|
||||
px={2}
|
||||
borderRadius={'md'}
|
||||
_hover={{
|
||||
bg: 'myBlue.100'
|
||||
}}
|
||||
onClick={() => setQuoteModalData(quoteList)}
|
||||
>
|
||||
{historyPreview.length}条上下文
|
||||
</Tag>
|
||||
</MyTooltip>
|
||||
)}
|
||||
<Image src={item.icon} alt={''} mr={1} w={'12px'} />
|
||||
{item.sourceName}
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
{chatAccount > 1 && (
|
||||
<Tag colorSchema="blue" {...TagStyles}>
|
||||
多组 AI 对话
|
||||
</Tag>
|
||||
)}
|
||||
<Flex alignItems={'center'} mt={2} flexWrap={'wrap'}>
|
||||
{quoteList.length > 0 && (
|
||||
<MyTooltip label="查看引用">
|
||||
<Tag
|
||||
colorSchema="blue"
|
||||
cursor={'pointer'}
|
||||
{...TagStyles}
|
||||
onClick={() => setQuoteModalData(quoteList)}
|
||||
>
|
||||
{quoteList.length}条引用
|
||||
</Tag>
|
||||
</MyTooltip>
|
||||
)}
|
||||
{chatAccount === 1 && (
|
||||
<>
|
||||
{historyPreview.length > 0 && (
|
||||
<MyTooltip label={'点击查看完整对话记录'}>
|
||||
<Tag
|
||||
colorSchema="green"
|
||||
cursor={'pointer'}
|
||||
{...TagStyles}
|
||||
onClick={() => setContextModalData(historyPreview)}
|
||||
>
|
||||
{historyPreview.length}条上下文
|
||||
</Tag>
|
||||
</MyTooltip>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{chatAccount > 1 && (
|
||||
<Tag colorSchema="blue" {...TagStyles}>
|
||||
多组 AI 对话
|
||||
</Tag>
|
||||
)}
|
||||
|
||||
{isPc && runningTime > 0 && (
|
||||
<MyTooltip label={'模块运行时间和'}>
|
||||
<Tag colorSchema="purple" cursor={'default'} {...TagStyles}>
|
||||
{runningTime}s
|
||||
{isPc && runningTime > 0 && (
|
||||
<MyTooltip label={'模块运行时间和'}>
|
||||
<Tag colorSchema="purple" cursor={'default'} {...TagStyles}>
|
||||
{runningTime}s
|
||||
</Tag>
|
||||
</MyTooltip>
|
||||
)}
|
||||
<MyTooltip label={'点击查看完整响应'}>
|
||||
<Tag colorSchema="gray" cursor={'pointer'} {...TagStyles} onClick={onOpenWholeModal}>
|
||||
{t('chat.Complete Response')}
|
||||
</Tag>
|
||||
</MyTooltip>
|
||||
)}
|
||||
<MyTooltip label={'点击查看完整响应'}>
|
||||
<Tag colorSchema="gray" cursor={'pointer'} {...TagStyles} onClick={onOpenWholeModal}>
|
||||
{t('chat.Complete Response')}
|
||||
</Tag>
|
||||
</MyTooltip>
|
||||
|
||||
{!!quoteModalData && (
|
||||
<QuoteModal rawSearch={quoteModalData} onClose={() => setQuoteModalData(undefined)} />
|
||||
)}
|
||||
{!!contextModalData && (
|
||||
<ContextModal context={contextModalData} onClose={() => setContextModalData(undefined)} />
|
||||
)}
|
||||
{isOpenWholeModal && (
|
||||
<WholeResponseModal response={responseData} onClose={onCloseWholeModal} />
|
||||
)}
|
||||
</Flex>
|
||||
{!!quoteModalData && (
|
||||
<QuoteModal rawSearch={quoteModalData} onClose={() => setQuoteModalData(undefined)} />
|
||||
)}
|
||||
{!!contextModalData && (
|
||||
<ContextModal context={contextModalData} onClose={() => setContextModalData(undefined)} />
|
||||
)}
|
||||
{isOpenWholeModal && (
|
||||
<WholeResponseModal response={responseData} onClose={onCloseWholeModal} />
|
||||
)}
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ function Row({ label, value }: { label: string; value?: string | number | React.
|
||||
) : null;
|
||||
}
|
||||
|
||||
const ResponseModal = ({
|
||||
const WholeResponseModal = ({
|
||||
response,
|
||||
onClose
|
||||
}: {
|
||||
@@ -50,6 +50,7 @@ const ResponseModal = ({
|
||||
<Image
|
||||
mr={2}
|
||||
src={
|
||||
item.moduleLogo ||
|
||||
ModuleTemplatesFlat.find((template) => item.moduleType === template.flowType)?.logo
|
||||
}
|
||||
alt={''}
|
||||
@@ -192,10 +193,22 @@ const ResponseModal = ({
|
||||
}
|
||||
})()}
|
||||
/>
|
||||
|
||||
{/* plugin */}
|
||||
<Row
|
||||
label={t('chat.response.plugin output')}
|
||||
value={(() => {
|
||||
try {
|
||||
return JSON.stringify(activeModule?.pluginOutput, null, 2);
|
||||
} catch (error) {
|
||||
return '';
|
||||
}
|
||||
})()}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResponseModal;
|
||||
export default WholeResponseModal;
|
||||
|
||||
@@ -35,7 +35,7 @@ import { feConfigs } from '@/web/common/system/staticData';
|
||||
import { eventBus } from '@/web/common/utils/eventbus';
|
||||
import { adaptChat2GptMessages } from '@/utils/common/adapt/message';
|
||||
import { useMarkdown } from '@/web/common/hooks/useMarkdown';
|
||||
import { AppModuleItemType } from '@/types/app';
|
||||
import { ModuleItemType } from '@fastgpt/global/core/module/type.d';
|
||||
import { VariableInputEnum } from '@/constants/app';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import type { MessageItemType } from '@/types/core/chat/type';
|
||||
@@ -54,6 +54,7 @@ import Avatar from '@/components/Avatar';
|
||||
import Markdown from '@/components/Markdown';
|
||||
import MySelect from '@/components/Select';
|
||||
import MyTooltip from '../MyTooltip';
|
||||
import ChatBoxDivider from '@/components/core/chat/Divider';
|
||||
import dynamic from 'next/dynamic';
|
||||
const ResponseTags = dynamic(() => import('./ResponseTags'));
|
||||
const FeedbackModal = dynamic(() => import('./FeedbackModal'));
|
||||
@@ -99,7 +100,7 @@ type Props = {
|
||||
showEmptyIntro?: boolean;
|
||||
appAvatar?: string;
|
||||
userAvatar?: string;
|
||||
userGuideModule?: AppModuleItemType;
|
||||
userGuideModule?: ModuleItemType;
|
||||
active?: boolean;
|
||||
onUpdateVariable?: (e: Record<string, any>) => void;
|
||||
onStartChat?: (e: StartChatFnProps) => Promise<{
|
||||
@@ -488,7 +489,7 @@ const ChatBox = (
|
||||
|
||||
return {
|
||||
bg: colorMap[chatContent.status] || colorMap.loading,
|
||||
name: t(chatContent.moduleName || 'Running')
|
||||
name: t(chatContent.moduleName || 'common.Loading')
|
||||
};
|
||||
}, [chatHistory, isChatting, t]);
|
||||
/* style end */
|
||||
@@ -496,6 +497,7 @@ const ChatBox = (
|
||||
// page change and abort request
|
||||
useEffect(() => {
|
||||
isNewChatReplace.current = false;
|
||||
setQuestionGuide([]);
|
||||
return () => {
|
||||
chatController.current?.abort('leave');
|
||||
if (!isNewChatReplace.current) {
|
||||
@@ -750,38 +752,28 @@ const ChatBox = (
|
||||
{index === chatHistory.length - 1 &&
|
||||
!isChatting &&
|
||||
questionGuides.length > 0 && (
|
||||
<Flex
|
||||
mt={2}
|
||||
borderTop={theme.borders.sm}
|
||||
alignItems={'center'}
|
||||
flexWrap={'wrap'}
|
||||
>
|
||||
<Box
|
||||
color={'myGray.500'}
|
||||
mt={2}
|
||||
mr={2}
|
||||
fontSize={'sm'}
|
||||
fontStyle={'italic'}
|
||||
>
|
||||
{t('chat.Question Guide Tips')}
|
||||
</Box>
|
||||
{questionGuides.map((item) => (
|
||||
<Button
|
||||
mt={2}
|
||||
key={item}
|
||||
mr="2"
|
||||
borderRadius={'md'}
|
||||
variant={'outline'}
|
||||
colorScheme={'gray'}
|
||||
size={'xs'}
|
||||
onClick={() => {
|
||||
resetInputVal(item);
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
</Button>
|
||||
))}
|
||||
</Flex>
|
||||
<Box mt={2}>
|
||||
<ChatBoxDivider
|
||||
icon="core/chat/QGFill"
|
||||
text={t('chat.Question Guide Tips')}
|
||||
/>
|
||||
<Flex alignItems={'center'} flexWrap={'wrap'} gap={2}>
|
||||
{questionGuides.map((item) => (
|
||||
<Button
|
||||
key={item}
|
||||
borderRadius={'md'}
|
||||
variant={'outline'}
|
||||
colorScheme={'gray'}
|
||||
size={'xs'}
|
||||
onClick={() => {
|
||||
resetInputVal(item);
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
</Button>
|
||||
))}
|
||||
</Flex>
|
||||
</Box>
|
||||
)}
|
||||
{/* admin mark content */}
|
||||
{showMarkIcon && item.adminFeedback && (
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1698493025597" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4931" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M845.4 481.9c24.1 0 47.1 4.6 68.9 13.9 21.8 9.3 40.9 22.1 57.2 38.3 16.3 16.3 29.2 35.2 38.5 56.9 9.3 21.7 14 44.5 14 68.5 0 24.8-4.7 47.8-14 69.1-9.3 21.3-22.2 40.1-38.5 56.3-16.3 16.3-35.4 29-57.2 38.3-21.8 9.3-44.8 13.9-68.9 13.9-22.6 0-43.8-4.1-63.6-12.2-19.9-8.1-37.6-18.8-53.1-31.9v145.2c0 20.9-7.4 38.7-22.2 53.4-14.8 14.7-32.7 22.1-53.7 22.1H77.1c-21 0-39.1-7.4-54.3-22.1C7.6 977 0 959.2 0 938.3V809.4c3.9-16.3 10.9-28.3 21-36 10.1-7.7 24.9-5.8 44.4 5.8 9.3 5.4 16.3 8.5 21 9.3 20.2 10.1 40.9 15.1 61.9 15.1s40.7-3.9 59-11.6c18.3-7.7 34.2-18.4 47.9-31.9 13.6-13.5 24.3-29.2 32.1-47 7.8-17.8 11.7-37.2 11.7-58.1s-3.9-40.4-11.7-58.6-18.5-34.1-32.1-47.6c-13.6-13.5-29.6-24.2-47.9-31.9-18.3-7.7-37.9-11.6-59-11.6-20.2 0-40.1 4.3-59.5 12.8-4.7 1.5-8.6 3.1-11.7 4.6-9.3 4.6-18.5 8.9-27.4 12.8s-16.9 5.4-23.9 4.6c-7-0.8-12.8-4.6-17.5-11.6-4.7-7-7.4-18.6-8.2-34.8V371.6c0-20.9 7.6-38.9 22.8-54C37.9 302.5 56 295 77.1 295h182.1c-12.5-15.5-22.4-32.7-29.8-51.7-7.4-19-11.1-39.3-11.1-61 0-25.5 4.9-49.4 14.6-71.4 9.7-22.1 22.8-41.2 39.1-57.5 16.3-16.3 35.6-29.2 57.8-38.9C352 4.8 375.6 0 400.5 0c24.9 0 48.5 4.8 70.6 14.5 22.2 9.7 41.5 22.6 57.8 38.9s29.4 35.4 39.1 57.5c9.7 22.1 14.6 45.9 14.6 71.4 0 43.4-13.6 80.9-40.9 112.6h110.9c21 0 38.9 7.5 53.7 22.6 14.8 15.1 22.2 33.1 22.2 54V526c15.6-13.2 33.3-23.8 53.1-31.9 20-8.1 41.2-12.2 63.8-12.2z" p-id="4932"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 5.7 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1698397550956" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2700" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M970.43915282 590.98409465a55.23363335 55.23363335 0 0 0-69.04204072 35.90186227A394.36813906 394.36813906 0 0 1 521.94205392 898.63543088 392.15879331 392.15879331 0 0 1 125.36456912 512a392.15879331 392.15879331 0 0 1 396.5774848-386.63543088 400.99617502 400.99617502 0 0 1 256.8363931 92.24016784l-119.85698388-19.88410786a55.23363335 55.23363335 0 0 0-63.518677 45.8439149 55.23363335 55.23363335 0 0 0 45.84391489 63.51867829l234.19060403 38.66354218h9.38971716a55.23363335 55.23363335 0 0 0 18.77943563-3.31401797 18.22709886 18.22709886 0 0 0 5.52336243-3.31401798 43.08223369 43.08223369 0 0 0 11.04672744-6.07569919l4.97102697-6.07569919c0-2.76168122 4.97102697-4.97102697 7.18037141-8.28504493s0-5.52336372 2.76168252-7.73270948a74.01306768 74.01306768 0 0 0 3.86635343-9.94205393l41.42522469-220.93453081a55.23363335 55.23363335 0 0 0-110.46726541-20.98878138l-14.91308088 80.08876817A508.7017592 508.7017592 0 0 0 521.94205392 14.8973037 502.62606002 502.62606002 0 0 0 14.8973037 512a502.62606002 502.62606002 0 0 0 507.04475022 497.1026963A503.73073225 503.73073225 0 0 0 1009.1026963 660.02613665a55.23363335 55.23363335 0 0 0-38.66354348-69.042042z" p-id="2701"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1698504394130" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4081" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M928 448h-64a19.2 19.2 0 0 1 0-38.4h64a19.2 19.2 0 0 1 0 38.4zM797.1072 738.4064l-45.2608-45.2608a19.2 19.2 0 0 1 27.1488-27.1488l45.3248 45.2608a19.2 19.2 0 0 1-27.2128 27.1488zM779.008 204.4032a19.2 19.2 0 0 1-27.1488-27.1488l45.2608-45.2608a19.2 19.2 0 0 1 27.2 27.1488z m-121.216 472.0128a282.368 282.368 0 0 0-17.2032 77.5808v20.8A37.7856 37.7856 0 0 1 614.4 810.6752V819.2H409.6v-8.5248a37.7856 37.7856 0 0 1-26.1888-35.84v-23.9488a290.0352 290.0352 0 0 0-16.9344-74.24A279.04 279.04 0 0 1 243.2 443.5968C243.2 290.56 363.52 166.4 512 166.4s268.8 124.16 268.8 277.1968a279.04 279.04 0 0 1-123.008 232.8192zM505.6 691.2a19.2 19.2 0 1 0-19.2-19.2 19.2 19.2 0 0 0 19.2 19.2z m6.4-358.4a115.2 115.2 0 0 0-114.4448 102.4h6.5024a17.728 17.728 0 1 0 20.9024 0h8.6656A79.8848 79.8848 0 0 1 512 368.64a77.7216 77.7216 0 0 1 76.8 79.36c0.8064 45.6064-64 76.8-64 76.8a97.4976 97.4976 0 0 0-34.432 46.4768 18.7392 18.7392 0 0 0-3.968 11.1232v7.68a56.6784 56.6784 0 0 0 0 11.52v6.4a19.2 19.2 0 0 0 38.4 0v-14.4768a18.6496 18.6496 0 0 1 0.384-4.6336C533.3632 557.9904 588.8 524.8 588.8 524.8c36.2368-23.296 38.4-76.8 38.4-76.8a115.2 115.2 0 0 0-115.2-115.2z m6.4-230.4A19.2 19.2 0 0 1 499.2 83.2v-64a19.2 19.2 0 1 1 38.4 0v64A19.2 19.2 0 0 1 518.4 102.4z m-264.3456 92.9536l-45.2608-45.2608A19.2 19.2 0 1 1 235.9424 122.88l45.2608 45.248a19.2 19.2 0 0 1-27.1488 27.2256zM160 448h-64a19.2 19.2 0 0 1 0-38.4h64a19.2 19.2 0 0 1 0 38.4z m94.0544 227.0464a19.2 19.2 0 0 1 27.1488 27.1488L235.9424 747.52a19.2 19.2 0 0 1-27.1488-27.1488zM652.8 846.9376a8.384 8.384 0 0 1 0.384 1.9072V870.4a8.4096 8.4096 0 0 1-0.384 1.9072V883.2H371.2v-51.2h281.6v14.9376zM627.2 947.2H396.8v-51.2h230.4v51.2z m-183.6672 23.9104H579.84a8.5248 8.5248 0 0 1 4.8896 1.6896H601.6v38.4H422.4v-38.4h16.2432a8.5248 8.5248 0 0 1 4.8896-1.6896z" fill="#1296db" p-id="4082"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1698497259520" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10081" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M156.09136 606.57001a457.596822 457.596822 0 0 1 221.680239-392.516385 50.844091 50.844091 0 1 1 50.844091 86.943396 355.90864 355.90864 0 0 0-138.804369 152.532274h16.77855a152.532274 152.532274 0 1 1-152.532274 152.532274z m406.752731 0a457.596822 457.596822 0 0 1 221.680239-392.007944 50.844091 50.844091 0 1 1 50.844091 86.943396 355.90864 355.90864 0 0 0-138.804369 152.532274h16.77855a152.532274 152.532274 0 1 1-152.532274 152.532274z" fill="#E67E22" p-id="10082"></path></svg>
|
||||
|
After Width: | Height: | Size: 819 B |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1698491522536" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4046" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M512 642.9c81.6 0 157.6-42.3 229.5-130.9-72-88.6-147.9-130.9-229.5-130.9S354.4 423.4 282.5 512c71.9 88.6 147.9 130.9 229.5 130.9z m0 62.3c-105.3 0-201.4-53.6-288.5-160.7-15.4-18.9-15.4-46 0-64.9C310.6 372.4 406.7 318.8 512 318.8s201.4 53.6 288.5 160.7c15.4 18.9 15.4 46 0 64.9C713.4 651.6 617.3 705.2 512 705.2z m0 0" fill="#333333" p-id="4047"></path><path d="M512 540c15.5 0 28-12.5 28-28 0-15.4-12.5-28-28-28s-28 12.5-28 28c0 15.4 12.5 28 28 28z m0 71.9c-35.7 0-68.7-19-86.6-49.9-17.9-30.9-17.9-69 0-99.9 17.9-30.9 50.9-49.9 86.6-49.9 55.2 0 100 44.7 100 99.9 0 55.1-44.8 99.8-100 99.8z m0 0" fill="#333333" p-id="4048"></path><path d="M136 888V745c0-19.9-16.1-36-36-36s-36 16.1-36 36v155c0 33.1 26.9 60 60 60h155c19.9 0 36-16.1 36-36s-16.1-36-36-36H136zM136 136h143c19.9 0 36-16.1 36-36s-16.1-36-36-36H124c-33.1 0-60 26.9-60 60v155c0 19.9 16.1 36 36 36s36-16.1 36-36V136zM888 136v143c0 19.9 16.1 36 36 36s36-16.1 36-36V124c0-33.1-26.9-60-60-60H745c-19.9 0-36 16.1-36 36s16.1 36 36 36h143zM888 888H745c-19.9 0-36 16.1-36 36s16.1 36 36 36h155c33.1 0 60-26.9 60-60V745c0-19.9-16.1-36-36-36s-36 16.1-36 36v143z" fill="#333333" p-id="4049"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -92,7 +92,13 @@ const iconPaths = {
|
||||
pause: () => import('./icons/common/pause.svg'),
|
||||
'core/app/aiLight': () => import('./icons/core/app/aiLight.svg'),
|
||||
'core/app/aiFill': () => import('./icons/core/app/aiFill.svg'),
|
||||
'common/text/t': () => import('./icons/common/text/t.svg')
|
||||
'common/text/t': () => import('./icons/common/text/t.svg'),
|
||||
'common/navbar/pluginLight': () => import('./icons/common/navbar/pluginLight.svg'),
|
||||
'common/navbar/pluginFill': () => import('./icons/common/navbar/pluginFill.svg'),
|
||||
'common/refreshLight': () => import('./icons/common/refreshLight.svg'),
|
||||
'core/module/previewLight': () => import('./icons/core/module/previewLight.svg'),
|
||||
'core/chat/quoteFill': () => import('./icons/core/chat/quoteFill.svg'),
|
||||
'core/chat/QGFill': () => import('./icons/core/chat/QGFill.svg')
|
||||
};
|
||||
|
||||
export type IconName = keyof typeof iconPaths;
|
||||
|
||||
@@ -40,6 +40,13 @@ const Navbar = ({ unread }: { unread: number }) => {
|
||||
link: `/app/list`,
|
||||
activeLink: ['/app/list', '/app/detail']
|
||||
},
|
||||
{
|
||||
label: t('navbar.Plugin'),
|
||||
icon: 'common/navbar/pluginLight',
|
||||
activeIcon: 'common/navbar/pluginFill',
|
||||
link: `/plugin/list`,
|
||||
activeLink: ['/plugin/list', '/plugin/edit']
|
||||
},
|
||||
{
|
||||
label: t('navbar.Datasets'),
|
||||
icon: 'dbLight',
|
||||
|
||||
@@ -43,7 +43,6 @@ const MyModal = ({
|
||||
{...props}
|
||||
>
|
||||
{!!title && <ModalHeader>{title}</ModalHeader>}
|
||||
{onClose && <ModalCloseButton />}
|
||||
<Box
|
||||
overflow={props.overflow || 'overlay'}
|
||||
h={'100%'}
|
||||
@@ -52,6 +51,7 @@ const MyModal = ({
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
{onClose && <ModalCloseButton />}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React from 'react';
|
||||
import { Box, useTheme, type BoxProps } from '@chakra-ui/react';
|
||||
import MyBox from '../common/MyBox';
|
||||
|
||||
const PageContainer = ({ children, ...props }: BoxProps) => {
|
||||
const PageContainer = ({ children, ...props }: BoxProps & { isLoading?: boolean }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Box bg={'myGray.100'} h={'100%'} p={[0, 5]} px={[0, 6]} {...props}>
|
||||
<MyBox bg={'myGray.100'} h={'100%'} p={[0, 5]} px={[0, 6]} {...props}>
|
||||
<Box
|
||||
h={'100%'}
|
||||
bg={'white'}
|
||||
@@ -14,7 +15,7 @@ const PageContainer = ({ children, ...props }: BoxProps) => {
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
</MyBox>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import MyModal from '../MyModal';
|
||||
import { Box, Button, Grid, useTheme } from '@chakra-ui/react';
|
||||
import { Box, Button, Flex, Grid, useTheme } from '@chakra-ui/react';
|
||||
import { PromptTemplateItem } from '@fastgpt/global/core/ai/type.d';
|
||||
import { ModalBody, ModalFooter } from '@chakra-ui/react';
|
||||
|
||||
@@ -13,14 +13,14 @@ const PromptTemplate = ({
|
||||
title: string;
|
||||
templates: PromptTemplateItem[];
|
||||
onClose: () => void;
|
||||
onSuccess: (e: string) => void;
|
||||
onSuccess: (e: PromptTemplateItem) => void;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const [selectTemplateTitle, setSelectTemplateTitle] = useState<PromptTemplateItem>();
|
||||
|
||||
return (
|
||||
<MyModal isOpen title={title} onClose={onClose}>
|
||||
<ModalBody w={'600px'}>
|
||||
<MyModal isOpen title={title} onClose={onClose} isCentered>
|
||||
<ModalBody h="100%" w={'600px'} maxW={'90vw'} overflowY={'auto'}>
|
||||
<Grid gridTemplateColumns={['1fr', '1fr 1fr']} gridGap={4}>
|
||||
{templates.map((item) => (
|
||||
<Box
|
||||
@@ -38,8 +38,9 @@ const PromptTemplate = ({
|
||||
onClick={() => setSelectTemplateTitle(item)}
|
||||
>
|
||||
<Box>{item.title}</Box>
|
||||
|
||||
<Box color={'myGray.600'} fontSize={'sm'} whiteSpace={'pre-wrap'}>
|
||||
{item.value}
|
||||
{item.desc}
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
@@ -50,7 +51,7 @@ const PromptTemplate = ({
|
||||
disabled={!selectTemplateTitle}
|
||||
onClick={() => {
|
||||
if (!selectTemplateTitle) return;
|
||||
onSuccess(selectTemplateTitle.value);
|
||||
onSuccess(selectTemplateTitle);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
|
||||
19
projects/app/src/components/common/MyBox/index.tsx
Normal file
19
projects/app/src/components/common/MyBox/index.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { Box, BoxProps } from '@chakra-ui/react';
|
||||
import Loading from '@/components/Loading';
|
||||
|
||||
type Props = BoxProps & {
|
||||
isLoading?: boolean;
|
||||
text?: string;
|
||||
};
|
||||
|
||||
const MyBox = ({ text, isLoading, children, ...props }: Props) => {
|
||||
return (
|
||||
<Box position={'relative'} {...props}>
|
||||
{children}
|
||||
{isLoading && <Loading fixed={false} text={text} />}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default MyBox;
|
||||
@@ -30,9 +30,9 @@ const ParentPaths = (props: {
|
||||
{concatPaths.map((item, i) => (
|
||||
<Flex key={item.parentId} alignItems={'center'}>
|
||||
<Box
|
||||
fontSize={['md', 'lg']}
|
||||
fontSize={['sm', 'lg']}
|
||||
py={1}
|
||||
px={[0, 2]}
|
||||
px={[1, 2]}
|
||||
borderRadius={'md'}
|
||||
{...(i === concatPaths.length - 1
|
||||
? {
|
||||
@@ -51,7 +51,7 @@ const ParentPaths = (props: {
|
||||
{item.parentName}
|
||||
</Box>
|
||||
{i !== concatPaths.length - 1 && (
|
||||
<MyIcon name={'rightArrowLight'} color={'myGray.500'} w={['18px', '24px']} />
|
||||
<MyIcon name={'rightArrowLight'} color={'myGray.500'} w={['14px', '24px']} />
|
||||
)}
|
||||
</Flex>
|
||||
))}
|
||||
|
||||
19
projects/app/src/components/core/chat/Divider/index.tsx
Normal file
19
projects/app/src/components/core/chat/Divider/index.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import MyIcon, { type IconName } from '@/components/Icon';
|
||||
|
||||
const ChatBoxDivider = ({ icon, text }: { icon: IconName; text: string }) => {
|
||||
return (
|
||||
<Box>
|
||||
<Flex alignItems={'center'} py={2} gap={2}>
|
||||
<MyIcon name={icon} w={'14px'} color={'myGray.900'} />
|
||||
<Box color={'myGray.500'} fontSize={'sm'}>
|
||||
{text}
|
||||
</Box>
|
||||
<Box h={'1px'} mt={1} bg={'myGray.200'} flex={'1'} />
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatBoxDivider;
|
||||
227
projects/app/src/components/core/module/AIChatSettingsModal.tsx
Normal file
227
projects/app/src/components/core/module/AIChatSettingsModal.tsx
Normal file
@@ -0,0 +1,227 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import MyModal from '@/components/MyModal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { EditFormType } from '@/web/core/app/basicSettings';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import {
|
||||
Box,
|
||||
BoxProps,
|
||||
Button,
|
||||
Flex,
|
||||
Link,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
Switch,
|
||||
Textarea
|
||||
} from '@chakra-ui/react';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import { Prompt_QuotePromptList, Prompt_QuoteTemplateList } from '@/global/core/prompt/AIChat';
|
||||
import { chatModelList, feConfigs } from '@/web/common/system/staticData';
|
||||
import MySlider from '@/components/Slider';
|
||||
import { SystemInputEnum } from '@/constants/app';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { PromptTemplateItem } from '@fastgpt/global/core/ai/type.d';
|
||||
|
||||
const PromptTemplate = dynamic(() => import('@/components/PromptTemplate'));
|
||||
|
||||
const AIChatSettingsModal = ({
|
||||
isAdEdit,
|
||||
onClose,
|
||||
onSuccess,
|
||||
defaultData
|
||||
}: {
|
||||
isAdEdit?: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess: (e: EditFormType['chatModel']) => void;
|
||||
defaultData: EditFormType['chatModel'];
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
|
||||
const { register, handleSubmit, getValues, setValue } = useForm({
|
||||
defaultValues: defaultData
|
||||
});
|
||||
|
||||
const [selectTemplateData, setSelectTemplateData] = useState<{
|
||||
title: string;
|
||||
templates: PromptTemplateItem[];
|
||||
}>();
|
||||
|
||||
const tokenLimit = useMemo(() => {
|
||||
return chatModelList.find((item) => item.model === getValues('model'))?.maxToken || 4000;
|
||||
}, [getValues, refresh]);
|
||||
|
||||
const LabelStyles: BoxProps = {
|
||||
fontSize: ['sm', 'md']
|
||||
};
|
||||
const selectTemplateBtn: BoxProps = {
|
||||
color: 'myBlue.600',
|
||||
cursor: 'pointer'
|
||||
};
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen
|
||||
title={
|
||||
<Flex alignItems={'flex-end'}>
|
||||
{t('app.AI Advanced Settings')}
|
||||
{feConfigs?.show_doc && (
|
||||
<Link
|
||||
href={`${feConfigs.docUrl}/docs/use-cases/ai_settings/`}
|
||||
target={'_blank'}
|
||||
ml={1}
|
||||
textDecoration={'underline'}
|
||||
fontWeight={'normal'}
|
||||
fontSize={'md'}
|
||||
>
|
||||
查看说明
|
||||
</Link>
|
||||
)}
|
||||
</Flex>
|
||||
}
|
||||
isCentered
|
||||
w={'700px'}
|
||||
h={['90vh', 'auto']}
|
||||
>
|
||||
<ModalBody flex={['1 0 0', 'auto']} overflowY={'auto'}>
|
||||
{isAdEdit && (
|
||||
<Flex alignItems={'center'}>
|
||||
<Box {...LabelStyles} w={'80px'}>
|
||||
返回AI内容
|
||||
</Box>
|
||||
<Box flex={1} ml={'10px'}>
|
||||
<Switch
|
||||
isChecked={getValues(SystemInputEnum.isResponseAnswerText)}
|
||||
size={'lg'}
|
||||
onChange={(e) => {
|
||||
const value = e.target.checked;
|
||||
setValue(SystemInputEnum.isResponseAnswerText, value);
|
||||
setRefresh((state) => !state);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
<Flex alignItems={'center'} mb={10} mt={isAdEdit ? 8 : 5}>
|
||||
<Box {...LabelStyles} mr={2} w={'80px'}>
|
||||
温度
|
||||
</Box>
|
||||
<Box flex={1} ml={'10px'}>
|
||||
<MySlider
|
||||
markList={[
|
||||
{ label: '严谨', value: 0 },
|
||||
{ label: '发散', value: 10 }
|
||||
]}
|
||||
width={'95%'}
|
||||
min={0}
|
||||
max={10}
|
||||
value={getValues('temperature')}
|
||||
onChange={(e) => {
|
||||
setValue('temperature', e);
|
||||
setRefresh(!refresh);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} mt={12} mb={10}>
|
||||
<Box {...LabelStyles} mr={2} w={'80px'}>
|
||||
回复上限
|
||||
</Box>
|
||||
<Box flex={1} ml={'10px'}>
|
||||
<MySlider
|
||||
markList={[
|
||||
{ label: '100', value: 100 },
|
||||
{ label: `${tokenLimit}`, value: tokenLimit }
|
||||
]}
|
||||
width={'95%'}
|
||||
min={100}
|
||||
max={tokenLimit}
|
||||
step={50}
|
||||
value={getValues('maxToken')}
|
||||
onChange={(val) => {
|
||||
setValue('maxToken', val);
|
||||
setRefresh(!refresh);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box>
|
||||
<Flex {...LabelStyles} mb={1}>
|
||||
引用内容模板
|
||||
<MyTooltip
|
||||
label={t('template.Quote Content Tip', {
|
||||
default: Prompt_QuoteTemplateList[0].value
|
||||
})}
|
||||
forceShow
|
||||
>
|
||||
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
|
||||
</MyTooltip>
|
||||
<Box flex={1} />
|
||||
<Box
|
||||
{...selectTemplateBtn}
|
||||
onClick={() =>
|
||||
setSelectTemplateData({
|
||||
title: '选择知识库提示词模板',
|
||||
templates: Prompt_QuoteTemplateList
|
||||
})
|
||||
}
|
||||
>
|
||||
选择模板
|
||||
</Box>
|
||||
</Flex>
|
||||
<Textarea
|
||||
rows={6}
|
||||
placeholder={
|
||||
t('template.Quote Content Tip', { default: Prompt_QuoteTemplateList[0].value }) || ''
|
||||
}
|
||||
borderColor={'myGray.100'}
|
||||
{...register('quoteTemplate')}
|
||||
/>
|
||||
</Box>
|
||||
<Box mt={4}>
|
||||
<Flex {...LabelStyles} mb={1}>
|
||||
引用内容提示词
|
||||
<MyTooltip
|
||||
label={t('template.Quote Prompt Tip', { default: Prompt_QuotePromptList[0].value })}
|
||||
forceShow
|
||||
>
|
||||
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
<Textarea
|
||||
rows={11}
|
||||
placeholder={
|
||||
t('template.Quote Prompt Tip', { default: Prompt_QuotePromptList[0].value }) || ''
|
||||
}
|
||||
borderColor={'myGray.100'}
|
||||
{...register('quotePrompt')}
|
||||
/>
|
||||
</Box>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant={'base'} onClick={onClose}>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
<Button ml={4} onClick={handleSubmit(onSuccess)}>
|
||||
{t('Confirm')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
{!!selectTemplateData && (
|
||||
<PromptTemplate
|
||||
title={selectTemplateData.title}
|
||||
templates={selectTemplateData.templates}
|
||||
onClose={() => setSelectTemplateData(undefined)}
|
||||
onSuccess={(e) => {
|
||||
const quoteVal = e.value;
|
||||
const promptVal = Prompt_QuotePromptList.find((item) => item.title === e.title)?.value;
|
||||
setValue('quoteTemplate', quoteVal);
|
||||
setValue('quotePrompt', promptVal);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AIChatSettingsModal;
|
||||
325
projects/app/src/components/core/module/DatasetSelectModal.tsx
Normal file
325
projects/app/src/components/core/module/DatasetSelectModal.tsx
Normal file
@@ -0,0 +1,325 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Flex,
|
||||
Box,
|
||||
Button,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
useTheme,
|
||||
Textarea,
|
||||
Grid,
|
||||
Divider
|
||||
} from '@chakra-ui/react';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import type { SelectedDatasetType } from '@/types/core/dataset';
|
||||
import { useToast } from '@/web/common/hooks/useToast';
|
||||
import MySlider from '@/components/Slider';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import MyModal from '@/components/MyModal';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constant';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
|
||||
import { feConfigs } from '@/web/common/system/staticData';
|
||||
import DatasetSelectContainer, { useDatasetSelect } from '@/components/core/dataset/SelectModal';
|
||||
|
||||
export type KbParamsType = {
|
||||
searchSimilarity: number;
|
||||
searchLimit: number;
|
||||
searchEmptyText: string;
|
||||
};
|
||||
|
||||
export const DatasetSelectModal = ({
|
||||
isOpen,
|
||||
activeDatasets = [],
|
||||
onChange,
|
||||
onClose
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
activeDatasets: SelectedDatasetType;
|
||||
onChange: (e: SelectedDatasetType) => void;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const { allDatasets } = useDatasetStore();
|
||||
const [selectedKbList, setSelectedKbList] = useState<SelectedDatasetType>(
|
||||
activeDatasets.filter((dataset) => {
|
||||
return allDatasets.find((item) => item._id === dataset.datasetId);
|
||||
})
|
||||
);
|
||||
const { toast } = useToast();
|
||||
const { paths, parentId, setParentId, datasets } = useDatasetSelect();
|
||||
|
||||
const filterKbList = useMemo(() => {
|
||||
return {
|
||||
selected: allDatasets.filter((item) =>
|
||||
selectedKbList.find((dataset) => dataset.datasetId === item._id)
|
||||
),
|
||||
unSelected: datasets.filter(
|
||||
(item) => !selectedKbList.find((dataset) => dataset.datasetId === item._id)
|
||||
)
|
||||
};
|
||||
}, [datasets, allDatasets, selectedKbList]);
|
||||
|
||||
return (
|
||||
<DatasetSelectContainer
|
||||
isOpen={isOpen}
|
||||
paths={paths}
|
||||
parentId={parentId}
|
||||
setParentId={setParentId}
|
||||
tips={'仅能选择同一个索引模型的知识库'}
|
||||
onClose={onClose}
|
||||
>
|
||||
<Flex h={'100%'} flexDirection={'column'} flex={'1 0 0'}>
|
||||
<ModalBody flex={'1 0 0'} overflowY={'auto'} userSelect={'none'}>
|
||||
<Grid
|
||||
gridTemplateColumns={[
|
||||
'repeat(1, minmax(0, 1fr))',
|
||||
'repeat(2, minmax(0, 1fr))',
|
||||
'repeat(3, minmax(0, 1fr))'
|
||||
]}
|
||||
gridGap={3}
|
||||
>
|
||||
{filterKbList.selected.map((item) =>
|
||||
(() => {
|
||||
return (
|
||||
<Card
|
||||
key={item._id}
|
||||
p={3}
|
||||
border={theme.borders.base}
|
||||
boxShadow={'sm'}
|
||||
bg={'myBlue.300'}
|
||||
>
|
||||
<Flex alignItems={'center'} h={'38px'}>
|
||||
<Avatar src={item.avatar} w={['24px', '28px']}></Avatar>
|
||||
<Box flex={'1 0 0'} w={0} className="textEllipsis" mx={3}>
|
||||
{item.name}
|
||||
</Box>
|
||||
<MyIcon
|
||||
name={'delete'}
|
||||
w={'14px'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'red.500' }}
|
||||
onClick={() => {
|
||||
setSelectedKbList((state) =>
|
||||
state.filter((kb) => kb.datasetId !== item._id)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Card>
|
||||
);
|
||||
})()
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
{filterKbList.selected.length > 0 && <Divider my={3} />}
|
||||
|
||||
<Grid
|
||||
gridTemplateColumns={[
|
||||
'repeat(1, minmax(0, 1fr))',
|
||||
'repeat(2, minmax(0, 1fr))',
|
||||
'repeat(3, minmax(0, 1fr))'
|
||||
]}
|
||||
gridGap={3}
|
||||
>
|
||||
{filterKbList.unSelected.map((item) =>
|
||||
(() => {
|
||||
return (
|
||||
<MyTooltip
|
||||
key={item._id}
|
||||
label={
|
||||
item.type === DatasetTypeEnum.dataset
|
||||
? t('dataset.Select Dataset')
|
||||
: t('dataset.Select Folder')
|
||||
}
|
||||
>
|
||||
<Card
|
||||
p={3}
|
||||
border={theme.borders.base}
|
||||
boxShadow={'sm'}
|
||||
h={'80px'}
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
boxShadow: 'md'
|
||||
}}
|
||||
onClick={() => {
|
||||
if (item.type === DatasetTypeEnum.folder) {
|
||||
setParentId(item._id);
|
||||
} else if (item.type === DatasetTypeEnum.dataset) {
|
||||
const vectorModel = selectedKbList[0]?.vectorModel?.model;
|
||||
|
||||
if (vectorModel && vectorModel !== item.vectorModel.model) {
|
||||
return toast({
|
||||
status: 'warning',
|
||||
title: '仅能选择同一个索引模型的知识库'
|
||||
});
|
||||
}
|
||||
setSelectedKbList((state) => [
|
||||
...state,
|
||||
{ datasetId: item._id, vectorModel: item.vectorModel }
|
||||
]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Flex alignItems={'center'} h={'38px'}>
|
||||
<Avatar src={item.avatar} w={['24px', '28px']}></Avatar>
|
||||
<Box
|
||||
flex={'1 0 0'}
|
||||
w={0}
|
||||
className="textEllipsis"
|
||||
ml={3}
|
||||
fontWeight={'bold'}
|
||||
fontSize={['md', 'lg', 'xl']}
|
||||
>
|
||||
{item.name}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex justifyContent={'flex-end'} alignItems={'center'} fontSize={'sm'}>
|
||||
{item.type === DatasetTypeEnum.folder ? (
|
||||
<Box color={'myGray.500'}>{t('Folder')}</Box>
|
||||
) : (
|
||||
<>
|
||||
<MyIcon mr={1} name="kbTest" w={'12px'} />
|
||||
<Box color={'myGray.500'}>{item.vectorModel.name}</Box>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</Card>
|
||||
</MyTooltip>
|
||||
);
|
||||
})()
|
||||
)}
|
||||
</Grid>
|
||||
{filterKbList.unSelected.length === 0 && (
|
||||
<Flex mt={5} flexDirection={'column'} alignItems={'center'}>
|
||||
<MyIcon name="empty" w={'48px'} h={'48px'} mt={'20vh'} color={'transparent'} />
|
||||
<Box mt={2} color={'myGray.500'}>
|
||||
这个目录已经没东西可选了~
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
onClick={() => {
|
||||
// filter out the dataset that is not in the kList
|
||||
const filterKbList = selectedKbList.filter((dataset) => {
|
||||
return allDatasets.find((item) => item._id === dataset.datasetId);
|
||||
});
|
||||
|
||||
onClose();
|
||||
onChange(filterKbList);
|
||||
}}
|
||||
>
|
||||
完成
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Flex>
|
||||
</DatasetSelectContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export const KbParamsModal = ({
|
||||
searchEmptyText,
|
||||
searchLimit,
|
||||
searchSimilarity,
|
||||
onClose,
|
||||
onChange
|
||||
}: KbParamsType & { onClose: () => void; onChange: (e: KbParamsType) => void }) => {
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
const { register, setValue, getValues, handleSubmit } = useForm<KbParamsType>({
|
||||
defaultValues: {
|
||||
searchEmptyText,
|
||||
searchLimit,
|
||||
searchSimilarity
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<MyModal isOpen={true} onClose={onClose} title={'搜索参数调整'} minW={['90vw', '600px']}>
|
||||
<Flex flexDirection={'column'}>
|
||||
<ModalBody>
|
||||
<Box display={['block', 'flex']} py={5} pt={[0, 5]}>
|
||||
<Box flex={'0 0 100px'} mb={[8, 0]}>
|
||||
相似度
|
||||
<MyTooltip
|
||||
label={'不同索引模型的相似度有区别,请通过搜索测试来选择合适的数值'}
|
||||
forceShow
|
||||
>
|
||||
<QuestionOutlineIcon ml={1} />
|
||||
</MyTooltip>
|
||||
</Box>
|
||||
<MySlider
|
||||
markList={[
|
||||
{ label: '0', value: 0 },
|
||||
{ label: '1', value: 1 }
|
||||
]}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
value={getValues('searchSimilarity')}
|
||||
onChange={(val) => {
|
||||
setValue('searchSimilarity', val);
|
||||
setRefresh(!refresh);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box display={['block', 'flex']} py={8}>
|
||||
<Box flex={'0 0 100px'} mb={[8, 0]}>
|
||||
单次搜索数量
|
||||
</Box>
|
||||
<Box flex={1}>
|
||||
<MySlider
|
||||
markList={[
|
||||
{ label: '1', value: 1 },
|
||||
{ label: '20', value: 20 }
|
||||
]}
|
||||
min={1}
|
||||
max={20}
|
||||
value={getValues('searchLimit')}
|
||||
onChange={(val) => {
|
||||
setValue('searchLimit', val);
|
||||
setRefresh(!refresh);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box display={['block', 'flex']} pt={3}>
|
||||
<Box flex={'0 0 100px'} mb={[2, 0]}>
|
||||
空搜索回复
|
||||
</Box>
|
||||
<Box flex={1}>
|
||||
<Textarea
|
||||
rows={5}
|
||||
maxLength={500}
|
||||
placeholder={`若填写该内容,没有搜索到对应内容时,将直接回复填写的内容。\n为了连贯上下文,${feConfigs?.systemTitle} 会取部分上一个聊天的搜索记录作为补充,因此在连续对话时,该功能可能会失效。`}
|
||||
{...register('searchEmptyText')}
|
||||
></Textarea>
|
||||
</Box>
|
||||
</Box>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant={'base'} mr={3} onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onClose();
|
||||
handleSubmit(onChange)();
|
||||
}}
|
||||
>
|
||||
完成
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Flex>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DatasetSelectModal;
|
||||
137
projects/app/src/components/core/module/Flow/ChatTest.tsx
Normal file
137
projects/app/src/components/core/module/Flow/ChatTest.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import type { ModuleItemType } from '@fastgpt/global/core/module/type.d';
|
||||
import { AppSchema } from '@/types/mongoSchema';
|
||||
import React, {
|
||||
useMemo,
|
||||
useCallback,
|
||||
useRef,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
ForwardedRef
|
||||
} from 'react';
|
||||
import { Box, Flex, IconButton } from '@chakra-ui/react';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
|
||||
import { streamFetch } from '@/web/common/api/fetch';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import ChatBox, { type ComponentRef, type StartChatFnProps } from '@/components/ChatBox';
|
||||
import { getGuideModule } from '@/global/core/app/modules/utils';
|
||||
|
||||
export type ChatTestComponentRef = {
|
||||
resetChatTest: () => void;
|
||||
};
|
||||
|
||||
const ChatTest = (
|
||||
{
|
||||
app,
|
||||
modules = [],
|
||||
onClose
|
||||
}: {
|
||||
app: AppSchema;
|
||||
modules?: ModuleItemType[];
|
||||
onClose: () => void;
|
||||
},
|
||||
ref: ForwardedRef<ChatTestComponentRef>
|
||||
) => {
|
||||
const ChatBoxRef = useRef<ComponentRef>(null);
|
||||
const { userInfo } = useUserStore();
|
||||
const isOpen = useMemo(() => modules && modules.length > 0, [modules]);
|
||||
|
||||
const startChat = useCallback(
|
||||
async ({ chatList, controller, generatingMessage, variables }: StartChatFnProps) => {
|
||||
const historyMaxLen =
|
||||
modules
|
||||
?.find((item) => item.flowType === FlowNodeTypeEnum.historyNode)
|
||||
?.inputs?.find((item) => item.key === 'maxContext')?.value || 0;
|
||||
const history = chatList.slice(-historyMaxLen - 2, -2);
|
||||
|
||||
// 流请求,获取数据
|
||||
const { responseText, responseData } = await streamFetch({
|
||||
url: '/api/chat/chatTest',
|
||||
data: {
|
||||
history,
|
||||
prompt: chatList[chatList.length - 2].value,
|
||||
modules,
|
||||
variables,
|
||||
appId: app._id,
|
||||
appName: `调试-${app.name}`
|
||||
},
|
||||
onMessage: generatingMessage,
|
||||
abortSignal: controller
|
||||
});
|
||||
|
||||
return { responseText, responseData };
|
||||
},
|
||||
[app._id, app.name, modules]
|
||||
);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
resetChatTest() {
|
||||
ChatBoxRef.current?.resetHistory([]);
|
||||
ChatBoxRef.current?.resetVariables();
|
||||
}
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex
|
||||
zIndex={3}
|
||||
flexDirection={'column'}
|
||||
position={'absolute'}
|
||||
top={5}
|
||||
right={0}
|
||||
h={isOpen ? '95%' : '0'}
|
||||
w={isOpen ? ['100%', '460px'] : '0'}
|
||||
bg={'white'}
|
||||
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
|
||||
borderRadius={'md'}
|
||||
overflow={'hidden'}
|
||||
transition={'.2s ease'}
|
||||
>
|
||||
<Flex py={4} px={5} whiteSpace={'nowrap'}>
|
||||
<Box fontSize={'xl'} fontWeight={'bold'} flex={1}>
|
||||
调试预览
|
||||
</Box>
|
||||
<MyTooltip label={'重置'}>
|
||||
<IconButton
|
||||
className="chat"
|
||||
size={'sm'}
|
||||
icon={<MyIcon name={'clear'} w={'14px'} />}
|
||||
variant={'base'}
|
||||
borderRadius={'md'}
|
||||
aria-label={'delete'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
ChatBoxRef.current?.resetHistory([]);
|
||||
ChatBoxRef.current?.resetVariables();
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
<Box flex={1}>
|
||||
<ChatBox
|
||||
ref={ChatBoxRef}
|
||||
appAvatar={app.avatar}
|
||||
userAvatar={userInfo?.avatar}
|
||||
showMarkIcon
|
||||
userGuideModule={getGuideModule(modules)}
|
||||
onStartChat={startChat}
|
||||
onDelMessage={() => {}}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box
|
||||
zIndex={2}
|
||||
display={isOpen ? 'block' : 'none'}
|
||||
position={'fixed'}
|
||||
top={0}
|
||||
left={0}
|
||||
bottom={0}
|
||||
right={0}
|
||||
onClick={onClose}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(forwardRef(ChatTest));
|
||||
482
projects/app/src/components/core/module/Flow/FlowProvider.tsx
Normal file
482
projects/app/src/components/core/module/Flow/FlowProvider.tsx
Normal file
@@ -0,0 +1,482 @@
|
||||
import {
|
||||
type Node,
|
||||
type NodeChange,
|
||||
type Edge,
|
||||
type EdgeChange,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
Connection,
|
||||
addEdge
|
||||
} from 'reactflow';
|
||||
import type {
|
||||
FlowModuleItemType,
|
||||
FlowModuleTemplateType
|
||||
} from '@fastgpt/global/core/module/type.d';
|
||||
import type {
|
||||
FlowNodeOutputTargetItemType,
|
||||
FlowNodeChangeProps
|
||||
} from '@fastgpt/global/core/module/node/type';
|
||||
import React, {
|
||||
type SetStateAction,
|
||||
type Dispatch,
|
||||
useContext,
|
||||
useCallback,
|
||||
createContext,
|
||||
useRef,
|
||||
useEffect
|
||||
} from 'react';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import { appModule2FlowEdge, appModule2FlowNode } from '@/utils/adapt';
|
||||
import { useToast } from '@/web/common/hooks/useToast';
|
||||
import {
|
||||
FlowNodeInputTypeEnum,
|
||||
FlowNodeTypeEnum,
|
||||
FlowNodeValTypeEnum
|
||||
} from '@fastgpt/global/core/module/node/constant';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { ModuleItemType } from '@fastgpt/global/core/module/type.d';
|
||||
import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus';
|
||||
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
|
||||
|
||||
type OnChange<ChangesType> = (changes: ChangesType[]) => void;
|
||||
export type useFlowProviderStoreType = {
|
||||
reactFlowWrapper: null | React.RefObject<HTMLDivElement>;
|
||||
filterAppIds: string[];
|
||||
nodes: Node<FlowModuleItemType, string | undefined>[];
|
||||
setNodes: Dispatch<SetStateAction<Node<FlowModuleItemType, string | undefined>[]>>;
|
||||
onNodesChange: OnChange<NodeChange>;
|
||||
edges: Edge<any>[];
|
||||
setEdges: Dispatch<SetStateAction<Edge<any>[]>>;
|
||||
onEdgesChange: OnChange<EdgeChange>;
|
||||
onFixView: () => void;
|
||||
onDelNode: (nodeId: string) => void;
|
||||
onChangeNode: (e: FlowNodeChangeProps) => void;
|
||||
onCopyNode: (nodeId: string) => void;
|
||||
onResetNode: (id: string, module: FlowModuleTemplateType) => void;
|
||||
onDelEdge: (e: {
|
||||
moduleId: string;
|
||||
sourceHandle?: string | undefined;
|
||||
targetHandle?: string | undefined;
|
||||
}) => void;
|
||||
onDelConnect: (id: string) => void;
|
||||
onConnect: ({ connect }: { connect: Connection }) => any;
|
||||
initData: (modules: ModuleItemType[]) => void;
|
||||
};
|
||||
|
||||
const StateContext = createContext<useFlowProviderStoreType>({
|
||||
reactFlowWrapper: null,
|
||||
filterAppIds: [],
|
||||
nodes: [],
|
||||
setNodes: function (
|
||||
value: React.SetStateAction<Node<FlowModuleItemType, string | undefined>[]>
|
||||
): void {
|
||||
return;
|
||||
},
|
||||
onNodesChange: function (changes: NodeChange[]): void {
|
||||
return;
|
||||
},
|
||||
edges: [],
|
||||
setEdges: function (value: React.SetStateAction<Edge<any>[]>): void {
|
||||
return;
|
||||
},
|
||||
onEdgesChange: function (changes: EdgeChange[]): void {
|
||||
return;
|
||||
},
|
||||
onFixView: function (): void {
|
||||
return;
|
||||
},
|
||||
onDelNode: function (nodeId: string): void {
|
||||
return;
|
||||
},
|
||||
onChangeNode: function (e: FlowNodeChangeProps): void {
|
||||
return;
|
||||
},
|
||||
onCopyNode: function (nodeId: string): void {
|
||||
return;
|
||||
},
|
||||
onDelEdge: function (e: {
|
||||
moduleId: string;
|
||||
sourceHandle?: string | undefined;
|
||||
targetHandle?: string | undefined;
|
||||
}): void {
|
||||
return;
|
||||
},
|
||||
onDelConnect: function (id: string): void {
|
||||
return;
|
||||
},
|
||||
onConnect: function ({ connect }: { connect: Connection }) {
|
||||
return;
|
||||
},
|
||||
initData: function (modules: ModuleItemType[]): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
onResetNode: function (id: string, module: FlowModuleTemplateType): void {
|
||||
throw new Error('Function not implemented.');
|
||||
}
|
||||
});
|
||||
export const useFlowProviderStore = () => useContext(StateContext);
|
||||
|
||||
export const FlowProvider = ({
|
||||
filterAppIds = [],
|
||||
children
|
||||
}: {
|
||||
filterAppIds?: string[];
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const [nodes = [], setNodes, onNodesChange] = useNodesState<FlowModuleItemType>([]);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
|
||||
const onFixView = useCallback(() => {
|
||||
const btn = document.querySelector('.react-flow__controls-fitview') as HTMLButtonElement;
|
||||
|
||||
setTimeout(() => {
|
||||
btn && btn.click();
|
||||
}, 100);
|
||||
}, []);
|
||||
|
||||
const onDelEdge = useCallback(
|
||||
({
|
||||
moduleId,
|
||||
sourceHandle,
|
||||
targetHandle
|
||||
}: {
|
||||
moduleId: string;
|
||||
sourceHandle?: string | undefined;
|
||||
targetHandle?: string | undefined;
|
||||
}) => {
|
||||
if (!sourceHandle && !targetHandle) return;
|
||||
setEdges((state) =>
|
||||
state.filter((edge) => {
|
||||
if (edge.source === moduleId && edge.sourceHandle === sourceHandle) return false;
|
||||
if (edge.target === moduleId && edge.targetHandle === targetHandle) return false;
|
||||
|
||||
return true;
|
||||
})
|
||||
);
|
||||
},
|
||||
[setEdges]
|
||||
);
|
||||
|
||||
const onDelConnect = useCallback(
|
||||
(id: string) => {
|
||||
setEdges((state) => state.filter((item) => item.id !== id));
|
||||
},
|
||||
[setEdges]
|
||||
);
|
||||
|
||||
const onConnect = useCallback(
|
||||
({ connect }: { connect: Connection }) => {
|
||||
const source = nodes.find((node) => node.id === connect.source)?.data;
|
||||
const sourceType = (() => {
|
||||
if (source?.flowType === FlowNodeTypeEnum.classifyQuestion) {
|
||||
return FlowNodeValTypeEnum.boolean;
|
||||
}
|
||||
if (source?.flowType === FlowNodeTypeEnum.pluginInput) {
|
||||
return source?.inputs.find((input) => input.key === connect.sourceHandle)?.valueType;
|
||||
}
|
||||
return source?.outputs.find((output) => output.key === connect.sourceHandle)?.valueType;
|
||||
})();
|
||||
|
||||
const targetType = nodes
|
||||
.find((node) => node.id === connect.target)
|
||||
?.data?.inputs.find((input) => input.key === connect.targetHandle)?.valueType;
|
||||
|
||||
if (!sourceType || !targetType) {
|
||||
return toast({
|
||||
status: 'warning',
|
||||
title: t('app.Connection is invalid')
|
||||
});
|
||||
}
|
||||
if (
|
||||
sourceType !== FlowNodeValTypeEnum.any &&
|
||||
targetType !== FlowNodeValTypeEnum.any &&
|
||||
sourceType !== targetType
|
||||
) {
|
||||
return toast({
|
||||
status: 'warning',
|
||||
title: t('app.Connection type is different')
|
||||
});
|
||||
}
|
||||
|
||||
setEdges((state) =>
|
||||
addEdge(
|
||||
{
|
||||
...connect,
|
||||
type: 'buttonedge',
|
||||
animated: true,
|
||||
data: {
|
||||
onDelete: onDelConnect
|
||||
}
|
||||
},
|
||||
state
|
||||
)
|
||||
);
|
||||
},
|
||||
[nodes, onDelConnect, setEdges, t, toast]
|
||||
);
|
||||
|
||||
const onDelNode = useCallback(
|
||||
(nodeId: string) => {
|
||||
setNodes((state) => state.filter((item) => item.id !== nodeId));
|
||||
setEdges((state) => state.filter((edge) => edge.source !== nodeId && edge.target !== nodeId));
|
||||
},
|
||||
[setEdges, setNodes]
|
||||
);
|
||||
|
||||
const onChangeNode = useCallback(
|
||||
({ moduleId, type, key, value, index }: FlowNodeChangeProps) => {
|
||||
setNodes((nodes) =>
|
||||
nodes.map((node) => {
|
||||
if (node.id !== moduleId) return node;
|
||||
|
||||
const updateObj: Record<string, any> = {};
|
||||
|
||||
if (type === 'attr') {
|
||||
if (key) {
|
||||
updateObj[key] = value;
|
||||
}
|
||||
} else if (type === 'updateInput') {
|
||||
updateObj.inputs = node.data.inputs.map((item) => (item.key === key ? value : item));
|
||||
} else if (type === 'replaceInput') {
|
||||
onDelEdge({ moduleId, targetHandle: key });
|
||||
const oldInputIndex = node.data.inputs.findIndex((item) => item.key === key);
|
||||
updateObj.inputs = node.data.inputs.filter((item) => item.key !== key);
|
||||
setTimeout(() => {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'addInput',
|
||||
index: oldInputIndex,
|
||||
value
|
||||
});
|
||||
});
|
||||
} else if (type === 'addInput') {
|
||||
const input = node.data.inputs.find((input) => input.key === value.key);
|
||||
if (input) {
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: 'key 重复'
|
||||
});
|
||||
updateObj.inputs = node.data.inputs;
|
||||
} else {
|
||||
if (index !== undefined) {
|
||||
const inputs = [...node.data.inputs];
|
||||
inputs.splice(index, 0, value);
|
||||
updateObj.inputs = inputs;
|
||||
} else {
|
||||
updateObj.inputs = node.data.inputs.concat(value);
|
||||
}
|
||||
}
|
||||
} else if (type === 'delInput') {
|
||||
onDelEdge({ moduleId, targetHandle: key });
|
||||
updateObj.inputs = node.data.inputs.filter((item) => item.key !== key);
|
||||
} else if (type === 'updateOutput') {
|
||||
updateObj.outputs = node.data.outputs.map((item) => (item.key === key ? value : item));
|
||||
} else if (type === 'replaceOutput') {
|
||||
onDelEdge({ moduleId, sourceHandle: key });
|
||||
const oldOutputIndex = node.data.outputs.findIndex((item) => item.key === key);
|
||||
updateObj.outputs = node.data.outputs.filter((item) => item.key !== key);
|
||||
setTimeout(() => {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'addOutput',
|
||||
index: oldOutputIndex,
|
||||
value
|
||||
});
|
||||
});
|
||||
} else if (type === 'addOutput') {
|
||||
const output = node.data.outputs.find((output) => output.key === value.key);
|
||||
if (output) {
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: 'key 重复'
|
||||
});
|
||||
updateObj.outputs = node.data.outputs;
|
||||
} else {
|
||||
if (index !== undefined) {
|
||||
const outputs = [...node.data.outputs];
|
||||
outputs.splice(index, 0, value);
|
||||
updateObj.outputs = outputs;
|
||||
} else {
|
||||
updateObj.outputs = node.data.outputs.concat(value);
|
||||
}
|
||||
}
|
||||
} else if (type === 'delOutput') {
|
||||
onDelEdge({ moduleId, sourceHandle: key });
|
||||
updateObj.outputs = node.data.outputs.filter((item) => item.key !== key);
|
||||
}
|
||||
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
...updateObj
|
||||
}
|
||||
};
|
||||
})
|
||||
);
|
||||
},
|
||||
[onDelEdge, setNodes, toast]
|
||||
);
|
||||
|
||||
const onCopyNode = useCallback(
|
||||
(nodeId: string) => {
|
||||
setNodes((nodes) => {
|
||||
const node = nodes.find((node) => node.id === nodeId);
|
||||
if (!node) return nodes;
|
||||
const template = {
|
||||
logo: node.data.logo,
|
||||
name: node.data.name,
|
||||
intro: node.data.intro,
|
||||
description: node.data.description,
|
||||
flowType: node.data.flowType,
|
||||
inputs: node.data.inputs,
|
||||
outputs: node.data.outputs,
|
||||
showStatus: node.data.showStatus
|
||||
};
|
||||
return nodes.concat(
|
||||
appModule2FlowNode({
|
||||
item: {
|
||||
...template,
|
||||
moduleId: nanoid(),
|
||||
position: { x: node.position.x + 200, y: node.position.y + 50 }
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
},
|
||||
[setNodes]
|
||||
);
|
||||
|
||||
// reset a node data. delete edge and replace it
|
||||
const onResetNode = useCallback(
|
||||
(id: string, module: FlowModuleTemplateType) => {
|
||||
setNodes((state) =>
|
||||
state.map((node) => {
|
||||
if (node.id === id) {
|
||||
// delete edge
|
||||
node.data.inputs.forEach((item) => {
|
||||
onDelEdge({ moduleId: id, targetHandle: item.key });
|
||||
});
|
||||
node.data.outputs.forEach((item) => {
|
||||
onDelEdge({ moduleId: id, sourceHandle: item.key });
|
||||
});
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
...module
|
||||
}
|
||||
};
|
||||
}
|
||||
return node;
|
||||
})
|
||||
);
|
||||
},
|
||||
[onDelEdge, setNodes]
|
||||
);
|
||||
|
||||
const initData = useCallback(
|
||||
(modules: ModuleItemType[]) => {
|
||||
const edges = appModule2FlowEdge({
|
||||
modules,
|
||||
onDelete: onDelConnect
|
||||
});
|
||||
setEdges(edges);
|
||||
|
||||
setNodes(modules.map((item) => appModule2FlowNode({ item })));
|
||||
|
||||
onFixView();
|
||||
},
|
||||
[onDelConnect, setEdges, setNodes, onFixView]
|
||||
);
|
||||
|
||||
// use eventbus to avoid refresh ReactComponents
|
||||
useEffect(() => {
|
||||
const update = (e: FlowNodeChangeProps) => {
|
||||
onChangeNode(e);
|
||||
};
|
||||
eventBus.on(EventNameEnum.updaterNode, update);
|
||||
return () => {
|
||||
eventBus.off(EventNameEnum.updaterNode);
|
||||
};
|
||||
}, [onChangeNode]);
|
||||
|
||||
const value = {
|
||||
reactFlowWrapper,
|
||||
filterAppIds,
|
||||
nodes,
|
||||
setNodes,
|
||||
onNodesChange,
|
||||
edges,
|
||||
setEdges,
|
||||
onEdgesChange,
|
||||
onFixView,
|
||||
onDelNode,
|
||||
onChangeNode,
|
||||
onResetNode,
|
||||
onCopyNode,
|
||||
onDelEdge,
|
||||
onDelConnect,
|
||||
onConnect,
|
||||
initData
|
||||
};
|
||||
|
||||
return <StateContext.Provider value={value}>{children}</StateContext.Provider>;
|
||||
};
|
||||
|
||||
export default React.memo(FlowProvider);
|
||||
|
||||
export const onChangeNode = (e: FlowNodeChangeProps) => {
|
||||
eventBus.emit(EventNameEnum.updaterNode, e);
|
||||
};
|
||||
|
||||
export function flowNode2Modules({
|
||||
nodes,
|
||||
edges
|
||||
}: {
|
||||
nodes: Node<FlowModuleItemType, string | undefined>[];
|
||||
edges: Edge<any>[];
|
||||
}) {
|
||||
const modules: ModuleItemType[] = nodes.map((item) => ({
|
||||
moduleId: item.data.moduleId,
|
||||
name: item.data.name,
|
||||
logo: item.data.logo,
|
||||
flowType: item.data.flowType,
|
||||
showStatus: item.data.showStatus,
|
||||
position: item.position,
|
||||
inputs: item.data.inputs.map((item) => ({
|
||||
...item,
|
||||
connected: item.connected ?? item.type !== FlowNodeInputTypeEnum.target
|
||||
})),
|
||||
outputs: item.data.outputs.map((item) => ({
|
||||
...item,
|
||||
targets: [] as FlowNodeOutputTargetItemType[]
|
||||
}))
|
||||
}));
|
||||
|
||||
// update inputs and outputs
|
||||
modules.forEach((module) => {
|
||||
module.inputs.forEach((input) => {
|
||||
input.connected =
|
||||
input.connected ||
|
||||
!!edges.find((edge) => edge.target === module.moduleId && edge.targetHandle === input.key);
|
||||
});
|
||||
module.outputs.forEach((output) => {
|
||||
output.targets = edges
|
||||
.filter(
|
||||
(edge) =>
|
||||
edge.source === module.moduleId && edge.sourceHandle === output.key && edge.targetHandle
|
||||
)
|
||||
.map((edge) => ({
|
||||
moduleId: edge.target,
|
||||
key: edge.targetHandle || ''
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
return modules;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Textarea, Button, ModalBody, ModalFooter } from '@chakra-ui/react';
|
||||
import MyModal from '@/components/MyModal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useToast } from '@/web/common/hooks/useToast';
|
||||
import { useFlowProviderStore } from './FlowProvider';
|
||||
|
||||
const ImportSettings = ({ onClose }: { onClose: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const [value, setValue] = useState('');
|
||||
const { setNodes, setEdges, initData } = useFlowProviderStore();
|
||||
|
||||
return (
|
||||
<MyModal isOpen w={'600px'} onClose={onClose} title={t('app.Import Config')}>
|
||||
<ModalBody>
|
||||
<Textarea
|
||||
placeholder={t('app.Paste Config') || 'app.Paste Config'}
|
||||
defaultValue={value}
|
||||
rows={16}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
/>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
variant="base"
|
||||
onClick={() => {
|
||||
if (!value) {
|
||||
return onClose();
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(value);
|
||||
setEdges([]);
|
||||
setNodes([]);
|
||||
setTimeout(() => {
|
||||
initData(data);
|
||||
}, 10);
|
||||
onClose();
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: t('app.Import Config Failed')
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
确认
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ImportSettings);
|
||||
110
projects/app/src/components/core/module/Flow/SelectAppModal.tsx
Normal file
110
projects/app/src/components/core/module/Flow/SelectAppModal.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { ModalBody, Flex, Box, useTheme, ModalFooter, Button } from '@chakra-ui/react';
|
||||
import MyModal from '@/components/MyModal';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { SelectAppItemType } from '@fastgpt/global/core/module/type';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLoading } from '@/web/common/hooks/useLoading';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
|
||||
const SelectAppModal = ({
|
||||
defaultApps = [],
|
||||
filterAppIds = [],
|
||||
max = 1,
|
||||
onClose,
|
||||
onSuccess
|
||||
}: {
|
||||
defaultApps: string[];
|
||||
filterAppIds?: string[];
|
||||
max?: number;
|
||||
onClose: () => void;
|
||||
onSuccess: (e: SelectAppItemType[]) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { Loading } = useLoading();
|
||||
const theme = useTheme();
|
||||
const [selectedApps, setSelectedApps] = React.useState<string[]>(defaultApps);
|
||||
/* 加载模型 */
|
||||
const { myApps, loadMyApps } = useUserStore();
|
||||
const { isLoading } = useQuery(['loadMyApos'], () => loadMyApps());
|
||||
|
||||
const apps = useMemo(
|
||||
() => myApps.filter((app) => !filterAppIds.includes(app._id)),
|
||||
[myApps, filterAppIds]
|
||||
);
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen
|
||||
title={`选择应用${max > 1 ? `(${selectedApps.length}/${max})` : ''}`}
|
||||
onClose={onClose}
|
||||
minW={'700px'}
|
||||
position={'relative'}
|
||||
>
|
||||
<ModalBody
|
||||
minH={'300px'}
|
||||
display={'grid'}
|
||||
gridTemplateColumns={['1fr', 'repeat(3, minmax(0, 1fr))']}
|
||||
gridGap={4}
|
||||
>
|
||||
{apps.map((app) => (
|
||||
<Flex
|
||||
key={app._id}
|
||||
alignItems={'center'}
|
||||
border={theme.borders.base}
|
||||
borderRadius={'md'}
|
||||
p={2}
|
||||
cursor={'pointer'}
|
||||
{...(selectedApps.includes(app._id)
|
||||
? {
|
||||
bg: 'myBlue.200',
|
||||
onClick: () => {
|
||||
setSelectedApps(selectedApps.filter((e) => e !== app._id));
|
||||
}
|
||||
}
|
||||
: {
|
||||
onClick: () => {
|
||||
if (max === 1) {
|
||||
setSelectedApps([app._id]);
|
||||
} else if (selectedApps.length < max) {
|
||||
setSelectedApps([...selectedApps, app._id]);
|
||||
}
|
||||
}
|
||||
})}
|
||||
>
|
||||
<Avatar src={app.avatar} w={['16px', '22px']} />
|
||||
<Box fontWeight={'bold'} ml={1}>
|
||||
{app.name}
|
||||
</Box>
|
||||
</Flex>
|
||||
))}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant={'base'} onClick={onClose}>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
ml={2}
|
||||
onClick={() => {
|
||||
onSuccess(
|
||||
apps
|
||||
.filter((app) => selectedApps.includes(app._id))
|
||||
.map((app) => ({
|
||||
id: app._id,
|
||||
name: app.name,
|
||||
logo: app.avatar
|
||||
}))
|
||||
);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
{t('Confirm')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
<Loading loading={isLoading} fixed={false} />
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(SelectAppModal);
|
||||
243
projects/app/src/components/core/module/Flow/TemplateList.tsx
Normal file
243
projects/app/src/components/core/module/Flow/TemplateList.tsx
Normal file
@@ -0,0 +1,243 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import type {
|
||||
FlowModuleTemplateType,
|
||||
SystemModuleTemplateType
|
||||
} from '@fastgpt/global/core/module/type.d';
|
||||
import { useViewport, XYPosition } from 'reactflow';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import { useFlowProviderStore } from './FlowProvider';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import { appModule2FlowNode } from '@/utils/adapt';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useRouter } from 'next/router';
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
|
||||
import MyIcon from '@/components/Icon';
|
||||
import EmptyTip from '@/components/EmptyTip';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
|
||||
import { getPluginModuleDetail } from '@/web/core/plugin/api';
|
||||
import { useToast } from '@/web/common/hooks/useToast';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
|
||||
enum TemplateTypeEnum {
|
||||
system = 'system',
|
||||
combine = 'combine'
|
||||
}
|
||||
|
||||
export type ModuleTemplateProps = {
|
||||
systemTemplates: SystemModuleTemplateType;
|
||||
pluginTemplates: SystemModuleTemplateType;
|
||||
show2Plugin?: boolean;
|
||||
};
|
||||
|
||||
const ModuleTemplateList = ({
|
||||
systemTemplates,
|
||||
pluginTemplates,
|
||||
show2Plugin = false,
|
||||
isOpen,
|
||||
onClose
|
||||
}: ModuleTemplateProps & {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const [templateType, setTemplateType] = React.useState(TemplateTypeEnum.system);
|
||||
|
||||
const typeList = useMemo(
|
||||
() => [
|
||||
{
|
||||
type: TemplateTypeEnum.system,
|
||||
label: t('app.module.System Module'),
|
||||
child: <RenderList templates={systemTemplates} onClose={onClose} />
|
||||
},
|
||||
{
|
||||
type: TemplateTypeEnum.combine,
|
||||
label: t('plugin.Plugin Module'),
|
||||
child: <RenderList templates={pluginTemplates} onClose={onClose} />
|
||||
}
|
||||
],
|
||||
[pluginTemplates, onClose, systemTemplates, t]
|
||||
);
|
||||
const TemplateItem = useMemo(
|
||||
() => typeList.find((item) => item.type === templateType)?.child,
|
||||
[templateType, typeList]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
zIndex={2}
|
||||
display={isOpen ? 'block' : 'none'}
|
||||
position={'absolute'}
|
||||
top={0}
|
||||
left={0}
|
||||
bottom={0}
|
||||
w={'360px'}
|
||||
onClick={onClose}
|
||||
/>
|
||||
<Flex
|
||||
zIndex={3}
|
||||
flexDirection={'column'}
|
||||
position={'absolute'}
|
||||
top={'65px'}
|
||||
left={0}
|
||||
pb={4}
|
||||
h={isOpen ? 'calc(100% - 100px)' : '0'}
|
||||
w={isOpen ? ['100%', '360px'] : '0'}
|
||||
bg={'white'}
|
||||
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
|
||||
borderRadius={'20px'}
|
||||
overflow={'hidden'}
|
||||
transition={'.2s ease'}
|
||||
userSelect={'none'}
|
||||
>
|
||||
<Flex pt={4} pb={1} px={5} gap={4} alignItems={'center'} fontSize={['md', 'xl']}>
|
||||
{typeList.map((item) => (
|
||||
<Box
|
||||
key={item.label}
|
||||
borderBottom={'2px solid transparent'}
|
||||
{...(item.type === templateType
|
||||
? {
|
||||
color: 'myBlue.700',
|
||||
borderBottomColor: 'myBlue.700',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
: {
|
||||
cursor: 'pointer',
|
||||
onClick: () => setTemplateType(item.type)
|
||||
})}
|
||||
>
|
||||
{item.label}
|
||||
</Box>
|
||||
))}
|
||||
<Box flex={1} />
|
||||
{show2Plugin && templateType === TemplateTypeEnum.combine && (
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
_hover={{ textDecoration: 'underline' }}
|
||||
cursor={'pointer'}
|
||||
onClick={() => router.push('/plugin/list')}
|
||||
>
|
||||
<Box fontSize={'sm'} transform={'translateY(-1px)'}>
|
||||
{t('plugin.To Edit Plugin')}
|
||||
</Box>
|
||||
<MyIcon name={'rightArrowLight'} w={'12px'} />
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
{TemplateItem}
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ModuleTemplateList);
|
||||
|
||||
var RenderList = React.memo(function RenderList({
|
||||
templates,
|
||||
onClose
|
||||
}: {
|
||||
templates: {
|
||||
label: string;
|
||||
list: FlowModuleTemplateType[];
|
||||
}[];
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { isPc } = useSystemStore();
|
||||
const { setNodes, reactFlowWrapper } = useFlowProviderStore();
|
||||
const { x, y, zoom } = useViewport();
|
||||
const { setLoading } = useSystemStore();
|
||||
const { toast } = useToast();
|
||||
|
||||
const onAddNode = useCallback(
|
||||
async ({ template, position }: { template: FlowModuleTemplateType; position: XYPosition }) => {
|
||||
if (!reactFlowWrapper?.current) return;
|
||||
|
||||
let templateModule = { ...template };
|
||||
|
||||
// get plugin module
|
||||
try {
|
||||
if (templateModule.flowType === FlowNodeTypeEnum.pluginModule) {
|
||||
setLoading(true);
|
||||
const pluginModule = await getPluginModuleDetail(templateModule.id);
|
||||
templateModule = {
|
||||
...templateModule,
|
||||
...pluginModule
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
return toast({
|
||||
status: 'error',
|
||||
title: getErrText(e, t('plugin.Get Plugin Module Detail Failed'))
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect();
|
||||
const mouseX = (position.x - reactFlowBounds.left - x) / zoom - 100;
|
||||
const mouseY = (position.y - reactFlowBounds.top - y) / zoom;
|
||||
|
||||
setNodes((state) =>
|
||||
state.concat(
|
||||
appModule2FlowNode({
|
||||
item: {
|
||||
...templateModule,
|
||||
moduleId: nanoid(),
|
||||
position: { x: mouseX, y: mouseY - 20 }
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
},
|
||||
[reactFlowWrapper, setLoading, setNodes, t, toast, x, y, zoom]
|
||||
);
|
||||
|
||||
const list = useMemo(() => templates.map((item) => item.list).flat(), [templates]);
|
||||
|
||||
return list.length === 0 ? (
|
||||
<EmptyTip text={t('app.module.No Modules')} />
|
||||
) : (
|
||||
<Box flex={'1 0 0'} overflow={'overlay'}>
|
||||
<Box w={['100%', '330px']} mx={'auto'}>
|
||||
{list.map((item) => (
|
||||
<Flex
|
||||
key={item.id}
|
||||
alignItems={'center'}
|
||||
p={5}
|
||||
cursor={'pointer'}
|
||||
_hover={{ bg: 'myWhite.600' }}
|
||||
borderRadius={'md'}
|
||||
draggable
|
||||
onDragEnd={(e) => {
|
||||
if (e.clientX < 360) return;
|
||||
onAddNode({
|
||||
template: item,
|
||||
position: { x: e.clientX, y: e.clientY }
|
||||
});
|
||||
}}
|
||||
onClick={(e) => {
|
||||
if (isPc) return;
|
||||
onClose();
|
||||
onAddNode({
|
||||
template: item,
|
||||
position: { x: e.clientX, y: e.clientY }
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Avatar src={item.logo} w={'34px'} objectFit={'contain'} borderRadius={'0'} />
|
||||
<Box ml={5} flex={'1 0 0'}>
|
||||
<Box color={'black'}>{item.name}</Box>
|
||||
<Box className="textEllipsis3" color={'myGray.500'} fontSize={'sm'}>
|
||||
{item.intro}
|
||||
</Box>
|
||||
</Box>
|
||||
</Flex>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import { BaseEdge, EdgeLabelRenderer, EdgeProps, getBezierPath } from 'reactflow';
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import MyIcon from '@/components/Icon';
|
||||
|
||||
const ButtonEdge = ({
|
||||
id,
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
style = {},
|
||||
markerEnd,
|
||||
data
|
||||
}: EdgeProps<{
|
||||
onDelete: (id: string) => void;
|
||||
}>) => {
|
||||
const [edgePath, labelX, labelY] = getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<BaseEdge path={edgePath} markerEnd={markerEnd} style={style} />
|
||||
<EdgeLabelRenderer>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
position={'absolute'}
|
||||
transform={`translate(-50%, -50%) translate(${labelX}px,${labelY}px)`}
|
||||
pointerEvents={'all'}
|
||||
w={'20px'}
|
||||
h={'20px'}
|
||||
bg={'white'}
|
||||
borderRadius={'20px'}
|
||||
color={'black'}
|
||||
cursor={'pointer'}
|
||||
border={'1px solid #fff'}
|
||||
_hover={{
|
||||
boxShadow: '0 0 6px 2px rgba(0, 0, 0, 0.08)'
|
||||
}}
|
||||
onClick={() => data?.onDelete(id)}
|
||||
>
|
||||
<MyIcon name="closeSolid" w={'100%'} color={'myGray.600'}></MyIcon>
|
||||
</Flex>
|
||||
</EdgeLabelRenderer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ButtonEdge);
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { BoxProps } from '@chakra-ui/react';
|
||||
|
||||
const Container = ({ children, ...props }: BoxProps) => {
|
||||
return (
|
||||
<Box px={4} py={3} position={'relative'} {...props}>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Container);
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { Box, useTheme } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
const Divider = ({ text }: { text: 'Input' | 'Output' | string }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Box
|
||||
textAlign={'center'}
|
||||
bg={'#f8f8f8'}
|
||||
py={2}
|
||||
borderTop={theme.borders.base}
|
||||
borderBottom={theme.borders.base}
|
||||
fontSize={'lg'}
|
||||
>
|
||||
{t(`common.${text}`)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Divider);
|
||||
@@ -0,0 +1,74 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
ModalBody,
|
||||
Flex,
|
||||
Switch,
|
||||
Input
|
||||
} from '@chakra-ui/react';
|
||||
import type { ContextExtractAgentItemType } from '@/types/app';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
|
||||
import MyModal from '@/components/MyModal';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
|
||||
const ExtractFieldModal = ({
|
||||
defaultField = {
|
||||
desc: '',
|
||||
key: '',
|
||||
required: true
|
||||
},
|
||||
onClose,
|
||||
onSubmit
|
||||
}: {
|
||||
defaultField?: ContextExtractAgentItemType;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: ContextExtractAgentItemType) => void;
|
||||
}) => {
|
||||
const { register, handleSubmit } = useForm<ContextExtractAgentItemType>({
|
||||
defaultValues: defaultField
|
||||
});
|
||||
|
||||
return (
|
||||
<MyModal isOpen={true} onClose={onClose}>
|
||||
<ModalHeader display={'flex'} alignItems={'center'}>
|
||||
<Avatar src={'/imgs/module/extract.png'} mr={2} w={'20px'} objectFit={'cover'} />
|
||||
提取字段配置
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box flex={'0 0 70px'}>必填</Box>
|
||||
<Switch {...register('required')} />
|
||||
</Flex>
|
||||
<Flex mt={5} alignItems={'center'}>
|
||||
<Box flex={'0 0 70px'}>字段描述</Box>
|
||||
<Input
|
||||
placeholder="姓名/年龄/sql语句……"
|
||||
{...register('desc', { required: '字段描述不能为空' })}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex mt={5} alignItems={'center'}>
|
||||
<Box flex={'0 0 70px'}>字段 key</Box>
|
||||
<Input
|
||||
placeholder="name/age/sql"
|
||||
{...register('key', { required: '字段 key 不能为空' })}
|
||||
/>
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button variant={'base'} mr={3} onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={handleSubmit(onSubmit)}>确认</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ExtractFieldModal);
|
||||
@@ -0,0 +1,153 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
ModalFooter,
|
||||
ModalBody,
|
||||
Flex,
|
||||
Switch,
|
||||
Input,
|
||||
Textarea
|
||||
} from '@chakra-ui/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import MyModal from '@/components/MyModal';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import { FlowNodeValTypeEnum } from '@fastgpt/global/core/module/node/constant';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import MySelect from '@/components/Select';
|
||||
|
||||
const typeSelectList = [
|
||||
{
|
||||
label: '字符串',
|
||||
value: FlowNodeValTypeEnum.string
|
||||
},
|
||||
{
|
||||
label: '数字',
|
||||
value: FlowNodeValTypeEnum.number
|
||||
},
|
||||
{
|
||||
label: '布尔',
|
||||
value: FlowNodeValTypeEnum.boolean
|
||||
},
|
||||
{
|
||||
label: '历史记录',
|
||||
value: FlowNodeValTypeEnum.chatHistory
|
||||
},
|
||||
{
|
||||
label: '引用内容',
|
||||
value: FlowNodeValTypeEnum.datasetQuote
|
||||
},
|
||||
{
|
||||
label: '任意',
|
||||
value: FlowNodeValTypeEnum.any
|
||||
}
|
||||
];
|
||||
|
||||
export type EditFieldModeType = 'input' | 'output' | 'pluginInput';
|
||||
export type EditFieldType = {
|
||||
key: string;
|
||||
label?: string;
|
||||
valueType?: `${FlowNodeValTypeEnum}`;
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
};
|
||||
|
||||
const FieldEditModal = ({
|
||||
mode,
|
||||
defaultField = {
|
||||
label: '',
|
||||
key: '',
|
||||
description: '',
|
||||
valueType: FlowNodeValTypeEnum.string,
|
||||
required: false
|
||||
},
|
||||
onClose,
|
||||
onSubmit
|
||||
}: {
|
||||
mode: EditFieldModeType;
|
||||
defaultField?: EditFieldType;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: EditFieldType) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { register, getValues, setValue, handleSubmit } = useForm<EditFieldType>({
|
||||
defaultValues: defaultField
|
||||
});
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
|
||||
const title = ['input', 'pluginInput'].includes(mode)
|
||||
? t('app.Input Field Settings')
|
||||
: t('app.Output Field Settings');
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen={true}
|
||||
title={
|
||||
<Flex alignItems={'center'}>
|
||||
<Avatar src={'/imgs/module/extract.png'} mr={2} w={'20px'} objectFit={'cover'} />
|
||||
{title}
|
||||
</Flex>
|
||||
}
|
||||
onClose={onClose}
|
||||
>
|
||||
<ModalBody minH={'260px'} overflow={'visible'}>
|
||||
{mode === 'input' && (
|
||||
<Flex alignItems={'center'} mb={5}>
|
||||
<Box flex={'0 0 70px'}>必填</Box>
|
||||
<Switch {...register('required')} />
|
||||
</Flex>
|
||||
)}
|
||||
<Flex mb={5} alignItems={'center'}>
|
||||
<Box flex={'0 0 70px'}>字段类型</Box>
|
||||
<MySelect
|
||||
w={'288px'}
|
||||
list={typeSelectList}
|
||||
value={getValues('valueType')}
|
||||
onchange={(e: string) => {
|
||||
const type = e as `${FlowNodeValTypeEnum}`;
|
||||
setValue('valueType', type);
|
||||
|
||||
if (
|
||||
type === FlowNodeValTypeEnum.chatHistory ||
|
||||
type === FlowNodeValTypeEnum.datasetQuote
|
||||
) {
|
||||
const label = typeSelectList.find((item) => item.value === type)?.label;
|
||||
setValue('label', label);
|
||||
}
|
||||
|
||||
setRefresh(!refresh);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex mb={5} alignItems={'center'}>
|
||||
<Box flex={'0 0 70px'}>字段名</Box>
|
||||
<Input
|
||||
placeholder="预约字段/sql语句……"
|
||||
{...register('label', { required: '字段名不能为空' })}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Flex mb={5} alignItems={'center'}>
|
||||
<Box flex={'0 0 70px'}>字段 key</Box>
|
||||
<Input
|
||||
placeholder="appointment/sql"
|
||||
{...register('key', { required: '字段 key 不能为空' })}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex mb={5} alignItems={'flex-start'}>
|
||||
<Box flex={'0 0 70px'}>字段描述</Box>
|
||||
<Textarea placeholder="可选" rows={3} {...register('description')} />
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button variant={'base'} mr={3} onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={handleSubmit(onSubmit)}>确认</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(FieldEditModal);
|
||||
@@ -0,0 +1,196 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Box, Flex, useTheme, Menu, MenuButton, MenuList, MenuItem } from '@chakra-ui/react';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import type { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useEditTitle } from '@/web/common/hooks/useEditTitle';
|
||||
import { useToast } from '@/web/common/hooks/useToast';
|
||||
import { useFlowProviderStore, onChangeNode } from '../../FlowProvider';
|
||||
import {
|
||||
FlowNodeSpecialInputKeyEnum,
|
||||
FlowNodeTypeEnum
|
||||
} from '@fastgpt/global/core/module/node/constant';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { getPluginModuleDetail } from '@/web/core/plugin/api';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
import { useConfirm } from '@/web/common/hooks/useConfirm';
|
||||
|
||||
type Props = FlowModuleItemType & {
|
||||
children?: React.ReactNode | React.ReactNode[] | string;
|
||||
minW?: string | number;
|
||||
isPreview?: boolean;
|
||||
};
|
||||
|
||||
const NodeCard = (props: Props) => {
|
||||
const {
|
||||
children,
|
||||
logo = '/icon/logo.svg',
|
||||
name = '未知模块',
|
||||
description,
|
||||
minW = '300px',
|
||||
moduleId,
|
||||
flowType,
|
||||
inputs,
|
||||
isPreview
|
||||
} = props;
|
||||
const { onCopyNode, onResetNode, onDelNode } = useFlowProviderStore();
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const { toast } = useToast();
|
||||
const { setLoading } = useSystemStore();
|
||||
|
||||
// custom title edit
|
||||
const { onOpenModal, EditModal: EditTitleModal } = useEditTitle({
|
||||
title: t('common.Custom Title'),
|
||||
placeholder: t('app.module.Custom Title Tip') || ''
|
||||
});
|
||||
const { openConfirm, ConfirmModal } = useConfirm({
|
||||
content: t('module.Confirm Sync Plugin')
|
||||
});
|
||||
|
||||
const menuList = useMemo(
|
||||
() => [
|
||||
...(flowType === FlowNodeTypeEnum.pluginModule
|
||||
? [
|
||||
{
|
||||
icon: 'common/refreshLight',
|
||||
label: t('plugin.Synchronous version'),
|
||||
onClick: () => {
|
||||
const pluginId = inputs.find(
|
||||
(item) => item.key === FlowNodeSpecialInputKeyEnum.pluginId
|
||||
)?.value;
|
||||
if (!pluginId) return;
|
||||
openConfirm(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const pluginModule = await getPluginModuleDetail(pluginId);
|
||||
onResetNode(moduleId, pluginModule);
|
||||
} catch (e) {
|
||||
return toast({
|
||||
status: 'error',
|
||||
title: getErrText(e, t('plugin.Get Plugin Module Detail Failed'))
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
})();
|
||||
}
|
||||
}
|
||||
]
|
||||
: [
|
||||
{
|
||||
icon: 'edit',
|
||||
label: t('common.Rename'),
|
||||
onClick: () =>
|
||||
onOpenModal({
|
||||
defaultVal: name,
|
||||
onSuccess: (e) => {
|
||||
if (!e) {
|
||||
return toast({
|
||||
title: t('app.modules.Title is required'),
|
||||
status: 'warning'
|
||||
});
|
||||
}
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'attr',
|
||||
key: 'name',
|
||||
value: e
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
]),
|
||||
{
|
||||
icon: 'copy',
|
||||
label: t('common.Copy'),
|
||||
onClick: () => onCopyNode(moduleId)
|
||||
},
|
||||
{
|
||||
icon: 'delete',
|
||||
label: t('common.Delete'),
|
||||
onClick: () => onDelNode(moduleId)
|
||||
},
|
||||
|
||||
{
|
||||
icon: 'back',
|
||||
label: t('common.Back'),
|
||||
onClick: () => {}
|
||||
}
|
||||
],
|
||||
[
|
||||
flowType,
|
||||
inputs,
|
||||
moduleId,
|
||||
name,
|
||||
onCopyNode,
|
||||
onDelNode,
|
||||
onOpenModal,
|
||||
onResetNode,
|
||||
openConfirm,
|
||||
setLoading,
|
||||
t,
|
||||
toast
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
minW={minW}
|
||||
maxW={'500px'}
|
||||
bg={'white'}
|
||||
border={theme.borders.md}
|
||||
borderRadius={'md'}
|
||||
boxShadow={'sm'}
|
||||
className={isPreview ? 'nodrag' : ''}
|
||||
>
|
||||
<Flex className="custom-drag-handle" px={4} py={3} alignItems={'center'}>
|
||||
<Avatar src={logo} borderRadius={'md'} objectFit={'contain'} w={'30px'} h={'30px'} />
|
||||
<Box ml={3} fontSize={'lg'} color={'myGray.600'}>
|
||||
{name}
|
||||
</Box>
|
||||
{description && (
|
||||
<MyTooltip label={description} forceShow>
|
||||
<QuestionOutlineIcon
|
||||
display={['none', 'inline']}
|
||||
transform={'translateY(1px)'}
|
||||
mb={'1px'}
|
||||
ml={1}
|
||||
/>
|
||||
</MyTooltip>
|
||||
)}
|
||||
<Box flex={1} />
|
||||
{!isPreview && (
|
||||
<Menu autoSelect={false} isLazy>
|
||||
<MenuButton
|
||||
className={'nodrag'}
|
||||
_hover={{ bg: 'myWhite.600' }}
|
||||
cursor={'pointer'}
|
||||
borderRadius={'md'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<MyIcon name={'more'} w={'14px'} p={2} />
|
||||
</MenuButton>
|
||||
<MenuList color={'myGray.700'} minW={`120px !important`} zIndex={10}>
|
||||
{menuList.map((item) => (
|
||||
<MenuItem key={item.label} onClick={item.onClick} py={[2, 3]}>
|
||||
<MyIcon name={item.icon as any} w={['14px', '16px']} />
|
||||
<Box ml={[1, 2]}>{item.label}</Box>
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
)}
|
||||
</Flex>
|
||||
{children}
|
||||
<EditTitleModal />
|
||||
<ConfirmModal />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(NodeCard);
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import NodeCard from '../modules/NodeCard';
|
||||
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
|
||||
import Container from '../modules/Container';
|
||||
import RenderInput from '../render/RenderInput';
|
||||
import RenderOutput from '../render/RenderOutput';
|
||||
|
||||
const NodeAnswer = ({ data }: NodeProps<FlowModuleItemType>) => {
|
||||
const { moduleId, inputs, outputs } = data;
|
||||
return (
|
||||
<NodeCard minW={'400px'} {...data}>
|
||||
<Container borderTop={'2px solid'} borderTopColor={'myGray.200'}>
|
||||
<RenderInput moduleId={moduleId} flowInputList={inputs} />
|
||||
<RenderOutput moduleId={moduleId} flowOutputList={outputs} />
|
||||
</Container>
|
||||
</NodeCard>
|
||||
);
|
||||
};
|
||||
export default React.memo(NodeAnswer);
|
||||
@@ -0,0 +1,145 @@
|
||||
import React from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import { Box, Input, Button, Flex, Textarea } from '@chakra-ui/react';
|
||||
import NodeCard from '../modules/NodeCard';
|
||||
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
|
||||
import Divider from '../modules/Divider';
|
||||
import Container from '../modules/Container';
|
||||
import RenderInput from '../render/RenderInput';
|
||||
import type { ClassifyQuestionAgentItemType } from '@/types/app';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 4);
|
||||
import MyIcon from '@/components/Icon';
|
||||
import {
|
||||
FlowNodeOutputTypeEnum,
|
||||
FlowNodeValTypeEnum,
|
||||
FlowNodeSpecialInputKeyEnum
|
||||
} from '@fastgpt/global/core/module/node/constant';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import SourceHandle from '../render/SourceHandle';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import { onChangeNode } from '../../FlowProvider';
|
||||
|
||||
const NodeCQNode = ({ data }: NodeProps<FlowModuleItemType>) => {
|
||||
const { t } = useTranslation();
|
||||
const { moduleId, inputs, outputs } = data;
|
||||
|
||||
return (
|
||||
<NodeCard minW={'400px'} {...data}>
|
||||
<Divider text="Input" />
|
||||
<Container>
|
||||
<RenderInput
|
||||
moduleId={moduleId}
|
||||
flowInputList={inputs}
|
||||
CustomComponent={{
|
||||
[FlowNodeSpecialInputKeyEnum.agents]: ({
|
||||
key: agentKey,
|
||||
value: agents = [],
|
||||
...props
|
||||
}: {
|
||||
key: string;
|
||||
value?: ClassifyQuestionAgentItemType[];
|
||||
}) => (
|
||||
<Box>
|
||||
{agents.map((item, i) => (
|
||||
<Box key={item.key} mb={4}>
|
||||
<Flex alignItems={'center'}>
|
||||
<MyTooltip label={t('common.Delete')}>
|
||||
<MyIcon
|
||||
mt={1}
|
||||
mr={2}
|
||||
name={'minus'}
|
||||
w={'14px'}
|
||||
cursor={'pointer'}
|
||||
color={'myGray.600'}
|
||||
_hover={{ color: 'red.600' }}
|
||||
onClick={() => {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'updateInput',
|
||||
key: agentKey,
|
||||
value: {
|
||||
...props,
|
||||
key: agentKey,
|
||||
value: agents.filter((input) => input.key !== item.key)
|
||||
}
|
||||
});
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'delOutput',
|
||||
key: item.key
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
<Box flex={1}>分类{i + 1}</Box>
|
||||
</Flex>
|
||||
<Box position={'relative'}>
|
||||
<Textarea
|
||||
rows={2}
|
||||
mt={1}
|
||||
defaultValue={item.value}
|
||||
onChange={(e) => {
|
||||
const newVal = agents.map((val) =>
|
||||
val.key === item.key
|
||||
? {
|
||||
...val,
|
||||
value: e.target.value
|
||||
}
|
||||
: val
|
||||
);
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'updateInput',
|
||||
key: agentKey,
|
||||
value: {
|
||||
...props,
|
||||
key: agentKey,
|
||||
value: newVal
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<SourceHandle handleKey={item.key} valueType={FlowNodeValTypeEnum.boolean} />
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
<Button
|
||||
onClick={() => {
|
||||
const key = nanoid();
|
||||
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'updateInput',
|
||||
key: agentKey,
|
||||
value: {
|
||||
...props,
|
||||
key: agentKey,
|
||||
value: agents.concat({ value: '', key })
|
||||
}
|
||||
});
|
||||
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'updateOutput',
|
||||
key: agentKey,
|
||||
value: outputs.concat({
|
||||
key,
|
||||
label: '',
|
||||
type: FlowNodeOutputTypeEnum.hidden,
|
||||
targets: []
|
||||
})
|
||||
});
|
||||
}}
|
||||
>
|
||||
添加问题类型
|
||||
</Button>
|
||||
</Box>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Container>
|
||||
</NodeCard>
|
||||
);
|
||||
};
|
||||
export default React.memo(NodeCQNode);
|
||||
@@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import NodeCard from '../modules/NodeCard';
|
||||
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
|
||||
|
||||
const NodeAnswer = ({ data }: NodeProps<FlowModuleItemType>) => {
|
||||
return <NodeCard {...data}></NodeCard>;
|
||||
};
|
||||
export default React.memo(NodeAnswer);
|
||||
@@ -0,0 +1,197 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Box, Button, Table, Thead, Tbody, Tr, Th, Td, TableContainer } from '@chakra-ui/react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import NodeCard from '../modules/NodeCard';
|
||||
import Container from '../modules/Container';
|
||||
import { AddIcon } from '@chakra-ui/icons';
|
||||
import RenderInput from '../render/RenderInput';
|
||||
import Divider from '../modules/Divider';
|
||||
import { ContextExtractAgentItemType } from '@/types/app';
|
||||
import RenderOutput from '../render/RenderOutput';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import ExtractFieldModal from '../modules/ExtractFieldModal';
|
||||
import { ContextExtractEnum } from '@/constants/flow/flowField';
|
||||
import {
|
||||
FlowNodeOutputTypeEnum,
|
||||
FlowNodeValTypeEnum
|
||||
} from '@fastgpt/global/core/module/node/constant';
|
||||
import { useFlowProviderStore, onChangeNode } from '../../FlowProvider';
|
||||
|
||||
const NodeExtract = ({ data }: NodeProps<FlowModuleItemType>) => {
|
||||
const { inputs, outputs, moduleId } = data;
|
||||
const { t } = useTranslation();
|
||||
const [editExtractFiled, setEditExtractField] = useState<ContextExtractAgentItemType>();
|
||||
const { onDelEdge } = useFlowProviderStore();
|
||||
|
||||
return (
|
||||
<NodeCard minW={'400px'} {...data}>
|
||||
<Divider text="Input" />
|
||||
<Container>
|
||||
<RenderInput
|
||||
moduleId={moduleId}
|
||||
flowInputList={inputs}
|
||||
CustomComponent={{
|
||||
[ContextExtractEnum.extractKeys]: ({
|
||||
value: extractKeys = [],
|
||||
...props
|
||||
}: {
|
||||
value?: ContextExtractAgentItemType[];
|
||||
}) => (
|
||||
<Box pt={2}>
|
||||
<Box position={'absolute'} top={0} right={0}>
|
||||
<Button
|
||||
variant={'base'}
|
||||
leftIcon={<AddIcon fontSize={'10px'} />}
|
||||
onClick={() =>
|
||||
setEditExtractField({
|
||||
desc: '',
|
||||
key: '',
|
||||
required: true
|
||||
})
|
||||
}
|
||||
>
|
||||
新增字段
|
||||
</Button>
|
||||
</Box>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>字段 key</Th>
|
||||
<Th>字段描述</Th>
|
||||
<Th>必须</Th>
|
||||
<Th></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{extractKeys.map((item, index) => (
|
||||
<Tr
|
||||
key={index}
|
||||
position={'relative'}
|
||||
whiteSpace={'pre-wrap'}
|
||||
wordBreak={'break-all'}
|
||||
>
|
||||
<Td>{item.key}</Td>
|
||||
<Td>{item.desc}</Td>
|
||||
<Td>{item.required ? '✔' : ''}</Td>
|
||||
<Td whiteSpace={'nowrap'}>
|
||||
<MyIcon
|
||||
mr={3}
|
||||
name={'settingLight'}
|
||||
w={'16px'}
|
||||
cursor={'pointer'}
|
||||
onClick={() => {
|
||||
setEditExtractField(item);
|
||||
}}
|
||||
/>
|
||||
<MyIcon
|
||||
name={'delete'}
|
||||
w={'16px'}
|
||||
cursor={'pointer'}
|
||||
onClick={() => {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'updateInput',
|
||||
key: ContextExtractEnum.extractKeys,
|
||||
value: {
|
||||
...props,
|
||||
value: extractKeys.filter((extract) => item.key !== extract.key)
|
||||
}
|
||||
});
|
||||
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'delOutput',
|
||||
key: item.key
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Container>
|
||||
<Divider text="Output" />
|
||||
<Container>
|
||||
<RenderOutput moduleId={moduleId} flowOutputList={outputs} />
|
||||
</Container>
|
||||
|
||||
{!!editExtractFiled && (
|
||||
<ExtractFieldModal
|
||||
defaultField={editExtractFiled}
|
||||
onClose={() => setEditExtractField(undefined)}
|
||||
onSubmit={(data) => {
|
||||
const extracts: ContextExtractAgentItemType[] =
|
||||
inputs.find((item) => item.key === ContextExtractEnum.extractKeys)?.value || [];
|
||||
|
||||
const exists = extracts.find((item) => item.key === editExtractFiled.key);
|
||||
|
||||
const newInputs = exists
|
||||
? extracts.map((item) => (item.key === editExtractFiled.key ? data : item))
|
||||
: extracts.concat(data);
|
||||
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'updateInput',
|
||||
key: ContextExtractEnum.extractKeys,
|
||||
value: {
|
||||
...inputs.find((input) => input.key === ContextExtractEnum.extractKeys),
|
||||
value: newInputs
|
||||
}
|
||||
});
|
||||
|
||||
const newOutput = {
|
||||
key: data.key,
|
||||
label: `提取结果-${data.desc}`,
|
||||
description: '无法提取时不会返回',
|
||||
valueType: FlowNodeValTypeEnum.string,
|
||||
type: FlowNodeOutputTypeEnum.source,
|
||||
targets: []
|
||||
};
|
||||
|
||||
if (exists) {
|
||||
if (editExtractFiled.key === data.key) {
|
||||
const output = outputs.find((output) => output.key === data.key);
|
||||
// update
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'updateOutput',
|
||||
key: data.key,
|
||||
value: {
|
||||
...output,
|
||||
label: `提取结果-${data.desc}`
|
||||
}
|
||||
});
|
||||
} else {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'replaceOutput',
|
||||
key: editExtractFiled.key,
|
||||
value: newOutput
|
||||
});
|
||||
}
|
||||
} else {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'addOutput',
|
||||
value: newOutput
|
||||
});
|
||||
}
|
||||
|
||||
setEditExtractField(undefined);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</NodeCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(NodeExtract);
|
||||
@@ -0,0 +1,80 @@
|
||||
import React from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import NodeCard from '../modules/NodeCard';
|
||||
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
|
||||
import Divider from '../modules/Divider';
|
||||
import Container from '../modules/Container';
|
||||
import RenderInput from '../render/RenderInput';
|
||||
import { Box, Button } from '@chakra-ui/react';
|
||||
import { SmallAddIcon } from '@chakra-ui/icons';
|
||||
import RenderOutput from '../render/RenderOutput';
|
||||
|
||||
import {
|
||||
FlowNodeInputTypeEnum,
|
||||
FlowNodeOutputTypeEnum,
|
||||
FlowNodeValTypeEnum
|
||||
} from '@fastgpt/global/core/module/node/constant';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
|
||||
import { onChangeNode } from '../../FlowProvider';
|
||||
|
||||
const NodeHttp = ({ data }: NodeProps<FlowModuleItemType>) => {
|
||||
const { moduleId, inputs, outputs } = data;
|
||||
|
||||
return (
|
||||
<NodeCard minW={'350px'} {...data}>
|
||||
<Container borderTop={'2px solid'} borderTopColor={'myGray.200'}>
|
||||
<RenderInput moduleId={moduleId} flowInputList={inputs} />
|
||||
<Button
|
||||
variant={'base'}
|
||||
mt={5}
|
||||
leftIcon={<SmallAddIcon />}
|
||||
onClick={() => {
|
||||
const key = nanoid();
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'addInput',
|
||||
key,
|
||||
value: {
|
||||
key,
|
||||
valueType: FlowNodeValTypeEnum.string,
|
||||
type: FlowNodeInputTypeEnum.target,
|
||||
label: `入参${inputs.length - 1}`,
|
||||
edit: true
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
添加入参
|
||||
</Button>
|
||||
</Container>
|
||||
<Divider text="Output" />
|
||||
<Container>
|
||||
<RenderOutput moduleId={moduleId} flowOutputList={outputs} />
|
||||
<Box textAlign={'right'} mt={5}>
|
||||
<Button
|
||||
variant={'base'}
|
||||
leftIcon={<SmallAddIcon />}
|
||||
onClick={() => {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'addOutput',
|
||||
value: {
|
||||
key: nanoid(),
|
||||
label: `出参${outputs.length}`,
|
||||
valueType: FlowNodeValTypeEnum.string,
|
||||
type: FlowNodeOutputTypeEnum.source,
|
||||
edit: true,
|
||||
targets: []
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
添加出参
|
||||
</Button>
|
||||
</Box>
|
||||
</Container>
|
||||
</NodeCard>
|
||||
);
|
||||
};
|
||||
export default React.memo(NodeHttp);
|
||||
@@ -0,0 +1,191 @@
|
||||
import React, { useState } from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import NodeCard from '../modules/NodeCard';
|
||||
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
|
||||
import { onChangeNode } from '../../FlowProvider';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
import { Box, Button, Flex } from '@chakra-ui/react';
|
||||
import { QuestionOutlineIcon, SmallAddIcon } from '@chakra-ui/icons';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import {
|
||||
FlowNodeInputTypeEnum,
|
||||
FlowNodeOutputTypeEnum,
|
||||
FlowNodeValTypeEnum
|
||||
} from '@fastgpt/global/core/module/node/constant';
|
||||
import Container from '../modules/Container';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import SourceHandle from '../render/SourceHandle';
|
||||
import { EditFieldType } from '../modules/FieldEditModal';
|
||||
|
||||
const FieldEditModal = dynamic(() => import('../modules/FieldEditModal'));
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
|
||||
|
||||
const NodeInput = ({ data }: NodeProps<FlowModuleItemType>) => {
|
||||
const { moduleId, inputs, outputs } = data;
|
||||
const [editField, setEditField] = useState<EditFieldType>();
|
||||
|
||||
return (
|
||||
<NodeCard minW={'300px'} {...data}>
|
||||
<Container mt={1} borderTop={'2px solid'} borderTopColor={'myGray.300'}>
|
||||
{inputs.map((item) => (
|
||||
<Flex
|
||||
key={item.key}
|
||||
className="nodrag"
|
||||
cursor={'default'}
|
||||
justifyContent={'right'}
|
||||
alignItems={'center'}
|
||||
position={'relative'}
|
||||
mb={4}
|
||||
>
|
||||
<MyIcon
|
||||
name={'settingLight'}
|
||||
w={'14px'}
|
||||
cursor={'pointer'}
|
||||
mr={3}
|
||||
_hover={{ color: 'myBlue.600' }}
|
||||
onClick={() =>
|
||||
setEditField({
|
||||
key: item.key,
|
||||
label: item.label,
|
||||
valueType: item.valueType,
|
||||
description: item.description,
|
||||
required: item.required
|
||||
})
|
||||
}
|
||||
/>
|
||||
<MyIcon
|
||||
className="delete"
|
||||
name={'delete'}
|
||||
w={'14px'}
|
||||
cursor={'pointer'}
|
||||
mr={3}
|
||||
_hover={{ color: 'red.500' }}
|
||||
onClick={() => {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'delInput',
|
||||
key: item.key
|
||||
});
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'delOutput',
|
||||
key: item.key
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
{item.description && (
|
||||
<MyTooltip label={item.description} forceShow>
|
||||
<QuestionOutlineIcon display={['none', 'inline']} mr={1} />
|
||||
</MyTooltip>
|
||||
)}
|
||||
<Box position={'relative'}>
|
||||
{item.label}
|
||||
{item.required && (
|
||||
<Box
|
||||
position={'absolute'}
|
||||
right={'-6px'}
|
||||
top={'-3px'}
|
||||
color={'red.500'}
|
||||
fontWeight={'bold'}
|
||||
>
|
||||
*
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<SourceHandle handleKey={item.key} valueType={item.valueType} />
|
||||
</Flex>
|
||||
))}
|
||||
<Box textAlign={'right'} mt={5}>
|
||||
<Button
|
||||
variant={'base'}
|
||||
leftIcon={<SmallAddIcon />}
|
||||
onClick={() => {
|
||||
const key = nanoid();
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'addInput',
|
||||
value: {
|
||||
key,
|
||||
valueType: FlowNodeValTypeEnum.string,
|
||||
type: FlowNodeInputTypeEnum.target,
|
||||
label: `入参${inputs.length + 1}`,
|
||||
edit: true,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'addOutput',
|
||||
value: {
|
||||
key,
|
||||
label: `入参${inputs.length + 1}`,
|
||||
valueType: FlowNodeValTypeEnum.string,
|
||||
type: FlowNodeOutputTypeEnum.source,
|
||||
edit: true,
|
||||
targets: []
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
添加入参
|
||||
</Button>
|
||||
</Box>
|
||||
</Container>
|
||||
{!!editField && (
|
||||
<FieldEditModal
|
||||
mode={'pluginInput'}
|
||||
defaultField={editField}
|
||||
onClose={() => setEditField(undefined)}
|
||||
onSubmit={(e) => {
|
||||
const memInput = inputs.find((item) => item.key === editField.key);
|
||||
const memOutput = outputs.find((item) => item.key === editField.key);
|
||||
|
||||
if (!memInput || !memOutput) return setEditField(undefined);
|
||||
const input = {
|
||||
...memInput,
|
||||
...e
|
||||
};
|
||||
const output = {
|
||||
...memOutput,
|
||||
...e
|
||||
};
|
||||
// not update key
|
||||
if (editField.key === e.key) {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'updateInput',
|
||||
key: editField.key,
|
||||
value: input
|
||||
});
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'updateOutput',
|
||||
key: editField.key,
|
||||
value: output
|
||||
});
|
||||
} else {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'replaceInput',
|
||||
key: editField.key,
|
||||
value: input
|
||||
});
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'replaceOutput',
|
||||
key: editField.key,
|
||||
value: output
|
||||
});
|
||||
}
|
||||
|
||||
setEditField(undefined);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</NodeCard>
|
||||
);
|
||||
};
|
||||
export default React.memo(NodeInput);
|
||||
@@ -0,0 +1,191 @@
|
||||
import React, { useState } from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import NodeCard from '../modules/NodeCard';
|
||||
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
|
||||
import { onChangeNode } from '../../FlowProvider';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
import { Box, Button, Flex } from '@chakra-ui/react';
|
||||
import { QuestionOutlineIcon, SmallAddIcon } from '@chakra-ui/icons';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import {
|
||||
FlowNodeInputTypeEnum,
|
||||
FlowNodeOutputTypeEnum,
|
||||
FlowNodeValTypeEnum
|
||||
} from '@fastgpt/global/core/module/node/constant';
|
||||
import Container from '../modules/Container';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import { EditFieldType } from '../modules/FieldEditModal';
|
||||
import TargetHandle from '../render/TargetHandle';
|
||||
|
||||
const FieldEditModal = dynamic(() => import('../modules/FieldEditModal'));
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
|
||||
|
||||
const NodeOutput = ({ data }: NodeProps<FlowModuleItemType>) => {
|
||||
const { moduleId, inputs, outputs } = data;
|
||||
const [editField, setEditField] = useState<EditFieldType>();
|
||||
|
||||
return (
|
||||
<NodeCard minW={'300px'} {...data}>
|
||||
<Container mt={1} borderTop={'2px solid'} borderTopColor={'myGray.300'}>
|
||||
{inputs.map((item) => (
|
||||
<Flex
|
||||
key={item.key}
|
||||
className="nodrag"
|
||||
cursor={'default'}
|
||||
justifyContent={'left'}
|
||||
alignItems={'center'}
|
||||
position={'relative'}
|
||||
mb={4}
|
||||
>
|
||||
<TargetHandle handleKey={item.key} valueType={item.valueType} />
|
||||
<Box position={'relative'}>
|
||||
{item.label}
|
||||
{item.required && (
|
||||
<Box
|
||||
position={'absolute'}
|
||||
right={'-6px'}
|
||||
top={'-3px'}
|
||||
color={'red.500'}
|
||||
fontWeight={'bold'}
|
||||
>
|
||||
*
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<MyIcon
|
||||
name={'settingLight'}
|
||||
w={'14px'}
|
||||
cursor={'pointer'}
|
||||
ml={3}
|
||||
_hover={{ color: 'myBlue.600' }}
|
||||
onClick={() =>
|
||||
setEditField({
|
||||
key: item.key,
|
||||
label: item.label,
|
||||
valueType: item.valueType,
|
||||
description: item.description
|
||||
})
|
||||
}
|
||||
/>
|
||||
<MyIcon
|
||||
className="delete"
|
||||
name={'delete'}
|
||||
w={'14px'}
|
||||
cursor={'pointer'}
|
||||
ml={3}
|
||||
_hover={{ color: 'red.500' }}
|
||||
onClick={() => {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'delInput',
|
||||
key: item.key,
|
||||
value: ''
|
||||
});
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'delOutput',
|
||||
key: item.key
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
{item.description && (
|
||||
<MyTooltip label={item.description} forceShow>
|
||||
<QuestionOutlineIcon display={['none', 'inline']} mr={1} />
|
||||
</MyTooltip>
|
||||
)}
|
||||
</Flex>
|
||||
))}
|
||||
<Box textAlign={'left'} mt={5}>
|
||||
<Button
|
||||
variant={'base'}
|
||||
leftIcon={<SmallAddIcon />}
|
||||
onClick={() => {
|
||||
const key = nanoid();
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'addInput',
|
||||
value: {
|
||||
key,
|
||||
valueType: FlowNodeValTypeEnum.string,
|
||||
type: FlowNodeInputTypeEnum.target,
|
||||
label: `入参${inputs.length + 1}`,
|
||||
edit: true,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'addOutput',
|
||||
value: {
|
||||
key,
|
||||
label: `入参${inputs.length + 1}`,
|
||||
valueType: FlowNodeValTypeEnum.string,
|
||||
type: FlowNodeOutputTypeEnum.source,
|
||||
edit: true,
|
||||
targets: []
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
添加入参
|
||||
</Button>
|
||||
</Box>
|
||||
</Container>
|
||||
{!!editField && (
|
||||
<FieldEditModal
|
||||
mode={'output'}
|
||||
defaultField={editField}
|
||||
onClose={() => setEditField(undefined)}
|
||||
onSubmit={(e) => {
|
||||
const memInput = inputs.find((item) => item.key === editField.key);
|
||||
const memOutput = outputs.find((item) => item.key === editField.key);
|
||||
if (!memInput || !memOutput) return;
|
||||
const input = {
|
||||
...memInput,
|
||||
...e
|
||||
};
|
||||
const output = {
|
||||
...memOutput,
|
||||
...e
|
||||
};
|
||||
// not update key
|
||||
if (editField.key === e.key) {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'updateInput',
|
||||
key: editField.key,
|
||||
value: input
|
||||
});
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'updateOutput',
|
||||
key: editField.key,
|
||||
value: output
|
||||
});
|
||||
} else {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'replaceInput',
|
||||
key: editField.key,
|
||||
value: input
|
||||
});
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'replaceOutput',
|
||||
key: editField.key,
|
||||
value: output
|
||||
});
|
||||
}
|
||||
|
||||
setEditField(undefined);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</NodeCard>
|
||||
);
|
||||
};
|
||||
export default React.memo(NodeOutput);
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import NodeCard from '../modules/NodeCard';
|
||||
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
|
||||
import Divider from '../modules/Divider';
|
||||
import Container from '../modules/Container';
|
||||
import RenderInput from '../render/RenderInput';
|
||||
import RenderOutput from '../render/RenderOutput';
|
||||
|
||||
const NodeSimple = ({ data }: NodeProps<FlowModuleItemType>) => {
|
||||
const { moduleId, inputs, outputs } = data;
|
||||
|
||||
return (
|
||||
<NodeCard minW={'300px'} isPreview {...data}>
|
||||
<Divider text="Input" />
|
||||
<Container>
|
||||
<RenderInput moduleId={moduleId} flowInputList={inputs} />
|
||||
</Container>
|
||||
<Divider text="Output" />
|
||||
<Container>
|
||||
<RenderOutput moduleId={moduleId} flowOutputList={outputs} />
|
||||
</Container>
|
||||
</NodeCard>
|
||||
);
|
||||
};
|
||||
export default React.memo(NodeSimple);
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import NodeCard from '../modules/NodeCard';
|
||||
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
|
||||
import Container from '../modules/Container';
|
||||
|
||||
import RenderOutput from '../render/RenderOutput';
|
||||
|
||||
const QuestionInputNode = ({ data }: NodeProps<FlowModuleItemType>) => {
|
||||
const { moduleId, inputs, outputs } = data;
|
||||
|
||||
return (
|
||||
<NodeCard minW={'240px'} {...data}>
|
||||
<Container borderTop={'2px solid'} borderTopColor={'myGray.200'} textAlign={'end'}>
|
||||
<RenderOutput moduleId={moduleId} flowOutputList={outputs} />
|
||||
</Container>
|
||||
</NodeCard>
|
||||
);
|
||||
};
|
||||
export default React.memo(QuestionInputNode);
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import NodeCard from '../modules/NodeCard';
|
||||
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
|
||||
import Divider from '../modules/Divider';
|
||||
import Container from '../modules/Container';
|
||||
import RenderInput from '../render/RenderInput';
|
||||
import RenderOutput from '../render/RenderOutput';
|
||||
|
||||
const NodeRunAPP = ({ data }: NodeProps<FlowModuleItemType>) => {
|
||||
const { moduleId, inputs, outputs } = data;
|
||||
|
||||
return (
|
||||
<NodeCard minW={'350px'} {...data}>
|
||||
<Container borderTop={'2px solid'} borderTopColor={'myGray.200'}>
|
||||
<RenderInput moduleId={moduleId} flowInputList={inputs} />
|
||||
</Container>
|
||||
<Divider text="Output" />
|
||||
<Container>
|
||||
<RenderOutput moduleId={moduleId} flowOutputList={outputs} />
|
||||
</Container>
|
||||
</NodeCard>
|
||||
);
|
||||
};
|
||||
export default React.memo(NodeRunAPP);
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import NodeCard from '../modules/NodeCard';
|
||||
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
|
||||
import Divider from '../modules/Divider';
|
||||
import Container from '../modules/Container';
|
||||
import RenderInput from '../render/RenderInput';
|
||||
import RenderOutput from '../render/RenderOutput';
|
||||
|
||||
const NodeSimple = ({ data }: NodeProps<FlowModuleItemType>) => {
|
||||
const { moduleId, inputs, outputs } = data;
|
||||
|
||||
return (
|
||||
<NodeCard minW={'300px'} {...data}>
|
||||
<Divider text="Input" />
|
||||
<Container>
|
||||
<RenderInput moduleId={moduleId} flowInputList={inputs} />
|
||||
</Container>
|
||||
<Divider text="Output" />
|
||||
<Container>
|
||||
<RenderOutput moduleId={moduleId} flowOutputList={outputs} />
|
||||
</Container>
|
||||
</NodeCard>
|
||||
);
|
||||
};
|
||||
export default React.memo(NodeSimple);
|
||||
@@ -0,0 +1,243 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Textarea,
|
||||
useTheme,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
TableContainer,
|
||||
Switch
|
||||
} from '@chakra-ui/react';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
|
||||
import { SystemInputEnum } from '@/constants/app';
|
||||
import { welcomeTextTip, variableTip, questionGuideTip } from '@/constants/flow/ModuleTemplate';
|
||||
import { onChangeNode } from '../../FlowProvider';
|
||||
|
||||
import VariableEditModal, { addVariable } from '../../../VariableEditModal';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import Container from '../modules/Container';
|
||||
import NodeCard from '../modules/NodeCard';
|
||||
import { VariableItemType } from '@/types/app';
|
||||
|
||||
const NodeUserGuide = ({ data }: NodeProps<FlowModuleItemType>) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<>
|
||||
<NodeCard minW={'300px'} {...data}>
|
||||
<Container borderTop={'2px solid'} borderTopColor={'myGray.200'}>
|
||||
<WelcomeText data={data} />
|
||||
<Box pt={4} pb={2}>
|
||||
<ChatStartVariable data={data} />
|
||||
</Box>
|
||||
<Box pt={3} borderTop={theme.borders.base}>
|
||||
<QuestionGuide data={data} />
|
||||
</Box>
|
||||
</Container>
|
||||
</NodeCard>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default React.memo(NodeUserGuide);
|
||||
|
||||
export function WelcomeText({ data }: { data: FlowModuleItemType }) {
|
||||
const { inputs, moduleId } = data;
|
||||
|
||||
const welcomeText = useMemo(
|
||||
() => inputs.find((item) => item.key === SystemInputEnum.welcomeText),
|
||||
[inputs]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex mb={1} alignItems={'center'}>
|
||||
<MyIcon name={'welcomeText'} mr={2} w={'16px'} color={'#E74694'} />
|
||||
<Box>开场白</Box>
|
||||
<MyTooltip label={welcomeTextTip} forceShow>
|
||||
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
{welcomeText && (
|
||||
<Textarea
|
||||
className="nodrag"
|
||||
rows={6}
|
||||
resize={'both'}
|
||||
defaultValue={welcomeText.value}
|
||||
bg={'myWhite.500'}
|
||||
placeholder={welcomeTextTip}
|
||||
onChange={(e) => {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
key: SystemInputEnum.welcomeText,
|
||||
type: 'updateInput',
|
||||
value: {
|
||||
...welcomeText,
|
||||
value: e.target.value
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ChatStartVariable({ data }: { data: FlowModuleItemType }) {
|
||||
const { inputs, moduleId } = data;
|
||||
|
||||
const variables = useMemo(
|
||||
() =>
|
||||
(inputs.find((item) => item.key === SystemInputEnum.variables)
|
||||
?.value as VariableItemType[]) || [],
|
||||
[inputs]
|
||||
);
|
||||
|
||||
const [editVariable, setEditVariable] = useState<VariableItemType>();
|
||||
|
||||
const updateVariables = useCallback(
|
||||
(value: VariableItemType[]) => {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
key: SystemInputEnum.variables,
|
||||
type: 'updateInput',
|
||||
value: {
|
||||
...inputs.find((item) => item.key === SystemInputEnum.variables),
|
||||
value
|
||||
}
|
||||
});
|
||||
},
|
||||
[inputs, moduleId]
|
||||
);
|
||||
|
||||
const onclickSubmit = useCallback(
|
||||
({ variable }: { variable: VariableItemType }) => {
|
||||
updateVariables(variables.map((item) => (item.id === variable.id ? variable : item)));
|
||||
setEditVariable(undefined);
|
||||
},
|
||||
[updateVariables, variables]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex mb={1} alignItems={'center'}>
|
||||
<MyIcon name={'variable'} mr={2} w={'16px'} color={'#fb7c3d'} />
|
||||
<Box>对话框变量</Box>
|
||||
<MyTooltip label={variableTip} forceShow>
|
||||
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
|
||||
</MyTooltip>
|
||||
<Box flex={1} />
|
||||
<Flex
|
||||
ml={2}
|
||||
textAlign={'right'}
|
||||
cursor={'pointer'}
|
||||
px={3}
|
||||
py={'2px'}
|
||||
borderRadius={'md'}
|
||||
_hover={{ bg: 'myGray.200' }}
|
||||
onClick={() => {
|
||||
const newVariable = addVariable();
|
||||
updateVariables(variables.concat(newVariable));
|
||||
setEditVariable(newVariable);
|
||||
}}
|
||||
>
|
||||
+ 新增
|
||||
</Flex>
|
||||
</Flex>
|
||||
{variables.length > 0 && (
|
||||
<TableContainer borderWidth={'1px'} borderBottom="none" borderRadius={'lg'}>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>变量名</Th>
|
||||
<Th>变量 key</Th>
|
||||
<Th>必填</Th>
|
||||
<Th></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{variables.map((item, index) => (
|
||||
<Tr key={index}>
|
||||
<Td>{item.label} </Td>
|
||||
<Td>{item.key}</Td>
|
||||
<Td>{item.required ? '✔' : ''}</Td>
|
||||
<Td>
|
||||
<MyIcon
|
||||
mr={3}
|
||||
name={'settingLight'}
|
||||
w={'16px'}
|
||||
cursor={'pointer'}
|
||||
onClick={() => {
|
||||
setEditVariable(item);
|
||||
}}
|
||||
/>
|
||||
<MyIcon
|
||||
name={'delete'}
|
||||
w={'16px'}
|
||||
cursor={'pointer'}
|
||||
onClick={() =>
|
||||
updateVariables(variables.filter((variable) => variable.id !== item.id))
|
||||
}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
|
||||
{!!editVariable && (
|
||||
<VariableEditModal
|
||||
defaultVariable={editVariable}
|
||||
onClose={() => setEditVariable(undefined)}
|
||||
onSubmit={onclickSubmit}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function QuestionGuide({ data }: { data: FlowModuleItemType }) {
|
||||
const { inputs, moduleId } = data;
|
||||
|
||||
const questionGuide = useMemo(
|
||||
() =>
|
||||
(inputs.find((item) => item.key === SystemInputEnum.questionGuide)?.value as boolean) ||
|
||||
false,
|
||||
[inputs]
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex alignItems={'center'}>
|
||||
<MyIcon name={'questionGuide'} mr={2} w={'16px'} />
|
||||
<Box>下一步指引</Box>
|
||||
<MyTooltip label={questionGuideTip} forceShow>
|
||||
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
|
||||
</MyTooltip>
|
||||
<Box flex={1} />
|
||||
<Switch
|
||||
isChecked={questionGuide}
|
||||
size={'lg'}
|
||||
onChange={(e) => {
|
||||
const value = e.target.checked;
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
key: SystemInputEnum.questionGuide,
|
||||
type: 'updateInput',
|
||||
value: {
|
||||
...inputs.find((item) => item.key === SystemInputEnum.questionGuide),
|
||||
value
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/* Abandon */
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import { Box, Button, Table, Thead, Tbody, Tr, Th, Td, TableContainer } from '@chakra-ui/react';
|
||||
import { AddIcon } from '@chakra-ui/icons';
|
||||
import NodeCard from '../modules/NodeCard';
|
||||
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
|
||||
import Container from '../modules/Container';
|
||||
import { SystemInputEnum, VariableInputEnum } from '@/constants/app';
|
||||
import type { VariableItemType } from '@/types/app';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
|
||||
import VariableEditModal, { addVariable } from '../../../VariableEditModal';
|
||||
import { onChangeNode } from '../../FlowProvider';
|
||||
|
||||
export const defaultVariable: VariableItemType = {
|
||||
id: nanoid(),
|
||||
key: 'key',
|
||||
label: 'label',
|
||||
type: VariableInputEnum.input,
|
||||
required: true,
|
||||
maxLen: 50,
|
||||
enums: [{ value: '' }]
|
||||
};
|
||||
|
||||
const NodeUserGuide = ({ data }: NodeProps<FlowModuleItemType>) => {
|
||||
const { inputs, moduleId } = data;
|
||||
|
||||
const variables = useMemo(
|
||||
() =>
|
||||
(inputs.find((item) => item.key === SystemInputEnum.variables)
|
||||
?.value as VariableItemType[]) || [],
|
||||
[inputs]
|
||||
);
|
||||
|
||||
const [editVariable, setEditVariable] = useState<VariableItemType>();
|
||||
|
||||
const updateVariables = useCallback(
|
||||
(value: VariableItemType[]) => {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
key: SystemInputEnum.variables,
|
||||
type: 'updateInput',
|
||||
value: {
|
||||
...inputs.find((item) => item.key === SystemInputEnum.variables),
|
||||
value
|
||||
}
|
||||
});
|
||||
},
|
||||
[inputs, moduleId]
|
||||
);
|
||||
|
||||
const onclickSubmit = useCallback(
|
||||
({ variable }: { variable: VariableItemType }) => {
|
||||
updateVariables(variables.map((item) => (item.id === variable.id ? variable : item)));
|
||||
setEditVariable(undefined);
|
||||
},
|
||||
[updateVariables, variables]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<NodeCard minW={'300px'} {...data}>
|
||||
<Container borderTop={'2px solid'} borderTopColor={'myGray.200'}>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>变量名</Th>
|
||||
<Th>变量 key</Th>
|
||||
<Th>必填</Th>
|
||||
<Th></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{variables.map((item, index) => (
|
||||
<Tr key={index}>
|
||||
<Td>{item.label} </Td>
|
||||
<Td>{item.key}</Td>
|
||||
<Td>{item.required ? '✔' : ''}</Td>
|
||||
<Td>
|
||||
<MyIcon
|
||||
mr={3}
|
||||
name={'settingLight'}
|
||||
w={'16px'}
|
||||
cursor={'pointer'}
|
||||
onClick={() => {
|
||||
setEditVariable(item);
|
||||
}}
|
||||
/>
|
||||
<MyIcon
|
||||
name={'delete'}
|
||||
w={'16px'}
|
||||
cursor={'pointer'}
|
||||
onClick={() =>
|
||||
updateVariables(variables.filter((variable) => variable.id !== item.id))
|
||||
}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Box mt={2} textAlign={'right'}>
|
||||
<Button
|
||||
variant={'base'}
|
||||
leftIcon={<AddIcon fontSize={'10px'} />}
|
||||
onClick={() => {
|
||||
const newVariable = addVariable();
|
||||
updateVariables(variables.concat(newVariable));
|
||||
setEditVariable(newVariable);
|
||||
}}
|
||||
>
|
||||
新增
|
||||
</Button>
|
||||
</Box>
|
||||
</Container>
|
||||
</NodeCard>
|
||||
{!!editVariable && (
|
||||
<VariableEditModal
|
||||
defaultVariable={editVariable}
|
||||
onClose={() => setEditVariable(undefined)}
|
||||
onSubmit={onclickSubmit}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default React.memo(NodeUserGuide);
|
||||
@@ -0,0 +1,618 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import type { SelectAppItemType } from '@fastgpt/global/core/module/type';
|
||||
import type { FlowNodeInputItemType } from '@fastgpt/global/core/module/node/type';
|
||||
import {
|
||||
Box,
|
||||
Textarea,
|
||||
Input,
|
||||
NumberInput,
|
||||
NumberInputField,
|
||||
NumberInputStepper,
|
||||
NumberIncrementStepper,
|
||||
NumberDecrementStepper,
|
||||
Flex,
|
||||
useDisclosure,
|
||||
Button,
|
||||
useTheme,
|
||||
Grid
|
||||
} from '@chakra-ui/react';
|
||||
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/module/node/constant';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { onChangeNode, useFlowProviderStore } from '../../FlowProvider';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import MySelect from '@/components/Select';
|
||||
import MySlider from '@/components/Slider';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import TargetHandle from './TargetHandle';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AIChatProps } from '@/types/core/aiChat';
|
||||
import { chatModelList } from '@/web/common/system/staticData';
|
||||
import { formatPrice } from '@fastgpt/global/common/bill/tools';
|
||||
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
|
||||
import { SelectedDatasetType } from '@/types/core/dataset';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { LLMModelItemType } from '@/types/model';
|
||||
import type { EditFieldModeType, EditFieldType } from '../modules/FieldEditModal';
|
||||
|
||||
const FieldEditModal = dynamic(() => import('../modules/FieldEditModal'));
|
||||
const SelectAppModal = dynamic(() => import('../../SelectAppModal'));
|
||||
const AIChatSettingsModal = dynamic(() => import('../../../AIChatSettingsModal'));
|
||||
const DatasetSelectModal = dynamic(() => import('../../../DatasetSelectModal'));
|
||||
|
||||
export const Label = React.memo(function Label({
|
||||
moduleId,
|
||||
inputKey,
|
||||
editFiledType = 'input',
|
||||
...item
|
||||
}: FlowNodeInputItemType & {
|
||||
moduleId: string;
|
||||
inputKey: string;
|
||||
editFiledType?: EditFieldModeType;
|
||||
}) {
|
||||
const { required = false, description, edit, label, type, valueType } = item;
|
||||
const [editField, setEditField] = useState<EditFieldType>();
|
||||
|
||||
return (
|
||||
<Flex className="nodrag" cursor={'default'} alignItems={'center'} position={'relative'}>
|
||||
<Box position={'relative'}>
|
||||
{label}
|
||||
{description && (
|
||||
<MyTooltip label={description} forceShow>
|
||||
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
|
||||
</MyTooltip>
|
||||
)}
|
||||
{required && (
|
||||
<Box
|
||||
position={'absolute'}
|
||||
top={'-2px'}
|
||||
right={'-8px'}
|
||||
color={'red.500'}
|
||||
fontWeight={'bold'}
|
||||
>
|
||||
*
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{(type === FlowNodeInputTypeEnum.target || valueType) && (
|
||||
<TargetHandle handleKey={inputKey} valueType={valueType} />
|
||||
)}
|
||||
|
||||
{edit && (
|
||||
<>
|
||||
<MyIcon
|
||||
name={'settingLight'}
|
||||
w={'14px'}
|
||||
cursor={'pointer'}
|
||||
ml={3}
|
||||
_hover={{ color: 'myBlue.600' }}
|
||||
onClick={() =>
|
||||
setEditField({
|
||||
label: item.label,
|
||||
valueType: item.valueType,
|
||||
required: item.required,
|
||||
key: inputKey,
|
||||
description: item.description
|
||||
})
|
||||
}
|
||||
/>
|
||||
<MyIcon
|
||||
className="delete"
|
||||
name={'delete'}
|
||||
w={'14px'}
|
||||
cursor={'pointer'}
|
||||
ml={2}
|
||||
_hover={{ color: 'red.500' }}
|
||||
onClick={() => {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'delInput',
|
||||
key: inputKey,
|
||||
value: ''
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{!!editField && (
|
||||
<FieldEditModal
|
||||
mode={editFiledType}
|
||||
defaultField={editField}
|
||||
onClose={() => setEditField(undefined)}
|
||||
onSubmit={(e) => {
|
||||
const data = {
|
||||
...item,
|
||||
...e
|
||||
};
|
||||
// same key
|
||||
if (editField.key === data.key) {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'updateInput',
|
||||
key: data.key,
|
||||
value: data
|
||||
});
|
||||
} else {
|
||||
// diff key. del and add
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'replaceInput',
|
||||
key: editField.key,
|
||||
value: data
|
||||
});
|
||||
}
|
||||
setEditField(undefined);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
const RenderInput = ({
|
||||
flowInputList,
|
||||
moduleId,
|
||||
CustomComponent = {},
|
||||
editFiledType
|
||||
}: {
|
||||
flowInputList: FlowNodeInputItemType[];
|
||||
moduleId: string;
|
||||
CustomComponent?: Record<string, (e: FlowNodeInputItemType) => React.ReactNode>;
|
||||
editFiledType?: EditFieldModeType;
|
||||
}) => {
|
||||
const sortInputs = useMemo(
|
||||
() => flowInputList.sort((a, b) => (a.key === FlowNodeInputTypeEnum.switch ? -1 : 1)),
|
||||
[flowInputList]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{sortInputs.map(
|
||||
(item) =>
|
||||
item.type !== FlowNodeInputTypeEnum.hidden && (
|
||||
<Box key={item.key} _notLast={{ mb: 7 }} position={'relative'}>
|
||||
{!!item.label && (
|
||||
<Label
|
||||
editFiledType={editFiledType}
|
||||
moduleId={moduleId}
|
||||
inputKey={item.key}
|
||||
{...item}
|
||||
/>
|
||||
)}
|
||||
<Box mt={2} className={'nodrag'}>
|
||||
{item.type === FlowNodeInputTypeEnum.numberInput && (
|
||||
<NumberInputRender item={item} moduleId={moduleId} />
|
||||
)}
|
||||
{item.type === FlowNodeInputTypeEnum.input && (
|
||||
<TextInputRender item={item} moduleId={moduleId} />
|
||||
)}
|
||||
{item.type === FlowNodeInputTypeEnum.textarea && (
|
||||
<TextareaRender item={item} moduleId={moduleId} />
|
||||
)}
|
||||
{item.type === FlowNodeInputTypeEnum.select && (
|
||||
<SelectRender item={item} moduleId={moduleId} />
|
||||
)}
|
||||
{item.type === FlowNodeInputTypeEnum.slider && (
|
||||
<SliderRender item={item} moduleId={moduleId} />
|
||||
)}
|
||||
{item.type === FlowNodeInputTypeEnum.selectApp && (
|
||||
<SelectAppRender item={item} moduleId={moduleId} />
|
||||
)}
|
||||
{item.type === FlowNodeInputTypeEnum.aiSettings && (
|
||||
<AISetting inputs={sortInputs} item={item} moduleId={moduleId} />
|
||||
)}
|
||||
{item.type === FlowNodeInputTypeEnum.maxToken && (
|
||||
<MaxTokenRender inputs={sortInputs} item={item} moduleId={moduleId} />
|
||||
)}
|
||||
{item.type === FlowNodeInputTypeEnum.selectChatModel && (
|
||||
<SelectChatModelRender inputs={sortInputs} item={item} moduleId={moduleId} />
|
||||
)}
|
||||
{item.type === FlowNodeInputTypeEnum.selectDataset && (
|
||||
<SelectDatasetRender item={item} moduleId={moduleId} />
|
||||
)}
|
||||
{item.type === FlowNodeInputTypeEnum.custom && CustomComponent[item.key] && (
|
||||
<>{CustomComponent[item.key]({ ...item })}</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(RenderInput);
|
||||
|
||||
type RenderProps = {
|
||||
inputs?: FlowNodeInputItemType[];
|
||||
item: FlowNodeInputItemType;
|
||||
moduleId: string;
|
||||
};
|
||||
|
||||
var NumberInputRender = React.memo(function NumberInputRender({ item, moduleId }: RenderProps) {
|
||||
return (
|
||||
<NumberInput
|
||||
defaultValue={item.value}
|
||||
min={item.min}
|
||||
max={item.max}
|
||||
onChange={(e) => {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'updateInput',
|
||||
key: item.key,
|
||||
value: {
|
||||
...item,
|
||||
value: Number(e)
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<NumberInputField />
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberInputStepper>
|
||||
</NumberInput>
|
||||
);
|
||||
});
|
||||
|
||||
var TextInputRender = React.memo(function TextInputRender({ item, moduleId }: RenderProps) {
|
||||
return (
|
||||
<Input
|
||||
placeholder={item.placeholder}
|
||||
defaultValue={item.value}
|
||||
onBlur={(e) => {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'updateInput',
|
||||
key: item.key,
|
||||
value: {
|
||||
...item,
|
||||
value: e.target.value
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
var TextareaRender = React.memo(function TextareaRender({ item, moduleId }: RenderProps) {
|
||||
return (
|
||||
<Textarea
|
||||
rows={5}
|
||||
placeholder={item.placeholder}
|
||||
resize={'both'}
|
||||
defaultValue={item.value}
|
||||
onBlur={(e) => {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'updateInput',
|
||||
key: item.key,
|
||||
value: {
|
||||
...item,
|
||||
value: e.target.value
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
var SelectRender = React.memo(function SelectRender({ item, moduleId }: RenderProps) {
|
||||
return (
|
||||
<MySelect
|
||||
width={'100%'}
|
||||
value={item.value}
|
||||
list={item.list || []}
|
||||
onchange={(e) => {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'updateInput',
|
||||
key: item.key,
|
||||
value: {
|
||||
...item,
|
||||
value: e
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
var SliderRender = React.memo(function SliderRender({ item, moduleId }: RenderProps) {
|
||||
return (
|
||||
<Box pt={5} pb={4} px={2}>
|
||||
<MySlider
|
||||
markList={item.markList}
|
||||
width={'100%'}
|
||||
min={item.min || 0}
|
||||
max={item.max}
|
||||
step={item.step || 1}
|
||||
value={item.value}
|
||||
onChange={(e) => {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'updateInput',
|
||||
key: item.key,
|
||||
value: {
|
||||
...item,
|
||||
value: e
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
var AISetting = React.memo(function AISetting({ inputs = [], moduleId }: RenderProps) {
|
||||
const { t } = useTranslation();
|
||||
const chatModulesData = useMemo(() => {
|
||||
const obj: Record<string, any> = {};
|
||||
inputs.forEach((item) => {
|
||||
obj[item.key] = item.value;
|
||||
});
|
||||
return obj as AIChatProps;
|
||||
}, [inputs]);
|
||||
|
||||
const {
|
||||
isOpen: isOpenAIChatSetting,
|
||||
onOpen: onOpenAIChatSetting,
|
||||
onClose: onCloseAIChatSetting
|
||||
} = useDisclosure();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant={'base'}
|
||||
leftIcon={<MyIcon name={'settingLight'} w={'14px'} />}
|
||||
onClick={onOpenAIChatSetting}
|
||||
>
|
||||
{t('app.AI Settings')}
|
||||
</Button>
|
||||
{isOpenAIChatSetting && (
|
||||
<AIChatSettingsModal
|
||||
isAdEdit
|
||||
onClose={onCloseAIChatSetting}
|
||||
onSuccess={(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]
|
||||
}
|
||||
});
|
||||
}
|
||||
onCloseAIChatSetting();
|
||||
}}
|
||||
defaultData={chatModulesData}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
var MaxTokenRender = React.memo(function MaxTokenRender({
|
||||
inputs = [],
|
||||
item,
|
||||
moduleId
|
||||
}: RenderProps) {
|
||||
const model = inputs.find((item) => item.key === 'model')?.value;
|
||||
const modelData = chatModelList.find((item) => item.model === model);
|
||||
const maxToken = modelData ? modelData.maxToken : 4000;
|
||||
const markList = [
|
||||
{ label: '100', value: 100 },
|
||||
{ label: `${maxToken}`, value: maxToken }
|
||||
];
|
||||
|
||||
return (
|
||||
<Box pt={5} pb={4} px={2}>
|
||||
<MySlider
|
||||
markList={markList}
|
||||
width={'100%'}
|
||||
min={item.min || 100}
|
||||
max={maxToken}
|
||||
step={item.step || 1}
|
||||
value={item.value}
|
||||
onChange={(e) => {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'updateInput',
|
||||
key: item.key,
|
||||
value: {
|
||||
...item,
|
||||
value: e
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
var SelectChatModelRender = React.memo(function SelectChatModelRender({
|
||||
inputs = [],
|
||||
item,
|
||||
moduleId
|
||||
}: RenderProps) {
|
||||
const modelList = (item.customData?.() as LLMModelItemType[]) || chatModelList || [];
|
||||
|
||||
function onChangeModel(e: string) {
|
||||
{
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'updateInput',
|
||||
key: item.key,
|
||||
value: {
|
||||
...item,
|
||||
value: e
|
||||
}
|
||||
});
|
||||
|
||||
// update max tokens
|
||||
const model = modelList.find((item) => item.model === e) || modelList[0];
|
||||
if (!model) return;
|
||||
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'updateInput',
|
||||
key: 'maxToken',
|
||||
value: {
|
||||
...inputs.find((input) => input.key === 'maxToken'),
|
||||
markList: [
|
||||
{ label: '100', value: 100 },
|
||||
{ label: `${model.maxToken}`, value: model.maxToken }
|
||||
],
|
||||
max: model.maxToken,
|
||||
value: model.maxToken / 2
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const list = modelList.map((item) => {
|
||||
const priceStr = `(${formatPrice(item.price, 1000)}元/1k Tokens)`;
|
||||
|
||||
return {
|
||||
value: item.model,
|
||||
label: `${item.name}${priceStr}`
|
||||
};
|
||||
});
|
||||
|
||||
if (!item.value && list.length > 0) {
|
||||
onChangeModel(list[0].value);
|
||||
}
|
||||
|
||||
return (
|
||||
<MySelect
|
||||
minW={'350px'}
|
||||
width={'100%'}
|
||||
value={item.value}
|
||||
list={list}
|
||||
onchange={onChangeModel}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
var SelectDatasetRender = React.memo(function SelectDatasetRender({ item, moduleId }: RenderProps) {
|
||||
const theme = useTheme();
|
||||
const { allDatasets, loadAllDatasets } = useDatasetStore();
|
||||
const {
|
||||
isOpen: isOpenKbSelect,
|
||||
onOpen: onOpenKbSelect,
|
||||
onClose: onCloseKbSelect
|
||||
} = useDisclosure();
|
||||
|
||||
const showKbList = useMemo(() => {
|
||||
const value = item.value as SelectedDatasetType;
|
||||
return allDatasets.filter((dataset) => value.find((kb) => kb.datasetId === dataset._id));
|
||||
}, [allDatasets, item.value]);
|
||||
|
||||
useQuery(['loadAllDatasets'], loadAllDatasets);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid gridTemplateColumns={'repeat(2, minmax(0, 1fr))'} gridGap={4} minW={'350px'} w={'100%'}>
|
||||
<Button h={'36px'} onClick={onOpenKbSelect}>
|
||||
选择知识库
|
||||
</Button>
|
||||
{showKbList.map((item) => (
|
||||
<Flex
|
||||
key={item._id}
|
||||
alignItems={'center'}
|
||||
h={'36px'}
|
||||
border={theme.borders.base}
|
||||
px={2}
|
||||
borderRadius={'md'}
|
||||
>
|
||||
<Avatar src={item.avatar} w={'24px'}></Avatar>
|
||||
<Box
|
||||
ml={3}
|
||||
flex={'1 0 0'}
|
||||
w={0}
|
||||
className="textEllipsis"
|
||||
fontWeight={'bold'}
|
||||
fontSize={['md', 'lg', 'xl']}
|
||||
>
|
||||
{item.name}
|
||||
</Box>
|
||||
</Flex>
|
||||
))}
|
||||
</Grid>
|
||||
<DatasetSelectModal
|
||||
isOpen={isOpenKbSelect}
|
||||
activeDatasets={item.value}
|
||||
onChange={(e) => {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
key: item.key,
|
||||
type: 'updateInput',
|
||||
value: {
|
||||
...item,
|
||||
value: e
|
||||
}
|
||||
});
|
||||
}}
|
||||
onClose={onCloseKbSelect}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
var SelectAppRender = React.memo(function SelectAppRender({ item, moduleId }: RenderProps) {
|
||||
const { filterAppIds } = useFlowProviderStore();
|
||||
const theme = useTheme();
|
||||
|
||||
const {
|
||||
isOpen: isOpenSelectApp,
|
||||
onOpen: onOpenSelectApp,
|
||||
onClose: onCloseSelectApp
|
||||
} = useDisclosure();
|
||||
|
||||
const value = item.value as SelectAppItemType | undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box onClick={onOpenSelectApp}>
|
||||
{!value ? (
|
||||
<Button variant={'base'} w={'100%'}>
|
||||
选择应用
|
||||
</Button>
|
||||
) : (
|
||||
<Flex alignItems={'center'} border={theme.borders.base} borderRadius={'md'} px={3} py={2}>
|
||||
<Avatar src={value?.logo} />
|
||||
<Box fontWeight={'bold'} ml={1}>
|
||||
{value?.name}
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{isOpenSelectApp && (
|
||||
<SelectAppModal
|
||||
defaultApps={item.value?.id ? [item.value.id] : []}
|
||||
filterAppIds={filterAppIds}
|
||||
onClose={onCloseSelectApp}
|
||||
onSuccess={(e) => {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'updateInput',
|
||||
key: 'app',
|
||||
value: {
|
||||
...item,
|
||||
value: e[0]
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,158 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import type { FlowNodeOutputItemType } from '@fastgpt/global/core/module/node/type';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/module/node/constant';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import SourceHandle from './SourceHandle';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { onChangeNode } from '../../FlowProvider';
|
||||
import { SystemOutputEnum } from '@/constants/app';
|
||||
|
||||
import type { EditFieldType, EditFieldModeType } from '../modules/FieldEditModal';
|
||||
const FieldEditModal = dynamic(() => import('../modules/FieldEditModal'));
|
||||
|
||||
export const Label = ({
|
||||
moduleId,
|
||||
outputKey,
|
||||
outputs,
|
||||
editFiledType = 'output',
|
||||
...item
|
||||
}: FlowNodeOutputItemType & {
|
||||
outputKey: string;
|
||||
moduleId: string;
|
||||
outputs: FlowNodeOutputItemType[];
|
||||
editFiledType?: EditFieldModeType;
|
||||
}) => {
|
||||
const { label, description, edit } = item;
|
||||
const [editField, setEditField] = useState<EditFieldType>();
|
||||
|
||||
return (
|
||||
<Flex
|
||||
className="nodrag"
|
||||
cursor={'default'}
|
||||
justifyContent={'right'}
|
||||
alignItems={'center'}
|
||||
position={'relative'}
|
||||
>
|
||||
{edit && (
|
||||
<>
|
||||
<MyIcon
|
||||
name={'settingLight'}
|
||||
w={'14px'}
|
||||
cursor={'pointer'}
|
||||
mr={3}
|
||||
_hover={{ color: 'myBlue.600' }}
|
||||
onClick={() =>
|
||||
setEditField({
|
||||
label: item.label,
|
||||
valueType: item.valueType,
|
||||
key: outputKey,
|
||||
description: item.description
|
||||
})
|
||||
}
|
||||
/>
|
||||
<MyIcon
|
||||
className="delete"
|
||||
name={'delete'}
|
||||
w={'14px'}
|
||||
cursor={'pointer'}
|
||||
mr={3}
|
||||
_hover={{ color: 'red.500' }}
|
||||
onClick={() => {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'delOutput',
|
||||
key: outputKey
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{description && (
|
||||
<MyTooltip label={description} forceShow>
|
||||
<QuestionOutlineIcon display={['none', 'inline']} mr={1} />
|
||||
</MyTooltip>
|
||||
)}
|
||||
<Box>{label}</Box>
|
||||
|
||||
{!!editField && (
|
||||
<FieldEditModal
|
||||
mode={editFiledType}
|
||||
defaultField={editField}
|
||||
onClose={() => setEditField(undefined)}
|
||||
onSubmit={(e) => {
|
||||
const data = {
|
||||
...item,
|
||||
...e
|
||||
};
|
||||
if (editField.key === data.key) {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'updateOutput',
|
||||
key: data.key,
|
||||
value: data
|
||||
});
|
||||
} else {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'replaceOutput',
|
||||
key: editField.key,
|
||||
value: data
|
||||
});
|
||||
}
|
||||
|
||||
setEditField(undefined);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
const RenderOutput = ({
|
||||
moduleId,
|
||||
flowOutputList,
|
||||
editFiledType
|
||||
}: {
|
||||
moduleId: string;
|
||||
flowOutputList: FlowNodeOutputItemType[];
|
||||
editFiledType?: EditFieldModeType;
|
||||
}) => {
|
||||
const sortOutput = useMemo(
|
||||
() =>
|
||||
[...flowOutputList].sort((a, b) => {
|
||||
if (a.key === SystemOutputEnum.finish) return -1;
|
||||
if (b.key === SystemOutputEnum.finish) return 1;
|
||||
return 0;
|
||||
}),
|
||||
[flowOutputList]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{sortOutput.map(
|
||||
(item) =>
|
||||
item.type !== FlowNodeOutputTypeEnum.hidden && (
|
||||
<Box key={item.key} _notLast={{ mb: 7 }} position={'relative'}>
|
||||
<Label
|
||||
editFiledType={editFiledType}
|
||||
moduleId={moduleId}
|
||||
outputKey={item.key}
|
||||
outputs={sortOutput}
|
||||
{...item}
|
||||
/>
|
||||
<Box mt={FlowNodeOutputTypeEnum.answer ? 0 : 2} className={'nodrag'}>
|
||||
{item.type === FlowNodeOutputTypeEnum.source && (
|
||||
<SourceHandle handleKey={item.key} valueType={item.valueType} />
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(RenderOutput);
|
||||
@@ -0,0 +1,56 @@
|
||||
import React, { useMemo, useTransition } from 'react';
|
||||
import { Box, BoxProps } from '@chakra-ui/react';
|
||||
import { Handle, Position } from 'reactflow';
|
||||
import { FlowValueTypeStyle, FlowValueTypeTip } from '@/constants/flow';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { FlowNodeValTypeEnum } from '@fastgpt/global/core/module/node/constant';
|
||||
|
||||
interface Props extends BoxProps {
|
||||
handleKey: string;
|
||||
valueType?: `${FlowNodeValTypeEnum}`;
|
||||
}
|
||||
|
||||
const SourceHandle = ({ handleKey, valueType, ...props }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const valType = valueType ?? FlowNodeValTypeEnum.any;
|
||||
|
||||
const valueStyle = useMemo(
|
||||
() =>
|
||||
valueType
|
||||
? FlowValueTypeStyle[valueType]
|
||||
: (FlowValueTypeStyle[FlowNodeValTypeEnum.any] as any),
|
||||
[valueType]
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
position={'absolute'}
|
||||
top={'50%'}
|
||||
right={'-16px'}
|
||||
transform={'translate(50%,-50%)'}
|
||||
{...props}
|
||||
>
|
||||
<MyTooltip
|
||||
label={t('app.module.type', {
|
||||
type: t(FlowValueTypeTip[valType].label),
|
||||
example: FlowValueTypeTip[valType].example
|
||||
})}
|
||||
>
|
||||
<Handle
|
||||
style={{
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
...valueStyle
|
||||
}}
|
||||
type="source"
|
||||
id={handleKey}
|
||||
position={Position.Right}
|
||||
/>
|
||||
</MyTooltip>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(SourceHandle);
|
||||
@@ -0,0 +1,57 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Box, BoxProps } from '@chakra-ui/react';
|
||||
import { Handle, OnConnect, Position } from 'reactflow';
|
||||
import { FlowValueTypeStyle, FlowValueTypeTip } from '@/constants/flow';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { FlowNodeValTypeEnum } from '@fastgpt/global/core/module/node/constant';
|
||||
|
||||
interface Props extends BoxProps {
|
||||
handleKey: string;
|
||||
valueType?: `${FlowNodeValTypeEnum}`;
|
||||
onConnect?: OnConnect;
|
||||
}
|
||||
|
||||
const TargetHandle = ({ handleKey, valueType, onConnect, ...props }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const valType = valueType ?? FlowNodeValTypeEnum.any;
|
||||
const valueStyle = useMemo(
|
||||
() =>
|
||||
valueType
|
||||
? FlowValueTypeStyle[valueType]
|
||||
: (FlowValueTypeStyle[FlowNodeValTypeEnum.any] as any),
|
||||
[valueType]
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={handleKey}
|
||||
position={'absolute'}
|
||||
top={'50%'}
|
||||
left={'-16px'}
|
||||
transform={'translate(50%,-50%)'}
|
||||
{...props}
|
||||
>
|
||||
<MyTooltip
|
||||
label={t('app.module.type', {
|
||||
type: t(FlowValueTypeTip[valType].label),
|
||||
example: FlowValueTypeTip[valType].example
|
||||
})}
|
||||
>
|
||||
<Handle
|
||||
style={{
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
...valueStyle
|
||||
}}
|
||||
type="target"
|
||||
id={handleKey}
|
||||
position={Position.Left}
|
||||
/>
|
||||
</MyTooltip>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(TargetHandle);
|
||||
141
projects/app/src/components/core/module/Flow/index.tsx
Normal file
141
projects/app/src/components/core/module/Flow/index.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import ReactFlow, { Background, Controls, ReactFlowProvider } from 'reactflow';
|
||||
import { Box, Flex, IconButton, useDisclosure } from '@chakra-ui/react';
|
||||
import { SmallCloseIcon } from '@chakra-ui/icons';
|
||||
import { edgeOptions, connectionLineStyle } from '@/constants/flow';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import ButtonEdge from './components/modules/ButtonEdge';
|
||||
import TemplateList, { type ModuleTemplateProps } from './TemplateList';
|
||||
import { useFlowProviderStore } from './FlowProvider';
|
||||
|
||||
import 'reactflow/dist/style.css';
|
||||
import type { ModuleItemType } from '@fastgpt/global/core/module/type.d';
|
||||
|
||||
const NodeSimple = dynamic(() => import('./components/nodes/NodeSimple'));
|
||||
const nodeTypes = {
|
||||
[FlowNodeTypeEnum.userGuide]: dynamic(() => import('./components/nodes/NodeUserGuide')),
|
||||
[FlowNodeTypeEnum.variable]: dynamic(() => import('./components/nodes/NodeVariable')),
|
||||
[FlowNodeTypeEnum.questionInput]: dynamic(() => import('./components/nodes/NodeQuestionInput')),
|
||||
[FlowNodeTypeEnum.historyNode]: NodeSimple,
|
||||
[FlowNodeTypeEnum.chatNode]: NodeSimple,
|
||||
[FlowNodeTypeEnum.datasetSearchNode]: NodeSimple,
|
||||
[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.runApp]: NodeSimple,
|
||||
[FlowNodeTypeEnum.pluginInput]: dynamic(() => import('./components/nodes/NodeInput')),
|
||||
[FlowNodeTypeEnum.pluginOutput]: dynamic(() => import('./components/nodes/NodeOutput')),
|
||||
[FlowNodeTypeEnum.pluginModule]: NodeSimple
|
||||
};
|
||||
const edgeTypes = {
|
||||
buttonedge: ButtonEdge
|
||||
};
|
||||
type Props = {
|
||||
modules: ModuleItemType[];
|
||||
Header: React.ReactNode;
|
||||
} & ModuleTemplateProps;
|
||||
|
||||
const Container = React.memo(function Container(props: Props) {
|
||||
const { modules = [], Header, systemTemplates, pluginTemplates, show2Plugin } = props;
|
||||
|
||||
const {
|
||||
isOpen: isOpenTemplate,
|
||||
onOpen: onOpenTemplate,
|
||||
onClose: onCloseTemplate
|
||||
} = useDisclosure();
|
||||
|
||||
const { reactFlowWrapper, nodes, onNodesChange, edges, onEdgesChange, onConnect, initData } =
|
||||
useFlowProviderStore();
|
||||
|
||||
useEffect(() => {
|
||||
initData(JSON.parse(JSON.stringify(modules)));
|
||||
}, [modules.length]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* header */}
|
||||
{Header}
|
||||
<Box
|
||||
minH={'400px'}
|
||||
flex={'1 0 0'}
|
||||
w={'100%'}
|
||||
h={0}
|
||||
position={'relative'}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
{/* open module template */}
|
||||
<IconButton
|
||||
position={'absolute'}
|
||||
top={5}
|
||||
left={5}
|
||||
w={'38px'}
|
||||
h={'38px'}
|
||||
borderRadius={'50%'}
|
||||
icon={<SmallCloseIcon fontSize={'26px'} />}
|
||||
transform={isOpenTemplate ? '' : 'rotate(135deg)'}
|
||||
transition={'0.2s ease'}
|
||||
aria-label={''}
|
||||
zIndex={1}
|
||||
boxShadow={'2px 2px 6px #85b1ff'}
|
||||
onClick={() => {
|
||||
isOpenTemplate ? onCloseTemplate() : onOpenTemplate();
|
||||
}}
|
||||
/>
|
||||
|
||||
<ReactFlow
|
||||
ref={reactFlowWrapper}
|
||||
fitView
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
minZoom={0.1}
|
||||
maxZoom={1.5}
|
||||
defaultEdgeOptions={edgeOptions}
|
||||
connectionLineStyle={connectionLineStyle}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={(connect) => {
|
||||
connect.sourceHandle &&
|
||||
connect.targetHandle &&
|
||||
onConnect({
|
||||
connect
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Background />
|
||||
<Controls position={'bottom-right'} style={{ display: 'flex' }} showInteractive={false} />
|
||||
</ReactFlow>
|
||||
|
||||
<TemplateList
|
||||
systemTemplates={systemTemplates}
|
||||
pluginTemplates={pluginTemplates}
|
||||
show2Plugin={show2Plugin}
|
||||
isOpen={isOpenTemplate}
|
||||
onClose={onCloseTemplate}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
const Flow = (data: Props) => {
|
||||
return (
|
||||
<Box h={'100%'} position={'fixed'} zIndex={999} top={0} left={0} right={0} bottom={0}>
|
||||
<ReactFlowProvider>
|
||||
<Flex h={'100%'} flexDirection={'column'} bg={'#fff'}>
|
||||
<Container {...data} />
|
||||
</Flex>
|
||||
</ReactFlowProvider>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Flow);
|
||||
209
projects/app/src/components/core/module/VariableEditModal.tsx
Normal file
209
projects/app/src/components/core/module/VariableEditModal.tsx
Normal file
@@ -0,0 +1,209 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
ModalBody,
|
||||
NumberInput,
|
||||
NumberInputField,
|
||||
NumberInputStepper,
|
||||
NumberIncrementStepper,
|
||||
NumberDecrementStepper,
|
||||
Flex,
|
||||
Switch,
|
||||
Input,
|
||||
Grid,
|
||||
FormControl,
|
||||
useTheme
|
||||
} from '@chakra-ui/react';
|
||||
import { SmallAddIcon } from '@chakra-ui/icons';
|
||||
import { VariableInputEnum } from '@/constants/app';
|
||||
import type { VariableItemType } from '@/types/app';
|
||||
import MyIcon from '@/components/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 '@/components/MyModal';
|
||||
|
||||
const VariableTypeList = [
|
||||
{ label: '文本', icon: 'settingLight', key: VariableInputEnum.input },
|
||||
{ label: '下拉单选', icon: 'settingLight', key: VariableInputEnum.select }
|
||||
];
|
||||
|
||||
export type VariableFormType = {
|
||||
variable: VariableItemType;
|
||||
};
|
||||
|
||||
const VariableEditModal = ({
|
||||
defaultVariable,
|
||||
onClose,
|
||||
onSubmit
|
||||
}: {
|
||||
defaultVariable: VariableItemType;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: VariableFormType) => void;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
|
||||
const { reset, getValues, setValue, register, control, handleSubmit } = useForm<VariableFormType>(
|
||||
{
|
||||
defaultValues: {
|
||||
variable: defaultVariable
|
||||
}
|
||||
}
|
||||
);
|
||||
const {
|
||||
fields: selectEnums,
|
||||
append: appendEnums,
|
||||
remove: removeEnums
|
||||
} = useFieldArray({
|
||||
control,
|
||||
name: 'variable.enums'
|
||||
});
|
||||
|
||||
return (
|
||||
<MyModal isOpen={true} onClose={onClose}>
|
||||
<ModalHeader display={'flex'}>
|
||||
<MyIcon name={'variable'} mr={2} w={'24px'} color={'#FF8A4C'} />
|
||||
变量设置
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box w={'70px'}>必填</Box>
|
||||
<Switch {...register('variable.required')} />
|
||||
</Flex>
|
||||
<Flex mt={5} alignItems={'center'}>
|
||||
<Box w={'80px'}>变量名</Box>
|
||||
<Input {...register('variable.label', { required: '变量名不能为空' })} />
|
||||
</Flex>
|
||||
<Flex mt={5} alignItems={'center'}>
|
||||
<Box w={'80px'}>变量 key</Box>
|
||||
<Input {...register('variable.key', { required: '变量 key 不能为空' })} />
|
||||
</Flex>
|
||||
|
||||
<Box mt={5} mb={2}>
|
||||
字段类型
|
||||
</Box>
|
||||
<Grid gridTemplateColumns={'repeat(2,130px)'} gridGap={4}>
|
||||
{VariableTypeList.map((item) => (
|
||||
<Flex
|
||||
key={item.key}
|
||||
px={4}
|
||||
py={1}
|
||||
border={theme.borders.base}
|
||||
borderRadius={'md'}
|
||||
cursor={'pointer'}
|
||||
{...(item.key === getValues('variable.type')
|
||||
? {
|
||||
bg: 'myWhite.600'
|
||||
}
|
||||
: {
|
||||
_hover: {
|
||||
boxShadow: 'md'
|
||||
},
|
||||
onClick: () => {
|
||||
setValue('variable.type', item.key);
|
||||
setRefresh(!refresh);
|
||||
}
|
||||
})}
|
||||
>
|
||||
<MyIcon name={item.icon as any} w={'16px'} />
|
||||
<Box ml={3}>{item.label}</Box>
|
||||
</Flex>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
{getValues('variable.type') === VariableInputEnum.input && (
|
||||
<>
|
||||
<Box mt={5} mb={2}>
|
||||
最大长度
|
||||
</Box>
|
||||
<Box>
|
||||
<NumberInput max={100} min={1} step={1} position={'relative'}>
|
||||
<NumberInputField
|
||||
{...register('variable.maxLen', {
|
||||
min: 1,
|
||||
max: 100,
|
||||
valueAsNumber: true
|
||||
})}
|
||||
max={100}
|
||||
/>
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberInputStepper>
|
||||
</NumberInput>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
{getValues('variable.type') === VariableInputEnum.select && (
|
||||
<>
|
||||
<Box mt={5} mb={2}>
|
||||
选项
|
||||
</Box>
|
||||
<Box>
|
||||
{selectEnums.map((item, i) => (
|
||||
<Flex key={item.id} mb={2} alignItems={'center'}>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...register(`variable.enums.${i}.value`, {
|
||||
required: '选项内容不能为空'
|
||||
})}
|
||||
/>
|
||||
</FormControl>
|
||||
<MyIcon
|
||||
ml={3}
|
||||
name={'delete'}
|
||||
w={'16px'}
|
||||
cursor={'pointer'}
|
||||
p={2}
|
||||
borderRadius={'lg'}
|
||||
_hover={{ bg: 'red.100' }}
|
||||
onClick={() => removeEnums(i)}
|
||||
/>
|
||||
</Flex>
|
||||
))}
|
||||
</Box>
|
||||
<Button
|
||||
variant={'solid'}
|
||||
w={'100%'}
|
||||
textAlign={'left'}
|
||||
leftIcon={<SmallAddIcon />}
|
||||
bg={'myGray.100 !important'}
|
||||
onClick={() => appendEnums({ value: '' })}
|
||||
>
|
||||
添加选项
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button variant={'base'} mr={3} onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={handleSubmit(onSubmit)}>确认</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(VariableEditModal);
|
||||
|
||||
export const defaultVariable: VariableItemType = {
|
||||
id: nanoid(),
|
||||
key: 'key',
|
||||
label: 'label',
|
||||
type: VariableInputEnum.input,
|
||||
required: true,
|
||||
maxLen: 50,
|
||||
enums: [{ value: '' }]
|
||||
};
|
||||
export const addVariable = () => {
|
||||
const newVariable = { ...defaultVariable, key: nanoid(), id: nanoid() };
|
||||
return newVariable;
|
||||
};
|
||||
Reference in New Issue
Block a user