mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-28 09:03:53 +00:00
feat: undo-redo & edit snapshots (#2436)
* feat: undo-redo & edit snapshots * fix merge * add simple history back * fix some undo * change app latest version * fix * chatconfig * fix snapshot * fix * fix * fix * fix compare * fix initial * fix merge: * fix useEffect * fix snapshot initial and saved state * chore * fix * compare snapshot * nodes edges useEffct * fix chatconfig * fix * delete unused method * fix * fix * fix * default version name
This commit is contained in:
2
projects/app/src/global/core/app/api.d.ts
vendored
2
projects/app/src/global/core/app/api.d.ts
vendored
@@ -20,6 +20,8 @@ export type PostPublishAppProps = {
|
||||
nodes: AppSchema['modules'];
|
||||
edges: AppSchema['edges'];
|
||||
chatConfig: AppSchema['chatConfig'];
|
||||
isPublish?: boolean;
|
||||
versionName?: string;
|
||||
};
|
||||
|
||||
export type PostRevertAppProps = {
|
||||
|
@@ -14,6 +14,8 @@ import { defaultNodeVersion } from '@fastgpt/global/core/workflow/node/constant'
|
||||
import { ClientSession } from '@fastgpt/service/common/mongo';
|
||||
import { authApp } from '@fastgpt/service/support/permission/app/auth';
|
||||
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
|
||||
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
|
||||
import { MongoUser } from '@fastgpt/service/support/user/schema';
|
||||
|
||||
export type CreateAppBody = {
|
||||
parentId?: ParentIdType;
|
||||
@@ -42,6 +44,8 @@ async function handler(req: ApiRequestProps<CreateAppBody>) {
|
||||
|
||||
// 上限校验
|
||||
await checkTeamAppLimit(teamId);
|
||||
const tmb = await MongoTeamMember.findById({ _id: tmbId });
|
||||
const user = await MongoUser.findById({ _id: tmb?.userId });
|
||||
|
||||
// 创建app
|
||||
const appId = await onCreateApp({
|
||||
@@ -53,7 +57,9 @@ async function handler(req: ApiRequestProps<CreateAppBody>) {
|
||||
edges,
|
||||
chatConfig,
|
||||
teamId,
|
||||
tmbId
|
||||
tmbId,
|
||||
userAvatar: user?.avatar,
|
||||
username: user?.username
|
||||
});
|
||||
|
||||
return appId;
|
||||
@@ -73,6 +79,8 @@ export const onCreateApp = async ({
|
||||
teamId,
|
||||
tmbId,
|
||||
pluginData,
|
||||
username,
|
||||
userAvatar,
|
||||
session
|
||||
}: {
|
||||
parentId?: ParentIdType;
|
||||
@@ -86,6 +94,8 @@ export const onCreateApp = async ({
|
||||
teamId: string;
|
||||
tmbId: string;
|
||||
pluginData?: AppSchema['pluginData'];
|
||||
username?: string;
|
||||
userAvatar?: string;
|
||||
session?: ClientSession;
|
||||
}) => {
|
||||
const create = async (session: ClientSession) => {
|
||||
@@ -117,7 +127,10 @@ export const onCreateApp = async ({
|
||||
appId,
|
||||
nodes: modules,
|
||||
edges,
|
||||
chatConfig
|
||||
chatConfig,
|
||||
versionName: name,
|
||||
username,
|
||||
avatar: userAvatar
|
||||
}
|
||||
],
|
||||
{ session }
|
||||
|
@@ -10,13 +10,18 @@ import { PostPublishAppProps } from '@/global/core/app/api';
|
||||
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
|
||||
type Response = {};
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse<any>): Promise<{}> {
|
||||
const { appId } = req.query as { appId: string };
|
||||
const { nodes = [], edges = [], chatConfig, type } = req.body as PostPublishAppProps;
|
||||
const {
|
||||
nodes = [],
|
||||
edges = [],
|
||||
chatConfig,
|
||||
type,
|
||||
isPublish,
|
||||
versionName
|
||||
} = req.body as PostPublishAppProps;
|
||||
|
||||
const { app } = await authApp({ appId, req, per: WritePermissionVal, authToken: true });
|
||||
const { app, tmbId } = await authApp({ appId, req, per: WritePermissionVal, authToken: true });
|
||||
|
||||
const { nodes: formatNodes } = beforeUpdateAppFormat({ nodes });
|
||||
|
||||
@@ -28,7 +33,10 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>): Promise<
|
||||
appId,
|
||||
nodes: formatNodes,
|
||||
edges,
|
||||
chatConfig
|
||||
chatConfig,
|
||||
isPublish,
|
||||
versionName,
|
||||
tmbId
|
||||
}
|
||||
],
|
||||
{ session }
|
||||
|
27
projects/app/src/pages/api/core/app/version/update.ts
Normal file
27
projects/app/src/pages/api/core/app/version/update.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { authApp } from '@fastgpt/service/support/permission/app/auth';
|
||||
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { ApiRequestProps } from '@fastgpt/service/type/next';
|
||||
import { MongoAppVersion } from '@fastgpt/service/core/app/version/schema';
|
||||
|
||||
export type UpdateAppVersionBody = {
|
||||
appId: string;
|
||||
versionId: string;
|
||||
versionName: string;
|
||||
};
|
||||
|
||||
async function handler(req: ApiRequestProps<UpdateAppVersionBody>) {
|
||||
const { appId, versionId, versionName } = req.body;
|
||||
await authApp({ appId, req, per: WritePermissionVal, authToken: true });
|
||||
|
||||
await MongoAppVersion.findByIdAndUpdate(
|
||||
{ _id: versionId },
|
||||
{
|
||||
versionName
|
||||
}
|
||||
);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
@@ -45,7 +45,7 @@ async function handler(
|
||||
}
|
||||
|
||||
// get app and history
|
||||
const [{ histories }, { nodes }] = await Promise.all([
|
||||
const [{ histories }, { nodes, chatConfig }] = await Promise.all([
|
||||
getChatItems({
|
||||
appId,
|
||||
chatId,
|
||||
@@ -68,7 +68,7 @@ async function handler(
|
||||
history: app.type === AppTypeEnum.plugin ? histories : transformPreviewHistories(histories),
|
||||
app: {
|
||||
chatConfig: getAppChatConfig({
|
||||
chatConfig: app.chatConfig,
|
||||
chatConfig,
|
||||
systemConfigNode: getGuideModule(nodes),
|
||||
storeVariables: chat?.variableList,
|
||||
storeWelcomeText: chat?.welcomeText,
|
||||
|
@@ -42,7 +42,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
throw new Error(ChatErrEnum.unAuthChat);
|
||||
}
|
||||
|
||||
const [{ histories }, { nodes }] = await Promise.all([
|
||||
const [{ histories }, { nodes, chatConfig }] = await Promise.all([
|
||||
getChatItems({
|
||||
appId: app._id,
|
||||
chatId,
|
||||
@@ -75,7 +75,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
history: app.type === AppTypeEnum.plugin ? histories : transformPreviewHistories(histories),
|
||||
app: {
|
||||
chatConfig: getAppChatConfig({
|
||||
chatConfig: app.chatConfig,
|
||||
chatConfig,
|
||||
systemConfigNode: getGuideModule(nodes),
|
||||
storeVariables: chat?.variableList,
|
||||
storeWelcomeText: chat?.welcomeText,
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@fastgpt/service/common/response';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { getGuideModule, getAppChatConfig } from '@fastgpt/global/core/workflow/utils';
|
||||
import { getChatModelNameListByModules } from '@/service/core/app/workflow';
|
||||
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
|
||||
@@ -48,7 +47,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
}
|
||||
|
||||
// get app and history
|
||||
const [{ histories }, { nodes }] = await Promise.all([
|
||||
const [{ histories }, { nodes, chatConfig }] = await Promise.all([
|
||||
getChatItems({
|
||||
appId,
|
||||
chatId,
|
||||
@@ -76,7 +75,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
history: app.type === AppTypeEnum.plugin ? histories : transformPreviewHistories(histories),
|
||||
app: {
|
||||
chatConfig: getAppChatConfig({
|
||||
chatConfig: app.chatConfig,
|
||||
chatConfig,
|
||||
systemConfigNode: getGuideModule(nodes),
|
||||
storeVariables: chat?.variableList,
|
||||
storeWelcomeText: chat?.welcomeText,
|
||||
|
@@ -1,68 +1,123 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { Box, Flex, Button, IconButton } from '@chakra-ui/react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Button,
|
||||
IconButton,
|
||||
HStack,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
useDisclosure
|
||||
} from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useBeforeunload } from '@fastgpt/web/hooks/useBeforeunload';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext, getWorkflowStore } from '../WorkflowComponents/context';
|
||||
import { useInterval } from 'ahooks';
|
||||
import { AppContext, TabEnum } from '../context';
|
||||
import PopoverConfirm from '@fastgpt/web/components/common/MyPopover/PopoverConfirm';
|
||||
import RouteTab from '../RouteTab';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
import AppCard from '../WorkflowComponents/AppCard';
|
||||
import { uiWorkflow2StoreWorkflow } from '../WorkflowComponents/utils';
|
||||
import RouteTab from '../RouteTab';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
const PublishHistories = dynamic(() => import('../PublishHistoriesSlider'));
|
||||
import MyPopover from '@fastgpt/web/components/common/MyPopover';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { compareSnapshot } from '@/web/core/workflow/utils';
|
||||
import SaveAndPublishModal from '../WorkflowComponents/Flow/components/SaveAndPublish';
|
||||
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
|
||||
|
||||
const PublishHistories = dynamic(() => import('../WorkflowPublishHistoriesSlider'));
|
||||
|
||||
const Header = () => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const { isPc } = useSystem();
|
||||
const router = useRouter();
|
||||
|
||||
const { appDetail, onPublish, currentTab } = useContextSelector(AppContext, (v) => v);
|
||||
const isV2Workflow = appDetail?.version === 'v2';
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const {
|
||||
isOpen: isSaveAndPublishModalOpen,
|
||||
onOpen: onSaveAndPublishModalOpen,
|
||||
onClose: onSaveAndPublishModalClose
|
||||
} = useDisclosure();
|
||||
const [isSave, setIsSave] = useState(false);
|
||||
|
||||
const {
|
||||
flowData2StoreData,
|
||||
flowData2StoreDataAndCheck,
|
||||
onSaveWorkflow,
|
||||
setHistoriesDefaultData,
|
||||
setWorkflowTestData,
|
||||
setHistoriesDefaultData,
|
||||
historiesDefaultData,
|
||||
initData
|
||||
nodes,
|
||||
edges,
|
||||
past,
|
||||
future,
|
||||
setPast
|
||||
} = useContextSelector(WorkflowContext, (v) => v);
|
||||
|
||||
const onclickPublish = useCallback(async () => {
|
||||
const data = flowData2StoreDataAndCheck();
|
||||
if (data) {
|
||||
await onPublish({
|
||||
...data,
|
||||
chatConfig: appDetail.chatConfig,
|
||||
//@ts-ignore
|
||||
version: 'v2'
|
||||
});
|
||||
}
|
||||
}, [flowData2StoreDataAndCheck, onPublish, appDetail.chatConfig]);
|
||||
const isPublished = useMemo(() => {
|
||||
const savedSnapshot =
|
||||
future.findLast((snapshot) => snapshot.isSaved) || past.find((snapshot) => snapshot.isSaved);
|
||||
|
||||
const saveAndBack = useCallback(async () => {
|
||||
return compareSnapshot(
|
||||
{
|
||||
nodes: savedSnapshot?.nodes,
|
||||
edges: savedSnapshot?.edges,
|
||||
chatConfig: savedSnapshot?.chatConfig
|
||||
},
|
||||
{
|
||||
nodes: nodes,
|
||||
edges: edges,
|
||||
chatConfig: appDetail.chatConfig
|
||||
}
|
||||
);
|
||||
}, [future, past, nodes, edges, appDetail.chatConfig]);
|
||||
|
||||
const { runAsync: onClickSave, loading } = useRequest2(
|
||||
async ({
|
||||
isPublish,
|
||||
versionName = formatTime2YMDHMS(new Date())
|
||||
}: {
|
||||
isPublish?: boolean;
|
||||
versionName?: string;
|
||||
}) => {
|
||||
const data = flowData2StoreData();
|
||||
|
||||
if (data) {
|
||||
await onPublish({
|
||||
...data,
|
||||
isPublish,
|
||||
versionName,
|
||||
chatConfig: appDetail.chatConfig,
|
||||
//@ts-ignore
|
||||
version: 'v2'
|
||||
});
|
||||
setPast((prevPast) =>
|
||||
prevPast.map((item, index) =>
|
||||
index === prevPast.length - 1
|
||||
? {
|
||||
...item,
|
||||
isSaved: true
|
||||
}
|
||||
: item
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const onBack = useCallback(async () => {
|
||||
try {
|
||||
await onSaveWorkflow();
|
||||
localStorage.removeItem(`${appDetail._id}-past`);
|
||||
localStorage.removeItem(`${appDetail._id}-future`);
|
||||
router.push('/app/list');
|
||||
} catch (error) {}
|
||||
}, [onSaveWorkflow, router]);
|
||||
|
||||
// effect
|
||||
useBeforeunload({
|
||||
callback: onSaveWorkflow,
|
||||
tip: t('common:core.common.tip.leave page')
|
||||
});
|
||||
useInterval(() => {
|
||||
if (!appDetail._id) return;
|
||||
onSaveWorkflow();
|
||||
}, 40000);
|
||||
}, [appDetail._id, router]);
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
@@ -78,9 +133,10 @@ const Header = () => {
|
||||
pl={[2, 4]}
|
||||
pr={[2, 6]}
|
||||
borderBottom={'base'}
|
||||
alignItems={'center'}
|
||||
alignItems={['flex-start', 'center']}
|
||||
userSelect={'none'}
|
||||
h={'67px'}
|
||||
h={['auto', '67px']}
|
||||
flexWrap={'wrap'}
|
||||
{...(currentTab === TabEnum.appEdit
|
||||
? {
|
||||
bg: 'myGray.25'
|
||||
@@ -95,15 +151,36 @@ const Header = () => {
|
||||
name={'common/leftArrowLight'}
|
||||
w={'1.75rem'}
|
||||
cursor={'pointer'}
|
||||
onClick={saveAndBack}
|
||||
onClick={isPublished ? onBack : onOpen}
|
||||
/>
|
||||
<MyModal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
iconSrc="common/warn"
|
||||
title={t('common:common.Exit')}
|
||||
w={'400px'}
|
||||
>
|
||||
<ModalBody>
|
||||
<Box>{t('workflow:workflow.exit_tips')}</Box>
|
||||
</ModalBody>
|
||||
<ModalFooter gap={3}>
|
||||
<Button variant={'whiteDanger'} onClick={onBack}>
|
||||
{t('common:common.Exit Directly')}
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={loading}
|
||||
onClick={async () => {
|
||||
await onClickSave({});
|
||||
onBack();
|
||||
}}
|
||||
>
|
||||
{t('common:common.Save_and_exit')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
{/* app info */}
|
||||
<Box ml={1}>
|
||||
<AppCard
|
||||
showSaveStatus={
|
||||
!historiesDefaultData && isV2Workflow && currentTab === TabEnum.appEdit
|
||||
}
|
||||
/>
|
||||
<AppCard isPublished={isPublished} showSaveStatus={isV2Workflow} />
|
||||
</Box>
|
||||
|
||||
{isPc && (
|
||||
@@ -114,10 +191,9 @@ const Header = () => {
|
||||
<Box flex={1} />
|
||||
|
||||
{currentTab === TabEnum.appEdit && (
|
||||
<>
|
||||
<HStack flexDirection={['column', 'row']} spacing={[2, 3]}>
|
||||
{!historiesDefaultData && (
|
||||
<IconButton
|
||||
mr={[2, 4]}
|
||||
icon={<MyIcon name={'history'} w={'18px'} />}
|
||||
aria-label={''}
|
||||
size={'sm'}
|
||||
@@ -145,52 +221,109 @@ const Header = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('common:core.workflow.run_test')}
|
||||
{t('common:core.workflow.Run')}
|
||||
</Button>
|
||||
|
||||
{!historiesDefaultData && (
|
||||
<PopoverConfirm
|
||||
showCancel
|
||||
content={t('common:core.app.Publish Confirm')}
|
||||
<MyPopover
|
||||
placement={'bottom-end'}
|
||||
hasArrow={false}
|
||||
offset={[2, 4]}
|
||||
w={'116px'}
|
||||
onOpenFunc={() => setIsSave(true)}
|
||||
onCloseFunc={() => setIsSave(false)}
|
||||
trigger={'hover'}
|
||||
Trigger={
|
||||
<Button
|
||||
ml={[2, 4]}
|
||||
size={'sm'}
|
||||
leftIcon={<MyIcon name={'common/publishFill'} w={['14px', '16px']} />}
|
||||
rightIcon={
|
||||
<MyIcon
|
||||
name={isSave ? 'core/chat/chevronUp' : 'core/chat/chevronDown'}
|
||||
w={['14px', '16px']}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{t('common:core.app.Publish')}
|
||||
<Box>{t('common:common.Save')}</Box>
|
||||
</Button>
|
||||
}
|
||||
onConfirm={() => onclickPublish()}
|
||||
/>
|
||||
>
|
||||
{({}) => (
|
||||
<MyBox p={1.5}>
|
||||
<MyBox
|
||||
display={'flex'}
|
||||
size={'md'}
|
||||
px={1}
|
||||
py={1.5}
|
||||
rounded={'4px'}
|
||||
_hover={{ color: 'primary.600', bg: 'rgba(17, 24, 36, 0.05)' }}
|
||||
cursor={'pointer'}
|
||||
isLoading={loading}
|
||||
onClick={async () => {
|
||||
await onClickSave({});
|
||||
}}
|
||||
>
|
||||
<MyIcon name={'core/workflow/upload'} w={'16px'} mr={2} />
|
||||
<Box fontSize={'sm'}>{t('common:core.workflow.Save to cloud')}</Box>
|
||||
</MyBox>
|
||||
<Flex
|
||||
px={1}
|
||||
py={1.5}
|
||||
rounded={'4px'}
|
||||
_hover={{ color: 'primary.600', bg: 'rgba(17, 24, 36, 0.05)' }}
|
||||
cursor={'pointer'}
|
||||
onClick={() => {
|
||||
const data = flowData2StoreDataAndCheck();
|
||||
if (data) {
|
||||
onSaveAndPublishModalOpen();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MyIcon name={'core/workflow/publish'} w={'16px'} mr={2} />
|
||||
<Box fontSize={'sm'}>{t('common:core.workflow.Save and publish')}</Box>
|
||||
{isSaveAndPublishModalOpen && (
|
||||
<SaveAndPublishModal
|
||||
isLoading={loading}
|
||||
onClose={onSaveAndPublishModalClose}
|
||||
onClickSave={onClickSave}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
</MyBox>
|
||||
)}
|
||||
</MyPopover>
|
||||
)}
|
||||
</>
|
||||
</HStack>
|
||||
)}
|
||||
</Flex>
|
||||
{historiesDefaultData && (
|
||||
{historiesDefaultData && isV2Workflow && currentTab === TabEnum.appEdit && (
|
||||
<PublishHistories
|
||||
initData={initData}
|
||||
onClose={() => {
|
||||
setHistoriesDefaultData(undefined);
|
||||
}}
|
||||
defaultData={historiesDefaultData}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}, [
|
||||
appDetail.chatConfig,
|
||||
currentTab,
|
||||
flowData2StoreDataAndCheck,
|
||||
historiesDefaultData,
|
||||
initData,
|
||||
isPc,
|
||||
currentTab,
|
||||
isPublished,
|
||||
isOpen,
|
||||
onClose,
|
||||
t,
|
||||
loading,
|
||||
isV2Workflow,
|
||||
onclickPublish,
|
||||
saveAndBack,
|
||||
historiesDefaultData,
|
||||
isSave,
|
||||
onBack,
|
||||
onOpen,
|
||||
onClickSave,
|
||||
setHistoriesDefaultData,
|
||||
appDetail.chatConfig,
|
||||
flowData2StoreDataAndCheck,
|
||||
setWorkflowTestData,
|
||||
t
|
||||
isSaveAndPublishModalOpen,
|
||||
onSaveAndPublishModalClose,
|
||||
onSaveAndPublishModalOpen
|
||||
]);
|
||||
|
||||
return Render;
|
||||
|
@@ -1,23 +1,36 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { Box, Flex, Button, IconButton, HStack } from '@chakra-ui/react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Button,
|
||||
IconButton,
|
||||
HStack,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
useDisclosure
|
||||
} from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useBeforeunload } from '@fastgpt/web/hooks/useBeforeunload';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext, getWorkflowStore } from '../WorkflowComponents/context';
|
||||
import { useInterval } from 'ahooks';
|
||||
import { AppContext, TabEnum } from '../context';
|
||||
import RouteTab from '../RouteTab';
|
||||
import PopoverConfirm from '@fastgpt/web/components/common/MyPopover/PopoverConfirm';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
import AppCard from '../WorkflowComponents/AppCard';
|
||||
import { uiWorkflow2StoreWorkflow } from '../WorkflowComponents/utils';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
const PublishHistories = dynamic(() => import('../PublishHistoriesSlider'));
|
||||
import MyPopover from '@fastgpt/web/components/common/MyPopover';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { compareSnapshot } from '@/web/core/workflow/utils';
|
||||
import SaveAndPublishModal from '../WorkflowComponents/Flow/components/SaveAndPublish';
|
||||
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
|
||||
|
||||
const PublishHistories = dynamic(() => import('../WorkflowPublishHistoriesSlider'));
|
||||
|
||||
const Header = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -26,44 +39,85 @@ const Header = () => {
|
||||
|
||||
const { appDetail, onPublish, currentTab } = useContextSelector(AppContext, (v) => v);
|
||||
const isV2Workflow = appDetail?.version === 'v2';
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const {
|
||||
isOpen: isSaveAndPublishModalOpen,
|
||||
onOpen: onSaveAndPublishModalOpen,
|
||||
onClose: onSaveAndPublishModalClose
|
||||
} = useDisclosure();
|
||||
const [isSave, setIsSave] = useState(false);
|
||||
|
||||
const {
|
||||
flowData2StoreData,
|
||||
flowData2StoreDataAndCheck,
|
||||
setWorkflowTestData,
|
||||
onSaveWorkflow,
|
||||
setHistoriesDefaultData,
|
||||
historiesDefaultData,
|
||||
initData
|
||||
nodes,
|
||||
edges,
|
||||
past,
|
||||
future,
|
||||
setPast
|
||||
} = useContextSelector(WorkflowContext, (v) => v);
|
||||
|
||||
const onclickPublish = useCallback(async () => {
|
||||
const data = flowData2StoreDataAndCheck();
|
||||
if (data) {
|
||||
await onPublish({
|
||||
...data,
|
||||
chatConfig: appDetail.chatConfig,
|
||||
//@ts-ignore
|
||||
version: 'v2'
|
||||
});
|
||||
}
|
||||
}, [flowData2StoreDataAndCheck, onPublish, appDetail.chatConfig]);
|
||||
const isPublished = useMemo(() => {
|
||||
const savedSnapshot =
|
||||
future.findLast((snapshot) => snapshot.isSaved) || past.find((snapshot) => snapshot.isSaved);
|
||||
|
||||
const saveAndBack = useCallback(async () => {
|
||||
return compareSnapshot(
|
||||
{
|
||||
nodes: savedSnapshot?.nodes,
|
||||
edges: savedSnapshot?.edges,
|
||||
chatConfig: savedSnapshot?.chatConfig
|
||||
},
|
||||
{
|
||||
nodes: nodes,
|
||||
edges: edges,
|
||||
chatConfig: appDetail.chatConfig
|
||||
}
|
||||
);
|
||||
}, [future, past, nodes, edges, appDetail.chatConfig]);
|
||||
|
||||
const { runAsync: onClickSave, loading } = useRequest2(
|
||||
async ({
|
||||
isPublish,
|
||||
versionName = formatTime2YMDHMS(new Date())
|
||||
}: {
|
||||
isPublish?: boolean;
|
||||
versionName?: string;
|
||||
}) => {
|
||||
const data = flowData2StoreData();
|
||||
|
||||
if (data) {
|
||||
await onPublish({
|
||||
...data,
|
||||
isPublish,
|
||||
versionName,
|
||||
chatConfig: appDetail.chatConfig,
|
||||
//@ts-ignore
|
||||
version: 'v2'
|
||||
});
|
||||
setPast((prevPast) =>
|
||||
prevPast.map((item, index) =>
|
||||
index === prevPast.length - 1
|
||||
? {
|
||||
...item,
|
||||
isSaved: true
|
||||
}
|
||||
: item
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const onBack = useCallback(async () => {
|
||||
try {
|
||||
await onSaveWorkflow();
|
||||
localStorage.removeItem(`${appDetail._id}-past`);
|
||||
localStorage.removeItem(`${appDetail._id}-future`);
|
||||
router.push('/app/list');
|
||||
} catch (error) {}
|
||||
}, [onSaveWorkflow, router]);
|
||||
|
||||
// effect
|
||||
useBeforeunload({
|
||||
callback: onSaveWorkflow,
|
||||
tip: t('common:core.common.tip.leave page')
|
||||
});
|
||||
useInterval(() => {
|
||||
if (!appDetail._id) return;
|
||||
onSaveWorkflow();
|
||||
}, 40000);
|
||||
}, [appDetail._id, router]);
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
@@ -97,15 +151,36 @@ const Header = () => {
|
||||
name={'common/leftArrowLight'}
|
||||
w={'1.75rem'}
|
||||
cursor={'pointer'}
|
||||
onClick={saveAndBack}
|
||||
onClick={isPublished ? onBack : onOpen}
|
||||
/>
|
||||
<MyModal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
iconSrc="common/warn"
|
||||
title={t('common:common.Exit')}
|
||||
w={'400px'}
|
||||
>
|
||||
<ModalBody>
|
||||
<Box>{t('workflow:workflow.exit_tips')}</Box>
|
||||
</ModalBody>
|
||||
<ModalFooter gap={3}>
|
||||
<Button variant={'whiteDanger'} onClick={onBack}>
|
||||
{t('common:common.Exit Directly')}
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={loading}
|
||||
onClick={async () => {
|
||||
await onClickSave({});
|
||||
onBack();
|
||||
}}
|
||||
>
|
||||
{t('common:common.Save_and_exit')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
{/* app info */}
|
||||
<Box ml={1}>
|
||||
<AppCard
|
||||
showSaveStatus={
|
||||
!historiesDefaultData && isV2Workflow && currentTab === TabEnum.appEdit
|
||||
}
|
||||
/>
|
||||
<AppCard isPublished={isPublished} showSaveStatus={isV2Workflow} />
|
||||
</Box>
|
||||
|
||||
{isPc && (
|
||||
@@ -146,38 +221,84 @@ const Header = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('common:core.workflow.run_test')}
|
||||
{t('common:core.workflow.Run')}
|
||||
</Button>
|
||||
|
||||
{!historiesDefaultData && (
|
||||
<PopoverConfirm
|
||||
showCancel
|
||||
content={t('common:core.app.Publish Confirm')}
|
||||
<MyPopover
|
||||
placement={'bottom-end'}
|
||||
hasArrow={false}
|
||||
offset={[2, 4]}
|
||||
w={'116px'}
|
||||
onOpenFunc={() => setIsSave(true)}
|
||||
onCloseFunc={() => setIsSave(false)}
|
||||
trigger={'hover'}
|
||||
Trigger={
|
||||
<Box>
|
||||
<MyTooltip label={t('common:core.app.Publish app tip')}>
|
||||
<Button
|
||||
size={'sm'}
|
||||
leftIcon={<MyIcon name={'common/publishFill'} w={['14px', '16px']} />}
|
||||
>
|
||||
{t('common:core.app.Publish')}
|
||||
</Button>
|
||||
</MyTooltip>
|
||||
</Box>
|
||||
<Button
|
||||
size={'sm'}
|
||||
rightIcon={
|
||||
<MyIcon
|
||||
name={isSave ? 'core/chat/chevronUp' : 'core/chat/chevronDown'}
|
||||
w={['14px', '16px']}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Box>{t('common:common.Save')}</Box>
|
||||
</Button>
|
||||
}
|
||||
onConfirm={() => onclickPublish()}
|
||||
/>
|
||||
>
|
||||
{({}) => (
|
||||
<MyBox p={1.5}>
|
||||
<MyBox
|
||||
display={'flex'}
|
||||
size={'md'}
|
||||
px={1}
|
||||
py={1.5}
|
||||
rounded={'4px'}
|
||||
_hover={{ color: 'primary.600', bg: 'rgba(17, 24, 36, 0.05)' }}
|
||||
cursor={'pointer'}
|
||||
isLoading={loading}
|
||||
onClick={async () => {
|
||||
await onClickSave({});
|
||||
}}
|
||||
>
|
||||
<MyIcon name={'core/workflow/upload'} w={'16px'} mr={2} />
|
||||
<Box fontSize={'sm'}>{t('common:core.workflow.Save to cloud')}</Box>
|
||||
</MyBox>
|
||||
<Flex
|
||||
px={1}
|
||||
py={1.5}
|
||||
rounded={'4px'}
|
||||
_hover={{ color: 'primary.600', bg: 'rgba(17, 24, 36, 0.05)' }}
|
||||
cursor={'pointer'}
|
||||
onClick={() => {
|
||||
const data = flowData2StoreDataAndCheck();
|
||||
if (data) {
|
||||
onSaveAndPublishModalOpen();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MyIcon name={'core/workflow/publish'} w={'16px'} mr={2} />
|
||||
<Box fontSize={'sm'}>{t('common:core.workflow.Save and publish')}</Box>
|
||||
{isSaveAndPublishModalOpen && (
|
||||
<SaveAndPublishModal
|
||||
isLoading={loading}
|
||||
onClose={onSaveAndPublishModalClose}
|
||||
onClickSave={onClickSave}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
</MyBox>
|
||||
)}
|
||||
</MyPopover>
|
||||
)}
|
||||
</HStack>
|
||||
)}
|
||||
</Flex>
|
||||
{historiesDefaultData && (
|
||||
{historiesDefaultData && isV2Workflow && currentTab === TabEnum.appEdit && (
|
||||
<PublishHistories
|
||||
initData={initData}
|
||||
onClose={() => {
|
||||
setHistoriesDefaultData(undefined);
|
||||
}}
|
||||
defaultData={historiesDefaultData}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
@@ -185,16 +306,24 @@ const Header = () => {
|
||||
}, [
|
||||
isPc,
|
||||
currentTab,
|
||||
saveAndBack,
|
||||
historiesDefaultData,
|
||||
isV2Workflow,
|
||||
isPublished,
|
||||
isOpen,
|
||||
onClose,
|
||||
t,
|
||||
initData,
|
||||
loading,
|
||||
isV2Workflow,
|
||||
historiesDefaultData,
|
||||
isSave,
|
||||
onBack,
|
||||
onOpen,
|
||||
onClickSave,
|
||||
setHistoriesDefaultData,
|
||||
appDetail.chatConfig,
|
||||
flowData2StoreDataAndCheck,
|
||||
setWorkflowTestData,
|
||||
onclickPublish
|
||||
isSaveAndPublishModalOpen,
|
||||
onSaveAndPublishModalClose,
|
||||
onSaveAndPublishModalOpen
|
||||
]);
|
||||
|
||||
return Render;
|
||||
|
@@ -31,14 +31,15 @@ const WorkflowEdit = () => {
|
||||
useMount(() => {
|
||||
if (!isV2Workflow) {
|
||||
openConfirm(() => {
|
||||
initData(JSON.parse(JSON.stringify(v1Workflow2V2((appDetail.modules || []) as any))));
|
||||
initData(JSON.parse(JSON.stringify(v1Workflow2V2((appDetail.modules || []) as any))), true);
|
||||
})();
|
||||
} else {
|
||||
initData(
|
||||
cloneDeep({
|
||||
nodes: appDetail.modules || [],
|
||||
edges: appDetail.edges || []
|
||||
})
|
||||
}),
|
||||
true
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@@ -2,14 +2,13 @@ import React, { useCallback, useMemo } from 'react';
|
||||
import { Box, Flex, HStack, useDisclosure } from '@chakra-ui/react';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppContext, TabEnum } from '../context';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import MyMenu from '@fastgpt/web/components/common/MyMenu';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import { WorkflowContext } from './context';
|
||||
import { compareWorkflow, filterSensitiveNodesData } from '@/web/core/workflow/utils';
|
||||
import { filterSensitiveNodesData } from '@/web/core/workflow/utils';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useCopyData } from '@/web/common/hooks/useCopyData';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
@@ -18,15 +17,21 @@ import { publishStatusStyle } from '../constants';
|
||||
|
||||
const ImportSettings = dynamic(() => import('./Flow/ImportSettings'));
|
||||
|
||||
const AppCard = ({ showSaveStatus }: { showSaveStatus: boolean }) => {
|
||||
const AppCard = ({
|
||||
showSaveStatus,
|
||||
isPublished
|
||||
}: {
|
||||
showSaveStatus: boolean;
|
||||
isPublished: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { appT } = useI18n();
|
||||
const { copyData } = useCopyData();
|
||||
const { feConfigs } = useSystemStore();
|
||||
|
||||
const { appDetail, appLatestVersion, onOpenInfoEdit, onOpenTeamTagModal, onDelApp, currentTab } =
|
||||
const { appDetail, onOpenInfoEdit, onOpenTeamTagModal, onDelApp, currentTab } =
|
||||
useContextSelector(AppContext, (v) => v);
|
||||
const { historiesDefaultData, flowData2StoreDataAndCheck, onSaveWorkflow, isSaving, saveLabel } =
|
||||
const { historiesDefaultData, flowData2StoreDataAndCheck, onSaveWorkflow, isSaving } =
|
||||
useContextSelector(WorkflowContext, (v) => v);
|
||||
|
||||
const { isOpen: isOpenImport, onOpen: onOpenImport, onClose: onCloseImport } = useDisclosure();
|
||||
@@ -48,27 +53,6 @@ const AppCard = ({ showSaveStatus }: { showSaveStatus: boolean }) => {
|
||||
}
|
||||
}, [appDetail.chatConfig, appT, copyData, flowData2StoreDataAndCheck]);
|
||||
|
||||
const isPublished = (() => {
|
||||
const data = flowData2StoreDataAndCheck(true);
|
||||
if (!appLatestVersion) return true;
|
||||
|
||||
if (data) {
|
||||
return compareWorkflow(
|
||||
{
|
||||
nodes: appLatestVersion.nodes,
|
||||
edges: appLatestVersion.edges,
|
||||
chatConfig: appLatestVersion.chatConfig
|
||||
},
|
||||
{
|
||||
nodes: data.nodes,
|
||||
edges: data.edges,
|
||||
chatConfig: appDetail.chatConfig
|
||||
}
|
||||
);
|
||||
}
|
||||
return false;
|
||||
})();
|
||||
|
||||
const InfoMenu = useCallback(
|
||||
({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
@@ -169,35 +153,25 @@ const AppCard = ({ showSaveStatus }: { showSaveStatus: boolean }) => {
|
||||
</HStack>
|
||||
</InfoMenu>
|
||||
{showSaveStatus && (
|
||||
<MyTooltip label={t('common:core.app.Onclick to save')}>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
h={'20px'}
|
||||
cursor={'pointer'}
|
||||
fontSize={'mini'}
|
||||
onClick={onSaveWorkflow}
|
||||
lineHeight={1}
|
||||
<Flex alignItems={'center'} h={'20px'} fontSize={'mini'} lineHeight={1}>
|
||||
<MyTag
|
||||
py={0}
|
||||
px={0}
|
||||
showDot
|
||||
bg={'transparent'}
|
||||
colorSchema={
|
||||
isPublished
|
||||
? publishStatusStyle.published.colorSchema
|
||||
: publishStatusStyle.unPublish.colorSchema
|
||||
}
|
||||
>
|
||||
{isSaving && <MyIcon name={'common/loading'} w={'0.8rem'} mr={0.5} />}
|
||||
<Box color={'myGray.500'}>{saveLabel}</Box>
|
||||
<MyTag
|
||||
py={0}
|
||||
showDot
|
||||
bg={'transparent'}
|
||||
colorSchema={
|
||||
isPublished
|
||||
? publishStatusStyle.published.colorSchema
|
||||
: publishStatusStyle.unPublish.colorSchema
|
||||
}
|
||||
>
|
||||
{t(
|
||||
isPublished
|
||||
? publishStatusStyle.published.text
|
||||
: publishStatusStyle.unPublish.text
|
||||
)}
|
||||
</MyTag>
|
||||
</Flex>
|
||||
</MyTooltip>
|
||||
{t(
|
||||
isPublished
|
||||
? publishStatusStyle.published.text
|
||||
: publishStatusStyle.unPublish.text
|
||||
)}
|
||||
</MyTag>
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -210,10 +184,7 @@ const AppCard = ({ showSaveStatus }: { showSaveStatus: boolean }) => {
|
||||
appDetail.name,
|
||||
isOpenImport,
|
||||
isPublished,
|
||||
isSaving,
|
||||
onCloseImport,
|
||||
onSaveWorkflow,
|
||||
saveLabel,
|
||||
showSaveStatus,
|
||||
t
|
||||
]);
|
||||
|
@@ -93,15 +93,16 @@ const ChatTest = ({
|
||||
</Flex>
|
||||
) : (
|
||||
<Flex
|
||||
py={4}
|
||||
py={2.5}
|
||||
px={5}
|
||||
whiteSpace={'nowrap'}
|
||||
bg={isPlugin ? 'myGray.25' : ''}
|
||||
borderBottom={isPlugin ? '1px solid #F4F4F7' : ''}
|
||||
bg={'myGray.25'}
|
||||
borderBottom={'1px solid #F4F4F7'}
|
||||
>
|
||||
<Box fontSize={'lg'} fontWeight={'bold'} flex={1}>
|
||||
{t('common:core.chat.Debug test')}
|
||||
</Box>
|
||||
<Flex fontSize={'16px'} fontWeight={'bold'} flex={1} alignItems={'center'}>
|
||||
<MyIcon name={'common/paused'} w={'14px'} mr={2.5} />
|
||||
{t('common:core.chat.Run test')}
|
||||
</Flex>
|
||||
<MyTooltip label={t('common:core.chat.Restart')}>
|
||||
<IconButton
|
||||
className="chat"
|
||||
@@ -121,6 +122,7 @@ const ChatTest = ({
|
||||
size={'smSquare'}
|
||||
aria-label={''}
|
||||
onClick={onClose}
|
||||
bg={'none'}
|
||||
/>
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
|
@@ -388,13 +388,12 @@ const RenderList = React.memo(function RenderList({
|
||||
setParentId
|
||||
}: RenderListProps) {
|
||||
const { t } = useTranslation();
|
||||
const { feConfigs } = useSystemStore();
|
||||
const { feConfigs, setLoading } = useSystemStore();
|
||||
|
||||
const { isPc } = useSystem();
|
||||
const isSystemPlugin = type === TemplateTypeEnum.systemPlugin;
|
||||
|
||||
const { x, y, zoom } = useViewport();
|
||||
const { setLoading } = useSystemStore();
|
||||
const { toast } = useToast();
|
||||
const reactFlowWrapper = useContextSelector(WorkflowContext, (v) => v.reactFlowWrapper);
|
||||
const setNodes = useContextSelector(WorkflowContext, (v) => v.setNodes);
|
||||
@@ -466,15 +465,16 @@ const RenderList = React.memo(function RenderList({
|
||||
selected: true
|
||||
});
|
||||
|
||||
setNodes((state) =>
|
||||
state
|
||||
setNodes((state) => {
|
||||
const newState = state
|
||||
.map((node) => ({
|
||||
...node,
|
||||
selected: false
|
||||
}))
|
||||
// @ts-ignore
|
||||
.concat(node)
|
||||
);
|
||||
.concat(node);
|
||||
return newState;
|
||||
});
|
||||
},
|
||||
[computedNewNodeName, reactFlowWrapper, setLoading, setNodes, t, toast, x, y, zoom]
|
||||
);
|
||||
|
@@ -30,7 +30,10 @@ const ButtonEdge = (props: EdgeProps) => {
|
||||
|
||||
const onDelConnect = useCallback(
|
||||
(id: string) => {
|
||||
setEdges((state) => state.filter((item) => item.id !== id));
|
||||
setEdges((state) => {
|
||||
const newState = state.filter((item) => item.id !== id);
|
||||
return newState;
|
||||
});
|
||||
},
|
||||
[setEdges]
|
||||
);
|
||||
|
@@ -0,0 +1,149 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { Background, ControlButton, MiniMap, Panel, useReactFlow } from 'reactflow';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../../context';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import styles from './index.module.scss';
|
||||
|
||||
const FlowController = React.memo(function FlowController() {
|
||||
const { fitView, zoomIn, zoomOut } = useReactFlow();
|
||||
const { undo, redo, canRedo, canUndo } = useContextSelector(WorkflowContext, (v) => v);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isMac = !window ? false : window.navigator.userAgent.toLocaleLowerCase().includes('mac');
|
||||
|
||||
useEffect(() => {
|
||||
const keyDownHandler = (event: KeyboardEvent) => {
|
||||
if (event.key === 'z' && (event.ctrlKey || event.metaKey) && event.shiftKey) {
|
||||
event.preventDefault();
|
||||
redo();
|
||||
} else if (event.key === 'z' && (event.ctrlKey || event.metaKey)) {
|
||||
event.preventDefault();
|
||||
undo();
|
||||
} else if ((event.key === '=' || event.key === '+') && (event.ctrlKey || event.metaKey)) {
|
||||
event.preventDefault();
|
||||
zoomIn();
|
||||
} else if (event.key === '-' && (event.ctrlKey || event.metaKey)) {
|
||||
event.preventDefault();
|
||||
zoomOut();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', keyDownHandler);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', keyDownHandler);
|
||||
};
|
||||
}, [undo, redo, zoomIn, zoomOut]);
|
||||
|
||||
const buttonStyle = {
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
padding: '7px'
|
||||
};
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
<MiniMap
|
||||
style={{
|
||||
height: 98,
|
||||
width: 184,
|
||||
marginBottom: 72,
|
||||
borderRadius: '10px',
|
||||
boxShadow: '0px 0px 1px rgba(19, 51, 107, 0.10), 0px 4px 10px rgba(19, 51, 107, 0.10)'
|
||||
}}
|
||||
pannable
|
||||
/>
|
||||
<Panel
|
||||
position={'bottom-right'}
|
||||
style={{
|
||||
display: 'flex',
|
||||
marginBottom: 24,
|
||||
padding: '5px 8px',
|
||||
background: 'white',
|
||||
borderRadius: '6px',
|
||||
overflow: 'hidden',
|
||||
alignItems: 'center',
|
||||
gap: '2px',
|
||||
boxShadow:
|
||||
'0px 0px 1px 0px rgba(19, 51, 107, 0.20), 0px 12px 16px -4px rgba(19, 51, 107, 0.20)'
|
||||
}}
|
||||
>
|
||||
{/* undo */}
|
||||
<MyTooltip label={isMac ? t('common:common.undo_tip_mac') : t('common:common.undo_tip')}>
|
||||
<ControlButton
|
||||
onClick={undo}
|
||||
style={buttonStyle}
|
||||
className={`${styles.customControlButton}`}
|
||||
disabled={!canUndo}
|
||||
>
|
||||
<MyIcon name={'core/workflow/undo'} />
|
||||
</ControlButton>
|
||||
</MyTooltip>
|
||||
|
||||
{/* redo */}
|
||||
<MyTooltip label={isMac ? t('common:common.redo_tip_mac') : t('common:common.redo_tip')}>
|
||||
<ControlButton
|
||||
onClick={redo}
|
||||
style={buttonStyle}
|
||||
className={`${styles.customControlButton}`}
|
||||
disabled={!canRedo}
|
||||
>
|
||||
<MyIcon name={'core/workflow/redo'} />
|
||||
</ControlButton>
|
||||
</MyTooltip>
|
||||
|
||||
<Box w="1px" h="20px" bg="gray.200" mx={1.5}></Box>
|
||||
|
||||
{/* zoom out */}
|
||||
<MyTooltip
|
||||
label={isMac ? t('common:common.zoomout_tip_mac') : t('common:common.zoomout_tip')}
|
||||
>
|
||||
<ControlButton
|
||||
onClick={() => zoomOut()}
|
||||
style={buttonStyle}
|
||||
className={`${styles.customControlButton}`}
|
||||
>
|
||||
<MyIcon name={'common/subtract'} />
|
||||
</ControlButton>
|
||||
</MyTooltip>
|
||||
|
||||
{/* zoom in */}
|
||||
<MyTooltip
|
||||
label={isMac ? t('common:common.zoomin_tip_mac') : t('common:common.zoomin_tip')}
|
||||
>
|
||||
<ControlButton
|
||||
onClick={() => zoomIn()}
|
||||
style={buttonStyle}
|
||||
className={`${styles.customControlButton}`}
|
||||
>
|
||||
<MyIcon name={'common/addLight'} />
|
||||
</ControlButton>
|
||||
</MyTooltip>
|
||||
|
||||
<Box w="1px" h="20px" bg="gray.200" mx={1.5}></Box>
|
||||
|
||||
{/* fit view */}
|
||||
<MyTooltip label={t('common:common.page_center')}>
|
||||
<ControlButton
|
||||
onClick={() => fitView()}
|
||||
style={buttonStyle}
|
||||
className={`custom-workflow-fix_view ${styles.customControlButton}`}
|
||||
>
|
||||
<MyIcon name={'core/modules/fixview'} />
|
||||
</ControlButton>
|
||||
</MyTooltip>
|
||||
</Panel>
|
||||
<Background />
|
||||
</>
|
||||
);
|
||||
}, [isMac, t, undo, buttonStyle, canUndo, redo, canRedo, zoomOut, zoomIn, fitView]);
|
||||
|
||||
return Render;
|
||||
});
|
||||
|
||||
export default FlowController;
|
@@ -0,0 +1,74 @@
|
||||
import { Box, Button, Input, ModalBody, ModalFooter } from '@chakra-ui/react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
type FormType = {
|
||||
versionName: string;
|
||||
isPublish: boolean | undefined;
|
||||
};
|
||||
|
||||
const SaveAndPublishModal = ({
|
||||
onClose,
|
||||
isLoading,
|
||||
onClickSave
|
||||
}: {
|
||||
onClose: () => void;
|
||||
isLoading: boolean;
|
||||
onClickSave: (data: { isPublish: boolean; versionName: string }) => Promise<void>;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { register, handleSubmit } = useForm<FormType>({
|
||||
defaultValues: {
|
||||
versionName: '',
|
||||
isPublish: undefined
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
title={t('common:core.workflow.Save and publish')}
|
||||
iconSrc={'core/workflow/publish'}
|
||||
maxW={'400px'}
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
>
|
||||
<ModalBody>
|
||||
<Box mb={2.5} color={'myGray.900'} fontSize={'14px'} fontWeight={'500'}>
|
||||
{t('common:common.Name')}
|
||||
</Box>
|
||||
<Box mb={3}>
|
||||
<Input
|
||||
autoFocus
|
||||
placeholder={t('app:app.Version name')}
|
||||
bg={'myWhite.600'}
|
||||
{...register('versionName', {
|
||||
required: t('app:app.version_name_tips')
|
||||
})}
|
||||
/>
|
||||
</Box>
|
||||
<Box fontSize={'14px'}>{t('app:app.version_publish_tips')}</Box>
|
||||
</ModalBody>
|
||||
<ModalFooter gap={3}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onClose();
|
||||
}}
|
||||
variant={'whiteBase'}
|
||||
>
|
||||
{t('common:common.Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
onClick={handleSubmit(async (data) => {
|
||||
await onClickSave({ ...data, isPublish: true });
|
||||
onClose();
|
||||
})}
|
||||
>
|
||||
{t('common:common.Confirm')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default SaveAndPublishModal;
|
@@ -0,0 +1,8 @@
|
||||
.customControlButton {
|
||||
svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
max-width: 18px;
|
||||
max-height: 18px;
|
||||
}
|
||||
}
|
@@ -6,7 +6,6 @@ import {
|
||||
addEdge,
|
||||
EdgeChange,
|
||||
Edge,
|
||||
applyNodeChanges,
|
||||
Node,
|
||||
NodePositionChange,
|
||||
XYPosition
|
||||
@@ -15,7 +14,6 @@ import { EDGE_TYPE } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import 'reactflow/dist/style.css';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import { useKeyboard } from './useKeyboard';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../../context';
|
||||
@@ -259,10 +257,6 @@ const computeHelperLines = (
|
||||
export const useWorkflow = () => {
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
const { openConfirm: onOpenConfirmDeleteNode, ConfirmModal: ConfirmDeleteModal } = useConfirm({
|
||||
content: t('common:core.module.Confirm Delete Node'),
|
||||
type: 'delete'
|
||||
});
|
||||
|
||||
const { isDowningCtrl } = useKeyboard();
|
||||
const {
|
||||
@@ -329,7 +323,7 @@ export const useWorkflow = () => {
|
||||
title: t('common:core.workflow.Can not delete node')
|
||||
});
|
||||
} else {
|
||||
return onOpenConfirmDeleteNode(() => {
|
||||
return (() => {
|
||||
onNodesChange(changes);
|
||||
setEdges((state) =>
|
||||
state.filter((edge) => edge.source !== change.id && edge.target !== change.id)
|
||||
@@ -387,6 +381,7 @@ export const useWorkflow = () => {
|
||||
title: t('common:core.module.Can not connect self')
|
||||
});
|
||||
}
|
||||
|
||||
onConnect({
|
||||
connect
|
||||
});
|
||||
@@ -406,7 +401,6 @@ export const useWorkflow = () => {
|
||||
}, [setHoverEdgeId]);
|
||||
|
||||
return {
|
||||
ConfirmDeleteModal,
|
||||
handleNodesChange,
|
||||
handleEdgeChange,
|
||||
onConnectStart,
|
||||
@@ -416,9 +410,7 @@ export const useWorkflow = () => {
|
||||
onEdgeMouseEnter,
|
||||
onEdgeMouseLeave,
|
||||
helperLineHorizontal,
|
||||
setHelperLineHorizontal,
|
||||
helperLineVertical,
|
||||
setHelperLineVertical
|
||||
helperLineVertical
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -1,13 +1,5 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
Controls,
|
||||
ControlButton,
|
||||
MiniMap,
|
||||
NodeProps,
|
||||
ReactFlowProvider,
|
||||
useReactFlow
|
||||
} from 'reactflow';
|
||||
import React from 'react';
|
||||
import ReactFlow, { NodeProps, ReactFlowProvider } from 'reactflow';
|
||||
import { Box, IconButton, useDisclosure } from '@chakra-ui/react';
|
||||
import { SmallCloseIcon } from '@chakra-ui/icons';
|
||||
import { EDGE_TYPE, FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
@@ -19,14 +11,12 @@ import NodeTemplatesModal from './NodeTemplatesModal';
|
||||
|
||||
import 'reactflow/dist/style.css';
|
||||
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { connectionLineStyle, defaultEdgeOptions } from '../constants';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../context';
|
||||
import { useWorkflow } from './hooks/useWorkflow';
|
||||
import { t } from 'i18next';
|
||||
import HelperLines from './components/HelperLines';
|
||||
import FlowController from './components/FlowController';
|
||||
|
||||
const NodeSimple = dynamic(() => import('./nodes/NodeSimple'));
|
||||
const nodeTypes: Record<FlowNodeTypeEnum, any> = {
|
||||
@@ -67,7 +57,6 @@ const Workflow = () => {
|
||||
const { nodes, edges, reactFlowWrapper } = useContextSelector(WorkflowContext, (v) => v);
|
||||
|
||||
const {
|
||||
ConfirmDeleteModal,
|
||||
handleNodesChange,
|
||||
handleEdgeChange,
|
||||
onConnectStart,
|
||||
@@ -142,8 +131,6 @@ const Workflow = () => {
|
||||
<HelperLines horizontal={helperLineHorizontal} vertical={helperLineVertical} />
|
||||
</ReactFlow>
|
||||
</Box>
|
||||
|
||||
<ConfirmDeleteModal />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -157,45 +144,3 @@ const Render = () => {
|
||||
};
|
||||
|
||||
export default React.memo(Render);
|
||||
|
||||
const FlowController = React.memo(function FlowController() {
|
||||
const { fitView } = useReactFlow();
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
<MiniMap
|
||||
style={{
|
||||
height: 78,
|
||||
width: 126,
|
||||
marginBottom: 35
|
||||
}}
|
||||
pannable
|
||||
/>
|
||||
<Controls
|
||||
position={'bottom-right'}
|
||||
style={{
|
||||
display: 'flex',
|
||||
marginBottom: 5,
|
||||
background: 'white',
|
||||
borderRadius: '6px',
|
||||
overflow: 'hidden',
|
||||
boxShadow:
|
||||
'0px 0px 1px 0px rgba(19, 51, 107, 0.20), 0px 12px 16px -4px rgba(19, 51, 107, 0.20)'
|
||||
}}
|
||||
showInteractive={false}
|
||||
showFitView={false}
|
||||
>
|
||||
<MyTooltip label={t('common:common.page_center')}>
|
||||
<ControlButton className="custom-workflow-fix_view" onClick={() => fitView()}>
|
||||
<MyIcon name={'core/modules/fixview'} w={'14px'} />
|
||||
</ControlButton>
|
||||
</MyTooltip>
|
||||
</Controls>
|
||||
<Background />
|
||||
</>
|
||||
);
|
||||
}, [fitView]);
|
||||
|
||||
return Render;
|
||||
});
|
||||
|
@@ -8,7 +8,6 @@ import TTSSelect from '@/components/core/app/TTSSelect';
|
||||
import WhisperConfig from '@/components/core/app/WhisperConfig';
|
||||
import InputGuideConfig from '@/components/core/app/InputGuideConfig';
|
||||
import { getAppChatConfig } from '@fastgpt/global/core/workflow/utils';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { TTSTypeEnum } from '@/web/core/app/constants';
|
||||
import NodeCard from './render/NodeCard';
|
||||
import ScheduledTriggerConfig from '@/components/core/app/ScheduledTriggerConfig';
|
||||
@@ -90,14 +89,12 @@ const NodeUserGuide = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
export default React.memo(NodeUserGuide);
|
||||
|
||||
function WelcomeText({ chatConfig: { welcomeText }, setAppDetail }: ComponentProps) {
|
||||
const { t } = useTranslation();
|
||||
const [, startTst] = useTransition();
|
||||
|
||||
return (
|
||||
<Box className="nodrag">
|
||||
<WelcomeTextConfig
|
||||
resize={'both'}
|
||||
defaultValue={welcomeText}
|
||||
value={welcomeText}
|
||||
onChange={(e) => {
|
||||
startTst(() => {
|
||||
setAppDetail((state) => ({
|
||||
@@ -164,8 +161,6 @@ function TTSGuide({ chatConfig: { ttsConfig }, setAppDetail }: ComponentProps) {
|
||||
}
|
||||
|
||||
function WhisperGuide({ chatConfig: { whisperConfig, ttsConfig }, setAppDetail }: ComponentProps) {
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
|
||||
return (
|
||||
<WhisperConfig
|
||||
isOpenAudio={ttsConfig?.type !== TTSTypeEnum.none}
|
||||
@@ -205,7 +200,6 @@ function ScheduledTrigger({
|
||||
|
||||
function QuestionInputGuide({ chatConfig: { chatInputGuide }, setAppDetail }: ComponentProps) {
|
||||
const appId = useContextSelector(WorkflowContext, (v) => v.appId);
|
||||
|
||||
return appId ? (
|
||||
<InputGuideConfig
|
||||
appId={appId}
|
||||
|
@@ -386,7 +386,7 @@ const MenuRender = React.memo(function MenuRender({
|
||||
icon: 'delete',
|
||||
label: t('common:common.Delete'),
|
||||
variant: 'whiteDanger',
|
||||
onClick: onOpenConfirmDeleteNode(() => onDelNode(nodeId))
|
||||
onClick: () => onDelNode(nodeId)
|
||||
}
|
||||
])
|
||||
];
|
||||
@@ -429,7 +429,6 @@ const MenuRender = React.memo(function MenuRender({
|
||||
menuForbid?.copy,
|
||||
menuForbid?.delete,
|
||||
t,
|
||||
onOpenConfirmDeleteNode,
|
||||
ConfirmDeleteModal,
|
||||
DebugInputModal,
|
||||
openDebugNode,
|
||||
|
@@ -2,7 +2,6 @@ import React, { useCallback, useMemo } from 'react';
|
||||
import type { RenderInputProps } from '../type';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor';
|
||||
import { formatEditorVariablePickerIcon } from '@fastgpt/global/core/workflow/utils';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context';
|
||||
import { computedNodeInputReference } from '@/web/core/workflow/utils';
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { postWorkflowDebug } from '@/web/core/workflow/api';
|
||||
import {
|
||||
checkWorkflowNodeAndConnection,
|
||||
compareSnapshot,
|
||||
storeEdgesRenderEdge,
|
||||
storeNode2FlowNode
|
||||
} from '@/web/core/workflow/utils';
|
||||
@@ -14,7 +15,7 @@ import { RuntimeEdgeItemType, StoreEdgeItemType } from '@fastgpt/global/core/wor
|
||||
import { FlowNodeChangeProps } from '@fastgpt/global/core/workflow/type/fe';
|
||||
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { useMemoizedFn, useUpdateEffect } from 'ahooks';
|
||||
import { useLocalStorageState, useMemoizedFn, useUpdateEffect } from 'ahooks';
|
||||
import React, {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
@@ -45,12 +46,19 @@ import { useDisclosure } from '@chakra-ui/react';
|
||||
import { uiWorkflow2StoreWorkflow } from './utils';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { formatTime2HM, formatTime2YMDHMW } from '@fastgpt/global/common/string/time';
|
||||
import { formatTime2YMDHMS, formatTime2YMDHMW } from '@fastgpt/global/common/string/time';
|
||||
import type { InitProps } from '@/pages/app/detail/components/PublishHistoriesSlider';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { cloneDeep, isEqual } from 'lodash';
|
||||
|
||||
type OnChange<ChangesType> = (changes: ChangesType[]) => void;
|
||||
|
||||
export type SnapshotsType = {
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
title: string;
|
||||
chatConfig: AppChatConfigType;
|
||||
isSaved?: boolean;
|
||||
};
|
||||
type WorkflowContextType = {
|
||||
appId?: string;
|
||||
basicNodeTemplates: FlowNodeTemplateType[];
|
||||
@@ -82,6 +90,27 @@ type WorkflowContextType = {
|
||||
hoverEdgeId?: string;
|
||||
setHoverEdgeId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
|
||||
// snapshots
|
||||
saveSnapshot: ({
|
||||
pastNodes,
|
||||
pastEdges,
|
||||
customTitle,
|
||||
chatConfig
|
||||
}: {
|
||||
pastNodes?: Node[];
|
||||
pastEdges?: Edge[];
|
||||
customTitle?: string;
|
||||
chatConfig?: AppChatConfigType;
|
||||
}) => Promise<boolean>;
|
||||
resetSnapshot: (state: SnapshotsType) => void;
|
||||
past: SnapshotsType[];
|
||||
setPast: Dispatch<SetStateAction<SnapshotsType[]>>;
|
||||
future: SnapshotsType[];
|
||||
redo: () => void;
|
||||
undo: () => void;
|
||||
canRedo: boolean;
|
||||
canUndo: boolean;
|
||||
|
||||
// connect
|
||||
connectingEdge?: OnConnectStartParams;
|
||||
setConnectingEdge: React.Dispatch<React.SetStateAction<OnConnectStartParams | undefined>>;
|
||||
@@ -96,19 +125,27 @@ type WorkflowContextType = {
|
||||
toolInputs: FlowNodeInputItemType[];
|
||||
commonInputs: FlowNodeInputItemType[];
|
||||
};
|
||||
initData: (e: {
|
||||
nodes: StoreNodeItemType[];
|
||||
edges: StoreEdgeItemType[];
|
||||
chatConfig?: AppChatConfigType;
|
||||
}) => Promise<void>;
|
||||
initData: (
|
||||
e: {
|
||||
nodes: StoreNodeItemType[];
|
||||
edges: StoreEdgeItemType[];
|
||||
chatConfig?: AppChatConfigType;
|
||||
},
|
||||
isSetInitial?: boolean
|
||||
) => Promise<void>;
|
||||
flowData2StoreDataAndCheck: (hideTip?: boolean) =>
|
||||
| {
|
||||
nodes: StoreNodeItemType[];
|
||||
edges: StoreEdgeItemType[];
|
||||
}
|
||||
| undefined;
|
||||
flowData2StoreData: () =>
|
||||
| {
|
||||
nodes: StoreNodeItemType[];
|
||||
edges: StoreEdgeItemType[];
|
||||
}
|
||||
| undefined;
|
||||
onSaveWorkflow: () => Promise<null | undefined>;
|
||||
saveLabel: string;
|
||||
isSaving: boolean;
|
||||
|
||||
// debug
|
||||
@@ -252,17 +289,40 @@ export const WorkflowContext = createContext<WorkflowContextType>({
|
||||
| undefined {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
flowData2StoreData: function ():
|
||||
| { nodes: StoreNodeItemType[]; edges: StoreEdgeItemType[] }
|
||||
| undefined {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
onSaveWorkflow: function (): Promise<null | undefined> {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
saveLabel: '',
|
||||
historiesDefaultData: undefined,
|
||||
setHistoriesDefaultData: function (value: React.SetStateAction<InitProps | undefined>): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
getNodeDynamicInputs: function (nodeId: string): FlowNodeInputItemType[] {
|
||||
throw new Error('Function not implemented.');
|
||||
}
|
||||
},
|
||||
saveSnapshot: function (): Promise<boolean> {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
resetSnapshot: function (): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
past: [],
|
||||
setPast: function (): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
future: [],
|
||||
redo: function (): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
undo: function (): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
canRedo: false,
|
||||
canUndo: false
|
||||
});
|
||||
|
||||
const WorkflowContextProvider = ({
|
||||
@@ -363,8 +423,8 @@ const WorkflowContextProvider = ({
|
||||
|
||||
const onChangeNode = useMemoizedFn((props: FlowNodeChangeProps) => {
|
||||
const { nodeId, type } = props;
|
||||
setNodes((nodes) =>
|
||||
nodes.map((node) => {
|
||||
setNodes((nodes) => {
|
||||
const newNodes = nodes.map((node) => {
|
||||
if (node.id !== nodeId) return node;
|
||||
|
||||
const updateObj = cloneDeep(node.data);
|
||||
@@ -429,8 +489,9 @@ const WorkflowContextProvider = ({
|
||||
...node,
|
||||
data: updateObj
|
||||
};
|
||||
})
|
||||
);
|
||||
});
|
||||
return newNodes;
|
||||
});
|
||||
});
|
||||
const getNodeDynamicInputs = useCallback(
|
||||
(nodeId: string) => {
|
||||
@@ -469,18 +530,35 @@ const WorkflowContextProvider = ({
|
||||
};
|
||||
};
|
||||
|
||||
const initData = useMemoizedFn(async (e: Parameters<WorkflowContextType['initData']>[0]) => {
|
||||
setNodes(e.nodes?.map((item) => storeNode2FlowNode({ item })) || []);
|
||||
setEdges(e.edges?.map((item) => storeEdgesRenderEdge({ edge: item })) || []);
|
||||
const initData = useMemoizedFn(
|
||||
async (e: Parameters<WorkflowContextType['initData']>[0], isInit?: boolean) => {
|
||||
setNodes(e.nodes?.map((item) => storeNode2FlowNode({ item })) || []);
|
||||
setEdges(e.edges?.map((item) => storeEdgesRenderEdge({ edge: item })) || []);
|
||||
|
||||
const chatConfig = e.chatConfig;
|
||||
if (chatConfig) {
|
||||
setAppDetail((state) => ({
|
||||
...state,
|
||||
chatConfig
|
||||
}));
|
||||
const chatConfig = e.chatConfig;
|
||||
if (chatConfig) {
|
||||
setAppDetail((state) => ({
|
||||
...state,
|
||||
chatConfig
|
||||
}));
|
||||
}
|
||||
|
||||
// If it is the initial data, save the initial snapshot
|
||||
if (!isInit) return;
|
||||
// If it has been initialized, it will not be saved
|
||||
if (past.length > 0) {
|
||||
resetSnapshot(past[0]);
|
||||
return;
|
||||
}
|
||||
saveSnapshot({
|
||||
pastNodes: e.nodes?.map((item) => storeNode2FlowNode({ item })) || [],
|
||||
pastEdges: e.edges?.map((item) => storeEdgesRenderEdge({ edge: item })) || [],
|
||||
customTitle: t(`app:app.version_initial`),
|
||||
chatConfig: appDetail.chatConfig,
|
||||
isSaved: true
|
||||
});
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
/* ui flow to store data */
|
||||
const flowData2StoreDataAndCheck = useMemoizedFn((hideTip = false) => {
|
||||
@@ -499,8 +577,13 @@ const WorkflowContextProvider = ({
|
||||
}
|
||||
});
|
||||
|
||||
const flowData2StoreData = useMemoizedFn(() => {
|
||||
const storeNodes = uiWorkflow2StoreWorkflow({ nodes, edges });
|
||||
|
||||
return storeNodes;
|
||||
});
|
||||
|
||||
/* save workflow */
|
||||
const [saveLabel, setSaveLabel] = useState(t('common:core.app.Onclick to save'));
|
||||
const { runAsync: onSaveWorkflow, loading: isSaving } = useRequest2(async () => {
|
||||
const { nodes } = await getWorkflowStore();
|
||||
|
||||
@@ -522,11 +605,6 @@ const WorkflowContextProvider = ({
|
||||
//@ts-ignore
|
||||
version: 'v2'
|
||||
});
|
||||
setSaveLabel(
|
||||
t('common:core.app.Saved time', {
|
||||
time: formatTime2HM()
|
||||
})
|
||||
);
|
||||
} catch (error) {}
|
||||
|
||||
return null;
|
||||
@@ -750,6 +828,121 @@ const WorkflowContextProvider = ({
|
||||
onOpenTest();
|
||||
}, [workflowTestData]);
|
||||
|
||||
/* snapshots */
|
||||
const [past, setPast] = useLocalStorageState<SnapshotsType[]>(`${appId}-past`, {
|
||||
defaultValue: []
|
||||
}) as [SnapshotsType[], (value: SetStateAction<SnapshotsType[]>) => void];
|
||||
|
||||
const [future, setFuture] = useLocalStorageState<SnapshotsType[]>(`${appId}-future`, {
|
||||
defaultValue: []
|
||||
}) as [SnapshotsType[], (value: SetStateAction<SnapshotsType[]>) => void];
|
||||
|
||||
const resetSnapshot = useCallback(
|
||||
(state: SnapshotsType) => {
|
||||
setNodes(state.nodes);
|
||||
setEdges(state.edges);
|
||||
setAppDetail((detail) => ({
|
||||
...detail,
|
||||
chatConfig: state.chatConfig
|
||||
}));
|
||||
},
|
||||
[setAppDetail, setEdges, setNodes]
|
||||
);
|
||||
|
||||
const { runAsync: saveSnapshot } = useRequest2(
|
||||
async ({
|
||||
pastNodes,
|
||||
pastEdges,
|
||||
customTitle,
|
||||
chatConfig,
|
||||
isSaved
|
||||
}: {
|
||||
pastNodes?: Node[];
|
||||
pastEdges?: Edge[];
|
||||
customTitle?: string;
|
||||
chatConfig?: AppChatConfigType;
|
||||
isSaved?: boolean;
|
||||
}) => {
|
||||
const pastState = past[0];
|
||||
const currentNodes = pastNodes || nodes;
|
||||
const currentEdges = pastEdges || edges;
|
||||
const currentChatConfig = chatConfig || appDetail.chatConfig;
|
||||
const isPastEqual = compareSnapshot(
|
||||
{
|
||||
nodes: currentNodes,
|
||||
edges: currentEdges,
|
||||
chatConfig: currentChatConfig
|
||||
},
|
||||
{
|
||||
nodes: pastState?.nodes,
|
||||
edges: pastState?.edges,
|
||||
chatConfig: pastState?.chatConfig
|
||||
}
|
||||
);
|
||||
|
||||
if (isPastEqual) return false;
|
||||
|
||||
setPast((past) => [
|
||||
{
|
||||
nodes: currentNodes,
|
||||
edges: currentEdges,
|
||||
title: customTitle || formatTime2YMDHMS(new Date()),
|
||||
chatConfig: currentChatConfig,
|
||||
isSaved
|
||||
},
|
||||
...past.slice(0, 199)
|
||||
]);
|
||||
|
||||
setFuture([]);
|
||||
|
||||
return true;
|
||||
},
|
||||
{
|
||||
debounceWait: 500,
|
||||
refreshDeps: [nodes, edges, appDetail.chatConfig, past]
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!nodes.length) return;
|
||||
saveSnapshot({
|
||||
pastNodes: nodes,
|
||||
pastEdges: edges,
|
||||
customTitle: formatTime2YMDHMS(new Date()),
|
||||
chatConfig: appDetail.chatConfig
|
||||
});
|
||||
}, [nodes, edges, appDetail.chatConfig]);
|
||||
|
||||
const undo = useCallback(() => {
|
||||
if (past[1]) {
|
||||
setFuture((future) => [past[0], ...future]);
|
||||
setPast((past) => past.slice(1));
|
||||
resetSnapshot(past[1]);
|
||||
}
|
||||
}, [past, setFuture, setPast, resetSnapshot]);
|
||||
|
||||
const redo = useCallback(() => {
|
||||
const futureState = future[0];
|
||||
|
||||
if (futureState) {
|
||||
setPast((past) => [future[0], ...past]);
|
||||
setFuture((future) => future.slice(1));
|
||||
resetSnapshot(futureState);
|
||||
}
|
||||
}, [future, setPast, setFuture, resetSnapshot]);
|
||||
|
||||
// remove other app's snapshot
|
||||
useEffect(() => {
|
||||
const keys = Object.keys(localStorage);
|
||||
const snapshotKeys = keys.filter((key) => key.endsWith('-past') || key.endsWith('-future'));
|
||||
snapshotKeys.forEach((key) => {
|
||||
const keyAppId = key.split('-')[0];
|
||||
if (keyAppId !== appId) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
});
|
||||
}, [appId]);
|
||||
|
||||
const value = {
|
||||
appId,
|
||||
reactFlowWrapper,
|
||||
@@ -777,14 +970,25 @@ const WorkflowContextProvider = ({
|
||||
setConnectingEdge,
|
||||
onDelEdge,
|
||||
|
||||
// snapshots
|
||||
past,
|
||||
setPast,
|
||||
future,
|
||||
undo,
|
||||
redo,
|
||||
saveSnapshot,
|
||||
resetSnapshot,
|
||||
canUndo: past.length > 1,
|
||||
canRedo: !!future.length,
|
||||
|
||||
// function
|
||||
onFixView,
|
||||
splitToolInputs,
|
||||
initData,
|
||||
flowData2StoreDataAndCheck,
|
||||
flowData2StoreData,
|
||||
onSaveWorkflow,
|
||||
isSaving,
|
||||
saveLabel,
|
||||
|
||||
// debug
|
||||
workflowDebugData,
|
||||
|
@@ -0,0 +1,330 @@
|
||||
import React, { useState } from 'react';
|
||||
import { getPublishList, updateAppVersion } from '@/web/core/app/api/version';
|
||||
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
|
||||
import CustomRightDrawer from '@fastgpt/web/components/common/MyDrawer/CustomRightDrawer';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { Box, Button, Flex, Input } from '@chakra-ui/react';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppContext } from './context';
|
||||
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
|
||||
import { WorkflowContext } from './WorkflowComponents/context';
|
||||
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import Tag from '@fastgpt/web/components/common/Tag';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import MyPopover from '@fastgpt/web/components/common/MyPopover';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { storeEdgesRenderEdge, storeNode2FlowNode } from '@/web/core/workflow/utils';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
|
||||
const WorkflowPublishHistoriesSlider = ({ onClose }: { onClose: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const [currentTab, setCurrentTab] = useState<'myEdit' | 'teamCloud'>('myEdit');
|
||||
|
||||
return (
|
||||
<>
|
||||
<CustomRightDrawer
|
||||
onClose={() => onClose()}
|
||||
title={
|
||||
(
|
||||
<>
|
||||
<LightRowTabs
|
||||
list={[
|
||||
{ label: t('workflow:workflow.My edit'), value: 'myEdit' },
|
||||
{ label: t('workflow:workflow.Team cloud'), value: 'teamCloud' }
|
||||
]}
|
||||
value={currentTab}
|
||||
onChange={setCurrentTab}
|
||||
inlineStyles={{ px: 0.5, pb: 2 }}
|
||||
gap={5}
|
||||
py={0}
|
||||
fontSize={'sm'}
|
||||
/>
|
||||
</>
|
||||
) as any
|
||||
}
|
||||
maxW={'340px'}
|
||||
px={0}
|
||||
showMask={false}
|
||||
top={'60px'}
|
||||
overflow={'unset'}
|
||||
>
|
||||
{currentTab === 'myEdit' ? <MyEdit /> : <TeamCloud />}
|
||||
</CustomRightDrawer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(WorkflowPublishHistoriesSlider);
|
||||
|
||||
const MyEdit = () => {
|
||||
const { past, saveSnapshot, resetSnapshot } = useContextSelector(WorkflowContext, (v) => v);
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
|
||||
return (
|
||||
<Flex px={5} flex={'1 0 0'} flexDirection={'column'}>
|
||||
{past.length > 0 && (
|
||||
<Box py={2} px={3}>
|
||||
<Button
|
||||
variant={'whiteBase'}
|
||||
w={'full'}
|
||||
h={'30px'}
|
||||
onClick={async () => {
|
||||
const initialSnapshot = past[past.length - 1];
|
||||
const res = await saveSnapshot({
|
||||
pastNodes: initialSnapshot.nodes,
|
||||
pastEdges: initialSnapshot.edges,
|
||||
chatConfig: initialSnapshot.chatConfig,
|
||||
customTitle: t(`app:app.version_initial_copy`)
|
||||
});
|
||||
if (!res) return;
|
||||
resetSnapshot(initialSnapshot);
|
||||
toast({
|
||||
title: t('workflow:workflow.Switch_success'),
|
||||
status: 'success'
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('app:app.version_back')}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
<Flex flex={'1 0 0'} flexDirection={'column'} overflow={'auto'}>
|
||||
{past.map((item, index) => {
|
||||
return (
|
||||
<Flex
|
||||
key={index}
|
||||
alignItems={'center'}
|
||||
py={2}
|
||||
px={3}
|
||||
borderRadius={'md'}
|
||||
cursor={'pointer'}
|
||||
fontWeight={500}
|
||||
_hover={{
|
||||
bg: 'primary.50'
|
||||
}}
|
||||
onClick={async () => {
|
||||
const res = await saveSnapshot({
|
||||
pastNodes: item.nodes,
|
||||
pastEdges: item.edges,
|
||||
chatConfig: item.chatConfig,
|
||||
customTitle: `${t('app:app.version_copy')}-${item.title}`
|
||||
});
|
||||
if (!res) return;
|
||||
resetSnapshot(item);
|
||||
toast({
|
||||
title: t('workflow:workflow.Switch_success'),
|
||||
status: 'success'
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
w={'12px'}
|
||||
h={'12px'}
|
||||
borderWidth={'2px'}
|
||||
borderColor={'primary.600'}
|
||||
borderRadius={'50%'}
|
||||
position={'relative'}
|
||||
{...(index !== past.length - 1 && {
|
||||
_after: {
|
||||
content: '""',
|
||||
height: '26px',
|
||||
width: '2px',
|
||||
bgColor: 'myGray.250',
|
||||
position: 'absolute',
|
||||
top: '10px',
|
||||
left: '3px'
|
||||
}
|
||||
})}
|
||||
></Box>
|
||||
<Box
|
||||
ml={3}
|
||||
flex={'1 0 0'}
|
||||
fontSize={'sm'}
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="nowrap"
|
||||
color={'myGray.900'}
|
||||
>
|
||||
{item.title}
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
<Box py={2} textAlign={'center'} color={'myGray.600'} fontSize={'xs'}>
|
||||
{t('common:common.No more data')}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
const TeamCloud = () => {
|
||||
const { t } = useTranslation();
|
||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||
const { saveSnapshot, resetSnapshot } = useContextSelector(WorkflowContext, (v) => v);
|
||||
const { loadAndGetTeamMembers } = useUserStore();
|
||||
const { feConfigs } = useSystemStore();
|
||||
|
||||
const { list, ScrollList, isLoading, fetchData } = useScrollPagination(getPublishList, {
|
||||
itemHeight: 40,
|
||||
overscan: 20,
|
||||
|
||||
pageSize: 30,
|
||||
defaultParams: {
|
||||
appId: appDetail._id
|
||||
}
|
||||
});
|
||||
const { data: members = [] } = useRequest2(loadAndGetTeamMembers, {
|
||||
manual: !feConfigs.isPlus
|
||||
});
|
||||
const [editIndex, setEditIndex] = useState<number | undefined>(undefined);
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | undefined>(undefined);
|
||||
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const { toast } = useToast();
|
||||
|
||||
return (
|
||||
<ScrollList isLoading={isLoading} flex={'1 0 0'} px={5}>
|
||||
{list.map((data, index) => {
|
||||
const item = data.data;
|
||||
const firstPublishedIndex = list.findIndex((data) => data.data.isPublish);
|
||||
const tmb = members.find((member) => member.tmbId === item.tmbId);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
key={data.index}
|
||||
alignItems={'center'}
|
||||
py={editIndex !== index ? 2 : 1}
|
||||
px={3}
|
||||
borderRadius={'md'}
|
||||
cursor={'pointer'}
|
||||
fontWeight={500}
|
||||
onMouseEnter={() => setHoveredIndex(index)}
|
||||
onMouseLeave={() => setHoveredIndex(undefined)}
|
||||
_hover={{
|
||||
bg: 'primary.50'
|
||||
}}
|
||||
onClick={async () => {
|
||||
const state = {
|
||||
nodes: item.nodes?.map((item) => storeNode2FlowNode({ item })),
|
||||
edges: item.edges?.map((item) => storeEdgesRenderEdge({ edge: item })),
|
||||
title: item.versionName,
|
||||
chatConfig: item.chatConfig
|
||||
};
|
||||
|
||||
const res = await saveSnapshot({
|
||||
pastNodes: state.nodes,
|
||||
pastEdges: state.edges,
|
||||
chatConfig: state.chatConfig,
|
||||
customTitle: `${t('app:app.version_copy')}-${state.title}`
|
||||
});
|
||||
|
||||
if (!res) return;
|
||||
resetSnapshot(state);
|
||||
toast({
|
||||
title: t('workflow:workflow.Switch_success'),
|
||||
status: 'success'
|
||||
});
|
||||
}}
|
||||
>
|
||||
<MyPopover
|
||||
trigger="hover"
|
||||
placement={'bottom-end'}
|
||||
w={'208px'}
|
||||
h={'72px'}
|
||||
Trigger={
|
||||
<Box>
|
||||
<Avatar src={tmb?.avatar} borderRadius={'50%'} w={'24px'} h={'24px'} />
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
{({ onClose }) => (
|
||||
<Flex alignItems={'center'} h={'full'} pl={5} gap={3}>
|
||||
<Box>
|
||||
<Avatar src={tmb?.avatar} borderRadius={'50%'} w={'36px'} h={'36px'} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Box fontSize={'14px'} color={'myGray.900'}>
|
||||
{tmb?.memberName}
|
||||
</Box>
|
||||
<Box fontSize={'12px'} color={'myGray.500'}>
|
||||
{formatTime2YMDHMS(item.time)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
</MyPopover>
|
||||
{editIndex !== index ? (
|
||||
<>
|
||||
<Box
|
||||
ml={3}
|
||||
flex={'1 0 0'}
|
||||
fontSize={'sm'}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
<Box minWidth={0} overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap">
|
||||
<Box as={'span'} color={'myGray.900'}>
|
||||
{item.versionName || formatTime2YMDHMS(item.time)}
|
||||
</Box>
|
||||
</Box>
|
||||
{item.isPublish && (
|
||||
<Tag
|
||||
ml={3}
|
||||
flexShrink={0}
|
||||
type="borderSolid"
|
||||
colorSchema={index === firstPublishedIndex ? 'green' : 'blue'}
|
||||
>
|
||||
{index === firstPublishedIndex
|
||||
? t('app:app.version_current')
|
||||
: t('app:app.version_past')}
|
||||
</Tag>
|
||||
)}
|
||||
</Box>
|
||||
{hoveredIndex === index && (
|
||||
<MyIcon
|
||||
name="edit"
|
||||
w={'18px'}
|
||||
ml={2}
|
||||
_hover={{ color: 'primary.600' }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setEditIndex(index);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<MyBox ml={3} isLoading={isEditing} size={'md'}>
|
||||
<Input
|
||||
autoFocus
|
||||
h={8}
|
||||
defaultValue={item.versionName || formatTime2YMDHMS(item.time)}
|
||||
onBlur={async (e) => {
|
||||
setIsEditing(true);
|
||||
await updateAppVersion({
|
||||
appId: item.appId,
|
||||
versionName: e.target.value,
|
||||
versionId: item._id
|
||||
});
|
||||
await fetchData();
|
||||
setEditIndex(undefined);
|
||||
setIsEditing(false);
|
||||
}}
|
||||
/>
|
||||
</MyBox>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</ScrollList>
|
||||
);
|
||||
};
|
@@ -20,11 +20,11 @@ export const workflowBoxStyles: FlexProps = {
|
||||
export const publishStatusStyle = {
|
||||
unPublish: {
|
||||
colorSchema: 'adora' as any,
|
||||
text: i18nT('common:core.app.not_published')
|
||||
text: i18nT('common:core.app.not_saved')
|
||||
},
|
||||
published: {
|
||||
colorSchema: 'green' as any,
|
||||
text: i18nT('common:core.app.have_publish')
|
||||
text: i18nT('common:core.app.have_saved')
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -6,13 +6,12 @@ import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { AppChatConfigType, AppDetailType } from '@fastgpt/global/core/app/type';
|
||||
import { AppUpdateParams, PostPublishAppProps } from '@/global/core/app/api';
|
||||
import { postPublishApp } from '@/web/core/app/api/version';
|
||||
import { postPublishApp, getAppLatestVersion } from '@/web/core/app/api/version';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useDisclosure } from '@chakra-ui/react';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import { getAppLatestVersion } from '@/web/core/app/api/version';
|
||||
import type { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
|
||||
import type { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
|
||||
|
||||
|
@@ -6,6 +6,7 @@ import type {
|
||||
getLatestVersionQuery,
|
||||
getLatestVersionResponse
|
||||
} from '@/pages/api/core/app/version/latest';
|
||||
import { UpdateAppVersionBody } from '@/pages/api/core/app/version/update';
|
||||
|
||||
export const getAppLatestVersion = (data: getLatestVersionQuery) =>
|
||||
GET<getLatestVersionResponse>('/core/app/version/latest', data);
|
||||
@@ -18,3 +19,6 @@ export const getPublishList = (data: PaginationProps<{ appId: string }>) =>
|
||||
|
||||
export const postRevertVersion = (appId: string, data: PostRevertAppProps) =>
|
||||
POST(`/core/app/version/revert?appId=${appId}`, data);
|
||||
|
||||
export const updateAppVersion = (data: UpdateAppVersionBody) =>
|
||||
POST(`/core/app/version/update`, data);
|
||||
|
@@ -512,3 +512,111 @@ export const compareWorkflow = (workflow1: WorkflowType, workflow2: WorkflowType
|
||||
|
||||
return isEqual(node1, node2);
|
||||
};
|
||||
|
||||
export const compareSnapshot = (
|
||||
snapshot1: {
|
||||
nodes: Node<FlowNodeItemType, string | undefined>[] | undefined;
|
||||
edges: Edge<any>[] | undefined;
|
||||
chatConfig: AppChatConfigType | undefined;
|
||||
},
|
||||
snapshot2: {
|
||||
nodes: Node<FlowNodeItemType, string | undefined>[];
|
||||
edges: Edge<any>[];
|
||||
chatConfig: AppChatConfigType;
|
||||
}
|
||||
) => {
|
||||
const clone1 = cloneDeep(snapshot1);
|
||||
const clone2 = cloneDeep(snapshot2);
|
||||
|
||||
if (!clone1.nodes || !clone2.nodes) return false;
|
||||
const formatEdge = (edges: Edge[] | undefined) => {
|
||||
if (!edges) return [];
|
||||
return edges.map((edge) => ({
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
sourceHandle: edge.sourceHandle,
|
||||
targetHandle: edge.targetHandle,
|
||||
type: edge.type
|
||||
}));
|
||||
};
|
||||
|
||||
if (!isEqual(formatEdge(clone1.edges), formatEdge(clone2.edges))) {
|
||||
console.log('Edge not equal');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
clone1.chatConfig &&
|
||||
clone2.chatConfig &&
|
||||
!isEqual(
|
||||
{
|
||||
welcomeText: clone1.chatConfig?.welcomeText || '',
|
||||
variables: clone1.chatConfig?.variables || [],
|
||||
questionGuide: clone1.chatConfig?.questionGuide || false,
|
||||
ttsConfig: clone1.chatConfig?.ttsConfig || undefined,
|
||||
whisperConfig: clone1.chatConfig?.whisperConfig || undefined,
|
||||
scheduledTriggerConfig: clone1.chatConfig?.scheduledTriggerConfig || undefined,
|
||||
chatInputGuide: clone1.chatConfig?.chatInputGuide || undefined,
|
||||
fileSelectConfig: clone1.chatConfig?.fileSelectConfig || undefined
|
||||
},
|
||||
{
|
||||
welcomeText: clone2.chatConfig?.welcomeText || '',
|
||||
variables: clone2.chatConfig?.variables || [],
|
||||
questionGuide: clone2.chatConfig?.questionGuide || false,
|
||||
ttsConfig: clone2.chatConfig?.ttsConfig || undefined,
|
||||
whisperConfig: clone2.chatConfig?.whisperConfig || undefined,
|
||||
scheduledTriggerConfig: clone2.chatConfig?.scheduledTriggerConfig || undefined,
|
||||
chatInputGuide: clone2.chatConfig?.chatInputGuide || undefined,
|
||||
fileSelectConfig: clone2.chatConfig?.fileSelectConfig || undefined
|
||||
}
|
||||
)
|
||||
) {
|
||||
console.log('chatConfig not equal');
|
||||
return false;
|
||||
}
|
||||
|
||||
const formatNodes = (nodes: Node[]) => {
|
||||
return nodes
|
||||
.filter((node) => {
|
||||
if (!node) return;
|
||||
if (FlowNodeTypeEnum.systemConfig === node.type) return;
|
||||
|
||||
return true;
|
||||
})
|
||||
.map((node) => ({
|
||||
id: node.id,
|
||||
type: node.type,
|
||||
position: node.position,
|
||||
data: {
|
||||
id: node.data.id,
|
||||
flowNodeType: node.data.flowNodeType,
|
||||
inputs: node.data.inputs.map((input: FlowNodeInputItemType) => ({
|
||||
key: input.key,
|
||||
selectedTypeIndex: input.selectedTypeIndex ?? 0,
|
||||
renderTypeLis: input.renderTypeList,
|
||||
valueType: input.valueType,
|
||||
value: input.value ?? undefined
|
||||
})),
|
||||
outputs: node.data.outputs.map((item: FlowNodeOutputItemType) => ({
|
||||
key: item.key,
|
||||
type: item.type,
|
||||
value: item.value ?? undefined
|
||||
})),
|
||||
name: node.data.name,
|
||||
intro: node.data.intro,
|
||||
avatar: node.data.avatar,
|
||||
version: node.data.version
|
||||
}
|
||||
}));
|
||||
};
|
||||
const node1 = formatNodes(clone1.nodes);
|
||||
const node2 = formatNodes(clone2.nodes);
|
||||
|
||||
node1.forEach((node, i) => {
|
||||
if (!isEqual(node, node2[i])) {
|
||||
console.log('node not equal');
|
||||
}
|
||||
});
|
||||
|
||||
return isEqual(node1, node2);
|
||||
};
|
||||
|
Reference in New Issue
Block a user