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 shilin66
parent 5cfdfabd4e
commit d4d3be34a6
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/en': () => import('./icons/common/language/en.svg'),
'common/language/zh': () => import('./icons/common/language/zh.svg'), 'common/language/zh': () => import('./icons/common/language/zh.svg'),
'common/leftArrowLight': () => import('./icons/common/leftArrowLight.svg'), 'common/leftArrowLight': () => import('./icons/common/leftArrowLight.svg'),
'common/line': () => import('./icons/common/line.svg'),
'common/lineChange': () => import('./icons/common/lineChange.svg'), 'common/lineChange': () => import('./icons/common/lineChange.svg'),
'common/linkBlue': () => import('./icons/common/linkBlue.svg'), 'common/linkBlue': () => import('./icons/common/linkBlue.svg'),
'common/list': () => import('./icons/common/list.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, iconSrc,
title, title,
maxW = ['90vw', '30vw'], maxW = ['90vw', '30vw'],
top = 16,
bottom = 0,
children, children,
isLoading, isLoading,
showMask = true, showMask = true,
@@ -31,8 +33,8 @@ const CustomRightDrawer = ({
zIndex={100} zIndex={100}
maxW={maxW} maxW={maxW}
w={'100%'} w={'100%'}
top={'60px'} top={top}
bottom={0} bottom={bottom}
borderLeftRadius={'lg'} borderLeftRadius={'lg'}
border={'base'} border={'base'}
boxShadow={'2'} 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 { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import MyIcon from '@fastgpt/web/components/common/Icon';
const FolderPath = (props: { const FolderPath = (props: {
paths: ParentTreePathItemType[]; paths: ParentTreePathItemType[];
@@ -35,7 +36,7 @@ const FolderPath = (props: {
return paths.length === 0 && !!FirstPathDom ? ( return paths.length === 0 && !!FirstPathDom ? (
<>{FirstPathDom}</> <>{FirstPathDom}</>
) : ( ) : (
<Flex flex={1} ml={-1.5}> <Flex flex={1}>
{concatPaths.map((item, i) => ( {concatPaths.map((item, i) => (
<Flex key={item.parentId || i} alignItems={'center'}> <Flex key={item.parentId || i} alignItems={'center'}>
<Box <Box
@@ -51,6 +52,7 @@ const FolderPath = (props: {
} }
: { : {
cursor: 'pointer', cursor: 'pointer',
fontWeight: 'medium',
color: 'myGray.500', color: 'myGray.500',
_hover: { _hover: {
bg: 'myGray.100', bg: 'myGray.100',
@@ -64,9 +66,7 @@ const FolderPath = (props: {
{item.parentName} {item.parentName}
</Box> </Box>
{i !== concatPaths.length - 1 && ( {i !== concatPaths.length - 1 && (
<Box mx={1.5} color={'myGray.500'}> <MyIcon name={'common/line'} color={'myGray.500'} mx={1} width={'5px'} />
/
</Box>
)} )}
</Flex> </Flex>
))} ))}

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useMemo, useState } from 'react';
import { import {
Box, Box,
Flex, Flex,
@@ -22,18 +22,16 @@ import { useRouter } from 'next/router';
import AppCard from '../WorkflowComponents/AppCard'; import AppCard from '../WorkflowComponents/AppCard';
import { uiWorkflow2StoreWorkflow } from '../WorkflowComponents/utils'; import { uiWorkflow2StoreWorkflow } from '../WorkflowComponents/utils';
import { useSystem } from '@fastgpt/web/hooks/useSystem'; 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 { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import MyModal from '@fastgpt/web/components/common/MyModal'; import MyModal from '@fastgpt/web/components/common/MyModal';
import { compareSnapshot } from '@/web/core/workflow/utils'; import { compareSnapshot } from '@/web/core/workflow/utils';
import SaveAndPublishModal from '../WorkflowComponents/Flow/components/SaveAndPublish';
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time'; import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
import { useToast } from '@fastgpt/web/hooks/useToast'; import { useToast } from '@fastgpt/web/hooks/useToast';
import { useDebounceEffect } from 'ahooks'; import { useDebounceEffect } from 'ahooks';
import { useSystemStore } from '@/web/common/system/useSystemStore'; 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 Header = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -48,12 +46,6 @@ const Header = () => {
onOpen: onOpenBackConfirm, onOpen: onOpenBackConfirm,
onClose: onCloseBackConfirm onClose: onCloseBackConfirm
} = useDisclosure(); } = useDisclosure();
const {
isOpen: isSaveAndPublishModalOpen,
onOpen: onSaveAndPublishModalOpen,
onClose: onSaveAndPublishModalClose
} = useDisclosure();
const [isSave, setIsSave] = useState(false);
const { const {
flowData2StoreData, flowData2StoreData,
@@ -65,7 +57,9 @@ const Header = () => {
edges, edges,
past, past,
future, future,
setPast setPast,
saveSnapshot,
resetSnapshot
} = useContextSelector(WorkflowContext, (v) => v); } = useContextSelector(WorkflowContext, (v) => v);
const { lastAppListRouteType } = useSystemStore(); const { lastAppListRouteType } = useSystemStore();
@@ -225,81 +219,11 @@ const Header = () => {
{t('common:core.workflow.Run')} {t('common:core.workflow.Run')}
</Button> </Button>
{!historiesDefaultData && ( {!historiesDefaultData && (
<MyPopover <SaveButton
placement={'bottom-end'} isLoading={loading}
hasArrow={false} onClickSave={onClickSave}
offset={[2, 4]} checkData={flowData2StoreDataAndCheck}
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>
)} )}
</HStack> </HStack>
)} )}
@@ -309,6 +233,9 @@ const Header = () => {
onClose={() => { onClose={() => {
setHistoriesDefaultData(undefined); setHistoriesDefaultData(undefined);
}} }}
past={past}
saveSnapshot={saveSnapshot}
resetSnapshot={resetSnapshot}
/> />
)} )}
<MyModal <MyModal
@@ -356,16 +283,12 @@ const Header = () => {
loading, loading,
isV2Workflow, isV2Workflow,
historiesDefaultData, historiesDefaultData,
isSave,
onClickSave, onClickSave,
setHistoriesDefaultData, setHistoriesDefaultData,
appDetail.chatConfig, appDetail.chatConfig,
flowData2StoreDataAndCheck, flowData2StoreDataAndCheck,
setWorkflowTestData, setWorkflowTestData,
isSaveAndPublishModalOpen, toast
onSaveAndPublishModalClose,
toast,
onSaveAndPublishModalOpen
]); ]);
return Render; return Render;

View File

@@ -1,204 +1,383 @@
import React, { useCallback, useState } from 'react'; import React, { useState } from 'react';
import { getPublishList, postRevertVersion } from '@/web/core/app/api/version'; import {
getAppVersionDetail,
getWorkflowVersionList,
updateAppVersion
} from '@/web/core/app/api/version';
import { useVirtualScrollPagination } from '@fastgpt/web/hooks/useScrollPagination'; import { useVirtualScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import CustomRightDrawer from '@fastgpt/web/components/common/MyDrawer/CustomRightDrawer'; import CustomRightDrawer from '@fastgpt/web/components/common/MyDrawer/CustomRightDrawer';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { Box, Button, Flex } from '@chakra-ui/react'; import { Box, Button, Flex, Input } from '@chakra-ui/react';
import { formatTime2YMDHM } from '@fastgpt/global/common/string/time';
import { useContextSelector } from 'use-context-selector'; 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 { AppContext } from './context';
import { useI18n } from '@/web/context/I18n'; import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
import { AppSchema } from '@fastgpt/global/core/app/type'; import { SaveSnapshotParams, SnapshotsType } from './WorkflowComponents/context';
import PopoverConfirm from '@fastgpt/web/components/common/MyPopover/PopoverConfirm'; import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
import Avatar from '@fastgpt/web/components/common/Avatar';
export type InitProps = { import Tag from '@fastgpt/web/components/common/Tag';
nodes: AppSchema['modules']; import MyIcon from '@fastgpt/web/components/common/Icon';
edges: AppSchema['edges']; import MyPopover from '@fastgpt/web/components/common/MyPopover';
chatConfig: AppSchema['chatConfig']; 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 = ({ const PublishHistoriesSlider = ({
onClose, onClose,
initData, past,
defaultData saveSnapshot,
resetSnapshot,
top,
bottom
}: { }: {
onClose: () => void; onClose: () => void;
initData: (data: InitProps) => void; past: SnapshotsType[];
defaultData: InitProps; saveSnapshot: (params: SaveSnapshotParams) => Promise<boolean>;
resetSnapshot: (state: SnapshotsType) => void;
top?: string | number;
bottom?: string | number;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { appT } = useI18n(); const [currentTab, setCurrentTab] = useState<'myEdit' | 'teamCloud'>('myEdit');
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;
return ( return (
<> <>
<CustomRightDrawer <CustomRightDrawer
onClose={() => onClose={() => onClose()}
onCloseSlider({ title={
nodes: defaultData.nodes, (
edges: defaultData.edges, <>
chatConfig: defaultData.chatConfig <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" maxW={'340px'}
title={t('common:core.workflow.publish.histories')}
maxW={'300px'}
px={0} px={0}
showMask={false} showMask={false}
overflow={'unset'} overflow={'unset'}
top={top}
bottom={bottom}
> >
<Button {currentTab === 'myEdit' ? (
mx={'20px'} <MyEdit past={past} saveSnapshot={saveSnapshot} resetSnapshot={resetSnapshot} />
variant={'whitePrimary'} ) : (
mb={2} <TeamCloud saveSnapshot={saveSnapshot} resetSnapshot={resetSnapshot} />
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>
</CustomRightDrawer> </CustomRightDrawer>
</> </>
); );
}; };
export default React.memo(PublishHistoriesSlider); 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 { AppContext, TabEnum } from './context';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useI18n } from '@/web/context/I18n';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
const RouteTab = () => { const RouteTab = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { appT } = useI18n();
const router = useRouter(); const router = useRouter();
const { appDetail, currentTab } = useContextSelector(AppContext, (v) => v); const { appDetail, currentTab } = useContextSelector(AppContext, (v) => v);
@@ -28,20 +26,21 @@ const RouteTab = () => {
const tabList = useMemo( 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 id: TabEnum.appEdit
}, },
...(appDetail.permission.hasManagePer ...(appDetail.permission.hasManagePer
? [ ? [
{ {
label: appT('publish_channel'), label: t('app:publish_channel'),
id: TabEnum.publish 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 ( return (
@@ -51,6 +50,7 @@ const RouteTab = () => {
key={tab.id} key={tab.id}
px={2} px={2}
py={0.5} py={0.5}
fontWeight={'medium'}
{...(currentTab === tab.id {...(currentTab === tab.id
? { ? {
color: 'primary.700' color: 'primary.700'

View File

@@ -15,21 +15,46 @@ import { cardStyles } from '../constants';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
import { useSystem } from '@fastgpt/web/hooks/useSystem'; 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 = ({ const Edit = ({
appForm, appForm,
setAppForm setAppForm,
past,
saveSnapshot
}: { }: {
appForm: AppSimpleEditFormType; appForm: AppSimpleEditFormType;
setAppForm: React.Dispatch<React.SetStateAction<AppSimpleEditFormType>>; setAppForm: React.Dispatch<React.SetStateAction<AppSimpleEditFormType>>;
past: SnapshotsType[];
saveSnapshot: (
this: any,
{ pastNodes, pastEdges, chatConfig, customTitle, isSaved }: any
) => Promise<boolean>;
}) => { }) => {
const { isPc } = useSystem(); const { isPc } = useSystem();
const { loadAllDatasets } = useDatasetStore(); const { loadAllDatasets } = useDatasetStore();
const { appDetail } = useContextSelector(AppContext, (v) => v); const { appDetail } = useContextSelector(AppContext, (v) => v);
const { t } = useTranslation();
// show selected dataset // show selected dataset
useMount(() => { useMount(() => {
loadAllDatasets(); 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( setAppForm(
appWorkflow2Form({ appWorkflow2Form({
nodes: appDetail.modules, nodes: appDetail.modules,
@@ -52,7 +77,7 @@ const Edit = ({
display={['block', 'flex']} display={['block', 'flex']}
flex={'1 0 0'} flex={'1 0 0'}
h={0} h={0}
pt={[2, 1.5]} mt={[4, 0]}
gap={1} gap={1}
borderRadius={'lg'} borderRadius={'lg'}
overflowY={['auto', 'unset']} overflowY={['auto', 'unset']}
@@ -73,7 +98,7 @@ const Edit = ({
</Box> </Box>
</Box> </Box>
{isPc && ( {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} /> <ChatTest appForm={appForm} />
</Box> </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 { useContextSelector } from 'use-context-selector';
import { AppContext } from '../context'; import { AppContext } from '../context';
import FolderPath from '@/components/common/folder/Path'; import FolderPath from '@/components/common/folder/Path';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { getAppFolderPath } from '@/web/core/app/api/app'; 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 { useRouter } from 'next/router';
import RouteTab from '../RouteTab'; import RouteTab from '../RouteTab';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import PopoverConfirm from '@fastgpt/web/components/common/MyPopover/PopoverConfirm';
import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type'; import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { form2AppWorkflow } from '@/web/core/app/utils'; import { form2AppWorkflow } from '@/web/core/app/utils';
import { TabEnum } from '../context'; 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 MyIcon from '@fastgpt/web/components/common/Icon';
import { compareWorkflow } from '@/web/core/workflow/utils';
import MyTag from '@fastgpt/web/components/common/Tag/index'; import MyTag from '@fastgpt/web/components/common/Tag/index';
import { publishStatusStyle } from '../constants'; import { publishStatusStyle } from '../constants';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useSystem } from '@fastgpt/web/hooks/useSystem'; import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time'; import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useDatasetStore } from '@/web/core/dataset/store/dataset'; 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 = ({ const Header = ({
appForm, appForm,
setAppForm setAppForm,
past,
setPast,
saveSnapshot
}: { }: {
appForm: AppSimpleEditFormType; 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 { t } = useTranslation();
const { isPc } = useSystem(); const { isPc } = useSystem();
const router = useRouter(); const router = useRouter();
const { toast } = useToast();
const { appId, appDetail, onSaveApp, currentTab } = useContextSelector(AppContext, (v) => v); const { appId, appDetail, onSaveApp, currentTab } = useContextSelector(AppContext, (v) => v);
const { lastAppListRouteType } = useSystemStore(); const { lastAppListRouteType } = useSystemStore();
const { allDatasets } = useDatasetStore(); const { allDatasets } = useDatasetStore();
@@ -45,8 +60,10 @@ const Header = ({
manual: false, manual: false,
refreshDeps: [appId] refreshDeps: [appId]
}); });
const onclickRoute = useCallback( const onClickRoute = useCallback(
(parentId: string) => { (parentId: string) => {
localStorage.removeItem(`${appDetail._id}-past`);
router.push({ router.push({
pathname: '/app/list', pathname: '/app/list',
query: { query: {
@@ -58,58 +75,98 @@ const Header = ({
[router, lastAppListRouteType] [router, lastAppListRouteType]
); );
const isPublished = useMemo(() => { const [isPublished, setIsPublished] = useState(false);
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 onSubmitPublish = useCallback( const { runAsync: onClickSave, loading } = useRequest2(
async (data: AppSimpleEditFormType) => { async ({
const { nodes, edges } = form2AppWorkflow(data, t); isPublish,
versionName = formatTime2YMDHMS(new Date())
}: {
isPublish?: boolean;
versionName?: string;
}) => {
const { nodes, edges } = form2AppWorkflow(appForm, t);
await onSaveApp({ await onSaveApp({
nodes, nodes,
edges, edges,
chatConfig: data.chatConfig, chatConfig: appForm.chatConfig,
type: AppTypeEnum.simple, type: AppTypeEnum.simple,
isPublish: true, isPublish,
versionName: formatTime2YMDHMS(new Date()) versionName
}); });
toast({ setPast((prevPast) =>
status: 'success', prevPast.map((item, index) =>
title: t('app:publish_success'), index === 0
position: 'top-right' ? {
}); ...item,
}, isSaved: true
[onSaveApp, t, toast] }
: item
)
);
}
); );
const [historiesDefaultData, setHistoriesDefaultData] = useState<InitProps>(); 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 ( return (
<Box> <Box h={14}>
{!isPc && ( {!isPc && (
<Flex pt={2} justifyContent={'center'}> <Flex justifyContent={'center'}>
<RouteTab /> <RouteTab />
</Flex> </Flex>
)} )}
<Flex pt={[2, 3]} alignItems={'flex-start'} position={'relative'}> <Flex w={'full'} alignItems={'center'} position={'relative'} h={'full'}>
<Box flex={'1'}> <Box flex={'1'}>
<FolderPath <FolderPath
rootName={t('app:all_apps')} rootName={t('app:all_apps')}
paths={paths} paths={paths}
hoverStyle={{ color: 'primary.600' }} hoverStyle={{ color: 'primary.600' }}
onClick={onclickRoute} onClick={onClickRoute}
fontSize={'14px'}
/> />
</Box> </Box>
{isPc && ( {isPc && (
@@ -156,36 +213,23 @@ const Header = ({
}); });
}} }}
/> />
<PopoverConfirm <SaveButton isLoading={loading} onClickSave={onClickSave} />
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)}
/>
</> </>
)} )}
</Flex> </Flex>
)} )}
</Flex> </Flex>
{!!historiesDefaultData && ( {historiesDefaultData && currentTab === TabEnum.appEdit && (
<PublishHistoriesSlider <PublishHistories
initData={({ nodes, chatConfig }) => { onClose={() => {
setAppForm( setHistoriesDefaultData(undefined);
appWorkflow2Form({
nodes,
chatConfig
})
);
}} }}
onClose={() => setHistoriesDefaultData(undefined)} past={past}
defaultData={historiesDefaultData} saveSnapshot={saveSnapshot}
resetSnapshot={resetSnapshot}
top={14}
bottom={3}
/> />
)} )}
</Box> </Box>

View File

@@ -9,13 +9,15 @@ import dynamic from 'next/dynamic';
import { Box, Flex } from '@chakra-ui/react'; import { Box, Flex } from '@chakra-ui/react';
import { useBeforeunload } from '@fastgpt/web/hooks/useBeforeunload'; import { useBeforeunload } from '@fastgpt/web/hooks/useBeforeunload';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import useSnapshots from './useSnapshots';
const Logs = dynamic(() => import('../Logs/index')); const Logs = dynamic(() => import('../Logs/index'));
const PublishChannel = dynamic(() => import('../Publish')); const PublishChannel = dynamic(() => import('../Publish'));
const SimpleEdit = () => { const SimpleEdit = () => {
const { t } = useTranslation(); 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()); const [appForm, setAppForm] = useState(getDefaultAppForm());
@@ -24,12 +26,18 @@ const SimpleEdit = () => {
}); });
return ( return (
<Flex h={'100%'} flexDirection={'column'} px={[3, 0]} pr={[3, 3]} pb={3}> <Flex h={'100%'} flexDirection={'column'} px={[3, 0]} pr={[3, 3]}>
<Header appForm={appForm} setAppForm={setAppForm} /> <Header
appForm={appForm}
setAppForm={setAppForm}
past={past}
setPast={setPast}
saveSnapshot={saveSnapshot}
/>
{currentTab === TabEnum.appEdit ? ( {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.publish && <PublishChannel />}
{currentTab === TabEnum.logs && <Logs />} {currentTab === TabEnum.logs && <Logs />}
</Box> </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 AppCard from '../WorkflowComponents/AppCard';
import { uiWorkflow2StoreWorkflow } from '../WorkflowComponents/utils'; import { uiWorkflow2StoreWorkflow } from '../WorkflowComponents/utils';
import { useSystem } from '@fastgpt/web/hooks/useSystem'; 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 { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import MyModal from '@fastgpt/web/components/common/MyModal'; import MyModal from '@fastgpt/web/components/common/MyModal';
import { compareSnapshot } from '@/web/core/workflow/utils'; import { compareSnapshot } from '@/web/core/workflow/utils';
import SaveAndPublishModal from '../WorkflowComponents/Flow/components/SaveAndPublish';
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time'; import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
import { useToast } from '@fastgpt/web/hooks/useToast'; import { useToast } from '@fastgpt/web/hooks/useToast';
import { useDebounceEffect } from 'ahooks'; import { useDebounceEffect } from 'ahooks';
import { useSystemStore } from '@/web/common/system/useSystemStore'; 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 Header = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -48,12 +46,6 @@ const Header = () => {
onOpen: onOpenBackConfirm, onOpen: onOpenBackConfirm,
onClose: onCloseBackConfirm onClose: onCloseBackConfirm
} = useDisclosure(); } = useDisclosure();
const {
isOpen: isSaveAndPublishModalOpen,
onOpen: onSaveAndPublishModalOpen,
onClose: onSaveAndPublishModalClose
} = useDisclosure();
const [isSave, setIsSave] = useState(false);
const { const {
flowData2StoreData, flowData2StoreData,
@@ -65,7 +57,9 @@ const Header = () => {
edges, edges,
past, past,
future, future,
setPast setPast,
saveSnapshot,
resetSnapshot
} = useContextSelector(WorkflowContext, (v) => v); } = useContextSelector(WorkflowContext, (v) => v);
const { lastAppListRouteType } = useSystemStore(); const { lastAppListRouteType } = useSystemStore();
@@ -227,81 +221,11 @@ const Header = () => {
{t('common:core.workflow.Run')} {t('common:core.workflow.Run')}
</Button> </Button>
{!historiesDefaultData && ( {!historiesDefaultData && (
<MyPopover <SaveButton
placement={'bottom-end'} isLoading={loading}
hasArrow={false} onClickSave={onClickSave}
offset={[2, 4]} checkData={flowData2StoreDataAndCheck}
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>
)} )}
</HStack> </HStack>
)} )}
@@ -311,8 +235,12 @@ const Header = () => {
onClose={() => { onClose={() => {
setHistoriesDefaultData(undefined); setHistoriesDefaultData(undefined);
}} }}
past={past}
saveSnapshot={saveSnapshot}
resetSnapshot={resetSnapshot}
/> />
)} )}
<MyModal <MyModal
isOpen={isOpenBackConfirm} isOpen={isOpenBackConfirm}
onClose={onCloseBackConfirm} onClose={onCloseBackConfirm}
@@ -358,16 +286,13 @@ const Header = () => {
loading, loading,
isV2Workflow, isV2Workflow,
historiesDefaultData, historiesDefaultData,
isSave,
onClickSave, onClickSave,
setHistoriesDefaultData, setHistoriesDefaultData,
appDetail.chatConfig, appDetail.chatConfig,
flowData2StoreDataAndCheck, flowData2StoreDataAndCheck,
setWorkflowTestData, setWorkflowTestData,
isSaveAndPublishModalOpen,
onSaveAndPublishModalClose,
toast, toast,
onSaveAndPublishModalOpen past
]); ]);
return Render; 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 { checkNodeRunStatus } from '@fastgpt/global/core/workflow/runtime/utils';
import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus'; import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus';
import { getHandleId } from '@fastgpt/global/core/workflow/utils'; 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 { AppContext } from '@/pages/app/detail/components/context';
import ChatTest from './Flow/ChatTest'; import ChatTest from './Flow/ChatTest';
import { useDisclosure } from '@chakra-ui/react'; import { useDisclosure } from '@chakra-ui/react';
@@ -47,7 +47,6 @@ import { uiWorkflow2StoreWorkflow } from './utils';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { formatTime2YMDHMS, 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 } from 'lodash';
import { SetState } from 'ahooks/lib/createUseStorageState'; import { SetState } from 'ahooks/lib/createUseStorageState';
@@ -60,6 +59,18 @@ export type SnapshotsType = {
chatConfig: AppChatConfigType; chatConfig: AppChatConfigType;
isSaved?: boolean; 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 = { type WorkflowContextType = {
appId?: string; appId?: string;
basicNodeTemplates: FlowNodeTemplateType[]; 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 { createContext } from 'use-context-selector';
import { defaultApp } from '@/web/core/app/constants'; import { defaultApp } from '@/web/core/app/constants';
import { delAppById, getAppDetailById, putAppById } from '@/web/core/app/api'; 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 dynamic from 'next/dynamic';
import { useDisclosure } from '@chakra-ui/react'; import { useDisclosure } from '@chakra-ui/react';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; 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 { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import type { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge'; 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) => export const postPublishApp = (appId: string, data: PostPublishAppProps) =>
POST(`/core/app/version/publish?appId=${appId}`, data); POST(`/core/app/version/publish?appId=${appId}`, data);
export const getPublishList = (data: PaginationProps<{ appId: string }>) => // export const getPublishList = (data: PaginationProps<{ appId: string }>) =>
POST<PaginationResponse<AppVersionSchemaType>>('/core/app/version/list', data); // POST<PaginationResponse<AppVersionSchemaType>>('/core/app/version/list', data);
export const getWorkflowVersionList = (data: PaginationProps<{ appId: string }>) => export const getWorkflowVersionList = (data: PaginationProps<{ appId: string }>) =>
POST<PaginationResponse<versionListResponse>>('/core/app/version/listWorkflow', data); 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) => export const getAppVersionDetail = (versionId: string, appId: string) =>
GET<AppVersionSchemaType>(`/core/app/version/detail?versionId=${versionId}&appId=${appId}`); GET<AppVersionSchemaType>(`/core/app/version/detail?versionId=${versionId}&appId=${appId}`);
export const postRevertVersion = (appId: string, data: PostRevertAppProps) => // export const postRevertVersion = (appId: string, data: PostRevertAppProps) =>
POST(`/core/app/version/revert?appId=${appId}`, data); // POST(`/core/app/version/revert?appId=${appId}`, data);
export const updateAppVersion = (data: UpdateAppVersionBody) => export const updateAppVersion = (data: UpdateAppVersionBody) =>
POST(`/core/app/version/update`, data); POST(`/core/app/version/update`, data);

View File

@@ -438,94 +438,6 @@ export const getLatestNodeTemplate = (
return updatedNode; 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 = ( export const compareSnapshot = (
snapshot1: { snapshot1: {
nodes: Node<FlowNodeItemType, string | undefined>[] | undefined; nodes: Node<FlowNodeItemType, string | undefined>[] | undefined;