This commit is contained in:
Archer
2023-10-30 13:26:42 +08:00
committed by GitHub
parent 008d0af010
commit 60ee160131
216 changed files with 4429 additions and 2229 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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;

View File

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

View 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;

View File

@@ -1,4 +1,4 @@
import React, { useMemo, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import MyModal from '@/components/MyModal';
import { useTranslation } from 'react-i18next';
import { EditFormType } from '@/web/core/app/basicSettings';
@@ -45,7 +45,6 @@ const AIChatSettingsModal = ({
const [selectTemplateData, setSelectTemplateData] = useState<{
title: string;
key: 'quoteTemplate' | 'quotePrompt';
templates: PromptTemplateItem[];
}>();
@@ -163,8 +162,7 @@ const AIChatSettingsModal = ({
{...selectTemplateBtn}
onClick={() =>
setSelectTemplateData({
title: '选择引用内容模板',
key: 'quoteTemplate',
title: '选择知识库提示词模板',
templates: Prompt_QuoteTemplateList
})
}
@@ -190,19 +188,6 @@ const AIChatSettingsModal = ({
>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</MyTooltip>
<Box flex={1} />
<Box
{...selectTemplateBtn}
onClick={() =>
setSelectTemplateData({
title: '选择引用提示词模板',
key: 'quotePrompt',
templates: Prompt_QuotePromptList
})
}
>
</Box>
</Flex>
<Textarea
rows={11}
@@ -227,7 +212,12 @@ const AIChatSettingsModal = ({
title={selectTemplateData.title}
templates={selectTemplateData.templates}
onClose={() => setSelectTemplateData(undefined)}
onSuccess={(e) => setValue(selectTemplateData.key, e)}
onSuccess={(e) => {
const quoteVal = e.value;
const promptVal = Prompt_QuotePromptList.find((item) => item.title === e.title)?.value;
setValue('quoteTemplate', quoteVal);
setValue('quotePrompt', promptVal);
}}
/>
)}
</MyModal>

View File

@@ -1,4 +1,4 @@
import { AppModuleItemType } from '@/types/app';
import type { ModuleItemType } from '@fastgpt/global/core/module/type.d';
import { AppSchema } from '@/types/mongoSchema';
import React, {
useMemo,
@@ -10,7 +10,7 @@ import React, {
} from 'react';
import { Box, Flex, IconButton } from '@chakra-ui/react';
import MyIcon from '@/components/Icon';
import { FlowModuleTypeEnum } from '@/constants/flow';
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';
@@ -28,7 +28,7 @@ const ChatTest = (
onClose
}: {
app: AppSchema;
modules?: AppModuleItemType[];
modules?: ModuleItemType[];
onClose: () => void;
},
ref: ForwardedRef<ChatTestComponentRef>
@@ -41,7 +41,7 @@ const ChatTest = (
async ({ chatList, controller, generatingMessage, variables }: StartChatFnProps) => {
const historyMaxLen =
modules
?.find((item) => item.flowType === FlowModuleTypeEnum.historyNode)
?.find((item) => item.flowType === FlowNodeTypeEnum.historyNode)
?.inputs?.find((item) => item.key === 'maxContext')?.value || 0;
const history = chatList.slice(-historyMaxLen - 2, -2);

View File

@@ -10,9 +10,12 @@ import {
} from 'reactflow';
import type {
FlowModuleItemType,
FlowOutputTargetItemType,
FlowModuleItemChangeProps
} from '@/types/core/app/flow';
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,
@@ -25,17 +28,21 @@ import React, {
import { customAlphabet } from 'nanoid';
import { appModule2FlowEdge, appModule2FlowNode } from '@/utils/adapt';
import { useToast } from '@/web/common/hooks/useToast';
import { FlowModuleTypeEnum, FlowValueTypeEnum } from '@/constants/flow';
import {
FlowNodeInputTypeEnum,
FlowNodeTypeEnum,
FlowNodeValTypeEnum
} from '@fastgpt/global/core/module/node/constant';
import { useTranslation } from 'next-i18next';
import { AppModuleItemType } from '@/types/app';
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 useFlowStoreType = {
appId: string;
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>;
@@ -44,8 +51,9 @@ export type useFlowStoreType = {
onEdgesChange: OnChange<EdgeChange>;
onFixView: () => void;
onDelNode: (nodeId: string) => void;
onChangeNode: (e: FlowModuleItemChangeProps) => void;
onChangeNode: (e: FlowNodeChangeProps) => void;
onCopyNode: (nodeId: string) => void;
onResetNode: (id: string, module: FlowModuleTemplateType) => void;
onDelEdge: (e: {
moduleId: string;
sourceHandle?: string | undefined;
@@ -53,12 +61,12 @@ export type useFlowStoreType = {
}) => void;
onDelConnect: (id: string) => void;
onConnect: ({ connect }: { connect: Connection }) => any;
initData: (modules: AppModuleItemType[]) => void;
initData: (modules: ModuleItemType[]) => void;
};
const StateContext = createContext<useFlowStoreType>({
appId: '',
const StateContext = createContext<useFlowProviderStoreType>({
reactFlowWrapper: null,
filterAppIds: [],
nodes: [],
setNodes: function (
value: React.SetStateAction<Node<FlowModuleItemType, string | undefined>[]>
@@ -78,11 +86,10 @@ const StateContext = createContext<useFlowStoreType>({
onFixView: function (): void {
return;
},
onDelNode: function (nodeId: string): void {
return;
},
onChangeNode: function (e: FlowModuleItemChangeProps): void {
onChangeNode: function (e: FlowNodeChangeProps): void {
return;
},
onCopyNode: function (nodeId: string): void {
@@ -101,13 +108,22 @@ const StateContext = createContext<useFlowStoreType>({
onConnect: function ({ connect }: { connect: Connection }) {
return;
},
initData: function (modules: AppModuleItemType[]): void {
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 useFlowStore = () => useContext(StateContext);
export const useFlowProviderStore = () => useContext(StateContext);
export const FlowProvider = ({ appId, children }: { appId: string; children: React.ReactNode }) => {
export const FlowProvider = ({
filterAppIds = [],
children
}: {
filterAppIds?: string[];
children: React.ReactNode;
}) => {
const reactFlowWrapper = useRef<HTMLDivElement>(null);
const { t } = useTranslation();
const { toast } = useToast();
@@ -156,8 +172,11 @@ export const FlowProvider = ({ appId, children }: { appId: string; children: Rea
({ connect }: { connect: Connection }) => {
const source = nodes.find((node) => node.id === connect.source)?.data;
const sourceType = (() => {
if (source?.flowType === FlowModuleTypeEnum.classifyQuestion) {
return FlowValueTypeEnum.boolean;
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;
})();
@@ -173,8 +192,8 @@ export const FlowProvider = ({ appId, children }: { appId: string; children: Rea
});
}
if (
sourceType !== FlowValueTypeEnum.any &&
targetType !== FlowValueTypeEnum.any &&
sourceType !== FlowNodeValTypeEnum.any &&
targetType !== FlowNodeValTypeEnum.any &&
sourceType !== targetType
) {
return toast({
@@ -209,15 +228,31 @@ export const FlowProvider = ({ appId, children }: { appId: string; children: Rea
);
const onChangeNode = useCallback(
({ moduleId, key, type = 'inputs', value }: FlowModuleItemChangeProps) => {
({ moduleId, type, key, value, index }: FlowNodeChangeProps) => {
setNodes((nodes) =>
nodes.map((node) => {
if (node.id !== moduleId) return node;
const updateObj: Record<string, any> = {};
if (type === 'inputs') {
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) {
@@ -227,22 +262,51 @@ export const FlowProvider = ({ appId, children }: { appId: string; children: Rea
});
updateObj.inputs = node.data.inputs;
} else {
updateObj.inputs = node.data.inputs.concat(value);
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 === 'attr') {
updateObj[key] = value;
} else if (type === 'outputs') {
// del output connect
const delOutputs = node.data.outputs.filter(
(item) => !value.find((output: FlowOutputTargetItemType) => output.key === item.key)
);
delOutputs.forEach((output) => {
onDelEdge({ moduleId, sourceHandle: output.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
});
});
updateObj.outputs = 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 {
@@ -287,8 +351,36 @@ export const FlowProvider = ({ appId, children }: { appId: string; children: Rea
[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: AppModuleItemType[]) => {
(modules: ModuleItemType[]) => {
const edges = appModule2FlowEdge({
modules,
onDelete: onDelConnect
@@ -304,7 +396,7 @@ export const FlowProvider = ({ appId, children }: { appId: string; children: Rea
// use eventbus to avoid refresh ReactComponents
useEffect(() => {
const update = (e: FlowModuleItemChangeProps) => {
const update = (e: FlowNodeChangeProps) => {
onChangeNode(e);
};
eventBus.on(EventNameEnum.updaterNode, update);
@@ -314,8 +406,8 @@ export const FlowProvider = ({ appId, children }: { appId: string; children: Rea
}, [onChangeNode]);
const value = {
appId,
reactFlowWrapper,
filterAppIds,
nodes,
setNodes,
onNodesChange,
@@ -325,6 +417,7 @@ export const FlowProvider = ({ appId, children }: { appId: string; children: Rea
onFixView,
onDelNode,
onChangeNode,
onResetNode,
onCopyNode,
onDelEdge,
onDelConnect,
@@ -337,6 +430,53 @@ export const FlowProvider = ({ appId, children }: { appId: string; children: Rea
export default React.memo(FlowProvider);
export const onChangeNode = (e: FlowModuleItemChangeProps) => {
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;
}

View File

@@ -3,13 +3,13 @@ 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 { useFlowStore } from './Provider';
import { useFlowProviderStore } from './FlowProvider';
const ImportSettings = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
const { toast } = useToast();
const [value, setValue] = useState('');
const { setNodes, setEdges, initData } = useFlowStore();
const { setNodes, setEdges, initData } = useFlowProviderStore();
return (
<MyModal isOpen w={'600px'} onClose={onClose} title={t('app.Import Config')}>

View File

@@ -1,22 +1,22 @@
import React, { useMemo } from 'react';
import { ModalBody, Flex, Box, useTheme, ModalFooter, Button } from '@chakra-ui/react';
import MyModal from '@/components/MyModal';
import { getMyModels } from '@/web/core/app/api';
import { useQuery } from '@tanstack/react-query';
import type { SelectAppItemType } from '@/types/core/app/flow';
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 = [],
filterApps = [],
filterAppIds = [],
max = 1,
onClose,
onSuccess
}: {
defaultApps: string[];
filterApps?: string[];
filterAppIds?: string[];
max?: number;
onClose: () => void;
onSuccess: (e: SelectAppItemType[]) => void;
@@ -26,11 +26,12 @@ const SelectAppModal = ({
const theme = useTheme();
const [selectedApps, setSelectedApps] = React.useState<string[]>(defaultApps);
/* 加载模型 */
const { data = [], isLoading } = useQuery(['loadMyApos'], () => getMyModels());
const { myApps, loadMyApps } = useUserStore();
const { isLoading } = useQuery(['loadMyApos'], () => loadMyApps());
const apps = useMemo(
() => data.filter((app) => !filterApps.includes(app._id)),
[data, filterApps]
() => myApps.filter((app) => !filterAppIds.includes(app._id)),
[myApps, filterAppIds]
);
return (
@@ -38,13 +39,13 @@ const SelectAppModal = ({
isOpen
title={`选择应用${max > 1 ? `(${selectedApps.length}/${max})` : ''}`}
onClose={onClose}
w={'700px'}
minW={'700px'}
position={'relative'}
>
<ModalBody
minH={'300px'}
display={'grid'}
gridTemplateColumns={['1fr', 'repeat(3,1fr)']}
gridTemplateColumns={['1fr', 'repeat(3, minmax(0, 1fr))']}
gridGap={4}
>
{apps.map((app) => (
@@ -53,8 +54,7 @@ const SelectAppModal = ({
alignItems={'center'}
border={theme.borders.base}
borderRadius={'md'}
px={1}
py={2}
p={2}
cursor={'pointer'}
{...(selectedApps.includes(app._id)
? {

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../modules/NodeCard';
import { FlowModuleItemType } from '@/types/core/app/flow';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import Container from '../modules/Container';
import RenderInput from '../render/RenderInput';
import RenderOutput from '../render/RenderOutput';

View File

@@ -2,7 +2,7 @@ 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 '@/types/core/app/flow';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import Divider from '../modules/Divider';
import Container from '../modules/Container';
import RenderInput from '../render/RenderInput';
@@ -10,11 +10,15 @@ import type { ClassifyQuestionAgentItemType } from '@/types/app';
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 4);
import MyIcon from '@/components/Icon';
import { FlowOutputItemTypeEnum, FlowValueTypeEnum, SpecialInputKeyEnum } from '@/constants/flow';
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 '../Provider';
import { onChangeNode } from '../../FlowProvider';
const NodeCQNode = ({ data }: NodeProps<FlowModuleItemType>) => {
const { t } = useTranslation();
@@ -28,7 +32,7 @@ const NodeCQNode = ({ data }: NodeProps<FlowModuleItemType>) => {
moduleId={moduleId}
flowInputList={inputs}
CustomComponent={{
[SpecialInputKeyEnum.agents]: ({
[FlowNodeSpecialInputKeyEnum.agents]: ({
key: agentKey,
value: agents = [],
...props
@@ -50,26 +54,20 @@ const NodeCQNode = ({ data }: NodeProps<FlowModuleItemType>) => {
color={'myGray.600'}
_hover={{ color: 'red.600' }}
onClick={() => {
const newInputValue = agents.filter((input) => input.key !== item.key);
const newOutputVal = outputs.filter(
(output) => output.key !== item.key
);
onChangeNode({
moduleId,
type: 'inputs',
type: 'updateInput',
key: agentKey,
value: {
...props,
key: agentKey,
value: newInputValue
value: agents.filter((input) => input.key !== item.key)
}
});
onChangeNode({
moduleId,
type: 'outputs',
key: '',
value: newOutputVal
type: 'delOutput',
key: item.key
});
}}
/>
@@ -92,7 +90,7 @@ const NodeCQNode = ({ data }: NodeProps<FlowModuleItemType>) => {
);
onChangeNode({
moduleId,
type: 'inputs',
type: 'updateInput',
key: agentKey,
value: {
...props,
@@ -102,36 +100,35 @@ const NodeCQNode = ({ data }: NodeProps<FlowModuleItemType>) => {
});
}}
/>
<SourceHandle handleKey={item.key} valueType={FlowValueTypeEnum.boolean} />
<SourceHandle handleKey={item.key} valueType={FlowNodeValTypeEnum.boolean} />
</Box>
</Box>
))}
<Button
onClick={() => {
const key = nanoid();
const newInputValue = agents.concat({ value: '', key });
const newOutputValue = outputs.concat({
key,
label: '',
type: FlowOutputItemTypeEnum.hidden,
targets: []
});
onChangeNode({
moduleId,
type: 'inputs',
type: 'updateInput',
key: agentKey,
value: {
...props,
key: agentKey,
value: newInputValue
value: agents.concat({ value: '', key })
}
});
onChangeNode({
moduleId,
type: 'outputs',
type: 'updateOutput',
key: agentKey,
value: newOutputValue
value: outputs.concat({
key,
label: '',
type: FlowNodeOutputTypeEnum.hidden,
targets: []
})
});
}}
>

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../modules/NodeCard';
import { FlowModuleItemType } from '@/types/core/app/flow';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
const NodeAnswer = ({ data }: NodeProps<FlowModuleItemType>) => {
return <NodeCard {...data}></NodeCard>;

View File

@@ -1,7 +1,7 @@
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 '@/types/core/app/flow';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import { useTranslation } from 'next-i18next';
import NodeCard from '../modules/NodeCard';
import Container from '../modules/Container';
@@ -13,14 +13,17 @@ import RenderOutput from '../render/RenderOutput';
import MyIcon from '@/components/Icon';
import ExtractFieldModal from '../modules/ExtractFieldModal';
import { ContextExtractEnum } from '@/constants/flow/flowField';
import { FlowOutputItemTypeEnum, FlowValueTypeEnum } from '@/constants/flow';
import { useFlowStore, onChangeNode } from '../Provider';
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 } = useFlowStore();
const { onDelEdge } = useFlowProviderStore();
return (
<NodeCard minW={'400px'} {...data}>
@@ -88,29 +91,21 @@ const NodeExtract = ({ data }: NodeProps<FlowModuleItemType>) => {
w={'16px'}
cursor={'pointer'}
onClick={() => {
const newInputValue = extractKeys.filter(
(extract) => item.key !== extract.key
);
const newOutputVal = outputs.filter(
(output) => output.key !== item.key
);
onChangeNode({
moduleId,
type: 'inputs',
type: 'updateInput',
key: ContextExtractEnum.extractKeys,
value: {
...props,
value: newInputValue
value: extractKeys.filter((extract) => item.key !== extract.key)
}
});
onChangeNode({
moduleId,
type: 'outputs',
key: '',
value: newOutputVal
type: 'delOutput',
key: item.key
});
onDelEdge({ moduleId, sourceHandle: item.key });
}}
/>
</Td>
@@ -145,7 +140,7 @@ const NodeExtract = ({ data }: NodeProps<FlowModuleItemType>) => {
onChangeNode({
moduleId,
type: 'inputs',
type: 'updateInput',
key: ContextExtractEnum.extractKeys,
value: {
...inputs.find((input) => input.key === ContextExtractEnum.extractKeys),
@@ -153,62 +148,42 @@ const NodeExtract = ({ data }: NodeProps<FlowModuleItemType>) => {
}
});
if (!exists) {
onChangeNode({
moduleId,
type: 'outputs',
key: '',
value: outputs.concat({
key: data.key,
label: `提取结果-${data.desc}`,
description: '无法提取时不会返回',
valueType: FlowValueTypeEnum.string,
type: FlowOutputItemTypeEnum.source,
targets: []
})
});
} else {
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: 'outputs',
key: '',
value: outputs.map((output) =>
output.key === data.key
? {
...output,
label: `提取结果-${data.desc}`
}
: output
)
type: 'updateOutput',
key: data.key,
value: {
...output,
label: `提取结果-${data.desc}`
}
});
} else {
// del and push
const newOutputs = outputs.filter((output) => output.key !== editExtractFiled.key);
onChangeNode({
moduleId,
type: 'outputs',
key: '',
value: newOutputs
type: 'replaceOutput',
key: editExtractFiled.key,
value: newOutput
});
setTimeout(() => {
onChangeNode({
moduleId,
type: 'outputs',
key: '',
value: newOutputs.concat({
key: data.key,
label: `提取结果-${data.desc}`,
description: '无法提取时不会返回',
valueType: FlowValueTypeEnum.string,
type: FlowOutputItemTypeEnum.source,
targets: []
})
});
}, 10);
}
} else {
onChangeNode({
moduleId,
type: 'addOutput',
value: newOutput
});
}
setEditExtractField(undefined);

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../modules/NodeCard';
import { FlowModuleItemType } from '@/types/core/app/flow';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import Divider from '../modules/Divider';
import Container from '../modules/Container';
import RenderInput from '../render/RenderInput';
@@ -9,10 +9,14 @@ import { Box, Button } from '@chakra-ui/react';
import { SmallAddIcon } from '@chakra-ui/icons';
import RenderOutput from '../render/RenderOutput';
import { FlowInputItemTypeEnum, FlowOutputItemTypeEnum, FlowValueTypeEnum } from '@/constants/flow';
import {
FlowNodeInputTypeEnum,
FlowNodeOutputTypeEnum,
FlowNodeValTypeEnum
} from '@fastgpt/global/core/module/node/constant';
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
import { onChangeNode } from '../Provider';
import { onChangeNode } from '../../FlowProvider';
const NodeHttp = ({ data }: NodeProps<FlowModuleItemType>) => {
const { moduleId, inputs, outputs } = data;
@@ -33,8 +37,8 @@ const NodeHttp = ({ data }: NodeProps<FlowModuleItemType>) => {
key,
value: {
key,
valueType: FlowValueTypeEnum.string,
type: FlowInputItemTypeEnum.target,
valueType: FlowNodeValTypeEnum.string,
type: FlowNodeInputTypeEnum.target,
label: `入参${inputs.length - 1}`,
edit: true
}
@@ -52,21 +56,17 @@ const NodeHttp = ({ data }: NodeProps<FlowModuleItemType>) => {
variant={'base'}
leftIcon={<SmallAddIcon />}
onClick={() => {
const key = nanoid();
onChangeNode({
moduleId,
type: 'outputs',
key,
value: [
{
key,
label: `出参${outputs.length}`,
valueType: FlowValueTypeEnum.string,
type: FlowOutputItemTypeEnum.source,
edit: true,
targets: []
}
].concat(outputs as any)
type: 'addOutput',
value: {
key: nanoid(),
label: `出参${outputs.length}`,
valueType: FlowNodeValTypeEnum.string,
type: FlowNodeOutputTypeEnum.source,
edit: true,
targets: []
}
});
}}
>

View File

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

View File

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

View File

@@ -1,17 +1,17 @@
import React, { useMemo } from 'react';
import React from 'react';
import { NodeProps } from 'reactflow';
import { FlowModuleItemType } from '@/types/core/app/flow';
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 NodeDatasetSearch = ({ data }: NodeProps<FlowModuleItemType>) => {
const NodeSimple = ({ data }: NodeProps<FlowModuleItemType>) => {
const { moduleId, inputs, outputs } = data;
return (
<NodeCard minW={'400px'} {...data}>
<NodeCard minW={'300px'} isPreview {...data}>
<Divider text="Input" />
<Container>
<RenderInput moduleId={moduleId} flowInputList={inputs} />
@@ -23,4 +23,4 @@ const NodeDatasetSearch = ({ data }: NodeProps<FlowModuleItemType>) => {
</NodeCard>
);
};
export default React.memo(NodeDatasetSearch);
export default React.memo(NodeSimple);

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../modules/NodeCard';
import { FlowModuleItemType } from '@/types/core/app/flow';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import Container from '../modules/Container';
import RenderOutput from '../render/RenderOutput';

View File

@@ -1,13 +1,13 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../modules/NodeCard';
import { FlowModuleItemType } from '@/types/core/app/flow';
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 NodeAPP = ({ data }: NodeProps<FlowModuleItemType>) => {
const NodeRunAPP = ({ data }: NodeProps<FlowModuleItemType>) => {
const { moduleId, inputs, outputs } = data;
return (
@@ -22,4 +22,4 @@ const NodeAPP = ({ data }: NodeProps<FlowModuleItemType>) => {
</NodeCard>
);
};
export default React.memo(NodeAPP);
export default React.memo(NodeRunAPP);

View File

@@ -1,17 +1,17 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../modules/NodeCard';
import { FlowModuleItemType } from '@/types/core/app/flow';
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 NodeChat = ({ data }: NodeProps<FlowModuleItemType>) => {
const NodeSimple = ({ data }: NodeProps<FlowModuleItemType>) => {
const { moduleId, inputs, outputs } = data;
return (
<NodeCard minW={'400px'} {...data}>
<NodeCard minW={'300px'} {...data}>
<Divider text="Input" />
<Container>
<RenderInput moduleId={moduleId} flowInputList={inputs} />
@@ -23,4 +23,4 @@ const NodeChat = ({ data }: NodeProps<FlowModuleItemType>) => {
</NodeCard>
);
};
export default React.memo(NodeChat);
export default React.memo(NodeSimple);

View File

@@ -15,10 +15,10 @@ import {
Switch
} from '@chakra-ui/react';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import { FlowModuleItemType } from '@/types/core/app/flow';
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 '../Provider';
import { onChangeNode } from '../../FlowProvider';
import VariableEditModal, { addVariable } from '../../../VariableEditModal';
import MyIcon from '@/components/Icon';
@@ -76,7 +76,7 @@ export function WelcomeText({ data }: { data: FlowModuleItemType }) {
onChangeNode({
moduleId,
key: SystemInputEnum.welcomeText,
type: 'inputs',
type: 'updateInput',
value: {
...welcomeText,
value: e.target.value
@@ -106,14 +106,14 @@ function ChatStartVariable({ data }: { data: FlowModuleItemType }) {
onChangeNode({
moduleId,
key: SystemInputEnum.variables,
type: 'inputs',
type: 'updateInput',
value: {
...inputs.find((item) => item.key === SystemInputEnum.variables),
value
}
});
},
[inputs, onChangeNode, moduleId]
[inputs, moduleId]
);
const onclickSubmit = useCallback(
@@ -230,7 +230,7 @@ function QuestionGuide({ data }: { data: FlowModuleItemType }) {
onChangeNode({
moduleId,
key: SystemInputEnum.questionGuide,
type: 'inputs',
type: 'updateInput',
value: {
...inputs.find((item) => item.key === SystemInputEnum.questionGuide),
value

View File

@@ -4,7 +4,7 @@ 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 '@/types/core/app/flow';
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';
@@ -12,7 +12,7 @@ import MyIcon from '@/components/Icon';
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
import VariableEditModal, { addVariable } from '../../../VariableEditModal';
import { onChangeNode } from '../Provider';
import { onChangeNode } from '../../FlowProvider';
export const defaultVariable: VariableItemType = {
id: nanoid(),
@@ -41,14 +41,14 @@ const NodeUserGuide = ({ data }: NodeProps<FlowModuleItemType>) => {
onChangeNode({
moduleId,
key: SystemInputEnum.variables,
type: 'inputs',
type: 'updateInput',
value: {
...inputs.find((item) => item.key === SystemInputEnum.variables),
value
}
});
},
[inputs, onChangeNode, moduleId]
[inputs, moduleId]
);
const onclickSubmit = useCallback(

View File

@@ -1,5 +1,6 @@
import React, { useMemo, useState } from 'react';
import type { FlowInputItemType, SelectAppItemType } from '@/types/core/app/flow';
import type { SelectAppItemType } from '@fastgpt/global/core/module/type';
import type { FlowNodeInputItemType } from '@fastgpt/global/core/module/node/type';
import {
Box,
Textarea,
@@ -15,10 +16,10 @@ import {
useTheme,
Grid
} from '@chakra-ui/react';
import { FlowInputItemTypeEnum } from '@/constants/flow';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import dynamic from 'next/dynamic';
import { onChangeNode, useFlowStore } from '../Provider';
import { onChangeNode, useFlowProviderStore } from '../../FlowProvider';
import Avatar from '@/components/Avatar';
import MySelect from '@/components/Select';
import MySlider from '@/components/Slider';
@@ -33,22 +34,25 @@ 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 SetInputFieldModal = dynamic(() => import('../modules/SetInputFieldModal'));
const SelectAppModal = dynamic(() => import('../../../SelectAppModal'));
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
}: FlowInputItemType & {
}: FlowNodeInputItemType & {
moduleId: string;
inputKey: string;
editFiledType?: EditFieldModeType;
}) {
const { required = false, description, edit, label, type, valueType } = item;
const [editField, setEditField] = useState<FlowInputItemType>();
const [editField, setEditField] = useState<EditFieldType>();
return (
<Flex className="nodrag" cursor={'default'} alignItems={'center'} position={'relative'}>
@@ -72,7 +76,7 @@ export const Label = React.memo(function Label({
)}
</Box>
{(type === FlowInputItemTypeEnum.target || valueType) && (
{(type === FlowNodeInputTypeEnum.target || valueType) && (
<TargetHandle handleKey={inputKey} valueType={valueType} />
)}
@@ -86,8 +90,11 @@ export const Label = React.memo(function Label({
_hover={{ color: 'myBlue.600' }}
onClick={() =>
setEditField({
...item,
key: inputKey
label: item.label,
valueType: item.valueType,
required: item.required,
key: inputKey,
description: item.description
})
}
/>
@@ -110,34 +117,31 @@ export const Label = React.memo(function Label({
</>
)}
{!!editField && (
<SetInputFieldModal
<FieldEditModal
mode={editFiledType}
defaultField={editField}
onClose={() => setEditField(undefined)}
onSubmit={(data) => {
onSubmit={(e) => {
const data = {
...item,
...e
};
// same key
if (editField.key === data.key) {
onChangeNode({
moduleId,
type: 'inputs',
key: inputKey,
type: 'updateInput',
key: data.key,
value: data
});
} else {
// diff key. del and add
onChangeNode({
moduleId,
type: 'addInput',
key: data.key,
type: 'replaceInput',
key: editField.key,
value: data
});
setTimeout(() => {
onChangeNode({
moduleId,
type: 'delInput',
key: editField.key,
value: ''
});
});
}
setEditField(undefined);
}}
@@ -150,55 +154,64 @@ export const Label = React.memo(function Label({
const RenderInput = ({
flowInputList,
moduleId,
CustomComponent = {}
CustomComponent = {},
editFiledType
}: {
flowInputList: FlowInputItemType[];
flowInputList: FlowNodeInputItemType[];
moduleId: string;
CustomComponent?: Record<string, (e: FlowInputItemType) => React.ReactNode>;
CustomComponent?: Record<string, (e: FlowNodeInputItemType) => React.ReactNode>;
editFiledType?: EditFieldModeType;
}) => {
const sortInputs = useMemo(
() => flowInputList.sort((a, b) => (a.key === FlowInputItemTypeEnum.switch ? -1 : 1)),
() => flowInputList.sort((a, b) => (a.key === FlowNodeInputTypeEnum.switch ? -1 : 1)),
[flowInputList]
);
return (
<>
{sortInputs.map(
(item) =>
item.type !== FlowInputItemTypeEnum.hidden && (
item.type !== FlowNodeInputTypeEnum.hidden && (
<Box key={item.key} _notLast={{ mb: 7 }} position={'relative'}>
{!!item.label && <Label moduleId={moduleId} inputKey={item.key} {...item} />}
{!!item.label && (
<Label
editFiledType={editFiledType}
moduleId={moduleId}
inputKey={item.key}
{...item}
/>
)}
<Box mt={2} className={'nodrag'}>
{item.type === FlowInputItemTypeEnum.numberInput && (
{item.type === FlowNodeInputTypeEnum.numberInput && (
<NumberInputRender item={item} moduleId={moduleId} />
)}
{item.type === FlowInputItemTypeEnum.input && (
{item.type === FlowNodeInputTypeEnum.input && (
<TextInputRender item={item} moduleId={moduleId} />
)}
{item.type === FlowInputItemTypeEnum.textarea && (
{item.type === FlowNodeInputTypeEnum.textarea && (
<TextareaRender item={item} moduleId={moduleId} />
)}
{item.type === FlowInputItemTypeEnum.select && (
{item.type === FlowNodeInputTypeEnum.select && (
<SelectRender item={item} moduleId={moduleId} />
)}
{item.type === FlowInputItemTypeEnum.slider && (
{item.type === FlowNodeInputTypeEnum.slider && (
<SliderRender item={item} moduleId={moduleId} />
)}
{item.type === FlowInputItemTypeEnum.selectApp && (
{item.type === FlowNodeInputTypeEnum.selectApp && (
<SelectAppRender item={item} moduleId={moduleId} />
)}
{item.type === FlowInputItemTypeEnum.aiSettings && (
{item.type === FlowNodeInputTypeEnum.aiSettings && (
<AISetting inputs={sortInputs} item={item} moduleId={moduleId} />
)}
{item.type === FlowInputItemTypeEnum.maxToken && (
{item.type === FlowNodeInputTypeEnum.maxToken && (
<MaxTokenRender inputs={sortInputs} item={item} moduleId={moduleId} />
)}
{item.type === FlowInputItemTypeEnum.selectChatModel && (
{item.type === FlowNodeInputTypeEnum.selectChatModel && (
<SelectChatModelRender inputs={sortInputs} item={item} moduleId={moduleId} />
)}
{item.type === FlowInputItemTypeEnum.selectDataset && (
{item.type === FlowNodeInputTypeEnum.selectDataset && (
<SelectDatasetRender item={item} moduleId={moduleId} />
)}
{item.type === FlowInputItemTypeEnum.custom && CustomComponent[item.key] && (
{item.type === FlowNodeInputTypeEnum.custom && CustomComponent[item.key] && (
<>{CustomComponent[item.key]({ ...item })}</>
)}
</Box>
@@ -212,8 +225,8 @@ const RenderInput = ({
export default React.memo(RenderInput);
type RenderProps = {
inputs?: FlowInputItemType[];
item: FlowInputItemType;
inputs?: FlowNodeInputItemType[];
item: FlowNodeInputItemType;
moduleId: string;
};
@@ -226,7 +239,7 @@ var NumberInputRender = React.memo(function NumberInputRender({ item, moduleId }
onChange={(e) => {
onChangeNode({
moduleId,
type: 'inputs',
type: 'updateInput',
key: item.key,
value: {
...item,
@@ -252,7 +265,7 @@ var TextInputRender = React.memo(function TextInputRender({ item, moduleId }: Re
onBlur={(e) => {
onChangeNode({
moduleId,
type: 'inputs',
type: 'updateInput',
key: item.key,
value: {
...item,
@@ -274,7 +287,7 @@ var TextareaRender = React.memo(function TextareaRender({ item, moduleId }: Rend
onBlur={(e) => {
onChangeNode({
moduleId,
type: 'inputs',
type: 'updateInput',
key: item.key,
value: {
...item,
@@ -295,7 +308,7 @@ var SelectRender = React.memo(function SelectRender({ item, moduleId }: RenderPr
onchange={(e) => {
onChangeNode({
moduleId,
type: 'inputs',
type: 'updateInput',
key: item.key,
value: {
...item,
@@ -320,7 +333,7 @@ var SliderRender = React.memo(function SliderRender({ item, moduleId }: RenderPr
onChange={(e) => {
onChangeNode({
moduleId,
type: 'inputs',
type: 'updateInput',
key: item.key,
value: {
...item,
@@ -368,7 +381,7 @@ var AISetting = React.memo(function AISetting({ inputs = [], moduleId }: RenderP
if (!item) continue;
onChangeNode({
moduleId,
type: 'inputs',
type: 'updateInput',
key,
value: {
...item,
@@ -411,7 +424,7 @@ var MaxTokenRender = React.memo(function MaxTokenRender({
onChange={(e) => {
onChangeNode({
moduleId,
type: 'inputs',
type: 'updateInput',
key: item.key,
value: {
...item,
@@ -435,7 +448,7 @@ var SelectChatModelRender = React.memo(function SelectChatModelRender({
{
onChangeNode({
moduleId,
type: 'inputs',
type: 'updateInput',
key: item.key,
value: {
...item,
@@ -449,7 +462,7 @@ var SelectChatModelRender = React.memo(function SelectChatModelRender({
onChangeNode({
moduleId,
type: 'inputs',
type: 'updateInput',
key: 'maxToken',
value: {
...inputs.find((input) => input.key === 'maxToken'),
@@ -477,7 +490,15 @@ var SelectChatModelRender = React.memo(function SelectChatModelRender({
onChangeModel(list[0].value);
}
return <MySelect width={'100%'} value={item.value} list={list} onchange={onChangeModel} />;
return (
<MySelect
minW={'350px'}
width={'100%'}
value={item.value}
list={list}
onchange={onChangeModel}
/>
);
});
var SelectDatasetRender = React.memo(function SelectDatasetRender({ item, moduleId }: RenderProps) {
@@ -498,7 +519,7 @@ var SelectDatasetRender = React.memo(function SelectDatasetRender({ item, module
return (
<>
<Grid gridTemplateColumns={'repeat(2, minmax(0, 1fr))'} gridGap={4} w={'100%'}>
<Grid gridTemplateColumns={'repeat(2, minmax(0, 1fr))'} gridGap={4} minW={'350px'} w={'100%'}>
<Button h={'36px'} onClick={onOpenKbSelect}>
</Button>
@@ -532,7 +553,7 @@ var SelectDatasetRender = React.memo(function SelectDatasetRender({ item, module
onChangeNode({
moduleId,
key: item.key,
type: 'inputs',
type: 'updateInput',
value: {
...item,
value: e
@@ -546,7 +567,7 @@ var SelectDatasetRender = React.memo(function SelectDatasetRender({ item, module
});
var SelectAppRender = React.memo(function SelectAppRender({ item, moduleId }: RenderProps) {
const { appId } = useFlowStore();
const { filterAppIds } = useFlowProviderStore();
const theme = useTheme();
const {
@@ -577,12 +598,12 @@ var SelectAppRender = React.memo(function SelectAppRender({ item, moduleId }: Re
{isOpenSelectApp && (
<SelectAppModal
defaultApps={item.value?.id ? [item.value.id] : []}
filterApps={[appId]}
filterAppIds={filterAppIds}
onClose={onCloseSelectApp}
onSuccess={(e) => {
onChangeNode({
moduleId,
type: 'inputs',
type: 'updateInput',
key: 'app',
value: {
...item,

View File

@@ -1,28 +1,32 @@
import React, { useMemo, useState } from 'react';
import type { FlowOutputItemType } from '@/types/core/app/flow';
import type { FlowNodeOutputItemType } from '@fastgpt/global/core/module/node/type';
import { Box, Flex } from '@chakra-ui/react';
import { FlowOutputItemTypeEnum } from '@/constants/flow';
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';
const SetOutputFieldModal = dynamic(() => import('../modules/SetOutputFieldModal'));
import { onChangeNode } from '../Provider';
import { onChangeNode } from '../../FlowProvider';
import { SystemOutputEnum } from '@/constants/app';
const Label = ({
import type { EditFieldType, EditFieldModeType } from '../modules/FieldEditModal';
const FieldEditModal = dynamic(() => import('../modules/FieldEditModal'));
export const Label = ({
moduleId,
outputKey,
outputs,
editFiledType = 'output',
...item
}: FlowOutputItemType & {
}: FlowNodeOutputItemType & {
outputKey: string;
moduleId: string;
outputs: FlowOutputItemType[];
outputs: FlowNodeOutputItemType[];
editFiledType?: EditFieldModeType;
}) => {
const { label, description, edit } = item;
const [editField, setEditField] = useState<FlowOutputItemType>();
const [editField, setEditField] = useState<EditFieldType>();
return (
<Flex
@@ -42,8 +46,10 @@ const Label = ({
_hover={{ color: 'myBlue.600' }}
onClick={() =>
setEditField({
...item,
key: outputKey
label: item.label,
valueType: item.valueType,
key: outputKey,
description: item.description
})
}
/>
@@ -57,9 +63,8 @@ const Label = ({
onClick={() => {
onChangeNode({
moduleId,
type: 'outputs',
key: '',
value: outputs.filter((output) => output.key !== outputKey)
type: 'delOutput',
key: outputKey
});
}}
/>
@@ -73,43 +78,29 @@ const Label = ({
<Box>{label}</Box>
{!!editField && (
<SetOutputFieldModal
<FieldEditModal
mode={editFiledType}
defaultField={editField}
onClose={() => setEditField(undefined)}
onSubmit={(data) => {
onSubmit={(e) => {
const data = {
...item,
...e
};
if (editField.key === data.key) {
onChangeNode({
moduleId,
type: 'outputs',
key: '',
value: outputs.map((output) => (output.key === outputKey ? data : output))
type: 'updateOutput',
key: data.key,
value: data
});
} else {
let index = 0;
const storeOutputs = outputs.filter((output, i) => {
if (output.key !== editField.key) {
return true;
}
index = i;
return false;
});
onChangeNode({
moduleId,
type: 'outputs',
key: '',
value: storeOutputs
type: 'replaceOutput',
key: editField.key,
value: data
});
setTimeout(() => {
storeOutputs.splice(index, 0, data);
console.log(index, storeOutputs);
onChangeNode({
moduleId,
type: 'outputs',
key: '',
value: [...storeOutputs]
});
}, 10);
}
setEditField(undefined);
@@ -122,10 +113,12 @@ const Label = ({
const RenderOutput = ({
moduleId,
flowOutputList
flowOutputList,
editFiledType
}: {
moduleId: string;
flowOutputList: FlowOutputItemType[];
flowOutputList: FlowNodeOutputItemType[];
editFiledType?: EditFieldModeType;
}) => {
const sortOutput = useMemo(
() =>
@@ -141,11 +134,17 @@ const RenderOutput = ({
<>
{sortOutput.map(
(item) =>
item.type !== FlowOutputItemTypeEnum.hidden && (
item.type !== FlowNodeOutputTypeEnum.hidden && (
<Box key={item.key} _notLast={{ mb: 7 }} position={'relative'}>
<Label moduleId={moduleId} outputKey={item.key} outputs={sortOutput} {...item} />
<Box mt={FlowOutputItemTypeEnum.answer ? 0 : 2} className={'nodrag'}>
{item.type === FlowOutputItemTypeEnum.source && (
<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>

View File

@@ -1,20 +1,26 @@
import React, { useMemo } from 'react';
import React, { useMemo, useTransition } from 'react';
import { Box, BoxProps } from '@chakra-ui/react';
import { Handle, Position } from 'reactflow';
import { FlowValueTypeEnum, FlowValueTypeStyle } from '@/constants/flow';
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?: `${FlowValueTypeEnum}`;
valueType?: `${FlowNodeValTypeEnum}`;
}
const SourceHandle = ({ handleKey, valueType, ...props }: Props) => {
const { t } = useTranslation();
const valType = valueType ?? FlowNodeValTypeEnum.any;
const valueStyle = useMemo(
() =>
valueType
? FlowValueTypeStyle[valueType]
: (FlowValueTypeStyle[FlowValueTypeEnum.any] as any),
: (FlowValueTypeStyle[FlowNodeValTypeEnum.any] as any),
[valueType]
);
@@ -26,7 +32,12 @@ const SourceHandle = ({ handleKey, valueType, ...props }: Props) => {
transform={'translate(50%,-50%)'}
{...props}
>
<MyTooltip label={`${valueType}类型`}>
<MyTooltip
label={t('app.module.type', {
type: t(FlowValueTypeTip[valType].label),
example: FlowValueTypeTip[valType].example
})}
>
<Handle
style={{
width: '12px',

View File

@@ -1,21 +1,26 @@
import React, { useMemo } from 'react';
import { Box, BoxProps } from '@chakra-ui/react';
import { Handle, OnConnect, Position } from 'reactflow';
import { FlowValueTypeEnum, FlowValueTypeStyle } from '@/constants/flow';
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?: `${FlowValueTypeEnum}`;
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[FlowValueTypeEnum.any] as any),
: (FlowValueTypeStyle[FlowNodeValTypeEnum.any] as any),
[valueType]
);
@@ -28,7 +33,12 @@ const TargetHandle = ({ handleKey, valueType, onConnect, ...props }: Props) => {
transform={'translate(50%,-50%)'}
{...props}
>
<MyTooltip label={`${valueType}类型`}>
<MyTooltip
label={t('app.module.type', {
type: t(FlowValueTypeTip[valType].label),
example: FlowValueTypeTip[valType].example
})}
>
<Handle
style={{
width: '12px',

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

View File

@@ -1,14 +1,14 @@
import { AppTypeEnum, SystemInputEnum } from '../app';
import { TaskResponseKeyEnum } from '../chat';
import {
FlowModuleTypeEnum,
FlowInputItemTypeEnum,
FlowOutputItemTypeEnum,
SpecialInputKeyEnum,
FlowValueTypeEnum
} from './index';
FlowNodeTypeEnum,
FlowNodeInputTypeEnum,
FlowNodeOutputTypeEnum,
FlowNodeSpecialInputKeyEnum,
FlowNodeValTypeEnum
} from '@fastgpt/global/core/module/node/constant';
import type { AppItemType } from '@/types/app';
import type { FlowModuleTemplateType } from '@/types/core/app/flow';
import type { FlowModuleTemplateType } from '@fastgpt/global/core/module/type';
import { chatModelList, cqModelList } from '@/web/common/system/staticData';
import {
Input_Template_History,
@@ -30,7 +30,8 @@ export const variableTip =
export const questionGuideTip = `对话结束后,会为生成 3 个引导性问题。`;
export const VariableModule: FlowModuleTemplateType = {
flowType: FlowModuleTypeEnum.variable,
id: FlowNodeTypeEnum.variable,
flowType: FlowNodeTypeEnum.variable,
logo: '/imgs/module/variable.png',
name: '全局变量',
intro: variableTip,
@@ -39,7 +40,7 @@ export const VariableModule: FlowModuleTemplateType = {
inputs: [
{
key: SystemInputEnum.variables,
type: FlowInputItemTypeEnum.systemInput,
type: FlowNodeInputTypeEnum.systemInput,
label: '变量输入',
value: []
}
@@ -47,39 +48,41 @@ export const VariableModule: FlowModuleTemplateType = {
outputs: []
};
export const UserGuideModule: FlowModuleTemplateType = {
flowType: FlowModuleTypeEnum.userGuide,
id: FlowNodeTypeEnum.userGuide,
flowType: FlowNodeTypeEnum.userGuide,
logo: '/imgs/module/userGuide.png',
name: '用户引导',
intro: userGuideTip,
inputs: [
{
key: SystemInputEnum.welcomeText,
type: FlowInputItemTypeEnum.hidden,
type: FlowNodeInputTypeEnum.hidden,
label: '开场白'
},
{
key: SystemInputEnum.variables,
type: FlowInputItemTypeEnum.hidden,
type: FlowNodeInputTypeEnum.hidden,
label: '对话框变量',
value: []
},
{
key: SystemInputEnum.questionGuide,
type: FlowInputItemTypeEnum.switch,
type: FlowNodeInputTypeEnum.switch,
label: '问题引导'
}
],
outputs: []
};
export const UserInputModule: FlowModuleTemplateType = {
flowType: FlowModuleTypeEnum.questionInput,
id: FlowNodeTypeEnum.questionInput,
flowType: FlowNodeTypeEnum.questionInput,
logo: '/imgs/module/userChatInput.png',
name: '用户问题(对话入口)',
intro: '用户输入的内容。该模块通常作为应用的入口,用户在发送消息后会首先执行该模块。',
inputs: [
{
key: SystemInputEnum.userChatInput,
type: FlowInputItemTypeEnum.systemInput,
type: FlowNodeInputTypeEnum.systemInput,
label: '用户问题'
}
],
@@ -87,21 +90,22 @@ export const UserInputModule: FlowModuleTemplateType = {
{
key: SystemInputEnum.userChatInput,
label: '用户问题',
type: FlowOutputItemTypeEnum.source,
valueType: FlowValueTypeEnum.string,
type: FlowNodeOutputTypeEnum.source,
valueType: FlowNodeValTypeEnum.string,
targets: []
}
]
};
export const HistoryModule: FlowModuleTemplateType = {
flowType: FlowModuleTypeEnum.historyNode,
id: FlowNodeTypeEnum.historyNode,
flowType: FlowNodeTypeEnum.historyNode,
logo: '/imgs/module/history.png',
name: '聊天记录',
intro: '用户输入的内容。该模块通常作为应用的入口,用户在发送消息后会首先执行该模块。',
inputs: [
{
key: 'maxContext',
type: FlowInputItemTypeEnum.numberInput,
type: FlowNodeInputTypeEnum.numberInput,
label: '最长记录数',
value: 6,
min: 0,
@@ -109,7 +113,7 @@ export const HistoryModule: FlowModuleTemplateType = {
},
{
key: SystemInputEnum.history,
type: FlowInputItemTypeEnum.hidden,
type: FlowNodeInputTypeEnum.hidden,
label: '聊天记录'
}
],
@@ -117,15 +121,15 @@ export const HistoryModule: FlowModuleTemplateType = {
{
key: SystemInputEnum.history,
label: '聊天记录',
valueType: FlowValueTypeEnum.chatHistory,
type: FlowOutputItemTypeEnum.source,
valueType: FlowNodeValTypeEnum.chatHistory,
type: FlowNodeOutputTypeEnum.source,
targets: []
}
]
};
export const ChatModule: FlowModuleTemplateType = {
flowType: FlowModuleTypeEnum.chatNode,
id: FlowNodeTypeEnum.chatNode,
flowType: FlowNodeTypeEnum.chatNode,
logo: '/imgs/module/AI.png',
name: 'AI 对话',
intro: 'AI 大模型对话',
@@ -134,7 +138,7 @@ export const ChatModule: FlowModuleTemplateType = {
Input_Template_TFSwitch,
{
key: 'model',
type: FlowInputItemTypeEnum.selectChatModel,
type: FlowNodeInputTypeEnum.selectChatModel,
label: '对话模型',
value: chatModelList?.[0]?.model,
customData: () => chatModelList,
@@ -143,7 +147,7 @@ export const ChatModule: FlowModuleTemplateType = {
},
{
key: 'temperature',
type: FlowInputItemTypeEnum.hidden,
type: FlowNodeInputTypeEnum.hidden,
label: '温度',
value: 0,
min: 0,
@@ -156,7 +160,7 @@ export const ChatModule: FlowModuleTemplateType = {
},
{
key: 'maxToken',
type: FlowInputItemTypeEnum.hidden,
type: FlowNodeInputTypeEnum.hidden,
label: '回复上限',
value: chatModelList?.[0] ? chatModelList[0].maxToken / 2 : 2000,
min: 100,
@@ -172,47 +176,47 @@ export const ChatModule: FlowModuleTemplateType = {
},
{
key: 'aiSettings',
type: FlowInputItemTypeEnum.aiSettings,
type: FlowNodeInputTypeEnum.aiSettings,
label: '',
connected: false
},
{
key: 'systemPrompt',
type: FlowInputItemTypeEnum.textarea,
type: FlowNodeInputTypeEnum.textarea,
label: '系统提示词',
max: 300,
valueType: FlowValueTypeEnum.string,
valueType: FlowNodeValTypeEnum.string,
description: ChatModelSystemTip,
placeholder: ChatModelSystemTip,
value: ''
},
{
key: SystemInputEnum.isResponseAnswerText,
type: FlowInputItemTypeEnum.hidden,
type: FlowNodeInputTypeEnum.hidden,
label: '返回AI内容',
valueType: FlowValueTypeEnum.boolean,
valueType: FlowNodeValTypeEnum.boolean,
value: true
},
{
key: 'quoteTemplate',
type: FlowInputItemTypeEnum.hidden,
type: FlowNodeInputTypeEnum.hidden,
label: '引用内容模板',
valueType: FlowValueTypeEnum.string,
valueType: FlowNodeValTypeEnum.string,
value: ''
},
{
key: 'quotePrompt',
type: FlowInputItemTypeEnum.hidden,
type: FlowNodeInputTypeEnum.hidden,
label: '引用内容提示词',
valueType: FlowValueTypeEnum.string,
valueType: FlowNodeValTypeEnum.string,
value: ''
},
{
key: 'quoteQA',
type: FlowInputItemTypeEnum.target,
type: FlowNodeInputTypeEnum.target,
label: '引用内容',
description: "对象数组格式,结构:\n [{q:'问题',a:'回答'}]",
valueType: FlowValueTypeEnum.kbQuote,
valueType: FlowNodeValTypeEnum.datasetQuote,
connected: false
},
Input_Template_History,
@@ -223,24 +227,24 @@ export const ChatModule: FlowModuleTemplateType = {
key: TaskResponseKeyEnum.history,
label: '新的上下文',
description: '将本次回复内容拼接上历史记录,作为新的上下文返回',
valueType: FlowValueTypeEnum.chatHistory,
type: FlowOutputItemTypeEnum.source,
valueType: FlowNodeValTypeEnum.chatHistory,
type: FlowNodeOutputTypeEnum.source,
targets: []
},
{
key: TaskResponseKeyEnum.answerText,
label: 'AI回复',
description: '将在 stream 回复完毕后触发',
valueType: FlowValueTypeEnum.string,
type: FlowOutputItemTypeEnum.source,
valueType: FlowNodeValTypeEnum.string,
type: FlowNodeOutputTypeEnum.source,
targets: []
},
Output_Template_Finish
]
};
export const KBSearchModule: FlowModuleTemplateType = {
flowType: FlowModuleTypeEnum.datasetSearchNode,
export const DatasetSearchModule: FlowModuleTemplateType = {
id: FlowNodeTypeEnum.datasetSearchNode,
flowType: FlowNodeTypeEnum.datasetSearchNode,
logo: '/imgs/module/db.png',
name: '知识库搜索',
intro: '去知识库中搜索对应的答案。可作为 AI 对话引用参考。',
@@ -249,7 +253,7 @@ export const KBSearchModule: FlowModuleTemplateType = {
Input_Template_TFSwitch,
{
key: 'datasets',
type: FlowInputItemTypeEnum.selectDataset,
type: FlowNodeInputTypeEnum.selectDataset,
label: '关联的知识库',
value: [],
list: [],
@@ -258,7 +262,7 @@ export const KBSearchModule: FlowModuleTemplateType = {
},
{
key: 'similarity',
type: FlowInputItemTypeEnum.slider,
type: FlowNodeInputTypeEnum.slider,
label: '相似度',
value: 0.4,
min: 0,
@@ -271,7 +275,7 @@ export const KBSearchModule: FlowModuleTemplateType = {
},
{
key: 'limit',
type: FlowInputItemTypeEnum.slider,
type: FlowNodeInputTypeEnum.slider,
label: '单次搜索上限',
description: '最多取 n 条记录作为本次问题引用',
value: 5,
@@ -289,15 +293,15 @@ export const KBSearchModule: FlowModuleTemplateType = {
{
key: 'isEmpty',
label: '搜索结果为空',
type: FlowOutputItemTypeEnum.source,
valueType: FlowValueTypeEnum.boolean,
type: FlowNodeOutputTypeEnum.source,
valueType: FlowNodeValTypeEnum.boolean,
targets: []
},
{
key: 'unEmpty',
label: '搜索结果不为空',
type: FlowOutputItemTypeEnum.source,
valueType: FlowValueTypeEnum.boolean,
type: FlowNodeOutputTypeEnum.source,
valueType: FlowNodeValTypeEnum.boolean,
targets: []
},
{
@@ -305,16 +309,16 @@ export const KBSearchModule: FlowModuleTemplateType = {
label: '引用内容',
description:
'始终返回数组,如果希望搜索结果为空时执行额外操作,需要用到上面的两个输入以及目标模块的触发器',
type: FlowOutputItemTypeEnum.source,
valueType: FlowValueTypeEnum.kbQuote,
type: FlowNodeOutputTypeEnum.source,
valueType: FlowNodeValTypeEnum.datasetQuote,
targets: []
},
Output_Template_Finish
]
};
export const AnswerModule: FlowModuleTemplateType = {
flowType: FlowModuleTypeEnum.answerNode,
id: FlowNodeTypeEnum.answerNode,
flowType: FlowNodeTypeEnum.answerNode,
logo: '/imgs/module/reply.png',
name: '指定回复',
intro: '该模块可以直接回复一段指定的内容。常用于引导、提示',
@@ -322,9 +326,9 @@ export const AnswerModule: FlowModuleTemplateType = {
inputs: [
Input_Template_TFSwitch,
{
key: SpecialInputKeyEnum.answerText,
type: FlowInputItemTypeEnum.textarea,
valueType: FlowValueTypeEnum.any,
key: FlowNodeSpecialInputKeyEnum.answerText,
type: FlowNodeInputTypeEnum.textarea,
valueType: FlowNodeValTypeEnum.any,
value: '',
label: '回复的内容',
description:
@@ -334,7 +338,8 @@ export const AnswerModule: FlowModuleTemplateType = {
outputs: [Output_Template_Finish]
};
export const ClassifyQuestionModule: FlowModuleTemplateType = {
flowType: FlowModuleTypeEnum.classifyQuestion,
id: FlowNodeTypeEnum.classifyQuestion,
flowType: FlowNodeTypeEnum.classifyQuestion,
logo: '/imgs/module/cq.png',
name: '问题分类',
intro: '可以判断用户问题属于哪方面问题,从而执行不同的操作。',
@@ -345,7 +350,7 @@ export const ClassifyQuestionModule: FlowModuleTemplateType = {
Input_Template_TFSwitch,
{
key: 'model',
type: FlowInputItemTypeEnum.selectChatModel,
type: FlowNodeInputTypeEnum.selectChatModel,
label: '分类模型',
value: cqModelList?.[0]?.model,
customData: () => cqModelList,
@@ -354,8 +359,8 @@ export const ClassifyQuestionModule: FlowModuleTemplateType = {
},
{
key: 'systemPrompt',
type: FlowInputItemTypeEnum.textarea,
valueType: FlowValueTypeEnum.string,
type: FlowNodeInputTypeEnum.textarea,
valueType: FlowNodeValTypeEnum.string,
value: '',
label: '背景知识',
description:
@@ -365,8 +370,8 @@ export const ClassifyQuestionModule: FlowModuleTemplateType = {
Input_Template_History,
Input_Template_UserChatInput,
{
key: SpecialInputKeyEnum.agents,
type: FlowInputItemTypeEnum.custom,
key: FlowNodeSpecialInputKeyEnum.agents,
type: FlowNodeInputTypeEnum.custom,
label: '',
value: [
{
@@ -388,25 +393,26 @@ export const ClassifyQuestionModule: FlowModuleTemplateType = {
{
key: 'fasw',
label: '',
type: FlowOutputItemTypeEnum.hidden,
type: FlowNodeOutputTypeEnum.hidden,
targets: []
},
{
key: 'fqsw',
label: '',
type: FlowOutputItemTypeEnum.hidden,
type: FlowNodeOutputTypeEnum.hidden,
targets: []
},
{
key: 'fesw',
label: '',
type: FlowOutputItemTypeEnum.hidden,
type: FlowNodeOutputTypeEnum.hidden,
targets: []
}
]
};
export const ContextExtractModule: FlowModuleTemplateType = {
flowType: FlowModuleTypeEnum.contentExtract,
id: FlowNodeTypeEnum.contentExtract,
flowType: FlowNodeTypeEnum.contentExtract,
logo: '/imgs/module/extract.png',
name: '文本内容提取',
intro: '从文本中提取出指定格式的数据',
@@ -416,8 +422,8 @@ export const ContextExtractModule: FlowModuleTemplateType = {
Input_Template_TFSwitch,
{
key: ContextExtractEnum.description,
type: FlowInputItemTypeEnum.textarea,
valueType: FlowValueTypeEnum.string,
type: FlowNodeInputTypeEnum.textarea,
valueType: FlowNodeValTypeEnum.string,
value: '',
label: '提取要求描述',
description: '写一段提取要求,告诉 AI 需要提取哪些内容',
@@ -427,14 +433,14 @@ export const ContextExtractModule: FlowModuleTemplateType = {
Input_Template_History,
{
key: ContextExtractEnum.content,
type: FlowInputItemTypeEnum.target,
type: FlowNodeInputTypeEnum.target,
label: '需要提取的文本',
required: true,
valueType: FlowValueTypeEnum.string
valueType: FlowNodeValTypeEnum.string
},
{
key: ContextExtractEnum.extractKeys,
type: FlowInputItemTypeEnum.custom,
type: FlowNodeInputTypeEnum.custom,
label: '目标字段',
description: "由 '描述' 和 'key' 组成一个目标字段,可提取多个目标字段",
value: []
@@ -444,29 +450,30 @@ export const ContextExtractModule: FlowModuleTemplateType = {
{
key: ContextExtractEnum.success,
label: '字段完全提取',
valueType: FlowValueTypeEnum.boolean,
type: FlowOutputItemTypeEnum.source,
valueType: FlowNodeValTypeEnum.boolean,
type: FlowNodeOutputTypeEnum.source,
targets: []
},
{
key: ContextExtractEnum.failed,
label: '提取字段缺失',
valueType: FlowValueTypeEnum.boolean,
type: FlowOutputItemTypeEnum.source,
valueType: FlowNodeValTypeEnum.boolean,
type: FlowNodeOutputTypeEnum.source,
targets: []
},
{
key: ContextExtractEnum.fields,
label: '完整提取结果',
description: '一个 JSON 字符串,例如:{"name:":"YY","Time":"2023/7/2 18:00"}',
valueType: FlowValueTypeEnum.string,
type: FlowOutputItemTypeEnum.source,
valueType: FlowNodeValTypeEnum.string,
type: FlowNodeOutputTypeEnum.source,
targets: []
}
]
};
export const HttpModule: FlowModuleTemplateType = {
flowType: FlowModuleTypeEnum.httpRequest,
id: FlowNodeTypeEnum.httpRequest,
flowType: FlowNodeTypeEnum.httpRequest,
logo: '/imgs/module/http.png',
name: 'HTTP模块',
intro: '可以发出一个 HTTP POST 请求,实现更为复杂的操作(联网搜索、数据库查询等)',
@@ -477,7 +484,7 @@ export const HttpModule: FlowModuleTemplateType = {
{
key: HttpPropsEnum.url,
value: '',
type: FlowInputItemTypeEnum.input,
type: FlowNodeInputTypeEnum.input,
label: '请求地址',
description: '请求目标地址',
placeholder: 'https://api.fastgpt.run/getInventory',
@@ -488,7 +495,8 @@ export const HttpModule: FlowModuleTemplateType = {
outputs: [Output_Template_Finish]
};
export const EmptyModule: FlowModuleTemplateType = {
flowType: FlowModuleTypeEnum.empty,
id: FlowNodeTypeEnum.empty,
flowType: FlowNodeTypeEnum.empty,
logo: '/imgs/module/cq.png',
name: '该模块已被移除',
intro: '',
@@ -496,10 +504,11 @@ export const EmptyModule: FlowModuleTemplateType = {
inputs: [],
outputs: []
};
export const AppModule: FlowModuleTemplateType = {
flowType: FlowModuleTypeEnum.app,
export const RunAppModule: FlowModuleTemplateType = {
id: FlowNodeTypeEnum.runApp,
flowType: FlowNodeTypeEnum.runApp,
logo: '/imgs/module/app.png',
name: '应用调用(测试版)',
name: '应用调用',
intro: '可以选择一个其他应用进行调用',
description: '可以选择一个其他应用进行调用',
showStatus: true,
@@ -507,7 +516,7 @@ export const AppModule: FlowModuleTemplateType = {
Input_Template_TFSwitch,
{
key: 'app',
type: FlowInputItemTypeEnum.selectApp,
type: FlowNodeInputTypeEnum.selectApp,
label: '选择一个应用',
description: '选择一个其他应用进行调用',
required: true
@@ -520,38 +529,87 @@ export const AppModule: FlowModuleTemplateType = {
key: TaskResponseKeyEnum.history,
label: '新的上下文',
description: '将该应用回复内容拼接到历史记录中,作为新的上下文返回',
valueType: FlowValueTypeEnum.chatHistory,
type: FlowOutputItemTypeEnum.source,
valueType: FlowNodeValTypeEnum.chatHistory,
type: FlowNodeOutputTypeEnum.source,
targets: []
},
{
key: TaskResponseKeyEnum.answerText,
label: 'AI回复',
description: '将在应用完全结束后触发',
valueType: FlowValueTypeEnum.string,
type: FlowOutputItemTypeEnum.source,
valueType: FlowNodeValTypeEnum.string,
type: FlowNodeOutputTypeEnum.source,
targets: []
},
Output_Template_Finish
]
};
export const PluginInputModule: FlowModuleTemplateType = {
id: FlowNodeTypeEnum.pluginInput,
flowType: FlowNodeTypeEnum.pluginInput,
logo: '/imgs/module/input.png',
name: '定义插件输入',
intro: '自定义配置外部输入,使用插件时,仅暴露自定义配置的输入',
description: '自定义配置外部输入,使用插件时,仅暴露自定义配置的输入',
showStatus: false,
inputs: [],
outputs: []
};
export const PluginOutputModule: FlowModuleTemplateType = {
id: FlowNodeTypeEnum.pluginOutput,
flowType: FlowNodeTypeEnum.pluginOutput,
logo: '/imgs/module/output.png',
name: '定义插件输出',
intro: '自定义配置外部输出,使用插件时,仅暴露自定义配置的输出',
description: '自定义配置外部输出,使用插件时,仅暴露自定义配置的输出',
showStatus: false,
inputs: [],
outputs: []
};
export const PluginModule: FlowModuleTemplateType = {
id: FlowNodeTypeEnum.pluginModule,
flowType: FlowNodeTypeEnum.pluginModule,
logo: '/imgs/module/custom.png',
name: '自定义模块',
showStatus: false,
inputs: [],
outputs: []
};
export const ModuleTemplates = [
{
label: '输入模块',
list: [UserInputModule, HistoryModule]
},
export const SystemModuleTemplates = [
{
label: '引导模块',
list: [UserGuideModule]
},
{
label: '输入模块',
list: [UserInputModule, HistoryModule]
},
{
label: '内容生成',
list: [ChatModule, AnswerModule]
},
{
label: '核心调用',
list: [KBSearchModule, AppModule]
list: [DatasetSearchModule, RunAppModule]
},
{
label: '函数模块',
list: [ClassifyQuestionModule, ContextExtractModule, HttpModule]
}
];
export const PluginModuleTemplates = [
{
label: '输入输出',
list: [PluginInputModule, PluginOutputModule, HistoryModule]
},
{
label: '内容生成',
list: [ChatModule, AnswerModule]
},
{
label: '核心调用',
list: [DatasetSearchModule, RunAppModule]
},
{
label: '函数模块',
@@ -564,13 +622,16 @@ export const ModuleTemplatesFlat = [
UserInputModule,
HistoryModule,
ChatModule,
KBSearchModule,
DatasetSearchModule,
AnswerModule,
ClassifyQuestionModule,
ContextExtractModule,
HttpModule,
EmptyModule,
AppModule
RunAppModule,
PluginInputModule,
PluginOutputModule,
PluginModule
];
// template
@@ -665,7 +726,7 @@ export const appTemplates: (AppItemType & {
{
key: 'history',
label: '聊天记录',
valueType: 'chat_history',
valueType: 'chatHistory',
type: 'source',
targets: [
{
@@ -757,14 +818,14 @@ export const appTemplates: (AppItemType & {
key: 'quoteQA',
type: 'target',
label: '引用内容',
valueType: 'kb_quote',
valueType: 'datasetQuote',
connected: false
},
{
key: 'history',
type: 'target',
label: '聊天记录',
valueType: 'chat_history',
valueType: 'chatHistory',
connected: true
},
{
@@ -886,7 +947,7 @@ export const appTemplates: (AppItemType & {
{
key: 'history',
label: '聊天记录',
valueType: 'chat_history',
valueType: 'chatHistory',
type: 'source',
targets: [
{
@@ -1003,7 +1064,7 @@ export const appTemplates: (AppItemType & {
description:
'始终返回数组,如果希望搜索结果为空时执行额外操作,需要用到上面的两个输入以及目标模块的触发器',
type: 'source',
valueType: 'kb_quote',
valueType: 'datasetQuote',
targets: [
{
moduleId: 'chatModule',
@@ -1094,14 +1155,14 @@ export const appTemplates: (AppItemType & {
key: 'quoteQA',
type: 'target',
label: '引用内容',
valueType: 'kb_quote',
valueType: 'datasetQuote',
connected: true
},
{
key: 'history',
type: 'target',
label: '聊天记录',
valueType: 'chat_history',
valueType: 'chatHistory',
connected: true
},
{
@@ -1292,7 +1353,7 @@ export const appTemplates: (AppItemType & {
{
key: 'history',
label: '聊天记录',
valueType: 'chat_history',
valueType: 'chatHistory',
type: 'source',
targets: [
{
@@ -1401,14 +1462,14 @@ export const appTemplates: (AppItemType & {
type: 'custom',
label: '引用内容',
description: "对象数组格式,结构:\n [{q:'问题',a:'回答'}]",
valueType: 'kb_quote',
valueType: 'datasetQuote',
connected: false
},
{
key: 'history',
type: 'target',
label: '聊天记录',
valueType: 'chat_history',
valueType: 'chatHistory',
connected: true
},
{
@@ -1441,7 +1502,7 @@ export const appTemplates: (AppItemType & {
key: 'history',
label: '新的上下文',
description: '将本次回复内容拼接上历史记录,作为新的上下文返回',
valueType: 'chat_history',
valueType: 'chatHistory',
type: 'source',
targets: []
}
@@ -1520,7 +1581,7 @@ export const appTemplates: (AppItemType & {
{
key: 'history',
label: '聊天记录',
valueType: 'chat_history',
valueType: 'chatHistory',
type: 'source',
targets: [
{
@@ -1564,7 +1625,7 @@ export const appTemplates: (AppItemType & {
key: 'history',
type: 'target',
label: '聊天记录',
valueType: 'chat_history',
valueType: 'chatHistory',
connected: true
},
{
@@ -1863,14 +1924,14 @@ export const appTemplates: (AppItemType & {
type: 'custom',
label: '引用内容',
description: "对象数组格式,结构:\n [{q:'问题',a:'回答'}]",
valueType: 'kb_quote',
valueType: 'datasetQuote',
connected: true
},
{
key: 'history',
type: 'target',
label: '聊天记录',
valueType: 'chat_history',
valueType: 'chatHistory',
connected: true
},
{
@@ -1903,7 +1964,7 @@ export const appTemplates: (AppItemType & {
key: 'history',
label: '新的上下文',
description: '将本次回复内容拼接上历史记录,作为新的上下文返回',
valueType: 'chat_history',
valueType: 'chatHistory',
type: 'source',
targets: []
}
@@ -1938,7 +1999,7 @@ export const appTemplates: (AppItemType & {
{
key: 'history',
label: '聊天记录',
valueType: 'chat_history',
valueType: 'chatHistory',
type: 'source',
targets: [
{
@@ -2055,7 +2116,7 @@ export const appTemplates: (AppItemType & {
description:
'始终返回数组,如果希望搜索结果为空时执行额外操作,需要用到上面的两个输入以及目标模块的触发器',
type: 'source',
valueType: 'kb_quote',
valueType: 'datasetQuote',
targets: [
{
moduleId: 'nlfwkc',

View File

@@ -1,86 +1,68 @@
import type { BoxProps } from '@chakra-ui/react';
import { FlowNodeTypeEnum, FlowNodeValTypeEnum } from '@fastgpt/global/core/module/node/constant';
export enum FlowInputItemTypeEnum {
systemInput = 'systemInput', // history, userChatInput, variableInput
input = 'input', // one line input
textarea = 'textarea',
numberInput = 'numberInput',
select = 'select',
slider = 'slider',
custom = 'custom',
target = 'target', // data input
switch = 'switch',
chatInput = 'chatInput',
selectApp = 'selectApp',
// chat special input
aiSettings = 'aiSettings',
maxToken = 'maxToken',
selectChatModel = 'selectChatModel',
// dataset special input
selectDataset = 'selectDataset',
hidden = 'hidden'
}
export enum FlowOutputItemTypeEnum {
answer = 'answer',
source = 'source',
hidden = 'hidden'
}
export enum FlowModuleTypeEnum {
empty = 'empty',
variable = 'variable',
userGuide = 'userGuide',
questionInput = 'questionInput',
historyNode = 'historyNode',
chatNode = 'chatNode',
datasetSearchNode = 'datasetSearchNode',
tfSwitchNode = 'tfSwitchNode',
answerNode = 'answerNode',
classifyQuestion = 'classifyQuestion',
contentExtract = 'contentExtract',
httpRequest = 'httpRequest',
app = 'app'
}
export enum SpecialInputKeyEnum {
'answerText' = 'text',
'agents' = 'agents' // cq agent key
}
export enum FlowValueTypeEnum {
'string' = 'string',
'number' = 'number',
'boolean' = 'boolean',
'chatHistory' = 'chat_history',
'kbQuote' = 'kb_quote',
'any' = 'any'
}
export const FlowValueTypeStyle: Record<`${FlowValueTypeEnum}`, BoxProps> = {
[FlowValueTypeEnum.string]: {
export const FlowValueTypeStyle: Record<`${FlowNodeValTypeEnum}`, BoxProps> = {
[FlowNodeValTypeEnum.string]: {
background: '#36ADEF'
},
[FlowValueTypeEnum.number]: {
[FlowNodeValTypeEnum.number]: {
background: '#FB7C3C'
},
[FlowValueTypeEnum.boolean]: {
[FlowNodeValTypeEnum.boolean]: {
background: '#E7D118'
},
[FlowValueTypeEnum.chatHistory]: {
[FlowNodeValTypeEnum.chatHistory]: {
background: '#00A9A6'
},
[FlowValueTypeEnum.kbQuote]: {
[FlowNodeValTypeEnum.datasetQuote]: {
background: '#A558C9'
},
[FlowValueTypeEnum.any]: {
[FlowNodeValTypeEnum.any]: {
background: '#9CA2A8'
}
};
export const FlowValueTypeTip = {
[FlowNodeValTypeEnum.string]: {
label: 'app.module.valueType.string',
example: ''
},
[FlowNodeValTypeEnum.number]: {
label: 'app.module.valueType.number',
example: ''
},
[FlowNodeValTypeEnum.boolean]: {
label: 'app.module.valueType.boolean',
example: ''
},
[FlowNodeValTypeEnum.chatHistory]: {
label: 'app.module.valueType.chatHistory',
example: `{
obj: System | Human | AI;
value: string;
}`
},
[FlowNodeValTypeEnum.datasetQuote]: {
label: 'app.module.valueType.datasetQuote',
example: `{
id: string;
datasetId: string;
collectionId: string;
sourceName: string;
sourceId?: string;
q: string;
a: string
}`
},
[FlowNodeValTypeEnum.any]: {
label: 'app.module.valueType.any',
example: ''
}
};
export const initModuleType: Record<string, boolean> = {
[FlowModuleTypeEnum.historyNode]: true,
[FlowModuleTypeEnum.questionInput]: true
[FlowNodeTypeEnum.historyNode]: true,
[FlowNodeTypeEnum.questionInput]: true,
[FlowNodeTypeEnum.pluginInput]: true
};
export const edgeOptions = {

View File

@@ -1,25 +1,28 @@
import type { FlowInputItemType } from '@/types/core/app/flow';
import type { FlowNodeInputItemType } from '@fastgpt/global/core/module/node/type.d';
import { SystemInputEnum } from '../app';
import { FlowInputItemTypeEnum, FlowValueTypeEnum } from './index';
import {
FlowNodeInputTypeEnum,
FlowNodeValTypeEnum
} from '@fastgpt/global/core/module/node/constant';
export const Input_Template_TFSwitch: FlowInputItemType = {
export const Input_Template_TFSwitch: FlowNodeInputItemType = {
key: SystemInputEnum.switch,
type: FlowInputItemTypeEnum.target,
type: FlowNodeInputTypeEnum.target,
label: '触发器',
valueType: FlowValueTypeEnum.any
valueType: FlowNodeValTypeEnum.any
};
export const Input_Template_History: FlowInputItemType = {
export const Input_Template_History: FlowNodeInputItemType = {
key: SystemInputEnum.history,
type: FlowInputItemTypeEnum.target,
type: FlowNodeInputTypeEnum.target,
label: '聊天记录',
valueType: FlowValueTypeEnum.chatHistory
valueType: FlowNodeValTypeEnum.chatHistory
};
export const Input_Template_UserChatInput: FlowInputItemType = {
export const Input_Template_UserChatInput: FlowNodeInputItemType = {
key: SystemInputEnum.userChatInput,
type: FlowInputItemTypeEnum.target,
type: FlowNodeInputTypeEnum.target,
label: '用户问题',
required: true,
valueType: FlowValueTypeEnum.string
valueType: FlowNodeValTypeEnum.string
};

View File

@@ -1,12 +1,15 @@
import type { FlowOutputItemType } from '@/types/core/app/flow';
import type { FlowNodeOutputItemType } from '@fastgpt/global/core/module/node/type';
import { SystemOutputEnum } from '../app';
import { FlowOutputItemTypeEnum, FlowValueTypeEnum } from './index';
import {
FlowNodeOutputTypeEnum,
FlowNodeValTypeEnum
} from '@fastgpt/global/core/module/node/constant';
export const Output_Template_Finish: FlowOutputItemType = {
export const Output_Template_Finish: FlowNodeOutputItemType = {
key: SystemOutputEnum.finish,
label: '模块调用结束',
description: '模块调用结束时触发',
valueType: FlowValueTypeEnum.boolean,
type: FlowOutputItemTypeEnum.source,
valueType: FlowNodeValTypeEnum.boolean,
type: FlowNodeOutputTypeEnum.source,
targets: []
};

View File

@@ -25,18 +25,3 @@ export enum PromotionEnum {
register = 'register',
pay = 'pay'
}
export enum InformTypeEnum {
system = 'system'
}
export const InformTypeMap = {
[InformTypeEnum.system]: {
label: '系统通知'
}
};
export enum MyModelsTypeEnum {
my = 'my',
collection = 'collection'
}

View File

@@ -7,9 +7,9 @@ import { countPromptTokens } from '@/global/common/tiktoken';
export function replaceVariable(text: string, obj: Record<string, string | number>) {
for (const key in obj) {
const val = obj[key];
if (typeof val !== 'string') continue;
if (!['string', 'number'].includes(typeof val)) continue;
text = text.replace(new RegExp(`{{(${key})}}`, 'g'), val);
text = text.replace(new RegExp(`{{(${key})}}`, 'g'), String(val));
}
return text || '';
}

View File

@@ -1,12 +1,13 @@
import type { AppSchema } from '@/types/mongoSchema';
import type { ChatItemType } from '@/types/chat';
import { AppModuleItemType, VariableItemType } from '@/types/app';
import { VariableItemType } from '@/types/app';
import type { ModuleItemType } from '@fastgpt/global/core/module/type';
export type InitChatResponse = {
chatId: string;
appId: string;
app: {
userGuideModule?: AppModuleItemType;
userGuideModule?: ModuleItemType;
chatModels?: string[];
name: string;
avatar: string;

View File

@@ -1,11 +1,12 @@
import { SystemInputEnum } from '@/constants/app';
import { FlowModuleTypeEnum } from '@/constants/flow';
import { AppModuleItemType, VariableItemType } from '@/types/app';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { VariableItemType } from '@/types/app';
import type { ModuleItemType } from '@fastgpt/global/core/module/type';
export const getGuideModule = (modules: AppModuleItemType[]) =>
modules.find((item) => item.flowType === FlowModuleTypeEnum.userGuide);
export const getGuideModule = (modules: ModuleItemType[]) =>
modules.find((item) => item.flowType === FlowNodeTypeEnum.userGuide);
export const splitGuideModule = (guideModules?: AppModuleItemType) => {
export const splitGuideModule = (guideModules?: ModuleItemType) => {
const welcomeText: string =
guideModules?.inputs?.find((item) => item.key === SystemInputEnum.welcomeText)?.value || '';

View File

@@ -3,41 +3,81 @@ import { PromptTemplateItem } from '@fastgpt/global/core/ai/type.d';
export const Prompt_QuoteTemplateList: PromptTemplateItem[] = [
{
title: '标准模板',
desc: '包含 q 和 a 两个变量的标准模板',
desc: '标准提示词,用于结构不固定的知识库。',
value: `{{q}}\n{{a}}`
},
{
title: '问答模板',
desc: '适合 QA 问答结构的知识库,或大部分核心介绍位于 a 的知识库。',
value: `{instruction:"{{q}}",output:"{{a}}"}`
},
{
title: '全部变量',
desc: '包含 q 和 a 两个变量的标准模板',
value: `{instruction:"{{q}}",output:"{{a}}",source:"{{source}}",sourceId:"{{sourceId}}",index:"{{index}}"}`
title: '标准严格模板',
desc: '在标准模板基础上,对模型的回答做更严格的要求。',
value: `{{q}}\n{{a}}`
},
{
title: '严格问答模板',
desc: '在问答模板基础上,对模型的回答做更严格的要求。',
value: `{question:"{{q}}",answer:"{{a}}"}`
}
];
export const Prompt_QuotePromptList: PromptTemplateItem[] = [
{
title: '标准模',
title: '标准模',
desc: '',
value: `你的背景知识:
"""
{{quote}}
"""
对话要求:
1. 背景知识是最新的,其中 instruction 是相关介绍output 是预期回答或补充
2. 使用背景知识回答问题
3. 使用对话的风格回答我的问题,答案要和背景知识表述一致
1. 背景知识是最新的实时的信息,使用背景知识回答问题
2. 优先使用背景知识的内容回答我的问题,答案应与背景知识严格一致
3. 背景知识无法回答我的问题时,可以忽略背景知识,根据你的知识来自由回答
4. 使用对话的风格,自然的回答问题。
我的问题是:"{{question}}"`
},
{
title: '严格模式',
title: '问答模板',
desc: '',
value: `你的背景知识:
"""
{{quote}}
"""
对话要求:
1. 背景知识是最新的,其中 instruction 是相关介绍output 是预期回答或补充。
2. 使用背景知识回答问题
3. 背景知识无法满足问题时你需要回答我不清楚关于xxx的内容
1. 背景知识是最新的实时的信息,使用背景知识回答问题,其中 instruction 是相关介绍output 是预期回答或补充。
2. 优先使用背景知识的内容回答我的问题,答案应与背景知识严格一致
3. 背景知识无法回答我的问题时,可以忽略背景知识,根据你的知识来自由回答
4. 使用对话的风格,自然的回答问题。
我的问题是:"{{question}}"`
},
{
title: '标准严格模板',
desc: '',
value: `你的背景知识:
"""
{{quote}}
"""
对话要求:
1. 背景知识是最新的实时的信息,是你的唯一信息来源,使用背景知识回答问题。
2. 优先使用背景知识回答我的问题,答案与背景知识完全一致,无需做其他回答。
3. 背景知识与问题无关或背景知识无法回答本次问题时则拒绝回答本次问题“我不太清除xxx”。
4. 使用对话的风格,自然的回答问题。
我的问题是:"{{question}}"`
},
{
title: '严格问答模板',
desc: '',
value: `你的背景知识:
"""
{{quote}}
"""
对话要求:
1. 背景知识是最新的实时的信息,是你的唯一信息来源,使用背景知识回答问题。
2. 在背景知识的 JSON 中question 是相关问题answer 是已知答案。
3. 选择 answer 中的内容作为答案,要求答案与 answer 完全一致,无需做其他回答。
4. answer 中的答案无法满足问题直接回复“我不太清除xxx”。
我的问题是:"{{question}}"`
}
];

View File

@@ -1,5 +1,5 @@
import type { UserType } from '@/types/user';
import type { PromotionRecordSchema } from '@/types/mongoSchema';
import type { PromotionRecordSchema } from '@fastgpt/global/support/activity/type.d';
export interface ResLogin {
user: UserType;
token: string;

View File

@@ -33,6 +33,7 @@ const BillTable = () => {
to: new Date()
});
const { isPc } = useSystemStore();
const [billDetail, setBillDetail] = useState<UserBillType>();
const {
data: bills,
@@ -48,8 +49,6 @@ const BillTable = () => {
}
});
const [billDetail, setBillDetail] = useState<UserBillType>();
return (
<Flex flexDirection={'column'} py={[0, 5]} h={'100%'} position={'relative'}>
<TableContainer px={[3, 8]} position={'relative'} flex={'1 0 0'} h={0} overflowY={'auto'}>
@@ -106,4 +105,4 @@ const BillTable = () => {
);
};
export default BillTable;
export default React.memo(BillTable);

View File

@@ -271,7 +271,7 @@ const UserInfo = () => {
>
<Avatar src={'/imgs/openai.png'} w={'18px'} />
<Box ml={2} flex={1}>
OpenAI
OpenAI/OneAPI
</Box>
<Box
w={'9px'}

View File

@@ -3,7 +3,7 @@ import { Box, Flex, useTheme } from '@chakra-ui/react';
import { getInforms, readInform } from '@/web/support/user/api';
import { usePagination } from '@/web/common/hooks/usePagination';
import { useLoading } from '@/web/common/hooks/useLoading';
import type { informSchema } from '@/types/mongoSchema';
import type { UserInformSchema } from '@fastgpt/global/support/user/type';
import { formatTimeToChatTime } from '@/utils/tools';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import MyIcon from '@/components/Icon';
@@ -20,7 +20,7 @@ const BillTable = () => {
Pagination,
getData,
pageNum
} = usePagination<informSchema>({
} = usePagination<UserInformSchema>({
api: getInforms,
pageSize: isPc ? 20 : 10
});

View File

@@ -32,8 +32,8 @@ const OpenAIAccountModal = ({
<MyModal isOpen onClose={onClose} title={t('user.OpenAI Account Setting')}>
<ModalBody>
<Box fontSize={'sm'} color={'myGray.500'}>
OpenAI key OneAPI 线使 OpenAI
Chat Key 访
OpenAI/OneAPI 线使 OpenAI Chat
Key 访
</Box>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 65px'}>API Key:</Box>

View File

@@ -12,7 +12,7 @@ import {
Box
} from '@chakra-ui/react';
import { getPayOrders, checkPayResult } from '@/web/common/bill/api';
import { PaySchema } from '@/types/mongoSchema';
import type { PaySchema } from '@fastgpt/global/support/wallet/type.d';
import dayjs from 'dayjs';
import { useQuery } from '@tanstack/react-query';
import { formatPrice } from '@fastgpt/global/common/bill/tools';

View File

@@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { authUser } from '@fastgpt/service/support/user/auth';
import { connectToDatabase, App } from '@/service/mongo';
import { FlowInputItemTypeEnum, FlowModuleTypeEnum } from '@/constants/flow';
import { FlowNodeInputTypeEnum, FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { SystemInputEnum } from '@/constants/app';
const limit = 300;
@@ -46,19 +46,19 @@ async function initVariable(): Promise<any> {
const modules = jsonAPP.modules;
// 找到 variable
const variable = modules.find((item) => item.flowType === FlowModuleTypeEnum.variable);
const variable = modules.find((item) => item.flowType === FlowNodeTypeEnum.variable);
if (!variable) return await app.save();
// 找到 guide 模块
const userGuideModule = modules.find(
(item) => item.flowType === FlowModuleTypeEnum.userGuide
(item) => item.flowType === FlowNodeTypeEnum.userGuide
);
if (userGuideModule) {
userGuideModule.inputs = [
userGuideModule.inputs[0],
{
key: SystemInputEnum.variables,
type: FlowInputItemTypeEnum.systemInput,
type: FlowNodeInputTypeEnum.systemInput,
label: '对话框变量',
value: variable.inputs[0]?.value
}
@@ -66,7 +66,7 @@ async function initVariable(): Promise<any> {
} else {
modules.unshift({
moduleId: 'userGuide',
flowType: FlowModuleTypeEnum.userGuide,
flowType: FlowNodeTypeEnum.userGuide,
name: '用户引导',
position: {
x: 447.98520778293346,
@@ -75,12 +75,12 @@ async function initVariable(): Promise<any> {
inputs: [
{
key: SystemInputEnum.welcomeText,
type: FlowInputItemTypeEnum.input,
type: FlowNodeInputTypeEnum.input,
label: '开场白'
},
{
key: SystemInputEnum.variables,
type: FlowInputItemTypeEnum.systemInput,
type: FlowNodeInputTypeEnum.systemInput,
label: '对话框变量',
value: variable.inputs[0]?.value
}
@@ -90,7 +90,7 @@ async function initVariable(): Promise<any> {
}
jsonAPP.modules = jsonAPP.modules.filter(
(item) => item.flowType !== FlowModuleTypeEnum.variable
(item) => item.flowType !== FlowNodeTypeEnum.variable
);
app.modules = JSON.parse(JSON.stringify(jsonAPP.modules));

View File

@@ -4,7 +4,7 @@ import { App, connectToDatabase } from '@/service/mongo';
import { PgClient } from '@/service/pg';
import { connectionMongo } from '@fastgpt/service/common/mongo';
import { PgDatasetTableName } from '@/constants/plugin';
import { FlowModuleTypeEnum } from '@/constants/flow';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { delay } from '@/utils/tools';
import { MongoDatasetCollection } from '@fastgpt/service/core/dataset/collection/schema';
import { DatasetCollectionTypeEnum } from '@fastgpt/global/core/dataset/constant';
@@ -98,7 +98,7 @@ async function initMongo(limit: number) {
let success = 0;
async function initApp(limit = 100): Promise<any> {
// 遍历所有 app更新 app modules 里的 FlowModuleTypeEnum.kbSearchNode
// 遍历所有 app更新 app modules 里的 FlowNodeTypeEnum.kbSearchNode
const apps = await App.find({ inited: false }).limit(limit);
if (apps.length === 0) return;
@@ -113,7 +113,7 @@ async function initMongo(limit: number) {
modules.forEach((module) => {
// @ts-ignore
if (module.flowType === 'kbSearchNode') {
module.flowType = FlowModuleTypeEnum.datasetSearchNode;
module.flowType = FlowNodeTypeEnum.datasetSearchNode;
module.inputs.forEach((input) => {
if (input.key === 'kbList') {
input.key = 'datasets';

View File

@@ -1,6 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { Chat, App, connectToDatabase, Collection } from '@/service/mongo';
import { Chat, App, connectToDatabase } from '@/service/mongo';
import { MongoOutLink } from '@fastgpt/service/support/outLink/schema';
import { authUser } from '@fastgpt/service/support/user/auth';
import { authApp } from '@/service/utils/auth';
@@ -29,11 +29,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
appId
});
// 删除收藏列表
await Collection.deleteMany({
modelId: appId
});
// 删除分享链接
await MongoOutLink.deleteMany({
appId

View File

@@ -1,43 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Collection, App } from '@/service/mongo';
import { authUser } from '@fastgpt/service/support/user/auth';
/* 模型收藏切换 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { appId } = req.query as { appId: string };
if (!appId) {
throw new Error('缺少参数');
}
// 凭证校验
const { userId } = await authUser({ req, authToken: true });
const collectionRecord = await Collection.findOne({
userId,
modelId: appId
});
if (collectionRecord) {
await Collection.findByIdAndRemove(collectionRecord._id);
} else {
await Collection.create({
userId,
modelId: appId
});
}
await App.findByIdAndUpdate(appId, {
'share.collection': await Collection.countDocuments({ modelId: appId })
});
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -1,105 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, App } from '@/service/mongo';
import type { PagingData } from '@/types';
import type { ShareAppItem } from '@/types/app';
import { authUser } from '@fastgpt/service/support/user/auth';
import { Types } from '@fastgpt/service/common/mongo';
/* 获取模型列表 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const {
searchText = '',
pageNum = 1,
pageSize = 20
} = req.body as { searchText: string; pageNum: number; pageSize: number };
const { userId } = await authUser({ req, authToken: true });
const regex = new RegExp(searchText, 'i');
const where = {
$and: [
{ 'share.isShare': true },
{
$or: [{ name: { $regex: regex } }, { intro: { $regex: regex } }]
}
]
};
const pipeline = [
{
$match: where
},
{
$lookup: {
from: 'collections',
let: { modelId: '$_id' },
pipeline: [
{
$match: {
$expr: {
$and: [
{ $eq: ['$modelId', '$$modelId'] },
{
$eq: ['$userId', userId ? new Types.ObjectId(userId) : new Types.ObjectId()]
}
]
}
}
}
],
as: 'collections'
}
},
{
$project: {
_id: 1,
avatar: { $ifNull: ['$avatar', '/icon/logo.svg'] },
name: 1,
userId: 1,
intro: 1,
share: 1,
isCollection: {
$cond: {
if: { $gt: [{ $size: '$collections' }, 0] },
then: true,
else: false
}
}
}
},
{
$sort: { 'share.topNum': -1, 'share.collection': -1 }
},
{
$skip: (pageNum - 1) * pageSize
},
{
$limit: pageSize
}
];
// 获取被分享的模型
const [models, total] = await Promise.all([
// @ts-ignore
App.aggregate(pipeline),
App.countDocuments(where)
]);
jsonRes<PagingData<ShareAppItem>>(res, {
data: {
pageNum,
pageSize,
data: models,
total
}
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -4,7 +4,7 @@ import { authUser } from '@fastgpt/service/support/user/auth';
import { sseErrRes } from '@/service/response';
import { sseResponseEventEnum } from '@/constants/chat';
import { responseWrite } from '@fastgpt/service/common/response';
import { AppModuleItemType } from '@/types/app';
import type { ModuleItemType } from '@fastgpt/global/core/module/type.d';
import { dispatchModules } from '@/pages/api/v1/chat/completions';
import { pushChatBill } from '@/service/common/bill/push';
import { BillSourceEnum } from '@/constants/user';
@@ -13,7 +13,7 @@ import { ChatItemType } from '@/types/chat';
export type Props = {
history: ChatItemType[];
prompt: string;
modules: AppModuleItemType[];
modules: ModuleItemType[];
variables: Record<string, any>;
appId: string;
appName: string;

View File

@@ -0,0 +1,24 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@fastgpt/service/support/user/auth';
import { createOnePlugin } from '@fastgpt/service/core/plugin/controller';
import type { CreateOnePluginParams } from '@fastgpt/global/core/plugin/controller';
import { defaultModules } from '@fastgpt/global/core/plugin/constants';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { userId } = await authUser({ req, authToken: true });
const body = req.body as CreateOnePluginParams;
jsonRes(res, {
data: await createOnePlugin({ userId, modules: defaultModules, ...body })
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -0,0 +1,22 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@fastgpt/service/support/user/auth';
import { deleteOnePlugin } from '@fastgpt/service/core/plugin/controller';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { id } = req.query as { id: string };
await connectToDatabase();
const { userId } = await authUser({ req, authToken: true });
jsonRes(res, {
data: await deleteOnePlugin({ id, userId })
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -0,0 +1,22 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@fastgpt/service/support/user/auth';
import { getOnePluginDetail } from '@fastgpt/service/core/plugin/controller';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { id } = req.query as { id: string };
await connectToDatabase();
const { userId } = await authUser({ req, authToken: true });
jsonRes(res, {
data: await getOnePluginDetail({ id, userId })
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -0,0 +1,21 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@fastgpt/service/support/user/auth';
import { getUserPlugins } from '@fastgpt/service/core/plugin/controller';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { userId } = await authUser({ req, authToken: true });
jsonRes(res, {
data: await getUserPlugins({ userId })
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -0,0 +1,22 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@fastgpt/service/support/user/auth';
import { getPluginModuleDetail } from '@fastgpt/service/core/plugin/controller';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { id } = req.query as { id: string };
await connectToDatabase();
const { userId } = await authUser({ req, authToken: true });
jsonRes(res, {
data: await getPluginModuleDetail({ id, userId })
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -0,0 +1,21 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@fastgpt/service/support/user/auth';
import { getUserPlugins2Templates } from '@fastgpt/service/core/plugin/controller';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { userId } = await authUser({ req, authToken: true });
jsonRes(res, {
data: await getUserPlugins2Templates({ userId })
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -0,0 +1,23 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@fastgpt/service/support/user/auth';
import { updateOnePlugin } from '@fastgpt/service/core/plugin/controller';
import type { UpdatePluginParams } from '@fastgpt/global/core/plugin/controller';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { userId } = await authUser({ req, authToken: true });
const body = req.body as UpdatePluginParams;
jsonRes(res, {
data: await updateOnePlugin({ userId, ...body })
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -22,7 +22,7 @@ type FileType = {
/**
* Creates the multer uploader
*/
const maxSize = 50 * 1024 * 1024;
const maxSize = 500 * 1024 * 1024;
class UploadModel {
uploader = multer({
limits: {

View File

@@ -1,7 +1,8 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Image } from '@/service/mongo';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@fastgpt/service/support/user/auth';
import { uploadMongoImg } from '@fastgpt/service/common/file/image/controller';
type Props = { base64Img: string };
@@ -11,7 +12,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const { userId } = await authUser({ req, authToken: true });
const { base64Img } = req.body as Props;
const data = await uploadImg({
const data = await uploadMongoImg({
userId,
base64Img
});
@@ -24,14 +25,3 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
}
}
export async function uploadImg({ base64Img, userId }: Props & { userId: string }) {
const base64Data = base64Img.split(',')[1];
const { _id } = await Image.create({
userId,
binary: Buffer.from(base64Data, 'base64')
});
return `/api/system/img/${_id}`;
}

View File

@@ -1,21 +1,17 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Image } from '@/service/mongo';
import { connectToDatabase } from '@/service/mongo';
import { readMongoImg } from '@fastgpt/service/common/file/image/controller';
// get the models available to the system
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
const { id } = req.query;
const { id } = req.query as { id: string };
const data = await Image.findById(id);
if (!data) {
throw new Error('no image');
}
res.setHeader('Content-Type', 'image/jpeg');
res.send(data.binary);
res.send(await readMongoImg({ id }));
} catch (error) {
jsonRes(res, {
code: 500,

View File

@@ -1,14 +1,15 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { authUser } from '@fastgpt/service/support/user/auth';
import { Pay, connectToDatabase } from '@/service/mongo';
import { connectToDatabase } from '@/service/mongo';
import { MongoPay } from '@fastgpt/service/support/wallet/pay/schema';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
const { userId } = await authUser({ req, authToken: true });
const records = await Pay.find({
const records = await MongoPay.find({
userId,
status: { $ne: 'CLOSED' }
})

View File

@@ -1,7 +1,8 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { Inform, connectToDatabase } from '@/service/mongo';
import { connectToDatabase } from '@/service/mongo';
import { MongoUserInform } from '@fastgpt/service/support/user/inform/schema';
import { authUser } from '@fastgpt/service/support/user/auth';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
@@ -14,7 +15,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
const { userId } = await authUser({ req, authToken: true });
const data = await Inform.countDocuments({
const data = await MongoUserInform.countDocuments({
userId,
read: false
});

View File

@@ -1,8 +1,9 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { Inform, connectToDatabase } from '@/service/mongo';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@fastgpt/service/support/user/auth';
import { MongoUserInform } from '@fastgpt/service/support/user/inform/schema';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
@@ -15,11 +16,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
};
const [informs, total] = await Promise.all([
Inform.find({ userId })
MongoUserInform.find({ userId })
.sort({ time: -1 }) // 按照创建时间倒序排列
.skip((pageNum - 1) * pageSize)
.limit(pageSize),
Inform.countDocuments({ userId })
MongoUserInform.countDocuments({ userId })
]);
jsonRes(res, {

View File

@@ -1,8 +1,9 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { Inform, connectToDatabase } from '@/service/mongo';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@fastgpt/service/support/user/auth';
import { MongoUserInform } from '@fastgpt/service/support/user/inform/schema';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
@@ -11,7 +12,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const { id } = req.query as { id: string };
await Inform.findOneAndUpdate(
await MongoUserInform.findOneAndUpdate(
{
_id: id,
userId

View File

@@ -1,11 +1,15 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { Inform, connectToDatabase } from '@/service/mongo';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@fastgpt/service/support/user/auth';
import { InformTypeEnum } from '@/constants/user';
import { startSendInform } from '@/service/events/sendInform';
import { MongoUser } from '@fastgpt/service/support/user/schema';
import { MongoUserInform } from '@fastgpt/service/support/user/inform/schema';
import { InformTypeEnum } from '@fastgpt/global/support/user/constant';
import {
sendInform2AllUser,
sendInform2OneUser
} from '@fastgpt/service/support/user/inform/controller';
export type Props = {
type: `${InformTypeEnum}`;
@@ -38,39 +42,13 @@ export async function sendInform({ type, title, content, userId }: Props) {
try {
if (userId) {
global.sendInformQueue.push(async () => {
// skip it if have same inform within 5 minutes
const inform = await Inform.findOne({
type,
title,
content,
userId,
time: { $gte: new Date(Date.now() - 5 * 60 * 1000) }
});
if (inform) return;
await Inform.create({
type,
title,
content,
userId
});
});
global.sendInformQueue.push(async () => sendInform2OneUser({ type, title, content, userId }));
startSendInform();
return;
}
// send to all user
const users = await MongoUser.find({}, '_id');
await Inform.insertMany(
users.map(({ _id }) => ({
type,
title,
content,
userId: _id
}))
);
sendInform2AllUser({ type, title, content });
} catch (error) {
console.log('send inform error', error);
}

View File

@@ -1,7 +1,8 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, promotionRecord } from '@/service/mongo';
import { connectToDatabase } from '@/service/mongo';
import { MongoPromotionRecord } from '@fastgpt/service/support/activity/promotion/schema';
import { authUser } from '@fastgpt/service/support/user/auth';
import mongoose from '@fastgpt/service/common/mongo';
import { MongoUser } from '@fastgpt/service/support/user/schema';
@@ -16,7 +17,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
// 计算累计合
const countHistory: { totalAmount: number }[] = await promotionRecord.aggregate([
const countHistory: { totalAmount: number }[] = await MongoPromotionRecord.aggregate([
{
$match: {
userId: new mongoose.Types.ObjectId(userId),

View File

@@ -1,7 +1,8 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, promotionRecord } from '@/service/mongo';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@fastgpt/service/support/user/auth';
import { MongoPromotionRecord } from '@fastgpt/service/support/activity/promotion/schema';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
@@ -13,13 +14,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const { userId } = await authUser({ req, authToken: true });
const data = await promotionRecord
.find(
{
userId
},
'_id createTime type amount'
)
const data = await MongoPromotionRecord.find(
{
userId
},
'_id createTime type amount'
)
.sort({ _id: -1 })
.skip((pageNum - 1) * pageSize)
.limit(pageSize);
@@ -29,7 +29,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
pageNum,
pageSize,
data,
total: await promotionRecord.countDocuments({
total: await MongoPromotionRecord.countDocuments({
userId
})
}

View File

@@ -9,12 +9,15 @@ import {
dispatchHistory,
dispatchChatInput,
dispatchChatCompletion,
dispatchKBSearch,
dispatchDatasetSearch,
dispatchAnswer,
dispatchClassifyQuestion,
dispatchContentExtract,
dispatchHttpRequest,
dispatchAppRequest
dispatchAppRequest,
dispatchRunPlugin,
dispatchPluginInput,
dispatchPluginOutput
} from '@/service/moduleDispatch';
import type { CreateChatCompletionRequest } from '@fastgpt/global/core/ai/type.d';
import type { MessageItemType } from '@/types/core/chat/type';
@@ -23,8 +26,10 @@ import { getChatHistory } from './getHistory';
import { saveChat } from '@/service/utils/chat/saveChat';
import { responseWrite } from '@fastgpt/service/common/response';
import { TaskResponseKeyEnum } from '@/constants/chat';
import { FlowModuleTypeEnum, initModuleType } from '@/constants/flow';
import { AppModuleItemType, RunningModuleItemType } from '@/types/app';
import { initModuleType } from '@/constants/flow';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { RunningModuleItemType } from '@/types/app';
import type { ModuleItemType } from '@fastgpt/global/core/module/type';
import { pushChatBill } from '@/service/common/bill/push';
import { BillSourceEnum } from '@/constants/user';
import { ChatHistoryItemResType } from '@/types/chat';
@@ -305,7 +310,7 @@ export async function dispatchModules({
detail = false
}: {
res: NextApiResponse;
modules: AppModuleItemType[];
modules: ModuleItemType[];
user: UserModelSchema;
params?: Record<string, any>;
variables?: Record<string, any>;
@@ -425,7 +430,6 @@ export async function dispatchModules({
stream,
detail,
variables,
moduleName: module.name,
outputs: module.outputs,
user,
inputs: params
@@ -433,15 +437,18 @@ export async function dispatchModules({
const dispatchRes: Record<string, any> = await (async () => {
const callbackMap: Record<string, Function> = {
[FlowModuleTypeEnum.historyNode]: dispatchHistory,
[FlowModuleTypeEnum.questionInput]: dispatchChatInput,
[FlowModuleTypeEnum.answerNode]: dispatchAnswer,
[FlowModuleTypeEnum.chatNode]: dispatchChatCompletion,
[FlowModuleTypeEnum.datasetSearchNode]: dispatchKBSearch,
[FlowModuleTypeEnum.classifyQuestion]: dispatchClassifyQuestion,
[FlowModuleTypeEnum.contentExtract]: dispatchContentExtract,
[FlowModuleTypeEnum.httpRequest]: dispatchHttpRequest,
[FlowModuleTypeEnum.app]: dispatchAppRequest
[FlowNodeTypeEnum.historyNode]: dispatchHistory,
[FlowNodeTypeEnum.questionInput]: dispatchChatInput,
[FlowNodeTypeEnum.answerNode]: dispatchAnswer,
[FlowNodeTypeEnum.chatNode]: dispatchChatCompletion,
[FlowNodeTypeEnum.datasetSearchNode]: dispatchDatasetSearch,
[FlowNodeTypeEnum.classifyQuestion]: dispatchClassifyQuestion,
[FlowNodeTypeEnum.contentExtract]: dispatchContentExtract,
[FlowNodeTypeEnum.httpRequest]: dispatchHttpRequest,
[FlowNodeTypeEnum.runApp]: dispatchAppRequest,
[FlowNodeTypeEnum.pluginModule]: dispatchRunPlugin,
[FlowNodeTypeEnum.pluginInput]: dispatchPluginInput,
[FlowNodeTypeEnum.pluginOutput]: dispatchPluginOutput
};
if (callbackMap[module.flowType]) {
return callbackMap[module.flowType](props);
@@ -449,9 +456,21 @@ export async function dispatchModules({
return {};
})();
const formatResponseData = (() => {
if (!dispatchRes[TaskResponseKeyEnum.responseData]) return undefined;
if (Array.isArray(dispatchRes[TaskResponseKeyEnum.responseData]))
return dispatchRes[TaskResponseKeyEnum.responseData];
return {
...dispatchRes[TaskResponseKeyEnum.responseData],
moduleName: module.name,
moduleType: module.flowType
};
})();
return moduleOutput(module, {
[SystemOutputEnum.finish]: true,
...dispatchRes
...dispatchRes,
[TaskResponseKeyEnum.responseData]: formatResponseData
});
}
@@ -468,7 +487,7 @@ export async function dispatchModules({
/* init store modules to running modules */
function loadModules(
modules: AppModuleItemType[],
modules: ModuleItemType[],
variables: Record<string, any>
): RunningModuleItemType[] {
return modules.map((module) => {
@@ -495,12 +514,19 @@ function loadModules(
value: replacedVal
};
}),
outputs: module.outputs.map((item) => ({
key: item.key,
answer: item.key === TaskResponseKeyEnum.answerText,
value: undefined,
targets: item.targets
}))
outputs: module.outputs
.map((item) => ({
key: item.key,
answer: item.key === TaskResponseKeyEnum.answerText,
value: undefined,
targets: item.targets
}))
.sort((a, b) => {
// finish output always at last
if (a.key === SystemOutputEnum.finish) return 1;
if (b.key === SystemOutputEnum.finish) return -1;
return 0;
})
};
});
}

View File

@@ -1,36 +1,36 @@
import React, { useCallback, useRef, useState } from 'react';
import { Box, Flex, IconButton, useTheme, useDisclosure } from '@chakra-ui/react';
import { SmallCloseIcon } from '@chakra-ui/icons';
import { FlowInputItemTypeEnum } from '@/constants/flow';
import { FlowOutputTargetItemType } from '@/types/core/app/flow';
import { AppModuleItemType } from '@/types/app';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { FlowNodeOutputTargetItemType } from '@fastgpt/global/core/module/node/type';
import { ModuleItemType } from '@fastgpt/global/core/module/type';
import { useRequest } from '@/web/common/hooks/useRequest';
import type { AppSchema } from '@/types/mongoSchema';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useTranslation } from 'next-i18next';
import { useCopyData } from '@/web/common/hooks/useCopyData';
import { AppTypeEnum, SystemOutputEnum } from '@/constants/app';
import { AppTypeEnum } from '@/constants/app';
import dynamic from 'next/dynamic';
import MyIcon from '@/components/Icon';
import MyTooltip from '@/components/MyTooltip';
import ChatTest, { type ChatTestComponentRef } from './ChatTest';
import { useFlowStore } from './Provider';
import ChatTest, { type ChatTestComponentRef } from '@/components/core/module/Flow/ChatTest';
import { flowNode2Modules, useFlowProviderStore } from '@/components/core/module/Flow/FlowProvider';
const ImportSettings = dynamic(() => import('./ImportSettings'));
const ImportSettings = dynamic(() => import('@/components/core/module/Flow/ImportSettings'));
type Props = { app: AppSchema; onCloseSettings: () => void };
type Props = { app: AppSchema; onClose: () => void };
const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
app,
ChatTestRef,
testModules,
setTestModules,
onCloseSettings
onClose
}: Props & {
ChatTestRef: React.RefObject<ChatTestComponentRef>;
testModules?: AppModuleItemType[];
setTestModules: React.Dispatch<AppModuleItemType[] | undefined>;
testModules?: ModuleItemType[];
setTestModules: React.Dispatch<ModuleItemType[] | undefined>;
}) {
const theme = useTheme();
const { t } = useTranslation();
@@ -38,54 +38,11 @@ const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
const { isOpen: isOpenImport, onOpen: onOpenImport, onClose: onCloseImport } = useDisclosure();
const { updateAppDetail } = useUserStore();
const { nodes, edges, onFixView } = useFlowStore();
const flow2AppModules = useCallback(() => {
const modules: AppModuleItemType[] = nodes.map((item) => ({
moduleId: item.data.moduleId,
name: item.data.name,
flowType: item.data.flowType,
showStatus: item.data.showStatus,
position: item.position,
inputs: item.data.inputs.map((item) => ({
...item,
connected: item.connected ?? item.type !== FlowInputItemTypeEnum.target
})),
outputs: item.data.outputs.map((item) => ({
...item,
targets: [] as FlowOutputTargetItemType[]
}))
}));
// 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;
}, [edges, nodes]);
const { nodes, edges, onFixView } = useFlowProviderStore();
const { mutate: onclickSave, isLoading } = useRequest({
mutationFn: () => {
const modules = flow2AppModules();
const modules = flowNode2Modules({ nodes, edges });
// check required connect
for (let i = 0; i < modules.length; i++) {
const item = modules[i];
@@ -127,7 +84,7 @@ const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
variant={'base'}
aria-label={''}
onClick={() => {
onCloseSettings();
onClose();
onFixView();
}}
/>
@@ -155,7 +112,7 @@ const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
aria-label={'save'}
onClick={() =>
copyData(
JSON.stringify(flow2AppModules(), null, 2),
JSON.stringify(flowNode2Modules({ nodes, edges }), null, 2),
t('app.Export Config Successful')
)
}
@@ -181,7 +138,7 @@ const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
aria-label={'save'}
variant={'base'}
onClick={() => {
setTestModules(flow2AppModules());
setTestModules(flowNode2Modules({ nodes, edges }));
}}
/>
</MyTooltip>
@@ -206,7 +163,7 @@ const Header = (props: Props) => {
const { app } = props;
const ChatTestRef = useRef<ChatTestComponentRef>(null);
const [testModules, setTestModules] = useState<AppModuleItemType[]>();
const [testModules, setTestModules] = useState<ModuleItemType[]>();
return (
<>

View File

@@ -1,25 +0,0 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../modules/NodeCard';
import { FlowModuleItemType } from '@/types/core/app/flow';
import Divider from '../modules/Divider';
import Container from '../modules/Container';
import RenderInput from '../render/RenderInput';
import RenderOutput from '../render/RenderOutput';
const NodeHistory = ({ data }: NodeProps<FlowModuleItemType>) => {
const { inputs, outputs, moduleId } = 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(NodeHistory);

View File

@@ -1,78 +0,0 @@
import React from 'react';
import { Handle, Position, NodeProps } from 'reactflow';
import { Flex, Box } from '@chakra-ui/react';
import NodeCard from '../modules/NodeCard';
import { SystemInputEnum } from '@/constants/app';
import { FlowModuleItemType } from '@/types/core/app/flow';
import Divider from '../modules/Divider';
import Container from '../modules/Container';
import Label from '../modules/Label';
const NodeTFSwitch = ({ data }: NodeProps<FlowModuleItemType>) => {
return (
<NodeCard minW={'220px'} {...data}>
<Divider text="输入输出" />
<Container h={'100px'} py={0} px={0} display={'flex'} alignItems={'center'}>
<Box flex={1} pl={'12px'}>
<Label
required
description="接收到 false、0、null、undefined或空字符串时执行 False反之执行 True"
>
</Label>
<Handle
style={{
top: '50%',
left: '0',
transform: 'translate(-50%,-50%)',
width: '12px',
height: '12px',
background: '#9CA2A8'
}}
id={SystemInputEnum.switch}
type="target"
position={Position.Left}
onConnect={(params) => console.log('input onConnect', params)}
/>
</Box>
<Box flex={1} pr={'12px'}>
<Flex alignItems={'center'} justifyContent={'flex-end'} mb={'26px'} position={'relative'}>
<Label>True</Label>
<Handle
style={{
top: '0',
right: '-12px',
transform: 'translate(50%,5px)',
width: '12px',
height: '12px',
background: '#9CA2A8'
}}
id={'true'}
type="source"
position={Position.Right}
onConnect={(params) => console.log('handle onConnect', params)}
/>
</Flex>
<Flex alignItems={'center'} justifyContent={'flex-end'} position={'relative'}>
<Label>False</Label>
<Handle
style={{
bottom: '0',
right: '-12px',
transform: 'translate(50%,-5px)',
width: '12px',
height: '12px',
background: '#9CA2A8'
}}
id={'false'}
type="source"
position={Position.Right}
onConnect={(params) => console.log('handle onConnect', params)}
/>
</Flex>
</Box>
</Container>
</NodeCard>
);
};
export default React.memo(NodeTFSwitch);

View File

@@ -1,144 +0,0 @@
import React, { useCallback, useMemo } from 'react';
import { Box, Flex } from '@chakra-ui/react';
import { ModuleTemplates } from '@/constants/flow/ModuleTemplate';
import { FlowModuleTemplateType } from '@/types/core/app/flow';
import { useViewport, XYPosition } from 'reactflow';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import Avatar from '@/components/Avatar';
import { FlowModuleTypeEnum } from '@/constants/flow';
import { useFlowStore } from './Provider';
import { customAlphabet } from 'nanoid';
import { appModule2FlowNode } from '@/utils/adapt';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
const ModuleTemplateList = ({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) => {
const { nodes, setNodes, reactFlowWrapper } = useFlowStore();
const { isPc } = useSystemStore();
const { x, y, zoom } = useViewport();
const filterTemplates = useMemo(() => {
const guideModulesIndex = ModuleTemplates.findIndex((item) => item.label === '引导模块');
const guideModule: {
label: string;
list: FlowModuleTemplateType[];
} = JSON.parse(JSON.stringify(ModuleTemplates[guideModulesIndex]));
if (nodes?.find((item) => item.type === FlowModuleTypeEnum.userGuide)) {
const index = guideModule.list.findIndex(
(item) => item.flowType === FlowModuleTypeEnum.userGuide
);
guideModule.list.splice(index, 1);
}
if (nodes?.find((item) => item.type === FlowModuleTypeEnum.variable)) {
const index = guideModule.list.findIndex(
(item) => item.flowType === FlowModuleTypeEnum.variable
);
guideModule.list.splice(index, 1);
}
return [
...ModuleTemplates.slice(0, guideModulesIndex),
guideModule,
...ModuleTemplates.slice(guideModulesIndex + 1)
];
}, [nodes]);
const onAddNode = useCallback(
({ template, position }: { template: FlowModuleTemplateType; position: XYPosition }) => {
if (!reactFlowWrapper?.current) return;
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: {
...template,
moduleId: nanoid(),
position: { x: mouseX, y: mouseY }
}
})
)
);
},
[reactFlowWrapper, setNodes, x, y, zoom]
);
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'}
>
<Box w={['100%', '330px']} py={4} px={5} fontSize={'xl'} fontWeight={'bold'}>
</Box>
<Box flex={'1 0 0'} overflow={'overlay'}>
<Box w={['100%', '330px']} mx={'auto'}>
{filterTemplates.map((item) =>
item.list.map((item) => (
<Flex
key={item.name}
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>
</Flex>
</>
);
};
export default React.memo(ModuleTemplateList);

View File

@@ -1,30 +0,0 @@
import React from 'react';
import { Box } from '@chakra-ui/react';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import MyTooltip from '@/components/MyTooltip';
const Label = ({
required = false,
children,
description
}: {
required?: boolean;
children: React.ReactNode | string;
description?: string;
}) => (
<Box as={'label'} display={'inline-block'} position={'relative'}>
{children}
{required && (
<Box position={'absolute'} top={'-2px'} right={'-10px'} color={'red.500'} fontWeight={'bold'}>
*
</Box>
)}
{description && (
<MyTooltip label={description} forceShow>
<QuestionOutlineIcon display={['none', 'inline']} fontSize={'12px'} mb={1} ml={1} />
</MyTooltip>
)}
</Box>
);
export default React.memo(Label);

View File

@@ -1,135 +0,0 @@
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 '@/types/core/app/flow';
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 { useFlowStore, onChangeNode } from '../Provider';
type Props = FlowModuleItemType & {
children?: React.ReactNode | React.ReactNode[] | string;
minW?: string | number;
};
const NodeCard = (props: Props) => {
const {
children,
logo = '/icon/logo.svg',
name = '未知模块',
description,
minW = '300px',
moduleId
} = props;
const { onCopyNode, onDelNode } = useFlowStore();
const { t } = useTranslation();
const theme = useTheme();
const { toast } = useToast();
// custom title edit
const { onOpenModal, EditModal: EditTitleModal } = useEditTitle({
title: t('common.Custom Title'),
placeholder: t('app.module.Custom Title Tip') || ''
});
const menuList = useMemo(
() => [
{
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.Close'),
onClick: () => {}
}
],
[moduleId, name, onChangeNode, onCopyNode, onDelNode, onOpenModal, t, toast]
);
return (
<Box
minW={minW}
maxW={'500px'}
bg={'white'}
border={theme.borders.md}
borderRadius={'md'}
boxShadow={'sm'}
>
<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} />
<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 />
</Box>
);
};
export default React.memo(NodeCard);

View File

@@ -1,115 +0,0 @@
import React, { useMemo, useState } from 'react';
import {
Box,
Button,
ModalHeader,
ModalFooter,
ModalBody,
Flex,
Switch,
Input,
FormControl
} from '@chakra-ui/react';
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';
import { FlowInputItemTypeEnum, FlowValueTypeEnum } from '@/constants/flow';
import { useTranslation } from 'react-i18next';
import MySelect from '@/components/Select';
import type { FlowInputItemType } from '@/types/core/app/flow';
const typeSelectList = [
{
label: '字符串',
value: FlowValueTypeEnum.string
},
{
label: '数字',
value: FlowValueTypeEnum.number
},
{
label: '布尔',
value: FlowValueTypeEnum.boolean
},
{
label: '任意',
value: FlowValueTypeEnum.any
}
];
const SetInputFieldModal = ({
defaultField = {
label: '',
key: '',
type: FlowInputItemTypeEnum.target,
valueType: FlowValueTypeEnum.string,
description: '',
required: false
},
onClose,
onSubmit
}: {
defaultField?: FlowInputItemType;
onClose: () => void;
onSubmit: (data: FlowInputItemType) => void;
}) => {
const { t } = useTranslation();
const { register, getValues, setValue, handleSubmit } = useForm<FlowInputItemType>({
defaultValues: defaultField
});
const [refresh, setRefresh] = useState(false);
return (
<MyModal isOpen={true} onClose={onClose}>
<ModalHeader display={'flex'} alignItems={'center'}>
<Avatar src={'/imgs/module/extract.png'} mr={2} w={'20px'} objectFit={'cover'} />
{t('app.Input Field Settings')}
</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>
<MySelect
w={'288px'}
list={typeSelectList}
value={getValues('valueType')}
onchange={(e: any) => {
setValue('valueType', e);
setRefresh(!refresh);
}}
/>
</Flex>
<Flex mt={5} alignItems={'center'}>
<Box flex={'0 0 70px'}></Box>
<Input
placeholder="预约字段/sql语句……"
{...register('label', { required: '字段名不能为空' })}
/>
</Flex>
<Flex mt={5} alignItems={'center'}>
<Box flex={'0 0 70px'}> key</Box>
<Input
placeholder="appointment/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(SetInputFieldModal);

View File

@@ -1,94 +0,0 @@
import React, { useMemo, useState } from 'react';
import { Box, Button, ModalHeader, ModalFooter, ModalBody, Flex, Input } from '@chakra-ui/react';
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';
import { FlowOutputItemTypeEnum, FlowValueTypeEnum, FlowValueTypeStyle } from '@/constants/flow';
import { useTranslation } from 'react-i18next';
import MySelect from '@/components/Select';
import { FlowOutputItemType } from '@/types/core/app/flow';
const typeSelectList = [
{
label: '字符串',
value: FlowValueTypeEnum.string
},
{
label: '数字',
value: FlowValueTypeEnum.number
},
{
label: '布尔',
value: FlowValueTypeEnum.boolean
},
{
label: '任意',
value: FlowValueTypeEnum.any
}
];
const SetInputFieldModal = ({
defaultField,
onClose,
onSubmit
}: {
defaultField: FlowOutputItemType;
onClose: () => void;
onSubmit: (data: FlowOutputItemType) => void;
}) => {
const { t } = useTranslation();
const { register, getValues, setValue, handleSubmit } = useForm<FlowOutputItemType>({
defaultValues: defaultField
});
const [refresh, setRefresh] = useState(false);
return (
<MyModal isOpen={true} onClose={onClose}>
<ModalHeader display={'flex'} alignItems={'center'}>
<Avatar src={'/imgs/module/extract.png'} mr={2} w={'20px'} objectFit={'cover'} />
{t('app.Output Field Settings')}
</ModalHeader>
<ModalBody>
<Flex mt={5} alignItems={'center'}>
<Box flex={'0 0 70px'}></Box>
<MySelect
w={'288px'}
list={typeSelectList}
value={getValues('valueType')}
onchange={(e: any) => {
setValue('valueType', e);
setRefresh(!refresh);
}}
/>
</Flex>
<Flex mt={5} alignItems={'center'}>
<Box flex={'0 0 70px'}></Box>
<Input
placeholder="预约字段/sql语句……"
{...register('label', { required: '字段名不能为空' })}
/>
</Flex>
<Flex mt={5} alignItems={'center'}>
<Box flex={'0 0 70px'}> key</Box>
<Input
placeholder="appointment/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(SetInputFieldModal);

View File

@@ -1,145 +1,60 @@
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, FlowModuleTypeEnum } from '@/constants/flow';
import type { AppSchema } from '@/types/mongoSchema';
import React, { useMemo } from 'react';
import { AppSchema } from '@/types/mongoSchema';
import Header from './Header';
import Flow from '@/components/core/module/Flow';
import FlowProvider, { useFlowProviderStore } from '@/components/core/module/Flow/FlowProvider';
import { SystemModuleTemplateType } from '@fastgpt/global/core/module/type.d';
import { SystemModuleTemplates } from '@/constants/flow/ModuleTemplate';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { usePluginStore } from '@/web/core/plugin/store/plugin';
import { useQuery } from '@tanstack/react-query';
import dynamic from 'next/dynamic';
type Props = { app: AppSchema; onClose: () => void };
import ButtonEdge from './components/modules/ButtonEdge';
import TemplateList from './components/TemplateList';
import FlowProvider, { useFlowStore } from './components/Provider';
import Header from './components/Header';
const Render = ({ app, onClose }: Props) => {
const { nodes } = useFlowProviderStore();
const { pluginModuleTemplates, loadPluginModuleTemplates } = usePluginStore();
import 'reactflow/dist/style.css';
const filterTemplates = useMemo(() => {
const copyTemplates: SystemModuleTemplateType = JSON.parse(
JSON.stringify(SystemModuleTemplates)
);
const filterType: Record<string, 1> = {
[FlowNodeTypeEnum.userGuide]: 1
};
// filter some template
nodes.forEach((node) => {
if (node.type && filterType[node.type]) {
copyTemplates.forEach((item) => {
item.list.forEach((module, index) => {
if (module.flowType === node.type) {
item.list.splice(index, 1);
}
});
});
}
});
const NodeChat = dynamic(() => import('./components/Nodes/NodeChat'));
const NodeDatasetSearch = dynamic(() => import('./components/Nodes/NodeDatasetSearch'));
const NodeHistory = dynamic(() => import('./components/Nodes/NodeHistory'));
const NodeTFSwitch = dynamic(() => import('./components/Nodes/NodeTFSwitch'));
const NodeAnswer = dynamic(() => import('./components/Nodes/NodeAnswer'));
const NodeQuestionInput = dynamic(() => import('./components/Nodes/NodeQuestionInput'));
const NodeCQNode = dynamic(() => import('./components/Nodes/NodeCQNode'));
const NodeVariable = dynamic(() => import('./components/Nodes/NodeVariable'));
const NodeUserGuide = dynamic(() => import('./components/Nodes/NodeUserGuide'));
const NodeExtract = dynamic(() => import('./components/Nodes/NodeExtract'));
const NodeHttp = dynamic(() => import('./components/Nodes/NodeHttp'));
const NodeAPP = dynamic(() => import('./components/Nodes/NodeAPP'));
return copyTemplates;
}, [nodes]);
const nodeTypes = {
[FlowModuleTypeEnum.userGuide]: NodeUserGuide,
[FlowModuleTypeEnum.variable]: NodeVariable,
[FlowModuleTypeEnum.questionInput]: NodeQuestionInput,
[FlowModuleTypeEnum.historyNode]: NodeHistory,
[FlowModuleTypeEnum.chatNode]: NodeChat,
[FlowModuleTypeEnum.datasetSearchNode]: NodeDatasetSearch,
[FlowModuleTypeEnum.tfSwitchNode]: NodeTFSwitch,
[FlowModuleTypeEnum.answerNode]: NodeAnswer,
[FlowModuleTypeEnum.classifyQuestion]: NodeCQNode,
[FlowModuleTypeEnum.contentExtract]: NodeExtract,
[FlowModuleTypeEnum.httpRequest]: NodeHttp,
[FlowModuleTypeEnum.app]: NodeAPP
// [FlowModuleTypeEnum.empty]: EmptyModule
};
const edgeTypes = {
buttonedge: ButtonEdge
};
type Props = { app: AppSchema; onCloseSettings: () => void };
const AppEdit = React.memo(function AppEdit(props: Props) {
const { app } = props;
const {
isOpen: isOpenTemplate,
onOpen: onOpenTemplate,
onClose: onCloseTemplate
} = useDisclosure();
const { reactFlowWrapper, nodes, onNodesChange, edges, onEdgesChange, onConnect, initData } =
useFlowStore();
useEffect(() => {
initData(JSON.parse(JSON.stringify(app.modules)));
}, [app.modules]);
useQuery(['getUserPlugs2ModuleTemplates'], () => loadPluginModuleTemplates());
return (
<>
{/* header */}
<Header app={app} onCloseSettings={props.onCloseSettings} />
<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();
}}
/>
<Flow
systemTemplates={filterTemplates}
pluginTemplates={[{ label: '', list: pluginModuleTemplates }]}
show2Plugin
modules={app.modules}
Header={<Header app={app} onClose={onClose} />}
/>
);
};
<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 isOpen={isOpenTemplate} onClose={onCloseTemplate} />
</Box>
</>
export default React.memo(function AdEdit(props: Props) {
return (
<FlowProvider filterAppIds={[props.app._id]}>
<Render {...props} />
</FlowProvider>
);
});
const Flow = (data: Props) => {
return (
<Box h={'100%'} position={'fixed'} zIndex={999} top={0} left={0} right={0} bottom={0}>
<ReactFlowProvider>
<FlowProvider appId={data?.app?._id}>
<Flex h={'100%'} flexDirection={'column'} bg={'#fff'}>
{!!data.app._id && <AppEdit {...data} />}
</Flex>
</FlowProvider>
</ReactFlowProvider>
</Box>
);
};
export default React.memo(Flow);

View File

@@ -37,10 +37,11 @@ import {
welcomeTextTip,
questionGuideTip
} from '@/constants/flow/ModuleTemplate';
import { AppModuleItemType, VariableItemType } from '@/types/app';
import { VariableItemType } from '@/types/app';
import type { ModuleItemType } from '@fastgpt/global/core/module/type';
import { useRequest } from '@/web/common/hooks/useRequest';
import { useConfirm } from '@/web/common/hooks/useConfirm';
import { FlowModuleTypeEnum } from '@/constants/flow';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { streamFetch } from '@/web/common/api/fetch';
import { useRouter } from 'next/router';
import { useToast } from '@/web/common/hooks/useToast';
@@ -56,15 +57,15 @@ import Avatar from '@/components/Avatar';
import MyIcon from '@/components/Icon';
import ChatBox, { type ComponentRef, type StartChatFnProps } from '@/components/ChatBox';
import { addVariable } from '../VariableEditModal';
import { KbParamsModal } from '../DatasetSelectModal';
import { addVariable } from '@/components/core/module/VariableEditModal';
import { KbParamsModal } from '@/components/core/module/DatasetSelectModal';
import { AppTypeEnum } from '@/constants/app';
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
const VariableEditModal = dynamic(() => import('../VariableEditModal'));
const VariableEditModal = dynamic(() => import('@/components/core/module/VariableEditModal'));
const InfoModal = dynamic(() => import('../InfoModal'));
const DatasetSelectModal = dynamic(() => import('../DatasetSelectModal'));
const AIChatSettingsModal = dynamic(() => import('../AIChatSettingsModal'));
const DatasetSelectModal = dynamic(() => import('@/components/core/module/DatasetSelectModal'));
const AIChatSettingsModal = dynamic(() => import('@/components/core/module/AIChatSettingsModal'));
const Settings = ({ appId }: { appId: string }) => {
const theme = useTheme();
@@ -596,13 +597,13 @@ const ChatTest = ({ appId }: { appId: string }) => {
const { t } = useTranslation();
const { appDetail, userInfo } = useUserStore();
const ChatBoxRef = useRef<ComponentRef>(null);
const [modules, setModules] = useState<AppModuleItemType[]>([]);
const [modules, setModules] = useState<ModuleItemType[]>([]);
const startChat = useCallback(
async ({ chatList, controller, generatingMessage, variables }: StartChatFnProps) => {
const historyMaxLen =
modules
?.find((item) => item.flowType === FlowModuleTypeEnum.historyNode)
?.find((item) => item.flowType === FlowNodeTypeEnum.historyNode)
?.inputs?.find((item) => item.key === 'maxContext')?.value || 0;
const history = chatList.slice(-historyMaxLen - 2, -2);

View File

@@ -48,8 +48,7 @@ const Logs = ({ appId }: { appId: string }) => {
data: logs,
isLoading,
Pagination,
getData,
pageNum
getData
} = usePagination<AppLogsListItemType>({
api: getAppChatLogs,
pageSize: 20,
@@ -178,7 +177,7 @@ const Logs = ({ appId }: { appId: string }) => {
);
};
export default Logs;
export default React.memo(Logs);
function DetailLogsModal({
appId,

Some files were not shown because too many files have changed in this diff Show More