perf: optimize simple app history (#2782)

* simple app history

* ui

* extract context content into hooks
This commit is contained in:
heheer
2024-09-24 21:09:59 +08:00
committed by GitHub
parent 7aa75f8ee0
commit 6bb10ca150
18 changed files with 736 additions and 893 deletions

View File

@@ -40,6 +40,7 @@ export const iconPaths = {
'common/language/en': () => import('./icons/common/language/en.svg'),
'common/language/zh': () => import('./icons/common/language/zh.svg'),
'common/leftArrowLight': () => import('./icons/common/leftArrowLight.svg'),
'common/line': () => import('./icons/common/line.svg'),
'common/lineChange': () => import('./icons/common/lineChange.svg'),
'common/linkBlue': () => import('./icons/common/linkBlue.svg'),
'common/list': () => import('./icons/common/list.svg'),

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 5 12" fill="none">
<path d="M4.42017 1.30151L0.999965 10.6984" stroke="#DFE2EA" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 164 B

View File

@@ -16,6 +16,8 @@ const CustomRightDrawer = ({
iconSrc,
title,
maxW = ['90vw', '30vw'],
top = 16,
bottom = 0,
children,
isLoading,
showMask = true,
@@ -31,8 +33,8 @@ const CustomRightDrawer = ({
zIndex={100}
maxW={maxW}
w={'100%'}
top={'60px'}
bottom={0}
top={top}
bottom={bottom}
borderLeftRadius={'lg'}
border={'base'}
boxShadow={'2'}

View File

@@ -2,6 +2,7 @@ import { Box, BoxProps, Flex } from '@chakra-ui/react';
import { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
import React, { useMemo } from 'react';
import { useTranslation } from 'next-i18next';
import MyIcon from '@fastgpt/web/components/common/Icon';
const FolderPath = (props: {
paths: ParentTreePathItemType[];
@@ -35,7 +36,7 @@ const FolderPath = (props: {
return paths.length === 0 && !!FirstPathDom ? (
<>{FirstPathDom}</>
) : (
<Flex flex={1} ml={-1.5}>
<Flex flex={1}>
{concatPaths.map((item, i) => (
<Flex key={item.parentId || i} alignItems={'center'}>
<Box
@@ -51,6 +52,7 @@ const FolderPath = (props: {
}
: {
cursor: 'pointer',
fontWeight: 'medium',
color: 'myGray.500',
_hover: {
bg: 'myGray.100',
@@ -64,9 +66,7 @@ const FolderPath = (props: {
{item.parentName}
</Box>
{i !== concatPaths.length - 1 && (
<Box mx={1.5} color={'myGray.500'}>
/
</Box>
<MyIcon name={'common/line'} color={'myGray.500'} mx={1} width={'5px'} />
)}
</Flex>
))}

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import {
Box,
Flex,
@@ -22,18 +22,16 @@ import { useRouter } from 'next/router';
import AppCard from '../WorkflowComponents/AppCard';
import { uiWorkflow2StoreWorkflow } from '../WorkflowComponents/utils';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
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';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useDebounceEffect } from 'ahooks';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import SaveButton from '../Workflow/components/SaveButton';
const PublishHistories = dynamic(() => import('../WorkflowPublishHistoriesSlider'));
const PublishHistories = dynamic(() => import('../PublishHistoriesSlider'));
const Header = () => {
const { t } = useTranslation();
@@ -48,12 +46,6 @@ const Header = () => {
onOpen: onOpenBackConfirm,
onClose: onCloseBackConfirm
} = useDisclosure();
const {
isOpen: isSaveAndPublishModalOpen,
onOpen: onSaveAndPublishModalOpen,
onClose: onSaveAndPublishModalClose
} = useDisclosure();
const [isSave, setIsSave] = useState(false);
const {
flowData2StoreData,
@@ -65,7 +57,9 @@ const Header = () => {
edges,
past,
future,
setPast
setPast,
saveSnapshot,
resetSnapshot
} = useContextSelector(WorkflowContext, (v) => v);
const { lastAppListRouteType } = useSystemStore();
@@ -225,81 +219,11 @@ const Header = () => {
{t('common:core.workflow.Run')}
</Button>
{!historiesDefaultData && (
<MyPopover
placement={'bottom-end'}
hasArrow={false}
offset={[2, 4]}
w={'116px'}
onOpenFunc={() => setIsSave(true)}
onCloseFunc={() => setIsSave(false)}
trigger={'hover'}
Trigger={
<Button
size={'sm'}
rightIcon={
<MyIcon
name={isSave ? 'core/chat/chevronUp' : 'core/chat/chevronDown'}
w={['14px', '16px']}
/>
}
>
<Box>{t('common:common.Save')}</Box>
</Button>
}
>
{({ onClose }) => (
<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({});
toast({
status: 'success',
title: t('app:saved_success'),
position: 'top-right'
});
onClose();
setIsSave(false);
}}
>
<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();
}
onClose();
setIsSave(false);
}}
>
<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>
<SaveButton
isLoading={loading}
onClickSave={onClickSave}
checkData={flowData2StoreDataAndCheck}
/>
)}
</HStack>
)}
@@ -309,6 +233,9 @@ const Header = () => {
onClose={() => {
setHistoriesDefaultData(undefined);
}}
past={past}
saveSnapshot={saveSnapshot}
resetSnapshot={resetSnapshot}
/>
)}
<MyModal
@@ -356,16 +283,12 @@ const Header = () => {
loading,
isV2Workflow,
historiesDefaultData,
isSave,
onClickSave,
setHistoriesDefaultData,
appDetail.chatConfig,
flowData2StoreDataAndCheck,
setWorkflowTestData,
isSaveAndPublishModalOpen,
onSaveAndPublishModalClose,
toast,
onSaveAndPublishModalOpen
toast
]);
return Render;

View File

@@ -1,204 +1,383 @@
import React, { useCallback, useState } from 'react';
import { getPublishList, postRevertVersion } from '@/web/core/app/api/version';
import React, { useState } from 'react';
import {
getAppVersionDetail,
getWorkflowVersionList,
updateAppVersion
} from '@/web/core/app/api/version';
import { useVirtualScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import CustomRightDrawer from '@fastgpt/web/components/common/MyDrawer/CustomRightDrawer';
import { useTranslation } from 'next-i18next';
import { Box, Button, Flex } from '@chakra-ui/react';
import { formatTime2YMDHM } from '@fastgpt/global/common/string/time';
import { Box, Button, Flex, Input } from '@chakra-ui/react';
import { useContextSelector } from 'use-context-selector';
import { AppVersionSchemaType } from '@fastgpt/global/core/app/version';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { AppContext } from './context';
import { useI18n } from '@/web/context/I18n';
import { AppSchema } from '@fastgpt/global/core/app/type';
import PopoverConfirm from '@fastgpt/web/components/common/MyPopover/PopoverConfirm';
export type InitProps = {
nodes: AppSchema['modules'];
edges: AppSchema['edges'];
chatConfig: AppSchema['chatConfig'];
};
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
import { SaveSnapshotParams, SnapshotsType } 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';
import { versionListResponse } from '@/pages/api/core/app/version/listWorkflow';
const PublishHistoriesSlider = ({
onClose,
initData,
defaultData
past,
saveSnapshot,
resetSnapshot,
top,
bottom
}: {
onClose: () => void;
initData: (data: InitProps) => void;
defaultData: InitProps;
past: SnapshotsType[];
saveSnapshot: (params: SaveSnapshotParams) => Promise<boolean>;
resetSnapshot: (state: SnapshotsType) => void;
top?: string | number;
bottom?: string | number;
}) => {
const { t } = useTranslation();
const { appT } = useI18n();
const { appDetail, setAppDetail, reloadAppLatestVersion } = useContextSelector(
AppContext,
(v) => v
);
const appId = appDetail._id;
const [selectedHistoryId, setSelectedHistoryId] = useState<string>();
const { scrollDataList, ScrollList, isLoading } = useVirtualScrollPagination(getPublishList, {
itemHeight: 49,
overscan: 20,
pageSize: 20,
defaultParams: {
appId
}
});
const onPreview = useCallback(
(data: AppVersionSchemaType) => {
setSelectedHistoryId(data._id);
initData({
nodes: data.nodes,
edges: data.edges,
chatConfig: data.chatConfig
});
},
[initData]
);
const onCloseSlider = useCallback(
(data: InitProps) => {
setSelectedHistoryId(undefined);
initData(data);
onClose();
},
[initData, onClose]
);
const { runAsync: onRevert } = useRequest2(
async (data: AppVersionSchemaType) => {
if (!appId) return;
await postRevertVersion(appId, {
versionId: data._id,
editNodes: defaultData.nodes, // old workflow
editEdges: defaultData.edges,
editChatConfig: defaultData.chatConfig
});
setAppDetail((state) => ({
...state,
modules: data.nodes,
edges: data.edges
}));
onCloseSlider(data);
reloadAppLatestVersion();
},
{
successToast: appT('version.Revert success')
}
);
const showLoading = isLoading;
const [currentTab, setCurrentTab] = useState<'myEdit' | 'teamCloud'>('myEdit');
return (
<>
<CustomRightDrawer
onClose={() =>
onCloseSlider({
nodes: defaultData.nodes,
edges: defaultData.edges,
chatConfig: defaultData.chatConfig
})
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
}
iconSrc="core/workflow/versionHistories"
title={t('common:core.workflow.publish.histories')}
maxW={'300px'}
maxW={'340px'}
px={0}
showMask={false}
overflow={'unset'}
top={top}
bottom={bottom}
>
<Button
mx={'20px'}
variant={'whitePrimary'}
mb={2}
isDisabled={!selectedHistoryId}
onClick={() => {
setSelectedHistoryId(undefined);
initData({
nodes: defaultData.nodes,
edges: defaultData.edges,
chatConfig: defaultData.chatConfig
});
}}
>
{appT('current_settings')}
</Button>
<ScrollList isLoading={showLoading} flex={'1 0 0'} px={5}>
{scrollDataList.map((data, index) => {
const item = data.data;
return (
<Flex
key={data.index}
alignItems={'center'}
py={3}
px={3}
borderRadius={'md'}
cursor={'pointer'}
fontWeight={500}
_hover={{
bg: 'primary.50'
}}
{...(selectedHistoryId === item._id && {
color: 'primary.600'
})}
onClick={() => onPreview(item)}
>
<Box
w={'12px'}
h={'12px'}
borderWidth={'2px'}
borderColor={'primary.600'}
borderRadius={'50%'}
position={'relative'}
{...(index !== scrollDataList.length - 1 && {
_after: {
content: '""',
height: '40px',
width: '2px',
bgColor: 'myGray.250',
position: 'absolute',
top: '10px',
left: '3px'
}
})}
></Box>
<Box ml={3} flex={'1 0 0'}>
{formatTime2YMDHM(item.time)}
</Box>
{item._id === selectedHistoryId && (
<PopoverConfirm
showCancel
content={t('common:core.workflow.publish.OnRevert version confirm')}
onConfirm={() => onRevert(item)}
Trigger={
<Box>
<MyTooltip label={t('common:core.workflow.publish.OnRevert version')}>
<MyIcon
name={'core/workflow/revertVersion'}
w={'20px'}
color={'primary.600'}
/>
</MyTooltip>
</Box>
}
/>
)}
</Flex>
);
})}
</ScrollList>
{currentTab === 'myEdit' ? (
<MyEdit past={past} saveSnapshot={saveSnapshot} resetSnapshot={resetSnapshot} />
) : (
<TeamCloud saveSnapshot={saveSnapshot} resetSnapshot={resetSnapshot} />
)}
</CustomRightDrawer>
</>
);
};
export default React.memo(PublishHistoriesSlider);
const MyEdit = ({
past,
saveSnapshot,
resetSnapshot
}: {
past: SnapshotsType[];
saveSnapshot: (params: SaveSnapshotParams) => Promise<boolean>;
resetSnapshot: (state: SnapshotsType) => void;
}) => {
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) {
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) {
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 = ({
saveSnapshot,
resetSnapshot
}: {
saveSnapshot: (params: SaveSnapshotParams) => Promise<boolean>;
resetSnapshot: (state: SnapshotsType) => void;
}) => {
const { t } = useTranslation();
const { appDetail } = useContextSelector(AppContext, (v) => v);
const { loadAndGetTeamMembers } = useUserStore();
const { feConfigs } = useSystemStore();
const { scrollDataList, ScrollList, isLoading, fetchData } = useVirtualScrollPagination(
getWorkflowVersionList,
{
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();
const { runAsync: onChangeVersion, loading: isLoadingVersion } = useRequest2(
async (versionItem: versionListResponse) => {
const versionDetail = await getAppVersionDetail(versionItem._id, versionItem.appId);
if (!versionDetail) return;
const state = {
nodes: versionDetail.nodes?.map((item) => storeNode2FlowNode({ item, t })),
edges: versionDetail.edges?.map((item) => storeEdgesRenderEdge({ edge: item })),
title: versionItem.versionName,
chatConfig: versionDetail.chatConfig
};
await saveSnapshot({
pastNodes: state.nodes,
pastEdges: state.edges,
chatConfig: state.chatConfig,
customTitle: `${t('app:app.version_copy')}-${state.title}`
});
resetSnapshot(state);
toast({
title: t('workflow:workflow.Switch_success'),
status: 'success'
});
}
);
return (
<ScrollList isLoading={isLoading || isLoadingVersion} flex={'1 0 0'} px={5}>
{scrollDataList.map((data, index) => {
const item = data.data;
const firstPublishedIndex = scrollDataList.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={() => editIndex === undefined && onChangeVersion(item)}
>
<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)}
onClick={(e) => e.stopPropagation()}
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>
);
};

View File

@@ -3,13 +3,11 @@ import React, { useCallback, useMemo } from 'react';
import { AppContext, TabEnum } from './context';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import { useI18n } from '@/web/context/I18n';
import { useContextSelector } from 'use-context-selector';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
const RouteTab = () => {
const { t } = useTranslation();
const { appT } = useI18n();
const router = useRouter();
const { appDetail, currentTab } = useContextSelector(AppContext, (v) => v);
@@ -28,20 +26,21 @@ const RouteTab = () => {
const tabList = useMemo(
() => [
{
label: appDetail.type === AppTypeEnum.plugin ? appT('setting_plugin') : appT('setting_app'),
label:
appDetail.type === AppTypeEnum.plugin ? t('app:setting_plugin') : t('app:setting_app'),
id: TabEnum.appEdit
},
...(appDetail.permission.hasManagePer
? [
{
label: appT('publish_channel'),
label: t('app:publish_channel'),
id: TabEnum.publish
},
{ label: appT('chat_logs'), id: TabEnum.logs }
{ label: t('app:chat_logs'), id: TabEnum.logs }
]
: [])
],
[appDetail.permission.hasManagePer, appDetail.type, appT]
[appDetail.permission.hasManagePer, appDetail.type]
);
return (
@@ -51,6 +50,7 @@ const RouteTab = () => {
key={tab.id}
px={2}
py={0.5}
fontWeight={'medium'}
{...(currentTab === tab.id
? {
color: 'primary.700'

View File

@@ -15,21 +15,46 @@ import { cardStyles } from '../constants';
import styles from './styles.module.scss';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { storeEdgesRenderEdge, storeNode2FlowNode } from '@/web/core/workflow/utils';
import { useTranslation } from 'next-i18next';
import { uiWorkflow2StoreWorkflow } from '../WorkflowComponents/utils';
import { SnapshotsType } from '../WorkflowComponents/context';
const Edit = ({
appForm,
setAppForm
setAppForm,
past,
saveSnapshot
}: {
appForm: AppSimpleEditFormType;
setAppForm: React.Dispatch<React.SetStateAction<AppSimpleEditFormType>>;
past: SnapshotsType[];
saveSnapshot: (
this: any,
{ pastNodes, pastEdges, chatConfig, customTitle, isSaved }: any
) => Promise<boolean>;
}) => {
const { isPc } = useSystem();
const { loadAllDatasets } = useDatasetStore();
const { appDetail } = useContextSelector(AppContext, (v) => v);
const { t } = useTranslation();
// show selected dataset
useMount(() => {
loadAllDatasets();
saveSnapshot({
pastNodes: appDetail.modules?.map((item) => storeNode2FlowNode({ item, t })),
pastEdges: appDetail.edges?.map((item) => storeEdgesRenderEdge({ edge: item })),
chatConfig: appDetail.chatConfig,
isSaved: true
});
if (past.length > 0) {
const storeWorkflow = uiWorkflow2StoreWorkflow(past[0]);
const currentAppForm = appWorkflow2Form({ ...storeWorkflow, chatConfig: past[0].chatConfig });
setAppForm(currentAppForm);
return;
}
setAppForm(
appWorkflow2Form({
nodes: appDetail.modules,
@@ -52,7 +77,7 @@ const Edit = ({
display={['block', 'flex']}
flex={'1 0 0'}
h={0}
pt={[2, 1.5]}
mt={[4, 0]}
gap={1}
borderRadius={'lg'}
overflowY={['auto', 'unset']}
@@ -73,7 +98,7 @@ const Edit = ({
</Box>
</Box>
{isPc && (
<Box {...cardStyles} boxShadow={'3'} flex={'2 0 0'} w={0}>
<Box {...cardStyles} boxShadow={'3'} flex={'2 0 0'} w={0} mb={3}>
<ChatTest appForm={appForm} />
</Box>
)}

View File

@@ -1,42 +1,57 @@
import React, { useCallback, useMemo, useState } from 'react';
import React, { useCallback, useState } from 'react';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '../context';
import FolderPath from '@/components/common/folder/Path';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { getAppFolderPath } from '@/web/core/app/api/app';
import { Box, Button, Flex, IconButton } from '@chakra-ui/react';
import { Box, Flex, IconButton } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import RouteTab from '../RouteTab';
import { useTranslation } from 'next-i18next';
import PopoverConfirm from '@fastgpt/web/components/common/MyPopover/PopoverConfirm';
import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { form2AppWorkflow } from '@/web/core/app/utils';
import { TabEnum } from '../context';
import PublishHistoriesSlider, { type InitProps } from '../PublishHistoriesSlider';
import { appWorkflow2Form } from '@fastgpt/global/core/app/utils';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { compareWorkflow } from '@/web/core/workflow/utils';
import MyTag from '@fastgpt/web/components/common/Tag/index';
import { publishStatusStyle } from '../constants';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
import SaveButton from '../Workflow/components/SaveButton';
import dynamic from 'next/dynamic';
import { useDebounceEffect } from 'ahooks';
import { InitProps, SnapshotsType } from '../WorkflowComponents/context';
import { appWorkflow2Form } from '@fastgpt/global/core/app/utils';
import {
compareSnapshot,
storeEdgesRenderEdge,
storeNode2FlowNode
} from '@/web/core/workflow/utils';
import { uiWorkflow2StoreWorkflow } from '../WorkflowComponents/utils';
const PublishHistories = dynamic(() => import('../PublishHistoriesSlider'));
const Header = ({
appForm,
setAppForm
setAppForm,
past,
setPast,
saveSnapshot
}: {
appForm: AppSimpleEditFormType;
setAppForm: React.Dispatch<React.SetStateAction<AppSimpleEditFormType>>;
setAppForm: (form: AppSimpleEditFormType) => void;
past: SnapshotsType[];
setPast: (value: React.SetStateAction<SnapshotsType[]>) => void;
saveSnapshot: (
this: any,
{ pastNodes, pastEdges, chatConfig, customTitle, isSaved }: any
) => Promise<boolean>;
}) => {
const { t } = useTranslation();
const { isPc } = useSystem();
const router = useRouter();
const { toast } = useToast();
const { appId, appDetail, onSaveApp, currentTab } = useContextSelector(AppContext, (v) => v);
const { lastAppListRouteType } = useSystemStore();
const { allDatasets } = useDatasetStore();
@@ -45,8 +60,10 @@ const Header = ({
manual: false,
refreshDeps: [appId]
});
const onclickRoute = useCallback(
const onClickRoute = useCallback(
(parentId: string) => {
localStorage.removeItem(`${appDetail._id}-past`);
router.push({
pathname: '/app/list',
query: {
@@ -58,58 +75,98 @@ const Header = ({
[router, lastAppListRouteType]
);
const isPublished = useMemo(() => {
const data = form2AppWorkflow(appForm, t);
return compareWorkflow(
{
nodes: appDetail.modules,
edges: [],
chatConfig: appDetail.chatConfig
},
{
nodes: data.nodes,
edges: [],
chatConfig: data.chatConfig
}
);
}, [appDetail.chatConfig, appDetail.modules, appForm, allDatasets, t]);
const [isPublished, setIsPublished] = useState(false);
const onSubmitPublish = useCallback(
async (data: AppSimpleEditFormType) => {
const { nodes, edges } = form2AppWorkflow(data, t);
const { runAsync: onClickSave, loading } = useRequest2(
async ({
isPublish,
versionName = formatTime2YMDHMS(new Date())
}: {
isPublish?: boolean;
versionName?: string;
}) => {
const { nodes, edges } = form2AppWorkflow(appForm, t);
await onSaveApp({
nodes,
edges,
chatConfig: data.chatConfig,
chatConfig: appForm.chatConfig,
type: AppTypeEnum.simple,
isPublish: true,
versionName: formatTime2YMDHMS(new Date())
isPublish,
versionName
});
toast({
status: 'success',
title: t('app:publish_success'),
position: 'top-right'
});
},
[onSaveApp, t, toast]
setPast((prevPast) =>
prevPast.map((item, index) =>
index === 0
? {
...item,
isSaved: true
}
: item
)
);
}
);
const [historiesDefaultData, setHistoriesDefaultData] = useState<InitProps>();
const resetSnapshot = (data: SnapshotsType) => {
const storeWorkflow = uiWorkflow2StoreWorkflow(data);
const currentAppForm = appWorkflow2Form({ ...storeWorkflow, chatConfig: data.chatConfig });
setAppForm(currentAppForm);
};
useDebounceEffect(
() => {
const data = form2AppWorkflow(appForm, t);
saveSnapshot({
pastNodes: data.nodes?.map((item) => storeNode2FlowNode({ item, t })),
pastEdges: data.edges?.map((item) => storeEdgesRenderEdge({ edge: item })),
chatConfig: data.chatConfig
});
},
[appForm],
{ wait: 500 }
);
useDebounceEffect(
() => {
const savedSnapshot = past.find((snapshot) => snapshot.isSaved);
const data = form2AppWorkflow(appForm, t);
const val = compareSnapshot(
{
nodes: savedSnapshot?.nodes,
edges: [],
chatConfig: savedSnapshot?.chatConfig
},
{
nodes: data.nodes?.map((item) => storeNode2FlowNode({ item, t })),
edges: [],
chatConfig: data.chatConfig
}
);
setIsPublished(val);
},
[past],
{ wait: 500 }
);
return (
<Box>
<Box h={14}>
{!isPc && (
<Flex pt={2} justifyContent={'center'}>
<Flex justifyContent={'center'}>
<RouteTab />
</Flex>
)}
<Flex pt={[2, 3]} alignItems={'flex-start'} position={'relative'}>
<Flex w={'full'} alignItems={'center'} position={'relative'} h={'full'}>
<Box flex={'1'}>
<FolderPath
rootName={t('app:all_apps')}
paths={paths}
hoverStyle={{ color: 'primary.600' }}
onClick={onclickRoute}
onClick={onClickRoute}
fontSize={'14px'}
/>
</Box>
{isPc && (
@@ -156,36 +213,23 @@ const Header = ({
});
}}
/>
<PopoverConfirm
showCancel
content={t('common:core.app.Publish Confirm')}
Trigger={
<Box>
<MyTooltip label={t('common:core.app.Publish app tip')}>
<Button isDisabled={isPublished}>{t('common:core.app.Publish')}</Button>
</MyTooltip>
</Box>
}
onConfirm={() => onSubmitPublish(appForm)}
/>
<SaveButton isLoading={loading} onClickSave={onClickSave} />
</>
)}
</Flex>
)}
</Flex>
{!!historiesDefaultData && (
<PublishHistoriesSlider
initData={({ nodes, chatConfig }) => {
setAppForm(
appWorkflow2Form({
nodes,
chatConfig
})
);
{historiesDefaultData && currentTab === TabEnum.appEdit && (
<PublishHistories
onClose={() => {
setHistoriesDefaultData(undefined);
}}
onClose={() => setHistoriesDefaultData(undefined)}
defaultData={historiesDefaultData}
past={past}
saveSnapshot={saveSnapshot}
resetSnapshot={resetSnapshot}
top={14}
bottom={3}
/>
)}
</Box>

View File

@@ -9,13 +9,15 @@ import dynamic from 'next/dynamic';
import { Box, Flex } from '@chakra-ui/react';
import { useBeforeunload } from '@fastgpt/web/hooks/useBeforeunload';
import { useTranslation } from 'next-i18next';
import useSnapshots from './useSnapshots';
const Logs = dynamic(() => import('../Logs/index'));
const PublishChannel = dynamic(() => import('../Publish'));
const SimpleEdit = () => {
const { t } = useTranslation();
const { currentTab } = useContextSelector(AppContext, (v) => v);
const { currentTab, appDetail } = useContextSelector(AppContext, (v) => v);
const { past, setPast, saveSnapshot } = useSnapshots(appDetail._id);
const [appForm, setAppForm] = useState(getDefaultAppForm());
@@ -24,12 +26,18 @@ const SimpleEdit = () => {
});
return (
<Flex h={'100%'} flexDirection={'column'} px={[3, 0]} pr={[3, 3]} pb={3}>
<Header appForm={appForm} setAppForm={setAppForm} />
<Flex h={'100%'} flexDirection={'column'} px={[3, 0]} pr={[3, 3]}>
<Header
appForm={appForm}
setAppForm={setAppForm}
past={past}
setPast={setPast}
saveSnapshot={saveSnapshot}
/>
{currentTab === TabEnum.appEdit ? (
<Edit appForm={appForm} setAppForm={setAppForm} />
<Edit appForm={appForm} setAppForm={setAppForm} past={past} saveSnapshot={saveSnapshot} />
) : (
<Box flex={'1 0 0'} h={0} mt={4}>
<Box flex={'1 0 0'} h={0} mt={[4, 0]}>
{currentTab === TabEnum.publish && <PublishChannel />}
{currentTab === TabEnum.logs && <Logs />}
</Box>

View File

@@ -0,0 +1,47 @@
import { useLocalStorageState, useMemoizedFn } from 'ahooks';
import { SnapshotsType } from '../WorkflowComponents/context';
import { SetStateAction } from 'react';
import { compareSnapshot } from '@/web/core/workflow/utils';
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
const useSnapshots = (appId: string) => {
const [past, setPast] = useLocalStorageState<SnapshotsType[]>(`${appId}-past-simple`, {
defaultValue: [],
listenStorageChange: true
}) as [SnapshotsType[], (value: SetStateAction<SnapshotsType[]>) => void];
const saveSnapshot = useMemoizedFn(
async ({ pastNodes, pastEdges, chatConfig, customTitle, isSaved }) => {
const pastState = past[0];
const isPastEqual = compareSnapshot(
{
nodes: pastNodes,
edges: pastEdges,
chatConfig: chatConfig
},
{
nodes: pastState?.nodes,
edges: pastState?.edges,
chatConfig: pastState?.chatConfig
}
);
if (isPastEqual) return false;
setPast((past) => [
{
nodes: pastNodes,
edges: pastEdges,
title: customTitle || formatTime2YMDHMS(new Date()),
chatConfig,
isSaved
},
...past.slice(0, 199)
]);
return true;
}
);
return { past, setPast, saveSnapshot };
};
export default useSnapshots;

View File

@@ -22,18 +22,16 @@ import { useRouter } from 'next/router';
import AppCard from '../WorkflowComponents/AppCard';
import { uiWorkflow2StoreWorkflow } from '../WorkflowComponents/utils';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
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';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useDebounceEffect } from 'ahooks';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import SaveButton from './components/SaveButton';
const PublishHistories = dynamic(() => import('../WorkflowPublishHistoriesSlider'));
const PublishHistories = dynamic(() => import('../PublishHistoriesSlider'));
const Header = () => {
const { t } = useTranslation();
@@ -48,12 +46,6 @@ const Header = () => {
onOpen: onOpenBackConfirm,
onClose: onCloseBackConfirm
} = useDisclosure();
const {
isOpen: isSaveAndPublishModalOpen,
onOpen: onSaveAndPublishModalOpen,
onClose: onSaveAndPublishModalClose
} = useDisclosure();
const [isSave, setIsSave] = useState(false);
const {
flowData2StoreData,
@@ -65,7 +57,9 @@ const Header = () => {
edges,
past,
future,
setPast
setPast,
saveSnapshot,
resetSnapshot
} = useContextSelector(WorkflowContext, (v) => v);
const { lastAppListRouteType } = useSystemStore();
@@ -227,81 +221,11 @@ const Header = () => {
{t('common:core.workflow.Run')}
</Button>
{!historiesDefaultData && (
<MyPopover
placement={'bottom-end'}
hasArrow={false}
offset={[2, 4]}
w={'116px'}
onOpenFunc={() => setIsSave(true)}
onCloseFunc={() => setIsSave(false)}
trigger={'hover'}
Trigger={
<Button
size={'sm'}
rightIcon={
<MyIcon
name={isSave ? 'core/chat/chevronUp' : 'core/chat/chevronDown'}
w={['14px', '16px']}
/>
}
>
<Box>{t('common:common.Save')}</Box>
</Button>
}
>
{({ onClose }) => (
<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({});
toast({
status: 'success',
title: t('app:saved_success'),
position: 'top-right'
});
onClose();
setIsSave(false);
}}
>
<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();
}
onClose();
setIsSave(false);
}}
>
<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>
<SaveButton
isLoading={loading}
onClickSave={onClickSave}
checkData={flowData2StoreDataAndCheck}
/>
)}
</HStack>
)}
@@ -311,8 +235,12 @@ const Header = () => {
onClose={() => {
setHistoriesDefaultData(undefined);
}}
past={past}
saveSnapshot={saveSnapshot}
resetSnapshot={resetSnapshot}
/>
)}
<MyModal
isOpen={isOpenBackConfirm}
onClose={onCloseBackConfirm}
@@ -358,16 +286,13 @@ const Header = () => {
loading,
isV2Workflow,
historiesDefaultData,
isSave,
onClickSave,
setHistoriesDefaultData,
appDetail.chatConfig,
flowData2StoreDataAndCheck,
setWorkflowTestData,
isSaveAndPublishModalOpen,
onSaveAndPublishModalClose,
toast,
onSaveAndPublishModalOpen
past
]);
return Render;

View File

@@ -0,0 +1,115 @@
import { Box, Button, Flex, useDisclosure } from '@chakra-ui/react';
import MyPopover from '@fastgpt/web/components/common/MyPopover';
import React, { useState } from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { useToast } from '@fastgpt/web/hooks/useToast';
import SaveAndPublishModal from '../../WorkflowComponents/Flow/components/SaveAndPublish';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
const SaveButton = ({
isLoading,
onClickSave,
checkData
}: {
isLoading: boolean;
onClickSave: (options: { isPublish?: boolean; versionName?: string }) => Promise<void>;
checkData?: (hideTip?: boolean) =>
| {
nodes: StoreNodeItemType[];
edges: StoreEdgeItemType[];
}
| undefined;
}) => {
const { t } = useTranslation();
const [isSave, setIsSave] = useState(false);
const { toast } = useToast();
const {
isOpen: isSaveAndPublishModalOpen,
onOpen: onSaveAndPublishModalOpen,
onClose: onSaveAndPublishModalClose
} = useDisclosure();
return (
<MyPopover
placement={'bottom-end'}
hasArrow={false}
offset={[2, 4]}
w={'116px'}
onOpenFunc={() => setIsSave(true)}
onCloseFunc={() => setIsSave(false)}
trigger={'hover'}
Trigger={
<Button
size={'sm'}
rightIcon={
<MyIcon
name={isSave ? 'core/chat/chevronUp' : 'core/chat/chevronDown'}
w={['14px', '16px']}
/>
}
>
<Box>{t('common:common.Save')}</Box>
</Button>
}
>
{({ onClose }) => (
<Box 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={isLoading}
onClick={async () => {
await onClickSave({});
toast({
status: 'success',
title: t('app:saved_success'),
position: 'top-right'
});
onClose();
setIsSave(false);
}}
>
<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 canOpen = !checkData || checkData();
if (canOpen) {
onSaveAndPublishModalOpen();
}
onClose();
setIsSave(false);
}}
>
<MyIcon name={'core/workflow/publish'} w={'16px'} mr={2} />
<Box fontSize={'sm'}>{t('common:core.workflow.Save and publish')}</Box>
{isSaveAndPublishModalOpen && (
<SaveAndPublishModal
isLoading={isLoading}
onClose={onSaveAndPublishModalClose}
onClickSave={onClickSave}
/>
)}
</Flex>
</Box>
)}
</MyPopover>
);
};
export default React.memo(SaveButton);

View File

@@ -39,7 +39,7 @@ import { defaultRunningStatus } from './constants';
import { checkNodeRunStatus } from '@fastgpt/global/core/workflow/runtime/utils';
import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus';
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
import { AppChatConfigType } from '@fastgpt/global/core/app/type';
import { AppChatConfigType, AppSchema } from '@fastgpt/global/core/app/type';
import { AppContext } from '@/pages/app/detail/components/context';
import ChatTest from './Flow/ChatTest';
import { useDisclosure } from '@chakra-ui/react';
@@ -47,7 +47,6 @@ import { uiWorkflow2StoreWorkflow } from './utils';
import { useTranslation } from 'next-i18next';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { formatTime2YMDHMS, formatTime2YMDHMW } from '@fastgpt/global/common/string/time';
import type { InitProps } from '@/pages/app/detail/components/PublishHistoriesSlider';
import { cloneDeep } from 'lodash';
import { SetState } from 'ahooks/lib/createUseStorageState';
@@ -60,6 +59,18 @@ export type SnapshotsType = {
chatConfig: AppChatConfigType;
isSaved?: boolean;
};
export type SaveSnapshotParams = {
pastNodes?: Node[];
pastEdges?: Edge[];
customTitle?: string;
chatConfig?: AppChatConfigType;
};
export type InitProps = {
nodes: AppSchema['modules'];
edges: AppSchema['edges'];
chatConfig: AppSchema['chatConfig'];
};
type WorkflowContextType = {
appId?: string;
basicNodeTemplates: FlowNodeTemplateType[];

View File

@@ -1,351 +0,0 @@
import React, { useState } from 'react';
import {
getAppVersionDetail,
getWorkflowVersionList,
updateAppVersion
} from '@/web/core/app/api/version';
import { useVirtualScrollPagination } 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';
import { versionListResponse } from '@/pages/api/core/app/version/listWorkflow';
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}
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) {
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) {
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 { scrollDataList, ScrollList, isLoading, fetchData } = useVirtualScrollPagination(
getWorkflowVersionList,
{
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();
const { runAsync: onChangeVersion, loading: isLoadingVersion } = useRequest2(
async (versionItem: versionListResponse) => {
const versionDetail = await getAppVersionDetail(versionItem._id, versionItem.appId);
if (!versionDetail) return;
const state = {
nodes: versionDetail.nodes?.map((item) => storeNode2FlowNode({ item, t })),
edges: versionDetail.edges?.map((item) => storeEdgesRenderEdge({ edge: item })),
title: versionItem.versionName,
chatConfig: versionDetail.chatConfig
};
await saveSnapshot({
pastNodes: state.nodes,
pastEdges: state.edges,
chatConfig: state.chatConfig,
customTitle: `${t('app:app.version_copy')}-${state.title}`
});
resetSnapshot(state);
toast({
title: t('workflow:workflow.Switch_success'),
status: 'success'
});
}
);
return (
<ScrollList isLoading={isLoading || isLoadingVersion} flex={'1 0 0'} px={5}>
{scrollDataList.map((data, index) => {
const item = data.data;
const firstPublishedIndex = scrollDataList.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={() => editIndex === undefined && onChangeVersion(item)}
>
<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)}
onClick={(e) => e.stopPropagation()}
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>
);
};

View File

@@ -1,4 +1,4 @@
import { Dispatch, ReactNode, SetStateAction, useCallback, useState } from 'react';
import { Dispatch, ReactNode, SetStateAction, useCallback, useEffect, useState } from 'react';
import { createContext } from 'use-context-selector';
import { defaultApp } from '@/web/core/app/constants';
import { delAppById, getAppDetailById, putAppById } from '@/web/core/app/api';
@@ -11,7 +11,6 @@ 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 type { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import type { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';

View File

@@ -15,8 +15,8 @@ export const getAppLatestVersion = (data: getLatestVersionQuery) =>
export const postPublishApp = (appId: string, data: PostPublishAppProps) =>
POST(`/core/app/version/publish?appId=${appId}`, data);
export const getPublishList = (data: PaginationProps<{ appId: string }>) =>
POST<PaginationResponse<AppVersionSchemaType>>('/core/app/version/list', data);
// export const getPublishList = (data: PaginationProps<{ appId: string }>) =>
// POST<PaginationResponse<AppVersionSchemaType>>('/core/app/version/list', data);
export const getWorkflowVersionList = (data: PaginationProps<{ appId: string }>) =>
POST<PaginationResponse<versionListResponse>>('/core/app/version/listWorkflow', data);
@@ -24,8 +24,8 @@ export const getWorkflowVersionList = (data: PaginationProps<{ appId: string }>)
export const getAppVersionDetail = (versionId: string, appId: string) =>
GET<AppVersionSchemaType>(`/core/app/version/detail?versionId=${versionId}&appId=${appId}`);
export const postRevertVersion = (appId: string, data: PostRevertAppProps) =>
POST(`/core/app/version/revert?appId=${appId}`, data);
// 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);

View File

@@ -438,94 +438,6 @@ export const getLatestNodeTemplate = (
return updatedNode;
};
type WorkflowType = {
nodes: StoreNodeItemType[];
edges: StoreEdgeItemType[];
chatConfig: AppChatConfigType;
};
export const compareWorkflow = (workflow1: WorkflowType, workflow2: WorkflowType) => {
const clone1 = cloneDeep(workflow1);
const clone2 = cloneDeep(workflow2);
if (!isEqual(clone1.edges, 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: StoreNodeItemType[]) => {
return nodes
.filter((node) => {
if (!node) return;
if ([FlowNodeTypeEnum.systemConfig].includes(node.flowNodeType)) return;
return true;
})
.map((node) => ({
flowNodeType: node.flowNodeType,
inputs: node.inputs.map((input) => ({
key: input.key,
selectedTypeIndex: input.selectedTypeIndex ?? 0,
renderTypeLis: input.renderTypeList,
valueType: input.valueType,
value: input.value ?? undefined
})),
outputs: node.outputs.map((item) => ({
key: item.key,
type: item.type,
value: item.value ?? undefined
})),
name: node.name,
intro: node.intro,
avatar: node.avatar,
version: node.version,
position: node.position
}));
};
const node1 = formatNodes(clone1.nodes);
const node2 = formatNodes(clone2.nodes);
// console.log(node1);
// console.log(node2);
node1.forEach((node, i) => {
if (!isEqual(node, node2[i])) {
console.log('node not equal');
}
});
return isEqual(node1, node2);
};
export const compareSnapshot = (
snapshot1: {
nodes: Node<FlowNodeItemType, string | undefined>[] | undefined;