Files
FastGPT/projects/app/src/pages/app/detail/components/FlowEdit/Header.tsx
Archer 3c6e5a6e00 4.8 test (#1394)
* fix: chat variable sync

* feat: chat save variable config

* fix: target handle hidden

* adapt v1 chat init

* adapt v1 chat init

* adapt v1 chat init

* adapt v1 chat init
2024-05-08 19:49:17 +08:00

369 lines
11 KiB
TypeScript

import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Box, Flex, IconButton, useTheme, useDisclosure, Button } from '@chakra-ui/react';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { AppSchema } from '@fastgpt/global/core/app/type.d';
import { useTranslation } from 'next-i18next';
import { useCopyData } from '@/web/common/hooks/useCopyData';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import dynamic from 'next/dynamic';
import MyIcon from '@fastgpt/web/components/common/Icon';
import ChatTest, { type ChatTestComponentRef } from '@/components/core/workflow/Flow/ChatTest';
import { flowNode2StoreNodes } from '@/components/core/workflow/utils';
import { useAppStore } from '@/web/core/app/store/useAppStore';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { getErrText } from '@fastgpt/global/common/error/utils';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import {
checkWorkflowNodeAndConnection,
filterSensitiveNodesData
} from '@/web/core/workflow/utils';
import { useBeforeunload } from '@fastgpt/web/hooks/useBeforeunload';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { formatTime2HM } from '@fastgpt/global/common/string/time';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext, getWorkflowStore } from '@/components/core/workflow/context';
import { useInterval, useUpdateEffect } from 'ahooks';
const ImportSettings = dynamic(() => import('@/components/core/workflow/Flow/ImportSettings'));
const PublishHistories = dynamic(
() => import('@/components/core/workflow/components/PublishHistoriesSlider')
);
type Props = { app: AppSchema; onClose: () => void };
const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
app,
ChatTestRef,
setWorkflowTestData,
onClose
}: Props & {
ChatTestRef: React.RefObject<ChatTestComponentRef>;
setWorkflowTestData: React.Dispatch<
React.SetStateAction<
| {
nodes: StoreNodeItemType[];
edges: StoreEdgeItemType[];
}
| undefined
>
>;
}) {
const theme = useTheme();
const { toast } = useToast();
const { t } = useTranslation();
const { copyData } = useCopyData();
const { openConfirm: openConfigPublish, ConfirmModal } = useConfirm({
content: t('core.app.Publish Confirm')
});
const { publishApp, updateAppDetail } = useAppStore();
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
const [isSaving, setIsSaving] = useState(false);
const [saveLabel, setSaveLabel] = useState(t('core.app.Onclick to save'));
const onUpdateNodeError = useContextSelector(WorkflowContext, (v) => v.onUpdateNodeError);
const { isOpen: isOpenImport, onOpen: onOpenImport, onClose: onCloseImport } = useDisclosure();
const isShowVersionHistories = useContextSelector(
WorkflowContext,
(v) => v.isShowVersionHistories
);
const setIsShowVersionHistories = useContextSelector(
WorkflowContext,
(v) => v.setIsShowVersionHistories
);
const workflowDebugData = useContextSelector(WorkflowContext, (v) => v.workflowDebugData);
const flowData2StoreDataAndCheck = useCallback(async () => {
const { nodes } = await getWorkflowStore();
const checkResults = checkWorkflowNodeAndConnection({ nodes, edges });
if (!checkResults) {
const storeNodes = flowNode2StoreNodes({ nodes, edges });
return storeNodes;
} else {
checkResults.forEach((nodeId) => onUpdateNodeError(nodeId, true));
toast({
status: 'warning',
title: t('core.workflow.Check Failed')
});
}
}, [edges, onUpdateNodeError, t, toast]);
const onclickSave = useCallback(
async (forbid?: boolean) => {
// version preview / debug mode, not save
if (isShowVersionHistories || forbid) return;
const { nodes } = await getWorkflowStore();
if (nodes.length === 0) return null;
setIsSaving(true);
const storeWorkflow = flowNode2StoreNodes({ nodes, edges });
try {
await updateAppDetail(app._id, {
...storeWorkflow,
type: AppTypeEnum.advanced,
//@ts-ignore
version: 'v2'
});
setSaveLabel(
t('core.app.Auto Save time', {
time: formatTime2HM()
})
);
// ChatTestRef.current?.resetChatTest();
} catch (error) {}
setIsSaving(false);
return null;
},
[isShowVersionHistories, edges, updateAppDetail, app._id, t]
);
const onclickPublish = useCallback(async () => {
setIsSaving(true);
const data = await flowData2StoreDataAndCheck();
if (data) {
try {
await publishApp(app._id, {
...data,
type: AppTypeEnum.advanced,
//@ts-ignore
version: 'v2'
});
toast({
status: 'success',
title: t('core.app.Publish Success')
});
ChatTestRef.current?.resetChatTest();
} catch (error) {
toast({
status: 'warning',
title: getErrText(error, t('core.app.Publish Failed'))
});
}
}
setIsSaving(false);
}, [flowData2StoreDataAndCheck, publishApp, app._id, toast, t, ChatTestRef]);
const saveAndBack = useCallback(async () => {
try {
await onclickSave();
onClose();
} catch (error) {}
}, [onClose, onclickSave]);
const onExportWorkflow = useCallback(async () => {
const data = await flowData2StoreDataAndCheck();
if (data) {
copyData(
JSON.stringify(
{
nodes: filterSensitiveNodesData(data.nodes),
edges: data.edges
},
null,
2
),
t('app.Export Config Successful')
);
}
}, [copyData, flowData2StoreDataAndCheck, t]);
// effect
useBeforeunload({
callback: onclickSave,
tip: t('core.common.tip.leave page')
});
useInterval(() => {
if (!app._id) return;
onclickSave(!!workflowDebugData);
}, 20000);
const Render = useMemo(() => {
return (
<>
<Flex
py={3}
px={[2, 5, 8]}
borderBottom={theme.borders.base}
alignItems={'center'}
userSelect={'none'}
bg={'myGray.25'}
h={'67px'}
>
<IconButton
size={'smSquare'}
icon={<MyIcon name={'common/backFill'} w={'14px'} />}
borderRadius={'50%'}
w={'26px'}
h={'26px'}
borderColor={'myGray.300'}
variant={'whiteBase'}
aria-label={''}
isLoading={isSaving}
onClick={saveAndBack}
/>
<Box ml={[2, 4]}>
<Box fontSize={['md', 'lg']} fontWeight={'bold'}>
{app.name}
</Box>
{!isShowVersionHistories && (
<MyTooltip label={t('core.app.Onclick to save')}>
<Box
fontSize={'sm'}
mt={1}
display={'inline-block'}
borderRadius={'xs'}
cursor={'pointer'}
onClick={() => onclickSave()}
color={'myGray.500'}
>
{saveLabel}
</Box>
</MyTooltip>
)}
</Box>
<Box flex={1} />
{!isShowVersionHistories && (
<>
<MyMenu
Button={
<IconButton
mr={[2, 4]}
icon={<MyIcon name={'more'} w={'14px'} p={2} />}
aria-label={''}
size={'sm'}
variant={'whitePrimary'}
/>
}
menuList={[
{
label: t('app.Import Configs'),
icon: 'common/importLight',
onClick: onOpenImport
},
{
label: t('app.Export Configs'),
icon: 'export',
onClick: onExportWorkflow
}
]}
/>
<IconButton
mr={[2, 4]}
icon={<MyIcon name={'history'} w={'18px'} />}
aria-label={''}
size={'sm'}
w={'30px'}
variant={'whitePrimary'}
onClick={() => setIsShowVersionHistories(true)}
/>
</>
)}
<Button
size={'sm'}
leftIcon={<MyIcon name={'core/workflow/debug'} w={['14px', '16px']} />}
variant={'whitePrimary'}
onClick={async () => {
const data = await flowData2StoreDataAndCheck();
if (data) {
setWorkflowTestData(data);
}
}}
>
{t('core.workflow.Debug')}
</Button>
{!isShowVersionHistories && (
<Button
ml={[2, 4]}
size={'sm'}
isLoading={isSaving}
leftIcon={<MyIcon name={'common/publishFill'} w={['14px', '16px']} />}
onClick={openConfigPublish(onclickPublish)}
>
{t('core.app.Publish')}
</Button>
)}
</Flex>
<ConfirmModal confirmText={t('core.app.Publish')} />
</>
);
}, [
ConfirmModal,
app.name,
flowData2StoreDataAndCheck,
isSaving,
onExportWorkflow,
onOpenImport,
onclickPublish,
onclickSave,
openConfigPublish,
isShowVersionHistories,
saveAndBack,
saveLabel,
setIsShowVersionHistories,
setWorkflowTestData,
t,
theme.borders.base
]);
return (
<>
{Render}
{isOpenImport && <ImportSettings onClose={onCloseImport} />}
{isShowVersionHistories && <PublishHistories />}
</>
);
});
const Header = (props: Props) => {
const { app } = props;
const ChatTestRef = useRef<ChatTestComponentRef>(null);
const [workflowTestData, setWorkflowTestData] = useState<{
nodes: StoreNodeItemType[];
edges: StoreEdgeItemType[];
}>();
const { isOpen: isOpenTest, onOpen: onOpenTest, onClose: onCloseTest } = useDisclosure();
useUpdateEffect(() => {
onOpenTest();
}, [workflowTestData]);
return (
<>
<RenderHeaderContainer
{...props}
ChatTestRef={ChatTestRef}
setWorkflowTestData={setWorkflowTestData}
/>
<ChatTest
ref={ChatTestRef}
isOpen={isOpenTest}
{...workflowTestData}
app={app}
onClose={onCloseTest}
/>
</>
);
};
export default React.memo(Header);