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

@@ -0,0 +1,227 @@
import React, { useEffect, useMemo, useState } from 'react';
import MyModal from '@/components/MyModal';
import { useTranslation } from 'react-i18next';
import { EditFormType } from '@/web/core/app/basicSettings';
import { useForm } from 'react-hook-form';
import {
Box,
BoxProps,
Button,
Flex,
Link,
ModalBody,
ModalFooter,
Switch,
Textarea
} from '@chakra-ui/react';
import MyTooltip from '@/components/MyTooltip';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import { Prompt_QuotePromptList, Prompt_QuoteTemplateList } from '@/global/core/prompt/AIChat';
import { chatModelList, feConfigs } from '@/web/common/system/staticData';
import MySlider from '@/components/Slider';
import { SystemInputEnum } from '@/constants/app';
import dynamic from 'next/dynamic';
import { PromptTemplateItem } from '@fastgpt/global/core/ai/type.d';
const PromptTemplate = dynamic(() => import('@/components/PromptTemplate'));
const AIChatSettingsModal = ({
isAdEdit,
onClose,
onSuccess,
defaultData
}: {
isAdEdit?: boolean;
onClose: () => void;
onSuccess: (e: EditFormType['chatModel']) => void;
defaultData: EditFormType['chatModel'];
}) => {
const { t } = useTranslation();
const [refresh, setRefresh] = useState(false);
const { register, handleSubmit, getValues, setValue } = useForm({
defaultValues: defaultData
});
const [selectTemplateData, setSelectTemplateData] = useState<{
title: string;
templates: PromptTemplateItem[];
}>();
const tokenLimit = useMemo(() => {
return chatModelList.find((item) => item.model === getValues('model'))?.maxToken || 4000;
}, [getValues, refresh]);
const LabelStyles: BoxProps = {
fontSize: ['sm', 'md']
};
const selectTemplateBtn: BoxProps = {
color: 'myBlue.600',
cursor: 'pointer'
};
return (
<MyModal
isOpen
title={
<Flex alignItems={'flex-end'}>
{t('app.AI Advanced Settings')}
{feConfigs?.show_doc && (
<Link
href={`${feConfigs.docUrl}/docs/use-cases/ai_settings/`}
target={'_blank'}
ml={1}
textDecoration={'underline'}
fontWeight={'normal'}
fontSize={'md'}
>
</Link>
)}
</Flex>
}
isCentered
w={'700px'}
h={['90vh', 'auto']}
>
<ModalBody flex={['1 0 0', 'auto']} overflowY={'auto'}>
{isAdEdit && (
<Flex alignItems={'center'}>
<Box {...LabelStyles} w={'80px'}>
AI内容
</Box>
<Box flex={1} ml={'10px'}>
<Switch
isChecked={getValues(SystemInputEnum.isResponseAnswerText)}
size={'lg'}
onChange={(e) => {
const value = e.target.checked;
setValue(SystemInputEnum.isResponseAnswerText, value);
setRefresh((state) => !state);
}}
/>
</Box>
</Flex>
)}
<Flex alignItems={'center'} mb={10} mt={isAdEdit ? 8 : 5}>
<Box {...LabelStyles} mr={2} w={'80px'}>
</Box>
<Box flex={1} ml={'10px'}>
<MySlider
markList={[
{ label: '严谨', value: 0 },
{ label: '发散', value: 10 }
]}
width={'95%'}
min={0}
max={10}
value={getValues('temperature')}
onChange={(e) => {
setValue('temperature', e);
setRefresh(!refresh);
}}
/>
</Box>
</Flex>
<Flex alignItems={'center'} mt={12} mb={10}>
<Box {...LabelStyles} mr={2} w={'80px'}>
</Box>
<Box flex={1} ml={'10px'}>
<MySlider
markList={[
{ label: '100', value: 100 },
{ label: `${tokenLimit}`, value: tokenLimit }
]}
width={'95%'}
min={100}
max={tokenLimit}
step={50}
value={getValues('maxToken')}
onChange={(val) => {
setValue('maxToken', val);
setRefresh(!refresh);
}}
/>
</Box>
</Flex>
<Box>
<Flex {...LabelStyles} mb={1}>
<MyTooltip
label={t('template.Quote Content Tip', {
default: Prompt_QuoteTemplateList[0].value
})}
forceShow
>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</MyTooltip>
<Box flex={1} />
<Box
{...selectTemplateBtn}
onClick={() =>
setSelectTemplateData({
title: '选择知识库提示词模板',
templates: Prompt_QuoteTemplateList
})
}
>
</Box>
</Flex>
<Textarea
rows={6}
placeholder={
t('template.Quote Content Tip', { default: Prompt_QuoteTemplateList[0].value }) || ''
}
borderColor={'myGray.100'}
{...register('quoteTemplate')}
/>
</Box>
<Box mt={4}>
<Flex {...LabelStyles} mb={1}>
<MyTooltip
label={t('template.Quote Prompt Tip', { default: Prompt_QuotePromptList[0].value })}
forceShow
>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</MyTooltip>
</Flex>
<Textarea
rows={11}
placeholder={
t('template.Quote Prompt Tip', { default: Prompt_QuotePromptList[0].value }) || ''
}
borderColor={'myGray.100'}
{...register('quotePrompt')}
/>
</Box>
</ModalBody>
<ModalFooter>
<Button variant={'base'} onClick={onClose}>
{t('Cancel')}
</Button>
<Button ml={4} onClick={handleSubmit(onSuccess)}>
{t('Confirm')}
</Button>
</ModalFooter>
{!!selectTemplateData && (
<PromptTemplate
title={selectTemplateData.title}
templates={selectTemplateData.templates}
onClose={() => setSelectTemplateData(undefined)}
onSuccess={(e) => {
const quoteVal = e.value;
const promptVal = Prompt_QuotePromptList.find((item) => item.title === e.title)?.value;
setValue('quoteTemplate', quoteVal);
setValue('quotePrompt', promptVal);
}}
/>
)}
</MyModal>
);
};
export default AIChatSettingsModal;

View File

@@ -0,0 +1,325 @@
import React, { useMemo, useState } from 'react';
import {
Card,
Flex,
Box,
Button,
ModalBody,
ModalFooter,
useTheme,
Textarea,
Grid,
Divider
} from '@chakra-ui/react';
import Avatar from '@/components/Avatar';
import { useForm } from 'react-hook-form';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import type { SelectedDatasetType } from '@/types/core/dataset';
import { useToast } from '@/web/common/hooks/useToast';
import MySlider from '@/components/Slider';
import MyTooltip from '@/components/MyTooltip';
import MyModal from '@/components/MyModal';
import MyIcon from '@/components/Icon';
import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constant';
import { useTranslation } from 'react-i18next';
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
import { feConfigs } from '@/web/common/system/staticData';
import DatasetSelectContainer, { useDatasetSelect } from '@/components/core/dataset/SelectModal';
export type KbParamsType = {
searchSimilarity: number;
searchLimit: number;
searchEmptyText: string;
};
export const DatasetSelectModal = ({
isOpen,
activeDatasets = [],
onChange,
onClose
}: {
isOpen: boolean;
activeDatasets: SelectedDatasetType;
onChange: (e: SelectedDatasetType) => void;
onClose: () => void;
}) => {
const { t } = useTranslation();
const theme = useTheme();
const { allDatasets } = useDatasetStore();
const [selectedKbList, setSelectedKbList] = useState<SelectedDatasetType>(
activeDatasets.filter((dataset) => {
return allDatasets.find((item) => item._id === dataset.datasetId);
})
);
const { toast } = useToast();
const { paths, parentId, setParentId, datasets } = useDatasetSelect();
const filterKbList = useMemo(() => {
return {
selected: allDatasets.filter((item) =>
selectedKbList.find((dataset) => dataset.datasetId === item._id)
),
unSelected: datasets.filter(
(item) => !selectedKbList.find((dataset) => dataset.datasetId === item._id)
)
};
}, [datasets, allDatasets, selectedKbList]);
return (
<DatasetSelectContainer
isOpen={isOpen}
paths={paths}
parentId={parentId}
setParentId={setParentId}
tips={'仅能选择同一个索引模型的知识库'}
onClose={onClose}
>
<Flex h={'100%'} flexDirection={'column'} flex={'1 0 0'}>
<ModalBody flex={'1 0 0'} overflowY={'auto'} userSelect={'none'}>
<Grid
gridTemplateColumns={[
'repeat(1, minmax(0, 1fr))',
'repeat(2, minmax(0, 1fr))',
'repeat(3, minmax(0, 1fr))'
]}
gridGap={3}
>
{filterKbList.selected.map((item) =>
(() => {
return (
<Card
key={item._id}
p={3}
border={theme.borders.base}
boxShadow={'sm'}
bg={'myBlue.300'}
>
<Flex alignItems={'center'} h={'38px'}>
<Avatar src={item.avatar} w={['24px', '28px']}></Avatar>
<Box flex={'1 0 0'} w={0} className="textEllipsis" mx={3}>
{item.name}
</Box>
<MyIcon
name={'delete'}
w={'14px'}
cursor={'pointer'}
_hover={{ color: 'red.500' }}
onClick={() => {
setSelectedKbList((state) =>
state.filter((kb) => kb.datasetId !== item._id)
);
}}
/>
</Flex>
</Card>
);
})()
)}
</Grid>
{filterKbList.selected.length > 0 && <Divider my={3} />}
<Grid
gridTemplateColumns={[
'repeat(1, minmax(0, 1fr))',
'repeat(2, minmax(0, 1fr))',
'repeat(3, minmax(0, 1fr))'
]}
gridGap={3}
>
{filterKbList.unSelected.map((item) =>
(() => {
return (
<MyTooltip
key={item._id}
label={
item.type === DatasetTypeEnum.dataset
? t('dataset.Select Dataset')
: t('dataset.Select Folder')
}
>
<Card
p={3}
border={theme.borders.base}
boxShadow={'sm'}
h={'80px'}
cursor={'pointer'}
_hover={{
boxShadow: 'md'
}}
onClick={() => {
if (item.type === DatasetTypeEnum.folder) {
setParentId(item._id);
} else if (item.type === DatasetTypeEnum.dataset) {
const vectorModel = selectedKbList[0]?.vectorModel?.model;
if (vectorModel && vectorModel !== item.vectorModel.model) {
return toast({
status: 'warning',
title: '仅能选择同一个索引模型的知识库'
});
}
setSelectedKbList((state) => [
...state,
{ datasetId: item._id, vectorModel: item.vectorModel }
]);
}
}}
>
<Flex alignItems={'center'} h={'38px'}>
<Avatar src={item.avatar} w={['24px', '28px']}></Avatar>
<Box
flex={'1 0 0'}
w={0}
className="textEllipsis"
ml={3}
fontWeight={'bold'}
fontSize={['md', 'lg', 'xl']}
>
{item.name}
</Box>
</Flex>
<Flex justifyContent={'flex-end'} alignItems={'center'} fontSize={'sm'}>
{item.type === DatasetTypeEnum.folder ? (
<Box color={'myGray.500'}>{t('Folder')}</Box>
) : (
<>
<MyIcon mr={1} name="kbTest" w={'12px'} />
<Box color={'myGray.500'}>{item.vectorModel.name}</Box>
</>
)}
</Flex>
</Card>
</MyTooltip>
);
})()
)}
</Grid>
{filterKbList.unSelected.length === 0 && (
<Flex mt={5} flexDirection={'column'} alignItems={'center'}>
<MyIcon name="empty" w={'48px'} h={'48px'} mt={'20vh'} color={'transparent'} />
<Box mt={2} color={'myGray.500'}>
西~
</Box>
</Flex>
)}
</ModalBody>
<ModalFooter>
<Button
onClick={() => {
// filter out the dataset that is not in the kList
const filterKbList = selectedKbList.filter((dataset) => {
return allDatasets.find((item) => item._id === dataset.datasetId);
});
onClose();
onChange(filterKbList);
}}
>
</Button>
</ModalFooter>
</Flex>
</DatasetSelectContainer>
);
};
export const KbParamsModal = ({
searchEmptyText,
searchLimit,
searchSimilarity,
onClose,
onChange
}: KbParamsType & { onClose: () => void; onChange: (e: KbParamsType) => void }) => {
const [refresh, setRefresh] = useState(false);
const { register, setValue, getValues, handleSubmit } = useForm<KbParamsType>({
defaultValues: {
searchEmptyText,
searchLimit,
searchSimilarity
}
});
return (
<MyModal isOpen={true} onClose={onClose} title={'搜索参数调整'} minW={['90vw', '600px']}>
<Flex flexDirection={'column'}>
<ModalBody>
<Box display={['block', 'flex']} py={5} pt={[0, 5]}>
<Box flex={'0 0 100px'} mb={[8, 0]}>
<MyTooltip
label={'不同索引模型的相似度有区别,请通过搜索测试来选择合适的数值'}
forceShow
>
<QuestionOutlineIcon ml={1} />
</MyTooltip>
</Box>
<MySlider
markList={[
{ label: '0', value: 0 },
{ label: '1', value: 1 }
]}
min={0}
max={1}
step={0.01}
value={getValues('searchSimilarity')}
onChange={(val) => {
setValue('searchSimilarity', val);
setRefresh(!refresh);
}}
/>
</Box>
<Box display={['block', 'flex']} py={8}>
<Box flex={'0 0 100px'} mb={[8, 0]}>
</Box>
<Box flex={1}>
<MySlider
markList={[
{ label: '1', value: 1 },
{ label: '20', value: 20 }
]}
min={1}
max={20}
value={getValues('searchLimit')}
onChange={(val) => {
setValue('searchLimit', val);
setRefresh(!refresh);
}}
/>
</Box>
</Box>
<Box display={['block', 'flex']} pt={3}>
<Box flex={'0 0 100px'} mb={[2, 0]}>
</Box>
<Box flex={1}>
<Textarea
rows={5}
maxLength={500}
placeholder={`若填写该内容,没有搜索到对应内容时,将直接回复填写的内容。\n为了连贯上下文${feConfigs?.systemTitle} 会取部分上一个聊天的搜索记录作为补充,因此在连续对话时,该功能可能会失效。`}
{...register('searchEmptyText')}
></Textarea>
</Box>
</Box>
</ModalBody>
<ModalFooter>
<Button variant={'base'} mr={3} onClick={onClose}>
</Button>
<Button
onClick={() => {
onClose();
handleSubmit(onChange)();
}}
>
</Button>
</ModalFooter>
</Flex>
</MyModal>
);
};
export default DatasetSelectModal;

View File

@@ -0,0 +1,137 @@
import type { ModuleItemType } from '@fastgpt/global/core/module/type.d';
import { AppSchema } from '@/types/mongoSchema';
import React, {
useMemo,
useCallback,
useRef,
forwardRef,
useImperativeHandle,
ForwardedRef
} from 'react';
import { Box, Flex, IconButton } from '@chakra-ui/react';
import MyIcon from '@/components/Icon';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { streamFetch } from '@/web/common/api/fetch';
import MyTooltip from '@/components/MyTooltip';
import { useUserStore } from '@/web/support/user/useUserStore';
import ChatBox, { type ComponentRef, type StartChatFnProps } from '@/components/ChatBox';
import { getGuideModule } from '@/global/core/app/modules/utils';
export type ChatTestComponentRef = {
resetChatTest: () => void;
};
const ChatTest = (
{
app,
modules = [],
onClose
}: {
app: AppSchema;
modules?: ModuleItemType[];
onClose: () => void;
},
ref: ForwardedRef<ChatTestComponentRef>
) => {
const ChatBoxRef = useRef<ComponentRef>(null);
const { userInfo } = useUserStore();
const isOpen = useMemo(() => modules && modules.length > 0, [modules]);
const startChat = useCallback(
async ({ chatList, controller, generatingMessage, variables }: StartChatFnProps) => {
const historyMaxLen =
modules
?.find((item) => item.flowType === FlowNodeTypeEnum.historyNode)
?.inputs?.find((item) => item.key === 'maxContext')?.value || 0;
const history = chatList.slice(-historyMaxLen - 2, -2);
// 流请求,获取数据
const { responseText, responseData } = await streamFetch({
url: '/api/chat/chatTest',
data: {
history,
prompt: chatList[chatList.length - 2].value,
modules,
variables,
appId: app._id,
appName: `调试-${app.name}`
},
onMessage: generatingMessage,
abortSignal: controller
});
return { responseText, responseData };
},
[app._id, app.name, modules]
);
useImperativeHandle(ref, () => ({
resetChatTest() {
ChatBoxRef.current?.resetHistory([]);
ChatBoxRef.current?.resetVariables();
}
}));
return (
<>
<Flex
zIndex={3}
flexDirection={'column'}
position={'absolute'}
top={5}
right={0}
h={isOpen ? '95%' : '0'}
w={isOpen ? ['100%', '460px'] : '0'}
bg={'white'}
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
borderRadius={'md'}
overflow={'hidden'}
transition={'.2s ease'}
>
<Flex py={4} px={5} whiteSpace={'nowrap'}>
<Box fontSize={'xl'} fontWeight={'bold'} flex={1}>
</Box>
<MyTooltip label={'重置'}>
<IconButton
className="chat"
size={'sm'}
icon={<MyIcon name={'clear'} w={'14px'} />}
variant={'base'}
borderRadius={'md'}
aria-label={'delete'}
onClick={(e) => {
e.stopPropagation();
ChatBoxRef.current?.resetHistory([]);
ChatBoxRef.current?.resetVariables();
}}
/>
</MyTooltip>
</Flex>
<Box flex={1}>
<ChatBox
ref={ChatBoxRef}
appAvatar={app.avatar}
userAvatar={userInfo?.avatar}
showMarkIcon
userGuideModule={getGuideModule(modules)}
onStartChat={startChat}
onDelMessage={() => {}}
/>
</Box>
</Flex>
<Box
zIndex={2}
display={isOpen ? 'block' : 'none'}
position={'fixed'}
top={0}
left={0}
bottom={0}
right={0}
onClick={onClose}
/>
</>
);
};
export default React.memo(forwardRef(ChatTest));

View File

@@ -0,0 +1,482 @@
import {
type Node,
type NodeChange,
type Edge,
type EdgeChange,
useNodesState,
useEdgesState,
Connection,
addEdge
} from 'reactflow';
import type {
FlowModuleItemType,
FlowModuleTemplateType
} from '@fastgpt/global/core/module/type.d';
import type {
FlowNodeOutputTargetItemType,
FlowNodeChangeProps
} from '@fastgpt/global/core/module/node/type';
import React, {
type SetStateAction,
type Dispatch,
useContext,
useCallback,
createContext,
useRef,
useEffect
} from 'react';
import { customAlphabet } from 'nanoid';
import { appModule2FlowEdge, appModule2FlowNode } from '@/utils/adapt';
import { useToast } from '@/web/common/hooks/useToast';
import {
FlowNodeInputTypeEnum,
FlowNodeTypeEnum,
FlowNodeValTypeEnum
} from '@fastgpt/global/core/module/node/constant';
import { useTranslation } from 'next-i18next';
import { ModuleItemType } from '@fastgpt/global/core/module/type.d';
import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
type OnChange<ChangesType> = (changes: ChangesType[]) => void;
export type useFlowProviderStoreType = {
reactFlowWrapper: null | React.RefObject<HTMLDivElement>;
filterAppIds: string[];
nodes: Node<FlowModuleItemType, string | undefined>[];
setNodes: Dispatch<SetStateAction<Node<FlowModuleItemType, string | undefined>[]>>;
onNodesChange: OnChange<NodeChange>;
edges: Edge<any>[];
setEdges: Dispatch<SetStateAction<Edge<any>[]>>;
onEdgesChange: OnChange<EdgeChange>;
onFixView: () => void;
onDelNode: (nodeId: string) => void;
onChangeNode: (e: FlowNodeChangeProps) => void;
onCopyNode: (nodeId: string) => void;
onResetNode: (id: string, module: FlowModuleTemplateType) => void;
onDelEdge: (e: {
moduleId: string;
sourceHandle?: string | undefined;
targetHandle?: string | undefined;
}) => void;
onDelConnect: (id: string) => void;
onConnect: ({ connect }: { connect: Connection }) => any;
initData: (modules: ModuleItemType[]) => void;
};
const StateContext = createContext<useFlowProviderStoreType>({
reactFlowWrapper: null,
filterAppIds: [],
nodes: [],
setNodes: function (
value: React.SetStateAction<Node<FlowModuleItemType, string | undefined>[]>
): void {
return;
},
onNodesChange: function (changes: NodeChange[]): void {
return;
},
edges: [],
setEdges: function (value: React.SetStateAction<Edge<any>[]>): void {
return;
},
onEdgesChange: function (changes: EdgeChange[]): void {
return;
},
onFixView: function (): void {
return;
},
onDelNode: function (nodeId: string): void {
return;
},
onChangeNode: function (e: FlowNodeChangeProps): void {
return;
},
onCopyNode: function (nodeId: string): void {
return;
},
onDelEdge: function (e: {
moduleId: string;
sourceHandle?: string | undefined;
targetHandle?: string | undefined;
}): void {
return;
},
onDelConnect: function (id: string): void {
return;
},
onConnect: function ({ connect }: { connect: Connection }) {
return;
},
initData: function (modules: ModuleItemType[]): void {
throw new Error('Function not implemented.');
},
onResetNode: function (id: string, module: FlowModuleTemplateType): void {
throw new Error('Function not implemented.');
}
});
export const useFlowProviderStore = () => useContext(StateContext);
export const FlowProvider = ({
filterAppIds = [],
children
}: {
filterAppIds?: string[];
children: React.ReactNode;
}) => {
const reactFlowWrapper = useRef<HTMLDivElement>(null);
const { t } = useTranslation();
const { toast } = useToast();
const [nodes = [], setNodes, onNodesChange] = useNodesState<FlowModuleItemType>([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const onFixView = useCallback(() => {
const btn = document.querySelector('.react-flow__controls-fitview') as HTMLButtonElement;
setTimeout(() => {
btn && btn.click();
}, 100);
}, []);
const onDelEdge = useCallback(
({
moduleId,
sourceHandle,
targetHandle
}: {
moduleId: string;
sourceHandle?: string | undefined;
targetHandle?: string | undefined;
}) => {
if (!sourceHandle && !targetHandle) return;
setEdges((state) =>
state.filter((edge) => {
if (edge.source === moduleId && edge.sourceHandle === sourceHandle) return false;
if (edge.target === moduleId && edge.targetHandle === targetHandle) return false;
return true;
})
);
},
[setEdges]
);
const onDelConnect = useCallback(
(id: string) => {
setEdges((state) => state.filter((item) => item.id !== id));
},
[setEdges]
);
const onConnect = useCallback(
({ connect }: { connect: Connection }) => {
const source = nodes.find((node) => node.id === connect.source)?.data;
const sourceType = (() => {
if (source?.flowType === FlowNodeTypeEnum.classifyQuestion) {
return FlowNodeValTypeEnum.boolean;
}
if (source?.flowType === FlowNodeTypeEnum.pluginInput) {
return source?.inputs.find((input) => input.key === connect.sourceHandle)?.valueType;
}
return source?.outputs.find((output) => output.key === connect.sourceHandle)?.valueType;
})();
const targetType = nodes
.find((node) => node.id === connect.target)
?.data?.inputs.find((input) => input.key === connect.targetHandle)?.valueType;
if (!sourceType || !targetType) {
return toast({
status: 'warning',
title: t('app.Connection is invalid')
});
}
if (
sourceType !== FlowNodeValTypeEnum.any &&
targetType !== FlowNodeValTypeEnum.any &&
sourceType !== targetType
) {
return toast({
status: 'warning',
title: t('app.Connection type is different')
});
}
setEdges((state) =>
addEdge(
{
...connect,
type: 'buttonedge',
animated: true,
data: {
onDelete: onDelConnect
}
},
state
)
);
},
[nodes, onDelConnect, setEdges, t, toast]
);
const onDelNode = useCallback(
(nodeId: string) => {
setNodes((state) => state.filter((item) => item.id !== nodeId));
setEdges((state) => state.filter((edge) => edge.source !== nodeId && edge.target !== nodeId));
},
[setEdges, setNodes]
);
const onChangeNode = useCallback(
({ moduleId, type, key, value, index }: FlowNodeChangeProps) => {
setNodes((nodes) =>
nodes.map((node) => {
if (node.id !== moduleId) return node;
const updateObj: Record<string, any> = {};
if (type === 'attr') {
if (key) {
updateObj[key] = value;
}
} else if (type === 'updateInput') {
updateObj.inputs = node.data.inputs.map((item) => (item.key === key ? value : item));
} else if (type === 'replaceInput') {
onDelEdge({ moduleId, targetHandle: key });
const oldInputIndex = node.data.inputs.findIndex((item) => item.key === key);
updateObj.inputs = node.data.inputs.filter((item) => item.key !== key);
setTimeout(() => {
onChangeNode({
moduleId,
type: 'addInput',
index: oldInputIndex,
value
});
});
} else if (type === 'addInput') {
const input = node.data.inputs.find((input) => input.key === value.key);
if (input) {
toast({
status: 'warning',
title: 'key 重复'
});
updateObj.inputs = node.data.inputs;
} else {
if (index !== undefined) {
const inputs = [...node.data.inputs];
inputs.splice(index, 0, value);
updateObj.inputs = inputs;
} else {
updateObj.inputs = node.data.inputs.concat(value);
}
}
} else if (type === 'delInput') {
onDelEdge({ moduleId, targetHandle: key });
updateObj.inputs = node.data.inputs.filter((item) => item.key !== key);
} else if (type === 'updateOutput') {
updateObj.outputs = node.data.outputs.map((item) => (item.key === key ? value : item));
} else if (type === 'replaceOutput') {
onDelEdge({ moduleId, sourceHandle: key });
const oldOutputIndex = node.data.outputs.findIndex((item) => item.key === key);
updateObj.outputs = node.data.outputs.filter((item) => item.key !== key);
setTimeout(() => {
onChangeNode({
moduleId,
type: 'addOutput',
index: oldOutputIndex,
value
});
});
} else if (type === 'addOutput') {
const output = node.data.outputs.find((output) => output.key === value.key);
if (output) {
toast({
status: 'warning',
title: 'key 重复'
});
updateObj.outputs = node.data.outputs;
} else {
if (index !== undefined) {
const outputs = [...node.data.outputs];
outputs.splice(index, 0, value);
updateObj.outputs = outputs;
} else {
updateObj.outputs = node.data.outputs.concat(value);
}
}
} else if (type === 'delOutput') {
onDelEdge({ moduleId, sourceHandle: key });
updateObj.outputs = node.data.outputs.filter((item) => item.key !== key);
}
return {
...node,
data: {
...node.data,
...updateObj
}
};
})
);
},
[onDelEdge, setNodes, toast]
);
const onCopyNode = useCallback(
(nodeId: string) => {
setNodes((nodes) => {
const node = nodes.find((node) => node.id === nodeId);
if (!node) return nodes;
const template = {
logo: node.data.logo,
name: node.data.name,
intro: node.data.intro,
description: node.data.description,
flowType: node.data.flowType,
inputs: node.data.inputs,
outputs: node.data.outputs,
showStatus: node.data.showStatus
};
return nodes.concat(
appModule2FlowNode({
item: {
...template,
moduleId: nanoid(),
position: { x: node.position.x + 200, y: node.position.y + 50 }
}
})
);
});
},
[setNodes]
);
// reset a node data. delete edge and replace it
const onResetNode = useCallback(
(id: string, module: FlowModuleTemplateType) => {
setNodes((state) =>
state.map((node) => {
if (node.id === id) {
// delete edge
node.data.inputs.forEach((item) => {
onDelEdge({ moduleId: id, targetHandle: item.key });
});
node.data.outputs.forEach((item) => {
onDelEdge({ moduleId: id, sourceHandle: item.key });
});
return {
...node,
data: {
...node.data,
...module
}
};
}
return node;
})
);
},
[onDelEdge, setNodes]
);
const initData = useCallback(
(modules: ModuleItemType[]) => {
const edges = appModule2FlowEdge({
modules,
onDelete: onDelConnect
});
setEdges(edges);
setNodes(modules.map((item) => appModule2FlowNode({ item })));
onFixView();
},
[onDelConnect, setEdges, setNodes, onFixView]
);
// use eventbus to avoid refresh ReactComponents
useEffect(() => {
const update = (e: FlowNodeChangeProps) => {
onChangeNode(e);
};
eventBus.on(EventNameEnum.updaterNode, update);
return () => {
eventBus.off(EventNameEnum.updaterNode);
};
}, [onChangeNode]);
const value = {
reactFlowWrapper,
filterAppIds,
nodes,
setNodes,
onNodesChange,
edges,
setEdges,
onEdgesChange,
onFixView,
onDelNode,
onChangeNode,
onResetNode,
onCopyNode,
onDelEdge,
onDelConnect,
onConnect,
initData
};
return <StateContext.Provider value={value}>{children}</StateContext.Provider>;
};
export default React.memo(FlowProvider);
export const onChangeNode = (e: FlowNodeChangeProps) => {
eventBus.emit(EventNameEnum.updaterNode, e);
};
export function flowNode2Modules({
nodes,
edges
}: {
nodes: Node<FlowModuleItemType, string | undefined>[];
edges: Edge<any>[];
}) {
const modules: ModuleItemType[] = nodes.map((item) => ({
moduleId: item.data.moduleId,
name: item.data.name,
logo: item.data.logo,
flowType: item.data.flowType,
showStatus: item.data.showStatus,
position: item.position,
inputs: item.data.inputs.map((item) => ({
...item,
connected: item.connected ?? item.type !== FlowNodeInputTypeEnum.target
})),
outputs: item.data.outputs.map((item) => ({
...item,
targets: [] as FlowNodeOutputTargetItemType[]
}))
}));
// update inputs and outputs
modules.forEach((module) => {
module.inputs.forEach((input) => {
input.connected =
input.connected ||
!!edges.find((edge) => edge.target === module.moduleId && edge.targetHandle === input.key);
});
module.outputs.forEach((output) => {
output.targets = edges
.filter(
(edge) =>
edge.source === module.moduleId && edge.sourceHandle === output.key && edge.targetHandle
)
.map((edge) => ({
moduleId: edge.target,
key: edge.targetHandle || ''
}));
});
});
return modules;
}

View File

@@ -0,0 +1,53 @@
import React, { useState } from 'react';
import { Textarea, Button, ModalBody, ModalFooter } from '@chakra-ui/react';
import MyModal from '@/components/MyModal';
import { useTranslation } from 'react-i18next';
import { useToast } from '@/web/common/hooks/useToast';
import { useFlowProviderStore } from './FlowProvider';
const ImportSettings = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
const { toast } = useToast();
const [value, setValue] = useState('');
const { setNodes, setEdges, initData } = useFlowProviderStore();
return (
<MyModal isOpen w={'600px'} onClose={onClose} title={t('app.Import Config')}>
<ModalBody>
<Textarea
placeholder={t('app.Paste Config') || 'app.Paste Config'}
defaultValue={value}
rows={16}
onChange={(e) => setValue(e.target.value)}
/>
</ModalBody>
<ModalFooter>
<Button
variant="base"
onClick={() => {
if (!value) {
return onClose();
}
try {
const data = JSON.parse(value);
setEdges([]);
setNodes([]);
setTimeout(() => {
initData(data);
}, 10);
onClose();
} catch (error) {
toast({
title: t('app.Import Config Failed')
});
}
}}
>
</Button>
</ModalFooter>
</MyModal>
);
};
export default React.memo(ImportSettings);

View File

@@ -0,0 +1,110 @@
import React, { useMemo } from 'react';
import { ModalBody, Flex, Box, useTheme, ModalFooter, Button } from '@chakra-ui/react';
import MyModal from '@/components/MyModal';
import { useQuery } from '@tanstack/react-query';
import type { SelectAppItemType } from '@fastgpt/global/core/module/type';
import Avatar from '@/components/Avatar';
import { useTranslation } from 'react-i18next';
import { useLoading } from '@/web/common/hooks/useLoading';
import { useUserStore } from '@/web/support/user/useUserStore';
const SelectAppModal = ({
defaultApps = [],
filterAppIds = [],
max = 1,
onClose,
onSuccess
}: {
defaultApps: string[];
filterAppIds?: string[];
max?: number;
onClose: () => void;
onSuccess: (e: SelectAppItemType[]) => void;
}) => {
const { t } = useTranslation();
const { Loading } = useLoading();
const theme = useTheme();
const [selectedApps, setSelectedApps] = React.useState<string[]>(defaultApps);
/* 加载模型 */
const { myApps, loadMyApps } = useUserStore();
const { isLoading } = useQuery(['loadMyApos'], () => loadMyApps());
const apps = useMemo(
() => myApps.filter((app) => !filterAppIds.includes(app._id)),
[myApps, filterAppIds]
);
return (
<MyModal
isOpen
title={`选择应用${max > 1 ? `(${selectedApps.length}/${max})` : ''}`}
onClose={onClose}
minW={'700px'}
position={'relative'}
>
<ModalBody
minH={'300px'}
display={'grid'}
gridTemplateColumns={['1fr', 'repeat(3, minmax(0, 1fr))']}
gridGap={4}
>
{apps.map((app) => (
<Flex
key={app._id}
alignItems={'center'}
border={theme.borders.base}
borderRadius={'md'}
p={2}
cursor={'pointer'}
{...(selectedApps.includes(app._id)
? {
bg: 'myBlue.200',
onClick: () => {
setSelectedApps(selectedApps.filter((e) => e !== app._id));
}
}
: {
onClick: () => {
if (max === 1) {
setSelectedApps([app._id]);
} else if (selectedApps.length < max) {
setSelectedApps([...selectedApps, app._id]);
}
}
})}
>
<Avatar src={app.avatar} w={['16px', '22px']} />
<Box fontWeight={'bold'} ml={1}>
{app.name}
</Box>
</Flex>
))}
</ModalBody>
<ModalFooter>
<Button variant={'base'} onClick={onClose}>
{t('Cancel')}
</Button>
<Button
ml={2}
onClick={() => {
onSuccess(
apps
.filter((app) => selectedApps.includes(app._id))
.map((app) => ({
id: app._id,
name: app.name,
logo: app.avatar
}))
);
onClose();
}}
>
{t('Confirm')}
</Button>
</ModalFooter>
<Loading loading={isLoading} fixed={false} />
</MyModal>
);
};
export default React.memo(SelectAppModal);

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,58 @@
import React from 'react';
import { BaseEdge, EdgeLabelRenderer, EdgeProps, getBezierPath } from 'reactflow';
import { Flex } from '@chakra-ui/react';
import MyIcon from '@/components/Icon';
const ButtonEdge = ({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
style = {},
markerEnd,
data
}: EdgeProps<{
onDelete: (id: string) => void;
}>) => {
const [edgePath, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition
});
return (
<>
<BaseEdge path={edgePath} markerEnd={markerEnd} style={style} />
<EdgeLabelRenderer>
<Flex
alignItems={'center'}
justifyContent={'center'}
position={'absolute'}
transform={`translate(-50%, -50%) translate(${labelX}px,${labelY}px)`}
pointerEvents={'all'}
w={'20px'}
h={'20px'}
bg={'white'}
borderRadius={'20px'}
color={'black'}
cursor={'pointer'}
border={'1px solid #fff'}
_hover={{
boxShadow: '0 0 6px 2px rgba(0, 0, 0, 0.08)'
}}
onClick={() => data?.onDelete(id)}
>
<MyIcon name="closeSolid" w={'100%'} color={'myGray.600'}></MyIcon>
</Flex>
</EdgeLabelRenderer>
</>
);
};
export default React.memo(ButtonEdge);

View File

@@ -0,0 +1,13 @@
import React from 'react';
import { Box } from '@chakra-ui/react';
import { BoxProps } from '@chakra-ui/react';
const Container = ({ children, ...props }: BoxProps) => {
return (
<Box px={4} py={3} position={'relative'} {...props}>
{children}
</Box>
);
};
export default React.memo(Container);

View File

@@ -0,0 +1,22 @@
import React from 'react';
import { Box, useTheme } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
const Divider = ({ text }: { text: 'Input' | 'Output' | string }) => {
const theme = useTheme();
const { t } = useTranslation();
return (
<Box
textAlign={'center'}
bg={'#f8f8f8'}
py={2}
borderTop={theme.borders.base}
borderBottom={theme.borders.base}
fontSize={'lg'}
>
{t(`common.${text}`)}
</Box>
);
};
export default React.memo(Divider);

View File

@@ -0,0 +1,74 @@
import React, { useMemo, useState } from 'react';
import {
Box,
Button,
ModalHeader,
ModalFooter,
ModalBody,
Flex,
Switch,
Input
} from '@chakra-ui/react';
import type { ContextExtractAgentItemType } from '@/types/app';
import { useForm } from 'react-hook-form';
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
import MyModal from '@/components/MyModal';
import Avatar from '@/components/Avatar';
import MyTooltip from '@/components/MyTooltip';
const ExtractFieldModal = ({
defaultField = {
desc: '',
key: '',
required: true
},
onClose,
onSubmit
}: {
defaultField?: ContextExtractAgentItemType;
onClose: () => void;
onSubmit: (data: ContextExtractAgentItemType) => void;
}) => {
const { register, handleSubmit } = useForm<ContextExtractAgentItemType>({
defaultValues: defaultField
});
return (
<MyModal isOpen={true} onClose={onClose}>
<ModalHeader display={'flex'} alignItems={'center'}>
<Avatar src={'/imgs/module/extract.png'} mr={2} w={'20px'} objectFit={'cover'} />
</ModalHeader>
<ModalBody>
<Flex alignItems={'center'}>
<Box flex={'0 0 70px'}></Box>
<Switch {...register('required')} />
</Flex>
<Flex mt={5} alignItems={'center'}>
<Box flex={'0 0 70px'}></Box>
<Input
placeholder="姓名/年龄/sql语句……"
{...register('desc', { required: '字段描述不能为空' })}
/>
</Flex>
<Flex mt={5} alignItems={'center'}>
<Box flex={'0 0 70px'}> key</Box>
<Input
placeholder="name/age/sql"
{...register('key', { required: '字段 key 不能为空' })}
/>
</Flex>
</ModalBody>
<ModalFooter>
<Button variant={'base'} mr={3} onClick={onClose}>
</Button>
<Button onClick={handleSubmit(onSubmit)}></Button>
</ModalFooter>
</MyModal>
);
};
export default React.memo(ExtractFieldModal);

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

@@ -0,0 +1,20 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../modules/NodeCard';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import Container from '../modules/Container';
import RenderInput from '../render/RenderInput';
import RenderOutput from '../render/RenderOutput';
const NodeAnswer = ({ data }: NodeProps<FlowModuleItemType>) => {
const { moduleId, inputs, outputs } = data;
return (
<NodeCard minW={'400px'} {...data}>
<Container borderTop={'2px solid'} borderTopColor={'myGray.200'}>
<RenderInput moduleId={moduleId} flowInputList={inputs} />
<RenderOutput moduleId={moduleId} flowOutputList={outputs} />
</Container>
</NodeCard>
);
};
export default React.memo(NodeAnswer);

View File

@@ -0,0 +1,145 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import { Box, Input, Button, Flex, Textarea } from '@chakra-ui/react';
import NodeCard from '../modules/NodeCard';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import Divider from '../modules/Divider';
import Container from '../modules/Container';
import RenderInput from '../render/RenderInput';
import type { ClassifyQuestionAgentItemType } from '@/types/app';
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 4);
import MyIcon from '@/components/Icon';
import {
FlowNodeOutputTypeEnum,
FlowNodeValTypeEnum,
FlowNodeSpecialInputKeyEnum
} from '@fastgpt/global/core/module/node/constant';
import { useTranslation } from 'react-i18next';
import SourceHandle from '../render/SourceHandle';
import MyTooltip from '@/components/MyTooltip';
import { onChangeNode } from '../../FlowProvider';
const NodeCQNode = ({ data }: NodeProps<FlowModuleItemType>) => {
const { t } = useTranslation();
const { moduleId, inputs, outputs } = data;
return (
<NodeCard minW={'400px'} {...data}>
<Divider text="Input" />
<Container>
<RenderInput
moduleId={moduleId}
flowInputList={inputs}
CustomComponent={{
[FlowNodeSpecialInputKeyEnum.agents]: ({
key: agentKey,
value: agents = [],
...props
}: {
key: string;
value?: ClassifyQuestionAgentItemType[];
}) => (
<Box>
{agents.map((item, i) => (
<Box key={item.key} mb={4}>
<Flex alignItems={'center'}>
<MyTooltip label={t('common.Delete')}>
<MyIcon
mt={1}
mr={2}
name={'minus'}
w={'14px'}
cursor={'pointer'}
color={'myGray.600'}
_hover={{ color: 'red.600' }}
onClick={() => {
onChangeNode({
moduleId,
type: 'updateInput',
key: agentKey,
value: {
...props,
key: agentKey,
value: agents.filter((input) => input.key !== item.key)
}
});
onChangeNode({
moduleId,
type: 'delOutput',
key: item.key
});
}}
/>
</MyTooltip>
<Box flex={1}>{i + 1}</Box>
</Flex>
<Box position={'relative'}>
<Textarea
rows={2}
mt={1}
defaultValue={item.value}
onChange={(e) => {
const newVal = agents.map((val) =>
val.key === item.key
? {
...val,
value: e.target.value
}
: val
);
onChangeNode({
moduleId,
type: 'updateInput',
key: agentKey,
value: {
...props,
key: agentKey,
value: newVal
}
});
}}
/>
<SourceHandle handleKey={item.key} valueType={FlowNodeValTypeEnum.boolean} />
</Box>
</Box>
))}
<Button
onClick={() => {
const key = nanoid();
onChangeNode({
moduleId,
type: 'updateInput',
key: agentKey,
value: {
...props,
key: agentKey,
value: agents.concat({ value: '', key })
}
});
onChangeNode({
moduleId,
type: 'updateOutput',
key: agentKey,
value: outputs.concat({
key,
label: '',
type: FlowNodeOutputTypeEnum.hidden,
targets: []
})
});
}}
>
</Button>
</Box>
)
}}
/>
</Container>
</NodeCard>
);
};
export default React.memo(NodeCQNode);

View File

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

View File

@@ -0,0 +1,197 @@
import React, { useState } from 'react';
import { Box, Button, Table, Thead, Tbody, Tr, Th, Td, TableContainer } from '@chakra-ui/react';
import { NodeProps } from 'reactflow';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import { useTranslation } from 'next-i18next';
import NodeCard from '../modules/NodeCard';
import Container from '../modules/Container';
import { AddIcon } from '@chakra-ui/icons';
import RenderInput from '../render/RenderInput';
import Divider from '../modules/Divider';
import { ContextExtractAgentItemType } from '@/types/app';
import RenderOutput from '../render/RenderOutput';
import MyIcon from '@/components/Icon';
import ExtractFieldModal from '../modules/ExtractFieldModal';
import { ContextExtractEnum } from '@/constants/flow/flowField';
import {
FlowNodeOutputTypeEnum,
FlowNodeValTypeEnum
} from '@fastgpt/global/core/module/node/constant';
import { useFlowProviderStore, onChangeNode } from '../../FlowProvider';
const NodeExtract = ({ data }: NodeProps<FlowModuleItemType>) => {
const { inputs, outputs, moduleId } = data;
const { t } = useTranslation();
const [editExtractFiled, setEditExtractField] = useState<ContextExtractAgentItemType>();
const { onDelEdge } = useFlowProviderStore();
return (
<NodeCard minW={'400px'} {...data}>
<Divider text="Input" />
<Container>
<RenderInput
moduleId={moduleId}
flowInputList={inputs}
CustomComponent={{
[ContextExtractEnum.extractKeys]: ({
value: extractKeys = [],
...props
}: {
value?: ContextExtractAgentItemType[];
}) => (
<Box pt={2}>
<Box position={'absolute'} top={0} right={0}>
<Button
variant={'base'}
leftIcon={<AddIcon fontSize={'10px'} />}
onClick={() =>
setEditExtractField({
desc: '',
key: '',
required: true
})
}
>
</Button>
</Box>
<TableContainer>
<Table>
<Thead>
<Tr>
<Th> key</Th>
<Th></Th>
<Th></Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{extractKeys.map((item, index) => (
<Tr
key={index}
position={'relative'}
whiteSpace={'pre-wrap'}
wordBreak={'break-all'}
>
<Td>{item.key}</Td>
<Td>{item.desc}</Td>
<Td>{item.required ? '✔' : ''}</Td>
<Td whiteSpace={'nowrap'}>
<MyIcon
mr={3}
name={'settingLight'}
w={'16px'}
cursor={'pointer'}
onClick={() => {
setEditExtractField(item);
}}
/>
<MyIcon
name={'delete'}
w={'16px'}
cursor={'pointer'}
onClick={() => {
onChangeNode({
moduleId,
type: 'updateInput',
key: ContextExtractEnum.extractKeys,
value: {
...props,
value: extractKeys.filter((extract) => item.key !== extract.key)
}
});
onChangeNode({
moduleId,
type: 'delOutput',
key: item.key
});
}}
/>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</Box>
)
}}
/>
</Container>
<Divider text="Output" />
<Container>
<RenderOutput moduleId={moduleId} flowOutputList={outputs} />
</Container>
{!!editExtractFiled && (
<ExtractFieldModal
defaultField={editExtractFiled}
onClose={() => setEditExtractField(undefined)}
onSubmit={(data) => {
const extracts: ContextExtractAgentItemType[] =
inputs.find((item) => item.key === ContextExtractEnum.extractKeys)?.value || [];
const exists = extracts.find((item) => item.key === editExtractFiled.key);
const newInputs = exists
? extracts.map((item) => (item.key === editExtractFiled.key ? data : item))
: extracts.concat(data);
onChangeNode({
moduleId,
type: 'updateInput',
key: ContextExtractEnum.extractKeys,
value: {
...inputs.find((input) => input.key === ContextExtractEnum.extractKeys),
value: newInputs
}
});
const newOutput = {
key: data.key,
label: `提取结果-${data.desc}`,
description: '无法提取时不会返回',
valueType: FlowNodeValTypeEnum.string,
type: FlowNodeOutputTypeEnum.source,
targets: []
};
if (exists) {
if (editExtractFiled.key === data.key) {
const output = outputs.find((output) => output.key === data.key);
// update
onChangeNode({
moduleId,
type: 'updateOutput',
key: data.key,
value: {
...output,
label: `提取结果-${data.desc}`
}
});
} else {
onChangeNode({
moduleId,
type: 'replaceOutput',
key: editExtractFiled.key,
value: newOutput
});
}
} else {
onChangeNode({
moduleId,
type: 'addOutput',
value: newOutput
});
}
setEditExtractField(undefined);
}}
/>
)}
</NodeCard>
);
};
export default React.memo(NodeExtract);

View File

@@ -0,0 +1,80 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../modules/NodeCard';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import Divider from '../modules/Divider';
import Container from '../modules/Container';
import RenderInput from '../render/RenderInput';
import { Box, Button } from '@chakra-ui/react';
import { SmallAddIcon } from '@chakra-ui/icons';
import RenderOutput from '../render/RenderOutput';
import {
FlowNodeInputTypeEnum,
FlowNodeOutputTypeEnum,
FlowNodeValTypeEnum
} from '@fastgpt/global/core/module/node/constant';
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
import { onChangeNode } from '../../FlowProvider';
const NodeHttp = ({ data }: NodeProps<FlowModuleItemType>) => {
const { moduleId, inputs, outputs } = data;
return (
<NodeCard minW={'350px'} {...data}>
<Container borderTop={'2px solid'} borderTopColor={'myGray.200'}>
<RenderInput moduleId={moduleId} flowInputList={inputs} />
<Button
variant={'base'}
mt={5}
leftIcon={<SmallAddIcon />}
onClick={() => {
const key = nanoid();
onChangeNode({
moduleId,
type: 'addInput',
key,
value: {
key,
valueType: FlowNodeValTypeEnum.string,
type: FlowNodeInputTypeEnum.target,
label: `入参${inputs.length - 1}`,
edit: true
}
});
}}
>
</Button>
</Container>
<Divider text="Output" />
<Container>
<RenderOutput moduleId={moduleId} flowOutputList={outputs} />
<Box textAlign={'right'} mt={5}>
<Button
variant={'base'}
leftIcon={<SmallAddIcon />}
onClick={() => {
onChangeNode({
moduleId,
type: 'addOutput',
value: {
key: nanoid(),
label: `出参${outputs.length}`,
valueType: FlowNodeValTypeEnum.string,
type: FlowNodeOutputTypeEnum.source,
edit: true,
targets: []
}
});
}}
>
</Button>
</Box>
</Container>
</NodeCard>
);
};
export default React.memo(NodeHttp);

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

@@ -0,0 +1,26 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../modules/NodeCard';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import Divider from '../modules/Divider';
import Container from '../modules/Container';
import RenderInput from '../render/RenderInput';
import RenderOutput from '../render/RenderOutput';
const NodeSimple = ({ data }: NodeProps<FlowModuleItemType>) => {
const { moduleId, inputs, outputs } = data;
return (
<NodeCard minW={'300px'} isPreview {...data}>
<Divider text="Input" />
<Container>
<RenderInput moduleId={moduleId} flowInputList={inputs} />
</Container>
<Divider text="Output" />
<Container>
<RenderOutput moduleId={moduleId} flowOutputList={outputs} />
</Container>
</NodeCard>
);
};
export default React.memo(NodeSimple);

View File

@@ -0,0 +1,20 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../modules/NodeCard';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import Container from '../modules/Container';
import RenderOutput from '../render/RenderOutput';
const QuestionInputNode = ({ data }: NodeProps<FlowModuleItemType>) => {
const { moduleId, inputs, outputs } = data;
return (
<NodeCard minW={'240px'} {...data}>
<Container borderTop={'2px solid'} borderTopColor={'myGray.200'} textAlign={'end'}>
<RenderOutput moduleId={moduleId} flowOutputList={outputs} />
</Container>
</NodeCard>
);
};
export default React.memo(QuestionInputNode);

View File

@@ -0,0 +1,25 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../modules/NodeCard';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import Divider from '../modules/Divider';
import Container from '../modules/Container';
import RenderInput from '../render/RenderInput';
import RenderOutput from '../render/RenderOutput';
const NodeRunAPP = ({ data }: NodeProps<FlowModuleItemType>) => {
const { moduleId, inputs, outputs } = data;
return (
<NodeCard minW={'350px'} {...data}>
<Container borderTop={'2px solid'} borderTopColor={'myGray.200'}>
<RenderInput moduleId={moduleId} flowInputList={inputs} />
</Container>
<Divider text="Output" />
<Container>
<RenderOutput moduleId={moduleId} flowOutputList={outputs} />
</Container>
</NodeCard>
);
};
export default React.memo(NodeRunAPP);

View File

@@ -0,0 +1,26 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../modules/NodeCard';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import Divider from '../modules/Divider';
import Container from '../modules/Container';
import RenderInput from '../render/RenderInput';
import RenderOutput from '../render/RenderOutput';
const NodeSimple = ({ data }: NodeProps<FlowModuleItemType>) => {
const { moduleId, inputs, outputs } = data;
return (
<NodeCard minW={'300px'} {...data}>
<Divider text="Input" />
<Container>
<RenderInput moduleId={moduleId} flowInputList={inputs} />
</Container>
<Divider text="Output" />
<Container>
<RenderOutput moduleId={moduleId} flowOutputList={outputs} />
</Container>
</NodeCard>
);
};
export default React.memo(NodeSimple);

View File

@@ -0,0 +1,243 @@
import React, { useCallback, useMemo, useState } from 'react';
import { NodeProps } from 'reactflow';
import {
Box,
Flex,
Textarea,
useTheme,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
Switch
} from '@chakra-ui/react';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import { SystemInputEnum } from '@/constants/app';
import { welcomeTextTip, variableTip, questionGuideTip } from '@/constants/flow/ModuleTemplate';
import { onChangeNode } from '../../FlowProvider';
import VariableEditModal, { addVariable } from '../../../VariableEditModal';
import MyIcon from '@/components/Icon';
import MyTooltip from '@/components/MyTooltip';
import Container from '../modules/Container';
import NodeCard from '../modules/NodeCard';
import { VariableItemType } from '@/types/app';
const NodeUserGuide = ({ data }: NodeProps<FlowModuleItemType>) => {
const theme = useTheme();
return (
<>
<NodeCard minW={'300px'} {...data}>
<Container borderTop={'2px solid'} borderTopColor={'myGray.200'}>
<WelcomeText data={data} />
<Box pt={4} pb={2}>
<ChatStartVariable data={data} />
</Box>
<Box pt={3} borderTop={theme.borders.base}>
<QuestionGuide data={data} />
</Box>
</Container>
</NodeCard>
</>
);
};
export default React.memo(NodeUserGuide);
export function WelcomeText({ data }: { data: FlowModuleItemType }) {
const { inputs, moduleId } = data;
const welcomeText = useMemo(
() => inputs.find((item) => item.key === SystemInputEnum.welcomeText),
[inputs]
);
return (
<>
<Flex mb={1} alignItems={'center'}>
<MyIcon name={'welcomeText'} mr={2} w={'16px'} color={'#E74694'} />
<Box></Box>
<MyTooltip label={welcomeTextTip} forceShow>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</MyTooltip>
</Flex>
{welcomeText && (
<Textarea
className="nodrag"
rows={6}
resize={'both'}
defaultValue={welcomeText.value}
bg={'myWhite.500'}
placeholder={welcomeTextTip}
onChange={(e) => {
onChangeNode({
moduleId,
key: SystemInputEnum.welcomeText,
type: 'updateInput',
value: {
...welcomeText,
value: e.target.value
}
});
}}
/>
)}
</>
);
}
function ChatStartVariable({ data }: { data: FlowModuleItemType }) {
const { inputs, moduleId } = data;
const variables = useMemo(
() =>
(inputs.find((item) => item.key === SystemInputEnum.variables)
?.value as VariableItemType[]) || [],
[inputs]
);
const [editVariable, setEditVariable] = useState<VariableItemType>();
const updateVariables = useCallback(
(value: VariableItemType[]) => {
onChangeNode({
moduleId,
key: SystemInputEnum.variables,
type: 'updateInput',
value: {
...inputs.find((item) => item.key === SystemInputEnum.variables),
value
}
});
},
[inputs, moduleId]
);
const onclickSubmit = useCallback(
({ variable }: { variable: VariableItemType }) => {
updateVariables(variables.map((item) => (item.id === variable.id ? variable : item)));
setEditVariable(undefined);
},
[updateVariables, variables]
);
return (
<>
<Flex mb={1} alignItems={'center'}>
<MyIcon name={'variable'} mr={2} w={'16px'} color={'#fb7c3d'} />
<Box></Box>
<MyTooltip label={variableTip} forceShow>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</MyTooltip>
<Box flex={1} />
<Flex
ml={2}
textAlign={'right'}
cursor={'pointer'}
px={3}
py={'2px'}
borderRadius={'md'}
_hover={{ bg: 'myGray.200' }}
onClick={() => {
const newVariable = addVariable();
updateVariables(variables.concat(newVariable));
setEditVariable(newVariable);
}}
>
+&ensp;
</Flex>
</Flex>
{variables.length > 0 && (
<TableContainer borderWidth={'1px'} borderBottom="none" borderRadius={'lg'}>
<Table>
<Thead>
<Tr>
<Th></Th>
<Th> key</Th>
<Th></Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{variables.map((item, index) => (
<Tr key={index}>
<Td>{item.label} </Td>
<Td>{item.key}</Td>
<Td>{item.required ? '✔' : ''}</Td>
<Td>
<MyIcon
mr={3}
name={'settingLight'}
w={'16px'}
cursor={'pointer'}
onClick={() => {
setEditVariable(item);
}}
/>
<MyIcon
name={'delete'}
w={'16px'}
cursor={'pointer'}
onClick={() =>
updateVariables(variables.filter((variable) => variable.id !== item.id))
}
/>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
)}
{!!editVariable && (
<VariableEditModal
defaultVariable={editVariable}
onClose={() => setEditVariable(undefined)}
onSubmit={onclickSubmit}
/>
)}
</>
);
}
function QuestionGuide({ data }: { data: FlowModuleItemType }) {
const { inputs, moduleId } = data;
const questionGuide = useMemo(
() =>
(inputs.find((item) => item.key === SystemInputEnum.questionGuide)?.value as boolean) ||
false,
[inputs]
);
return (
<Flex alignItems={'center'}>
<MyIcon name={'questionGuide'} mr={2} w={'16px'} />
<Box></Box>
<MyTooltip label={questionGuideTip} forceShow>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</MyTooltip>
<Box flex={1} />
<Switch
isChecked={questionGuide}
size={'lg'}
onChange={(e) => {
const value = e.target.checked;
onChangeNode({
moduleId,
key: SystemInputEnum.questionGuide,
type: 'updateInput',
value: {
...inputs.find((item) => item.key === SystemInputEnum.questionGuide),
value
}
});
}}
/>
</Flex>
);
}

View File

@@ -0,0 +1,131 @@
/* Abandon */
import React, { useCallback, useMemo, useState } from 'react';
import { NodeProps } from 'reactflow';
import { Box, Button, Table, Thead, Tbody, Tr, Th, Td, TableContainer } from '@chakra-ui/react';
import { AddIcon } from '@chakra-ui/icons';
import NodeCard from '../modules/NodeCard';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import Container from '../modules/Container';
import { SystemInputEnum, VariableInputEnum } from '@/constants/app';
import type { VariableItemType } from '@/types/app';
import MyIcon from '@/components/Icon';
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
import VariableEditModal, { addVariable } from '../../../VariableEditModal';
import { onChangeNode } from '../../FlowProvider';
export const defaultVariable: VariableItemType = {
id: nanoid(),
key: 'key',
label: 'label',
type: VariableInputEnum.input,
required: true,
maxLen: 50,
enums: [{ value: '' }]
};
const NodeUserGuide = ({ data }: NodeProps<FlowModuleItemType>) => {
const { inputs, moduleId } = data;
const variables = useMemo(
() =>
(inputs.find((item) => item.key === SystemInputEnum.variables)
?.value as VariableItemType[]) || [],
[inputs]
);
const [editVariable, setEditVariable] = useState<VariableItemType>();
const updateVariables = useCallback(
(value: VariableItemType[]) => {
onChangeNode({
moduleId,
key: SystemInputEnum.variables,
type: 'updateInput',
value: {
...inputs.find((item) => item.key === SystemInputEnum.variables),
value
}
});
},
[inputs, moduleId]
);
const onclickSubmit = useCallback(
({ variable }: { variable: VariableItemType }) => {
updateVariables(variables.map((item) => (item.id === variable.id ? variable : item)));
setEditVariable(undefined);
},
[updateVariables, variables]
);
return (
<>
<NodeCard minW={'300px'} {...data}>
<Container borderTop={'2px solid'} borderTopColor={'myGray.200'}>
<TableContainer>
<Table>
<Thead>
<Tr>
<Th></Th>
<Th> key</Th>
<Th></Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{variables.map((item, index) => (
<Tr key={index}>
<Td>{item.label} </Td>
<Td>{item.key}</Td>
<Td>{item.required ? '✔' : ''}</Td>
<Td>
<MyIcon
mr={3}
name={'settingLight'}
w={'16px'}
cursor={'pointer'}
onClick={() => {
setEditVariable(item);
}}
/>
<MyIcon
name={'delete'}
w={'16px'}
cursor={'pointer'}
onClick={() =>
updateVariables(variables.filter((variable) => variable.id !== item.id))
}
/>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
<Box mt={2} textAlign={'right'}>
<Button
variant={'base'}
leftIcon={<AddIcon fontSize={'10px'} />}
onClick={() => {
const newVariable = addVariable();
updateVariables(variables.concat(newVariable));
setEditVariable(newVariable);
}}
>
</Button>
</Box>
</Container>
</NodeCard>
{!!editVariable && (
<VariableEditModal
defaultVariable={editVariable}
onClose={() => setEditVariable(undefined)}
onSubmit={onclickSubmit}
/>
)}
</>
);
};
export default React.memo(NodeUserGuide);

View File

@@ -0,0 +1,618 @@
import React, { useMemo, useState } from 'react';
import type { SelectAppItemType } from '@fastgpt/global/core/module/type';
import type { FlowNodeInputItemType } from '@fastgpt/global/core/module/node/type';
import {
Box,
Textarea,
Input,
NumberInput,
NumberInputField,
NumberInputStepper,
NumberIncrementStepper,
NumberDecrementStepper,
Flex,
useDisclosure,
Button,
useTheme,
Grid
} from '@chakra-ui/react';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import dynamic from 'next/dynamic';
import { onChangeNode, useFlowProviderStore } from '../../FlowProvider';
import Avatar from '@/components/Avatar';
import MySelect from '@/components/Select';
import MySlider from '@/components/Slider';
import MyTooltip from '@/components/MyTooltip';
import TargetHandle from './TargetHandle';
import MyIcon from '@/components/Icon';
import { useTranslation } from 'react-i18next';
import { AIChatProps } from '@/types/core/aiChat';
import { chatModelList } from '@/web/common/system/staticData';
import { formatPrice } from '@fastgpt/global/common/bill/tools';
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
import { SelectedDatasetType } from '@/types/core/dataset';
import { useQuery } from '@tanstack/react-query';
import { LLMModelItemType } from '@/types/model';
import type { EditFieldModeType, EditFieldType } from '../modules/FieldEditModal';
const FieldEditModal = dynamic(() => import('../modules/FieldEditModal'));
const SelectAppModal = dynamic(() => import('../../SelectAppModal'));
const AIChatSettingsModal = dynamic(() => import('../../../AIChatSettingsModal'));
const DatasetSelectModal = dynamic(() => import('../../../DatasetSelectModal'));
export const Label = React.memo(function Label({
moduleId,
inputKey,
editFiledType = 'input',
...item
}: FlowNodeInputItemType & {
moduleId: string;
inputKey: string;
editFiledType?: EditFieldModeType;
}) {
const { required = false, description, edit, label, type, valueType } = item;
const [editField, setEditField] = useState<EditFieldType>();
return (
<Flex className="nodrag" cursor={'default'} alignItems={'center'} position={'relative'}>
<Box position={'relative'}>
{label}
{description && (
<MyTooltip label={description} forceShow>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</MyTooltip>
)}
{required && (
<Box
position={'absolute'}
top={'-2px'}
right={'-8px'}
color={'red.500'}
fontWeight={'bold'}
>
*
</Box>
)}
</Box>
{(type === FlowNodeInputTypeEnum.target || valueType) && (
<TargetHandle handleKey={inputKey} valueType={valueType} />
)}
{edit && (
<>
<MyIcon
name={'settingLight'}
w={'14px'}
cursor={'pointer'}
ml={3}
_hover={{ color: 'myBlue.600' }}
onClick={() =>
setEditField({
label: item.label,
valueType: item.valueType,
required: item.required,
key: inputKey,
description: item.description
})
}
/>
<MyIcon
className="delete"
name={'delete'}
w={'14px'}
cursor={'pointer'}
ml={2}
_hover={{ color: 'red.500' }}
onClick={() => {
onChangeNode({
moduleId,
type: 'delInput',
key: inputKey,
value: ''
});
}}
/>
</>
)}
{!!editField && (
<FieldEditModal
mode={editFiledType}
defaultField={editField}
onClose={() => setEditField(undefined)}
onSubmit={(e) => {
const data = {
...item,
...e
};
// same key
if (editField.key === data.key) {
onChangeNode({
moduleId,
type: 'updateInput',
key: data.key,
value: data
});
} else {
// diff key. del and add
onChangeNode({
moduleId,
type: 'replaceInput',
key: editField.key,
value: data
});
}
setEditField(undefined);
}}
/>
)}
</Flex>
);
});
const RenderInput = ({
flowInputList,
moduleId,
CustomComponent = {},
editFiledType
}: {
flowInputList: FlowNodeInputItemType[];
moduleId: string;
CustomComponent?: Record<string, (e: FlowNodeInputItemType) => React.ReactNode>;
editFiledType?: EditFieldModeType;
}) => {
const sortInputs = useMemo(
() => flowInputList.sort((a, b) => (a.key === FlowNodeInputTypeEnum.switch ? -1 : 1)),
[flowInputList]
);
return (
<>
{sortInputs.map(
(item) =>
item.type !== FlowNodeInputTypeEnum.hidden && (
<Box key={item.key} _notLast={{ mb: 7 }} position={'relative'}>
{!!item.label && (
<Label
editFiledType={editFiledType}
moduleId={moduleId}
inputKey={item.key}
{...item}
/>
)}
<Box mt={2} className={'nodrag'}>
{item.type === FlowNodeInputTypeEnum.numberInput && (
<NumberInputRender item={item} moduleId={moduleId} />
)}
{item.type === FlowNodeInputTypeEnum.input && (
<TextInputRender item={item} moduleId={moduleId} />
)}
{item.type === FlowNodeInputTypeEnum.textarea && (
<TextareaRender item={item} moduleId={moduleId} />
)}
{item.type === FlowNodeInputTypeEnum.select && (
<SelectRender item={item} moduleId={moduleId} />
)}
{item.type === FlowNodeInputTypeEnum.slider && (
<SliderRender item={item} moduleId={moduleId} />
)}
{item.type === FlowNodeInputTypeEnum.selectApp && (
<SelectAppRender item={item} moduleId={moduleId} />
)}
{item.type === FlowNodeInputTypeEnum.aiSettings && (
<AISetting inputs={sortInputs} item={item} moduleId={moduleId} />
)}
{item.type === FlowNodeInputTypeEnum.maxToken && (
<MaxTokenRender inputs={sortInputs} item={item} moduleId={moduleId} />
)}
{item.type === FlowNodeInputTypeEnum.selectChatModel && (
<SelectChatModelRender inputs={sortInputs} item={item} moduleId={moduleId} />
)}
{item.type === FlowNodeInputTypeEnum.selectDataset && (
<SelectDatasetRender item={item} moduleId={moduleId} />
)}
{item.type === FlowNodeInputTypeEnum.custom && CustomComponent[item.key] && (
<>{CustomComponent[item.key]({ ...item })}</>
)}
</Box>
</Box>
)
)}
</>
);
};
export default React.memo(RenderInput);
type RenderProps = {
inputs?: FlowNodeInputItemType[];
item: FlowNodeInputItemType;
moduleId: string;
};
var NumberInputRender = React.memo(function NumberInputRender({ item, moduleId }: RenderProps) {
return (
<NumberInput
defaultValue={item.value}
min={item.min}
max={item.max}
onChange={(e) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: Number(e)
}
});
}}
>
<NumberInputField />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
);
});
var TextInputRender = React.memo(function TextInputRender({ item, moduleId }: RenderProps) {
return (
<Input
placeholder={item.placeholder}
defaultValue={item.value}
onBlur={(e) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e.target.value
}
});
}}
/>
);
});
var TextareaRender = React.memo(function TextareaRender({ item, moduleId }: RenderProps) {
return (
<Textarea
rows={5}
placeholder={item.placeholder}
resize={'both'}
defaultValue={item.value}
onBlur={(e) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e.target.value
}
});
}}
/>
);
});
var SelectRender = React.memo(function SelectRender({ item, moduleId }: RenderProps) {
return (
<MySelect
width={'100%'}
value={item.value}
list={item.list || []}
onchange={(e) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e
}
});
}}
/>
);
});
var SliderRender = React.memo(function SliderRender({ item, moduleId }: RenderProps) {
return (
<Box pt={5} pb={4} px={2}>
<MySlider
markList={item.markList}
width={'100%'}
min={item.min || 0}
max={item.max}
step={item.step || 1}
value={item.value}
onChange={(e) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e
}
});
}}
/>
</Box>
);
});
var AISetting = React.memo(function AISetting({ inputs = [], moduleId }: RenderProps) {
const { t } = useTranslation();
const chatModulesData = useMemo(() => {
const obj: Record<string, any> = {};
inputs.forEach((item) => {
obj[item.key] = item.value;
});
return obj as AIChatProps;
}, [inputs]);
const {
isOpen: isOpenAIChatSetting,
onOpen: onOpenAIChatSetting,
onClose: onCloseAIChatSetting
} = useDisclosure();
return (
<>
<Button
variant={'base'}
leftIcon={<MyIcon name={'settingLight'} w={'14px'} />}
onClick={onOpenAIChatSetting}
>
{t('app.AI Settings')}
</Button>
{isOpenAIChatSetting && (
<AIChatSettingsModal
isAdEdit
onClose={onCloseAIChatSetting}
onSuccess={(e) => {
for (let key in e) {
const item = inputs.find((input) => input.key === key);
if (!item) continue;
onChangeNode({
moduleId,
type: 'updateInput',
key,
value: {
...item,
//@ts-ignore
value: e[key]
}
});
}
onCloseAIChatSetting();
}}
defaultData={chatModulesData}
/>
)}
</>
);
});
var MaxTokenRender = React.memo(function MaxTokenRender({
inputs = [],
item,
moduleId
}: RenderProps) {
const model = inputs.find((item) => item.key === 'model')?.value;
const modelData = chatModelList.find((item) => item.model === model);
const maxToken = modelData ? modelData.maxToken : 4000;
const markList = [
{ label: '100', value: 100 },
{ label: `${maxToken}`, value: maxToken }
];
return (
<Box pt={5} pb={4} px={2}>
<MySlider
markList={markList}
width={'100%'}
min={item.min || 100}
max={maxToken}
step={item.step || 1}
value={item.value}
onChange={(e) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e
}
});
}}
/>
</Box>
);
});
var SelectChatModelRender = React.memo(function SelectChatModelRender({
inputs = [],
item,
moduleId
}: RenderProps) {
const modelList = (item.customData?.() as LLMModelItemType[]) || chatModelList || [];
function onChangeModel(e: string) {
{
onChangeNode({
moduleId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e
}
});
// update max tokens
const model = modelList.find((item) => item.model === e) || modelList[0];
if (!model) return;
onChangeNode({
moduleId,
type: 'updateInput',
key: 'maxToken',
value: {
...inputs.find((input) => input.key === 'maxToken'),
markList: [
{ label: '100', value: 100 },
{ label: `${model.maxToken}`, value: model.maxToken }
],
max: model.maxToken,
value: model.maxToken / 2
}
});
}
}
const list = modelList.map((item) => {
const priceStr = `(${formatPrice(item.price, 1000)}元/1k Tokens)`;
return {
value: item.model,
label: `${item.name}${priceStr}`
};
});
if (!item.value && list.length > 0) {
onChangeModel(list[0].value);
}
return (
<MySelect
minW={'350px'}
width={'100%'}
value={item.value}
list={list}
onchange={onChangeModel}
/>
);
});
var SelectDatasetRender = React.memo(function SelectDatasetRender({ item, moduleId }: RenderProps) {
const theme = useTheme();
const { allDatasets, loadAllDatasets } = useDatasetStore();
const {
isOpen: isOpenKbSelect,
onOpen: onOpenKbSelect,
onClose: onCloseKbSelect
} = useDisclosure();
const showKbList = useMemo(() => {
const value = item.value as SelectedDatasetType;
return allDatasets.filter((dataset) => value.find((kb) => kb.datasetId === dataset._id));
}, [allDatasets, item.value]);
useQuery(['loadAllDatasets'], loadAllDatasets);
return (
<>
<Grid gridTemplateColumns={'repeat(2, minmax(0, 1fr))'} gridGap={4} minW={'350px'} w={'100%'}>
<Button h={'36px'} onClick={onOpenKbSelect}>
</Button>
{showKbList.map((item) => (
<Flex
key={item._id}
alignItems={'center'}
h={'36px'}
border={theme.borders.base}
px={2}
borderRadius={'md'}
>
<Avatar src={item.avatar} w={'24px'}></Avatar>
<Box
ml={3}
flex={'1 0 0'}
w={0}
className="textEllipsis"
fontWeight={'bold'}
fontSize={['md', 'lg', 'xl']}
>
{item.name}
</Box>
</Flex>
))}
</Grid>
<DatasetSelectModal
isOpen={isOpenKbSelect}
activeDatasets={item.value}
onChange={(e) => {
onChangeNode({
moduleId,
key: item.key,
type: 'updateInput',
value: {
...item,
value: e
}
});
}}
onClose={onCloseKbSelect}
/>
</>
);
});
var SelectAppRender = React.memo(function SelectAppRender({ item, moduleId }: RenderProps) {
const { filterAppIds } = useFlowProviderStore();
const theme = useTheme();
const {
isOpen: isOpenSelectApp,
onOpen: onOpenSelectApp,
onClose: onCloseSelectApp
} = useDisclosure();
const value = item.value as SelectAppItemType | undefined;
return (
<>
<Box onClick={onOpenSelectApp}>
{!value ? (
<Button variant={'base'} w={'100%'}>
</Button>
) : (
<Flex alignItems={'center'} border={theme.borders.base} borderRadius={'md'} px={3} py={2}>
<Avatar src={value?.logo} />
<Box fontWeight={'bold'} ml={1}>
{value?.name}
</Box>
</Flex>
)}
</Box>
{isOpenSelectApp && (
<SelectAppModal
defaultApps={item.value?.id ? [item.value.id] : []}
filterAppIds={filterAppIds}
onClose={onCloseSelectApp}
onSuccess={(e) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: 'app',
value: {
...item,
value: e[0]
}
});
}}
/>
)}
</>
);
});

View File

@@ -0,0 +1,158 @@
import React, { useMemo, useState } from 'react';
import type { FlowNodeOutputItemType } from '@fastgpt/global/core/module/node/type';
import { Box, Flex } from '@chakra-ui/react';
import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import MyTooltip from '@/components/MyTooltip';
import SourceHandle from './SourceHandle';
import MyIcon from '@/components/Icon';
import dynamic from 'next/dynamic';
import { onChangeNode } from '../../FlowProvider';
import { SystemOutputEnum } from '@/constants/app';
import type { EditFieldType, EditFieldModeType } from '../modules/FieldEditModal';
const FieldEditModal = dynamic(() => import('../modules/FieldEditModal'));
export const Label = ({
moduleId,
outputKey,
outputs,
editFiledType = 'output',
...item
}: FlowNodeOutputItemType & {
outputKey: string;
moduleId: string;
outputs: FlowNodeOutputItemType[];
editFiledType?: EditFieldModeType;
}) => {
const { label, description, edit } = item;
const [editField, setEditField] = useState<EditFieldType>();
return (
<Flex
className="nodrag"
cursor={'default'}
justifyContent={'right'}
alignItems={'center'}
position={'relative'}
>
{edit && (
<>
<MyIcon
name={'settingLight'}
w={'14px'}
cursor={'pointer'}
mr={3}
_hover={{ color: 'myBlue.600' }}
onClick={() =>
setEditField({
label: item.label,
valueType: item.valueType,
key: outputKey,
description: item.description
})
}
/>
<MyIcon
className="delete"
name={'delete'}
w={'14px'}
cursor={'pointer'}
mr={3}
_hover={{ color: 'red.500' }}
onClick={() => {
onChangeNode({
moduleId,
type: 'delOutput',
key: outputKey
});
}}
/>
</>
)}
{description && (
<MyTooltip label={description} forceShow>
<QuestionOutlineIcon display={['none', 'inline']} mr={1} />
</MyTooltip>
)}
<Box>{label}</Box>
{!!editField && (
<FieldEditModal
mode={editFiledType}
defaultField={editField}
onClose={() => setEditField(undefined)}
onSubmit={(e) => {
const data = {
...item,
...e
};
if (editField.key === data.key) {
onChangeNode({
moduleId,
type: 'updateOutput',
key: data.key,
value: data
});
} else {
onChangeNode({
moduleId,
type: 'replaceOutput',
key: editField.key,
value: data
});
}
setEditField(undefined);
}}
/>
)}
</Flex>
);
};
const RenderOutput = ({
moduleId,
flowOutputList,
editFiledType
}: {
moduleId: string;
flowOutputList: FlowNodeOutputItemType[];
editFiledType?: EditFieldModeType;
}) => {
const sortOutput = useMemo(
() =>
[...flowOutputList].sort((a, b) => {
if (a.key === SystemOutputEnum.finish) return -1;
if (b.key === SystemOutputEnum.finish) return 1;
return 0;
}),
[flowOutputList]
);
return (
<>
{sortOutput.map(
(item) =>
item.type !== FlowNodeOutputTypeEnum.hidden && (
<Box key={item.key} _notLast={{ mb: 7 }} position={'relative'}>
<Label
editFiledType={editFiledType}
moduleId={moduleId}
outputKey={item.key}
outputs={sortOutput}
{...item}
/>
<Box mt={FlowNodeOutputTypeEnum.answer ? 0 : 2} className={'nodrag'}>
{item.type === FlowNodeOutputTypeEnum.source && (
<SourceHandle handleKey={item.key} valueType={item.valueType} />
)}
</Box>
</Box>
)
)}
</>
);
};
export default React.memo(RenderOutput);

View File

@@ -0,0 +1,56 @@
import React, { useMemo, useTransition } from 'react';
import { Box, BoxProps } from '@chakra-ui/react';
import { Handle, Position } from 'reactflow';
import { FlowValueTypeStyle, FlowValueTypeTip } from '@/constants/flow';
import MyTooltip from '@/components/MyTooltip';
import { useTranslation } from 'next-i18next';
import { FlowNodeValTypeEnum } from '@fastgpt/global/core/module/node/constant';
interface Props extends BoxProps {
handleKey: string;
valueType?: `${FlowNodeValTypeEnum}`;
}
const SourceHandle = ({ handleKey, valueType, ...props }: Props) => {
const { t } = useTranslation();
const valType = valueType ?? FlowNodeValTypeEnum.any;
const valueStyle = useMemo(
() =>
valueType
? FlowValueTypeStyle[valueType]
: (FlowValueTypeStyle[FlowNodeValTypeEnum.any] as any),
[valueType]
);
return (
<Box
position={'absolute'}
top={'50%'}
right={'-16px'}
transform={'translate(50%,-50%)'}
{...props}
>
<MyTooltip
label={t('app.module.type', {
type: t(FlowValueTypeTip[valType].label),
example: FlowValueTypeTip[valType].example
})}
>
<Handle
style={{
width: '12px',
height: '12px',
...valueStyle
}}
type="source"
id={handleKey}
position={Position.Right}
/>
</MyTooltip>
</Box>
);
};
export default React.memo(SourceHandle);

View File

@@ -0,0 +1,57 @@
import React, { useMemo } from 'react';
import { Box, BoxProps } from '@chakra-ui/react';
import { Handle, OnConnect, Position } from 'reactflow';
import { FlowValueTypeStyle, FlowValueTypeTip } from '@/constants/flow';
import MyTooltip from '@/components/MyTooltip';
import { useTranslation } from 'next-i18next';
import { FlowNodeValTypeEnum } from '@fastgpt/global/core/module/node/constant';
interface Props extends BoxProps {
handleKey: string;
valueType?: `${FlowNodeValTypeEnum}`;
onConnect?: OnConnect;
}
const TargetHandle = ({ handleKey, valueType, onConnect, ...props }: Props) => {
const { t } = useTranslation();
const valType = valueType ?? FlowNodeValTypeEnum.any;
const valueStyle = useMemo(
() =>
valueType
? FlowValueTypeStyle[valueType]
: (FlowValueTypeStyle[FlowNodeValTypeEnum.any] as any),
[valueType]
);
return (
<Box
key={handleKey}
position={'absolute'}
top={'50%'}
left={'-16px'}
transform={'translate(50%,-50%)'}
{...props}
>
<MyTooltip
label={t('app.module.type', {
type: t(FlowValueTypeTip[valType].label),
example: FlowValueTypeTip[valType].example
})}
>
<Handle
style={{
width: '12px',
height: '12px',
...valueStyle
}}
type="target"
id={handleKey}
position={Position.Left}
/>
</MyTooltip>
</Box>
);
};
export default React.memo(TargetHandle);

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

@@ -0,0 +1,209 @@
import React, { useState } from 'react';
import {
Box,
Button,
ModalHeader,
ModalFooter,
ModalBody,
NumberInput,
NumberInputField,
NumberInputStepper,
NumberIncrementStepper,
NumberDecrementStepper,
Flex,
Switch,
Input,
Grid,
FormControl,
useTheme
} from '@chakra-ui/react';
import { SmallAddIcon } from '@chakra-ui/icons';
import { VariableInputEnum } from '@/constants/app';
import type { VariableItemType } from '@/types/app';
import MyIcon from '@/components/Icon';
import { useForm } from 'react-hook-form';
import { useFieldArray } from 'react-hook-form';
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
import MyModal from '@/components/MyModal';
const VariableTypeList = [
{ label: '文本', icon: 'settingLight', key: VariableInputEnum.input },
{ label: '下拉单选', icon: 'settingLight', key: VariableInputEnum.select }
];
export type VariableFormType = {
variable: VariableItemType;
};
const VariableEditModal = ({
defaultVariable,
onClose,
onSubmit
}: {
defaultVariable: VariableItemType;
onClose: () => void;
onSubmit: (data: VariableFormType) => void;
}) => {
const theme = useTheme();
const [refresh, setRefresh] = useState(false);
const { reset, getValues, setValue, register, control, handleSubmit } = useForm<VariableFormType>(
{
defaultValues: {
variable: defaultVariable
}
}
);
const {
fields: selectEnums,
append: appendEnums,
remove: removeEnums
} = useFieldArray({
control,
name: 'variable.enums'
});
return (
<MyModal isOpen={true} onClose={onClose}>
<ModalHeader display={'flex'}>
<MyIcon name={'variable'} mr={2} w={'24px'} color={'#FF8A4C'} />
</ModalHeader>
<ModalBody>
<Flex alignItems={'center'}>
<Box w={'70px'}></Box>
<Switch {...register('variable.required')} />
</Flex>
<Flex mt={5} alignItems={'center'}>
<Box w={'80px'}></Box>
<Input {...register('variable.label', { required: '变量名不能为空' })} />
</Flex>
<Flex mt={5} alignItems={'center'}>
<Box w={'80px'}> key</Box>
<Input {...register('variable.key', { required: '变量 key 不能为空' })} />
</Flex>
<Box mt={5} mb={2}>
</Box>
<Grid gridTemplateColumns={'repeat(2,130px)'} gridGap={4}>
{VariableTypeList.map((item) => (
<Flex
key={item.key}
px={4}
py={1}
border={theme.borders.base}
borderRadius={'md'}
cursor={'pointer'}
{...(item.key === getValues('variable.type')
? {
bg: 'myWhite.600'
}
: {
_hover: {
boxShadow: 'md'
},
onClick: () => {
setValue('variable.type', item.key);
setRefresh(!refresh);
}
})}
>
<MyIcon name={item.icon as any} w={'16px'} />
<Box ml={3}>{item.label}</Box>
</Flex>
))}
</Grid>
{getValues('variable.type') === VariableInputEnum.input && (
<>
<Box mt={5} mb={2}>
</Box>
<Box>
<NumberInput max={100} min={1} step={1} position={'relative'}>
<NumberInputField
{...register('variable.maxLen', {
min: 1,
max: 100,
valueAsNumber: true
})}
max={100}
/>
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
</Box>
</>
)}
{getValues('variable.type') === VariableInputEnum.select && (
<>
<Box mt={5} mb={2}>
</Box>
<Box>
{selectEnums.map((item, i) => (
<Flex key={item.id} mb={2} alignItems={'center'}>
<FormControl>
<Input
{...register(`variable.enums.${i}.value`, {
required: '选项内容不能为空'
})}
/>
</FormControl>
<MyIcon
ml={3}
name={'delete'}
w={'16px'}
cursor={'pointer'}
p={2}
borderRadius={'lg'}
_hover={{ bg: 'red.100' }}
onClick={() => removeEnums(i)}
/>
</Flex>
))}
</Box>
<Button
variant={'solid'}
w={'100%'}
textAlign={'left'}
leftIcon={<SmallAddIcon />}
bg={'myGray.100 !important'}
onClick={() => appendEnums({ value: '' })}
>
</Button>
</>
)}
</ModalBody>
<ModalFooter>
<Button variant={'base'} mr={3} onClick={onClose}>
</Button>
<Button onClick={handleSubmit(onSubmit)}></Button>
</ModalFooter>
</MyModal>
);
};
export default React.memo(VariableEditModal);
export const defaultVariable: VariableItemType = {
id: nanoid(),
key: 'key',
label: 'label',
type: VariableInputEnum.input,
required: true,
maxLen: 50,
enums: [{ value: '' }]
};
export const addVariable = () => {
const newVariable = { ...defaultVariable, key: nanoid(), id: nanoid() };
return newVariable;
};