mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-27 16:33:49 +00:00
perf: workflow snapshots;fix: extrat node selected (#3334)
* doc * perf: workflow snapshots * fix: extrat node selected * refresh page reset snapshot
This commit is contained in:
@@ -3,7 +3,7 @@ import { defaultAutoExecuteConfig } from '@fastgpt/global/core/app/constants';
|
||||
import { AppAutoExecuteConfigType } from '@fastgpt/global/core/app/type';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import ChatFunctionTip from './Tip';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
@@ -22,19 +22,13 @@ import AppCard from '../WorkflowComponents/AppCard';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { compareSnapshot } from '@/web/core/workflow/utils';
|
||||
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';
|
||||
import PublishHistories from '../PublishHistoriesSlider';
|
||||
import {
|
||||
WorkflowNodeEdgeContext,
|
||||
WorkflowInitContext
|
||||
} from '../WorkflowComponents/context/workflowInitContext';
|
||||
import { WorkflowEventContext } from '../WorkflowComponents/context/workflowEventContext';
|
||||
import { getAppConfigByDiff } from '@/web/core/app/diff';
|
||||
import { WorkflowStatusContext } from '../WorkflowComponents/context/workflowStatusContext';
|
||||
|
||||
const Header = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -50,9 +44,6 @@ const Header = () => {
|
||||
onClose: onCloseBackConfirm
|
||||
} = useDisclosure();
|
||||
|
||||
const nodes = useContextSelector(WorkflowInitContext, (v) => v.nodes);
|
||||
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
|
||||
|
||||
const flowData2StoreData = useContextSelector(WorkflowContext, (v) => v.flowData2StoreData);
|
||||
const flowData2StoreDataAndCheck = useContextSelector(
|
||||
WorkflowContext,
|
||||
@@ -60,7 +51,6 @@ const Header = () => {
|
||||
);
|
||||
const setWorkflowTestData = useContextSelector(WorkflowContext, (v) => v.setWorkflowTestData);
|
||||
const past = useContextSelector(WorkflowContext, (v) => v.past);
|
||||
const future = useContextSelector(WorkflowContext, (v) => v.future);
|
||||
const setPast = useContextSelector(WorkflowContext, (v) => v.setPast);
|
||||
const onSwitchTmpVersion = useContextSelector(WorkflowContext, (v) => v.onSwitchTmpVersion);
|
||||
const onSwitchCloudVersion = useContextSelector(WorkflowContext, (v) => v.onSwitchCloudVersion);
|
||||
@@ -71,40 +61,11 @@ const Header = () => {
|
||||
(v) => v.setShowHistoryModal
|
||||
);
|
||||
|
||||
const isSaved = useContextSelector(WorkflowStatusContext, (v) => v.isSaved);
|
||||
const leaveSaveSign = useContextSelector(WorkflowStatusContext, (v) => v.leaveSaveSign);
|
||||
|
||||
const { lastAppListRouteType } = useSystemStore();
|
||||
|
||||
const [isPublished, setIsPublished] = useState(false);
|
||||
useDebounceEffect(
|
||||
() => {
|
||||
const savedSnapshot =
|
||||
[...future].reverse().find((snapshot) => snapshot.isSaved) ||
|
||||
past.find((snapshot) => snapshot.isSaved);
|
||||
|
||||
const initialState = past[past.length - 1]?.state;
|
||||
const savedSnapshotState = getAppConfigByDiff(initialState, savedSnapshot?.diff);
|
||||
|
||||
const val = compareSnapshot(
|
||||
// nodes of the saved snapshot
|
||||
{
|
||||
nodes: savedSnapshotState?.nodes,
|
||||
edges: savedSnapshotState?.edges,
|
||||
chatConfig: savedSnapshotState?.chatConfig
|
||||
},
|
||||
// nodes of the current canvas
|
||||
{
|
||||
nodes,
|
||||
edges,
|
||||
chatConfig: appDetail.chatConfig
|
||||
}
|
||||
);
|
||||
setIsPublished(val);
|
||||
},
|
||||
[future, past, nodes, edges, appDetail.chatConfig],
|
||||
{
|
||||
wait: 500
|
||||
}
|
||||
);
|
||||
|
||||
const { runAsync: onClickSave, loading } = useRequest2(
|
||||
async ({
|
||||
isPublish,
|
||||
@@ -140,16 +101,15 @@ const Header = () => {
|
||||
);
|
||||
|
||||
const onBack = useCallback(async () => {
|
||||
try {
|
||||
router.push({
|
||||
pathname: '/app/list',
|
||||
query: {
|
||||
parentId: appDetail.parentId,
|
||||
type: lastAppListRouteType
|
||||
}
|
||||
});
|
||||
} catch (error) {}
|
||||
}, [appDetail.parentId, lastAppListRouteType, router]);
|
||||
leaveSaveSign.current = false;
|
||||
router.push({
|
||||
pathname: '/app/list',
|
||||
query: {
|
||||
parentId: appDetail.parentId,
|
||||
type: lastAppListRouteType
|
||||
}
|
||||
});
|
||||
}, [appDetail.parentId, lastAppListRouteType, leaveSaveSign, router]);
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
@@ -189,13 +149,13 @@ const Header = () => {
|
||||
name={'common/leftArrowLight'}
|
||||
w={6}
|
||||
cursor={'pointer'}
|
||||
onClick={isPublished ? onBack : onOpenBackConfirm}
|
||||
onClick={isSaved ? onBack : onOpenBackConfirm}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* app info */}
|
||||
<Box ml={1}>
|
||||
<AppCard isPublished={isPublished} showSaveStatus={isV2Workflow} />
|
||||
<AppCard isSaved={isSaved} showSaveStatus={isV2Workflow} />
|
||||
</Box>
|
||||
|
||||
{isPc && (
|
||||
@@ -247,7 +207,7 @@ const Header = () => {
|
||||
}, [
|
||||
isPc,
|
||||
currentTab,
|
||||
isPublished,
|
||||
isSaved,
|
||||
onBack,
|
||||
onOpenBackConfirm,
|
||||
isV2Workflow,
|
||||
@@ -290,14 +250,16 @@ const Header = () => {
|
||||
<Button
|
||||
isLoading={loading}
|
||||
onClick={async () => {
|
||||
await onClickSave({});
|
||||
onCloseBackConfirm();
|
||||
onBack();
|
||||
toast({
|
||||
status: 'success',
|
||||
title: t('app:saved_success'),
|
||||
position: 'top-right'
|
||||
});
|
||||
try {
|
||||
await onClickSave({});
|
||||
onCloseBackConfirm();
|
||||
onBack();
|
||||
toast({
|
||||
status: 'success',
|
||||
title: t('app:saved_success'),
|
||||
position: 'top-right'
|
||||
});
|
||||
} catch (error) {}
|
||||
}}
|
||||
>
|
||||
{t('common:common.Save_and_exit')}
|
||||
|
@@ -1,129 +1,26 @@
|
||||
import React from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { useDebounceEffect, useLocalStorageState, useMount } from 'ahooks';
|
||||
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
|
||||
import { appWorkflow2Form } from '@fastgpt/global/core/app/utils';
|
||||
|
||||
import ChatTest from './ChatTest';
|
||||
import AppCard from './AppCard';
|
||||
import EditForm from './EditForm';
|
||||
import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
|
||||
import { v1Workflow2V2 } from '@/web/core/workflow/adapt';
|
||||
import { AppContext } from '@/pages/app/detail/components/context';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { cardStyles } from '../constants';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { onSaveSnapshotFnType, SimpleAppSnapshotType } from './useSnapshots';
|
||||
import { getAppConfigByDiff, getAppDiffConfig } from '@/web/core/app/diff';
|
||||
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
|
||||
|
||||
const convertOldFormatHistory = (past: SimpleAppSnapshotType[]) => {
|
||||
const baseState = past[past.length - 1].appForm;
|
||||
|
||||
return past.map((item, index) => {
|
||||
if (index === past.length - 1) {
|
||||
return {
|
||||
title: item.title,
|
||||
isSaved: item.isSaved,
|
||||
state: baseState
|
||||
};
|
||||
}
|
||||
|
||||
const currentState = item.appForm;
|
||||
|
||||
const diff = getAppDiffConfig(baseState, currentState);
|
||||
|
||||
return {
|
||||
title: item.title || formatTime2YMDHMS(new Date()),
|
||||
isSaved: item.isSaved,
|
||||
diff
|
||||
};
|
||||
});
|
||||
};
|
||||
import { SimpleAppSnapshotType } from './useSnapshots';
|
||||
|
||||
const Edit = ({
|
||||
appForm,
|
||||
setAppForm,
|
||||
past,
|
||||
setPast,
|
||||
saveSnapshot
|
||||
setPast
|
||||
}: {
|
||||
appForm: AppSimpleEditFormType;
|
||||
setAppForm: React.Dispatch<React.SetStateAction<AppSimpleEditFormType>>;
|
||||
past: SimpleAppSnapshotType[];
|
||||
setPast: (value: React.SetStateAction<SimpleAppSnapshotType[]>) => void;
|
||||
saveSnapshot: onSaveSnapshotFnType;
|
||||
}) => {
|
||||
const { isPc } = useSystem();
|
||||
const { loadAllDatasets } = useDatasetStore();
|
||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Reset old edit history to new variables
|
||||
const [oldPast, setOldPast] = useLocalStorageState<SimpleAppSnapshotType[]>(
|
||||
`${appDetail._id}-past-simple`,
|
||||
{}
|
||||
);
|
||||
|
||||
// Save snapshot to local
|
||||
useDebounceEffect(
|
||||
() => {
|
||||
saveSnapshot({
|
||||
appForm
|
||||
});
|
||||
},
|
||||
[appForm],
|
||||
{ wait: 500 }
|
||||
);
|
||||
|
||||
// Init app form
|
||||
useMount(() => {
|
||||
// show selected dataset
|
||||
loadAllDatasets();
|
||||
|
||||
if (appDetail.version !== 'v2') {
|
||||
return setAppForm(
|
||||
appWorkflow2Form({
|
||||
nodes: v1Workflow2V2((appDetail.modules || []) as any)?.nodes,
|
||||
chatConfig: appDetail.chatConfig
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Get the latest snapshot
|
||||
if (past?.[0]?.diff) {
|
||||
const pastState = getAppConfigByDiff(past[past.length - 1].state, past[0].diff);
|
||||
|
||||
return setAppForm(pastState);
|
||||
} else if (oldPast && oldPast.length > 0 && oldPast?.every((item) => item.appForm)) {
|
||||
// 格式化成 diff
|
||||
const newPast = convertOldFormatHistory(oldPast);
|
||||
|
||||
setPast(newPast);
|
||||
setOldPast && setOldPast([]);
|
||||
|
||||
return setAppForm(getAppConfigByDiff(newPast[newPast.length - 1].state, newPast[0].diff));
|
||||
}
|
||||
|
||||
const appForm = appWorkflow2Form({
|
||||
nodes: appDetail.modules,
|
||||
chatConfig: appDetail.chatConfig
|
||||
});
|
||||
|
||||
// Set the first snapshot
|
||||
if (past.length === 0) {
|
||||
saveSnapshot({
|
||||
appForm,
|
||||
title: t('app:initial_form'),
|
||||
isSaved: true
|
||||
});
|
||||
}
|
||||
|
||||
setAppForm(appForm);
|
||||
});
|
||||
|
||||
return (
|
||||
<Box
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppContext } from '../context';
|
||||
import FolderPath from '@/components/common/folder/Path';
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
} from './useSnapshots';
|
||||
import PublishHistories from '../PublishHistoriesSlider';
|
||||
import { AppVersionSchemaType } from '@fastgpt/global/core/app/version';
|
||||
import { getAppConfigByDiff } from '@/web/core/app/diff';
|
||||
import { useBeforeunload } from '@fastgpt/web/hooks/useBeforeunload';
|
||||
|
||||
const Header = ({
|
||||
forbiddenSaveSnapshot,
|
||||
@@ -51,7 +51,6 @@ const Header = ({
|
||||
const appId = useContextSelector(AppContext, (v) => v.appId);
|
||||
const onSaveApp = useContextSelector(AppContext, (v) => v.onSaveApp);
|
||||
const currentTab = useContextSelector(AppContext, (v) => v.currentTab);
|
||||
const appLatestVersion = useContextSelector(AppContext, (v) => v.appLatestVersion);
|
||||
|
||||
const { lastAppListRouteType } = useSystemStore();
|
||||
const { allDatasets } = useDatasetStore();
|
||||
@@ -105,19 +104,9 @@ const Header = ({
|
||||
const [isShowHistories, { setTrue: setIsShowHistories, setFalse: closeHistories }] =
|
||||
useBoolean(false);
|
||||
|
||||
const initialAppForm = useMemo(
|
||||
() =>
|
||||
appWorkflow2Form({
|
||||
nodes: appLatestVersion?.nodes || [],
|
||||
chatConfig: appLatestVersion?.chatConfig || {}
|
||||
}),
|
||||
[appLatestVersion]
|
||||
);
|
||||
|
||||
const onSwitchTmpVersion = useCallback(
|
||||
(data: SimpleAppSnapshotType, customTitle: string) => {
|
||||
const pastState = getAppConfigByDiff(initialAppForm, data.diff);
|
||||
setAppForm(pastState);
|
||||
setAppForm(data.appForm);
|
||||
|
||||
// Remove multiple "copy-"
|
||||
const copyText = t('app:version_copy');
|
||||
@@ -125,11 +114,11 @@ const Header = ({
|
||||
const title = customTitle.replace(regex, `$1`);
|
||||
|
||||
return saveSnapshot({
|
||||
appForm: pastState,
|
||||
appForm: data.appForm,
|
||||
title
|
||||
});
|
||||
},
|
||||
[initialAppForm, saveSnapshot, setAppForm, t]
|
||||
[saveSnapshot, setAppForm, t]
|
||||
);
|
||||
const onSwitchCloudVersion = useCallback(
|
||||
(appVersion: AppVersionSchemaType) => {
|
||||
@@ -152,18 +141,31 @@ const Header = ({
|
||||
);
|
||||
|
||||
// Check if the workflow is published
|
||||
const [isPublished, setIsPublished] = useState(false);
|
||||
const [isSaved, setIsSaved] = useState(false);
|
||||
useDebounceEffect(
|
||||
() => {
|
||||
const savedSnapshot = past.find((snapshot) => snapshot.isSaved);
|
||||
const pastState = getAppConfigByDiff(initialAppForm, savedSnapshot?.diff);
|
||||
const val = compareSimpleAppSnapshot(pastState, appForm);
|
||||
setIsPublished(val);
|
||||
const val = compareSimpleAppSnapshot(savedSnapshot?.appForm, appForm);
|
||||
setIsSaved(val);
|
||||
},
|
||||
[past, allDatasets],
|
||||
{ wait: 500 }
|
||||
);
|
||||
|
||||
const onLeaveAutoSave = useCallback(() => {
|
||||
if (isSaved) return;
|
||||
try {
|
||||
console.log('Leave auto save');
|
||||
onClickSave({ isPublish: false, versionName: t('app:auto_save') });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}, [isSaved, onClickSave, t]);
|
||||
useBeforeunload({
|
||||
tip: t('common:core.common.tip.leave page'),
|
||||
callback: onLeaveAutoSave
|
||||
});
|
||||
|
||||
return (
|
||||
<Box h={14}>
|
||||
{!isPc && (
|
||||
@@ -196,13 +198,13 @@ const Header = ({
|
||||
type={'borderFill'}
|
||||
showDot
|
||||
colorSchema={
|
||||
isPublished
|
||||
isSaved
|
||||
? publishStatusStyle.published.colorSchema
|
||||
: publishStatusStyle.unPublish.colorSchema
|
||||
}
|
||||
>
|
||||
{t(
|
||||
isPublished
|
||||
isSaved
|
||||
? publishStatusStyle.published.text
|
||||
: publishStatusStyle.unPublish.text
|
||||
)}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { getDefaultAppForm } from '@fastgpt/global/core/app/utils';
|
||||
import { appWorkflow2Form, getDefaultAppForm } from '@fastgpt/global/core/app/utils';
|
||||
|
||||
import Header from './Header';
|
||||
import Edit from './Edit';
|
||||
@@ -7,16 +7,20 @@ import { useContextSelector } from 'use-context-selector';
|
||||
import { AppContext, TabEnum } from '../context';
|
||||
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 { useSimpleAppSnapshots } from './useSnapshots';
|
||||
import { useDebounceEffect } from 'ahooks';
|
||||
import { SimpleAppSnapshotType, useSimpleAppSnapshots } from './useSnapshots';
|
||||
import { useDebounceEffect, useMount } from 'ahooks';
|
||||
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
|
||||
import { v1Workflow2V2 } from '@/web/core/workflow/adapt';
|
||||
import { getAppConfigByDiff } from '@/web/core/app/diff';
|
||||
|
||||
const Logs = dynamic(() => import('../Logs/index'));
|
||||
const PublishChannel = dynamic(() => import('../Publish'));
|
||||
|
||||
const SimpleEdit = () => {
|
||||
const { t } = useTranslation();
|
||||
const { loadAllDatasets } = useDatasetStore();
|
||||
|
||||
const { currentTab, appDetail } = useContextSelector(AppContext, (v) => v);
|
||||
const { forbiddenSaveSnapshot, past, setPast, saveSnapshot } = useSimpleAppSnapshots(
|
||||
appDetail._id
|
||||
@@ -24,10 +28,86 @@ const SimpleEdit = () => {
|
||||
|
||||
const [appForm, setAppForm] = useState(getDefaultAppForm());
|
||||
|
||||
useBeforeunload({
|
||||
tip: t('common:core.common.tip.leave page')
|
||||
// Init app form
|
||||
useMount(() => {
|
||||
// show selected dataset
|
||||
loadAllDatasets();
|
||||
|
||||
if (appDetail.version !== 'v2') {
|
||||
return setAppForm(
|
||||
appWorkflow2Form({
|
||||
nodes: v1Workflow2V2((appDetail.modules || []) as any)?.nodes,
|
||||
chatConfig: appDetail.chatConfig
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// 读取旧的存储记录
|
||||
const pastSnapshot = (() => {
|
||||
try {
|
||||
const pastSnapshot = localStorage.getItem(`${appDetail._id}-past`);
|
||||
return pastSnapshot ? (JSON.parse(pastSnapshot) as SimpleAppSnapshotType[]) : [];
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
})();
|
||||
const defaultState = pastSnapshot?.[pastSnapshot.length - 1]?.state;
|
||||
if (pastSnapshot?.[0]?.diff && defaultState) {
|
||||
setPast(
|
||||
pastSnapshot
|
||||
.map((item) => {
|
||||
if (!item.state && !item.diff) return;
|
||||
if (!item.diff) {
|
||||
return {
|
||||
title: t('app:initial_form'),
|
||||
isSaved: true,
|
||||
appForm: defaultState
|
||||
};
|
||||
}
|
||||
|
||||
const currentState = getAppConfigByDiff(defaultState, item.diff);
|
||||
return {
|
||||
title: item.title,
|
||||
isSaved: item.isSaved,
|
||||
appForm: currentState
|
||||
};
|
||||
})
|
||||
.filter(Boolean) as SimpleAppSnapshotType[]
|
||||
);
|
||||
|
||||
const pastState = getAppConfigByDiff(defaultState, pastSnapshot[0].diff);
|
||||
localStorage.removeItem(`${appDetail._id}-past`);
|
||||
return setAppForm(pastState);
|
||||
}
|
||||
|
||||
// 无旧的记录,正常初始化
|
||||
if (past.length === 0) {
|
||||
const appForm = appWorkflow2Form({
|
||||
nodes: appDetail.modules,
|
||||
chatConfig: appDetail.chatConfig
|
||||
});
|
||||
saveSnapshot({
|
||||
appForm,
|
||||
title: t('app:initial_form'),
|
||||
isSaved: true
|
||||
});
|
||||
setAppForm(appForm);
|
||||
} else {
|
||||
setAppForm(past[0].appForm);
|
||||
}
|
||||
});
|
||||
|
||||
// Save snapshot to local
|
||||
useDebounceEffect(
|
||||
() => {
|
||||
saveSnapshot({
|
||||
appForm
|
||||
});
|
||||
},
|
||||
[appForm],
|
||||
{ wait: 500 }
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex h={'100%'} flexDirection={'column'} px={[3, 0]} pr={[3, 3]}>
|
||||
<Header
|
||||
@@ -39,13 +119,7 @@ const SimpleEdit = () => {
|
||||
saveSnapshot={saveSnapshot}
|
||||
/>
|
||||
{currentTab === TabEnum.appEdit ? (
|
||||
<Edit
|
||||
appForm={appForm}
|
||||
setAppForm={setAppForm}
|
||||
past={past}
|
||||
setPast={setPast}
|
||||
saveSnapshot={saveSnapshot}
|
||||
/>
|
||||
<Edit appForm={appForm} setAppForm={setAppForm} setPast={setPast} />
|
||||
) : (
|
||||
<Box flex={'1 0 0'} h={0} mt={[4, 0]}>
|
||||
{currentTab === TabEnum.publish && <PublishChannel />}
|
||||
|
@@ -1,18 +1,17 @@
|
||||
import { useLocalStorageState, useMemoizedFn } from 'ahooks';
|
||||
import { SetStateAction, useEffect, useRef } from 'react';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { useRef, useState } from 'react';
|
||||
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
|
||||
import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
|
||||
import { isEqual } from 'lodash';
|
||||
import { getAppDiffConfig } from '@/web/core/app/diff';
|
||||
|
||||
export type SimpleAppSnapshotType = {
|
||||
diff?: Record<string, any>;
|
||||
title: string;
|
||||
isSaved?: boolean;
|
||||
state?: AppSimpleEditFormType;
|
||||
appForm: AppSimpleEditFormType;
|
||||
|
||||
// old format
|
||||
appForm?: AppSimpleEditFormType;
|
||||
// abandon
|
||||
state?: AppSimpleEditFormType;
|
||||
diff?: Record<string, any>;
|
||||
};
|
||||
export type onSaveSnapshotFnType = (props: {
|
||||
appForm: AppSimpleEditFormType; // Current edited app form data
|
||||
@@ -57,9 +56,7 @@ export const compareSimpleAppSnapshot = (
|
||||
|
||||
export const useSimpleAppSnapshots = (appId: string) => {
|
||||
const forbiddenSaveSnapshot = useRef(false);
|
||||
const [past, setPast] = useLocalStorageState<SimpleAppSnapshotType[]>(`${appId}-past`, {
|
||||
defaultValue: []
|
||||
}) as [SimpleAppSnapshotType[], (value: SetStateAction<SimpleAppSnapshotType[]>) => void];
|
||||
const [past, setPast] = useState<SimpleAppSnapshotType[]>([]);
|
||||
|
||||
const saveSnapshot: onSaveSnapshotFnType = useMemoizedFn(async ({ appForm, title, isSaved }) => {
|
||||
if (forbiddenSaveSnapshot.current) {
|
||||
@@ -72,50 +69,28 @@ export const useSimpleAppSnapshots = (appId: string) => {
|
||||
{
|
||||
title: title || formatTime2YMDHMS(new Date()),
|
||||
isSaved,
|
||||
state: appForm
|
||||
appForm
|
||||
}
|
||||
]);
|
||||
return true;
|
||||
}
|
||||
|
||||
const lastPast = past[past.length - 1];
|
||||
if (!lastPast?.state) return false;
|
||||
const pastState = past[0];
|
||||
const isPastEqual = compareSimpleAppSnapshot(pastState?.appForm, appForm);
|
||||
if (isPastEqual) return false;
|
||||
|
||||
// Get the diff between the current app form data and the initial state
|
||||
const diff = getAppDiffConfig(lastPast.state, appForm);
|
||||
|
||||
// If the diff is the same as the previous snapshot, do not save
|
||||
if (past[0].diff && isEqual(past[0].diff, diff)) return false;
|
||||
|
||||
setPast((past) => {
|
||||
const newPast = {
|
||||
diff,
|
||||
setPast((past) => [
|
||||
{
|
||||
appForm,
|
||||
title: title || formatTime2YMDHMS(new Date()),
|
||||
isSaved
|
||||
};
|
||||
},
|
||||
...past.slice(0, 99)
|
||||
]);
|
||||
|
||||
if (past.length >= 100) {
|
||||
return [newPast, ...past.slice(0, 98), lastPast];
|
||||
}
|
||||
return [newPast, ...past];
|
||||
});
|
||||
return true;
|
||||
});
|
||||
|
||||
// remove other app's snapshot
|
||||
useEffect(() => {
|
||||
const keys = Object.keys(localStorage);
|
||||
const snapshotKeys = keys.filter(
|
||||
(key) => key.endsWith('-past') || key.endsWith('-past-simple')
|
||||
);
|
||||
snapshotKeys.forEach((key) => {
|
||||
const keyAppId = key.split('-')[0];
|
||||
if (keyAppId !== appId) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
});
|
||||
}, [appId]);
|
||||
|
||||
return { forbiddenSaveSnapshot, past, setPast, saveSnapshot };
|
||||
};
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
@@ -22,19 +22,13 @@ import AppCard from '../WorkflowComponents/AppCard';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { compareSnapshot } from '@/web/core/workflow/utils';
|
||||
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';
|
||||
import PublishHistories from '../PublishHistoriesSlider';
|
||||
import {
|
||||
WorkflowNodeEdgeContext,
|
||||
WorkflowInitContext
|
||||
} from '../WorkflowComponents/context/workflowInitContext';
|
||||
import { WorkflowEventContext } from '../WorkflowComponents/context/workflowEventContext';
|
||||
import { getAppConfigByDiff } from '@/web/core/app/diff';
|
||||
import { WorkflowStatusContext } from '../WorkflowComponents/context/workflowStatusContext';
|
||||
|
||||
const Header = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -54,9 +48,6 @@ const Header = () => {
|
||||
onClose: onCloseBackConfirm
|
||||
} = useDisclosure();
|
||||
|
||||
const nodes = useContextSelector(WorkflowInitContext, (v) => v.nodes);
|
||||
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
|
||||
|
||||
const flowData2StoreData = useContextSelector(WorkflowContext, (v) => v.flowData2StoreData);
|
||||
const flowData2StoreDataAndCheck = useContextSelector(
|
||||
WorkflowContext,
|
||||
@@ -64,7 +55,6 @@ const Header = () => {
|
||||
);
|
||||
const setWorkflowTestData = useContextSelector(WorkflowContext, (v) => v.setWorkflowTestData);
|
||||
const past = useContextSelector(WorkflowContext, (v) => v.past);
|
||||
const future = useContextSelector(WorkflowContext, (v) => v.future);
|
||||
const setPast = useContextSelector(WorkflowContext, (v) => v.setPast);
|
||||
const onSwitchTmpVersion = useContextSelector(WorkflowContext, (v) => v.onSwitchTmpVersion);
|
||||
const onSwitchCloudVersion = useContextSelector(WorkflowContext, (v) => v.onSwitchCloudVersion);
|
||||
@@ -75,39 +65,11 @@ const Header = () => {
|
||||
(v) => v.setShowHistoryModal
|
||||
);
|
||||
|
||||
const isSaved = useContextSelector(WorkflowStatusContext, (v) => v.isSaved);
|
||||
const leaveSaveSign = useContextSelector(WorkflowStatusContext, (v) => v.leaveSaveSign);
|
||||
|
||||
const { lastAppListRouteType } = useSystemStore();
|
||||
|
||||
// Check if the workflow is published
|
||||
const [isPublished, setIsPublished] = useState(false);
|
||||
useDebounceEffect(
|
||||
() => {
|
||||
const savedSnapshot =
|
||||
[...future].reverse().find((snapshot) => snapshot.isSaved) ||
|
||||
past.find((snapshot) => snapshot.isSaved);
|
||||
|
||||
const initialState = past[past.length - 1]?.state;
|
||||
const savedSnapshotState = getAppConfigByDiff(initialState, savedSnapshot?.diff);
|
||||
|
||||
const val = compareSnapshot(
|
||||
{
|
||||
nodes: savedSnapshotState?.nodes,
|
||||
edges: savedSnapshotState?.edges,
|
||||
chatConfig: savedSnapshotState?.chatConfig
|
||||
},
|
||||
{
|
||||
nodes: nodes,
|
||||
edges: edges,
|
||||
chatConfig: appDetail.chatConfig
|
||||
}
|
||||
);
|
||||
setIsPublished(val);
|
||||
},
|
||||
[future, past, nodes, edges, appDetail.chatConfig],
|
||||
{
|
||||
wait: 500
|
||||
}
|
||||
);
|
||||
|
||||
const { runAsync: onClickSave, loading } = useRequest2(
|
||||
async ({
|
||||
isPublish,
|
||||
@@ -143,16 +105,15 @@ const Header = () => {
|
||||
);
|
||||
|
||||
const onBack = useCallback(async () => {
|
||||
try {
|
||||
router.push({
|
||||
pathname: '/app/list',
|
||||
query: {
|
||||
parentId: appDetail.parentId,
|
||||
type: lastAppListRouteType
|
||||
}
|
||||
});
|
||||
} catch (error) {}
|
||||
}, [appDetail.parentId, lastAppListRouteType, router]);
|
||||
leaveSaveSign.current = false;
|
||||
router.push({
|
||||
pathname: '/app/list',
|
||||
query: {
|
||||
parentId: appDetail.parentId,
|
||||
type: lastAppListRouteType
|
||||
}
|
||||
});
|
||||
}, [appDetail.parentId, lastAppListRouteType, leaveSaveSign, router]);
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
@@ -192,13 +153,13 @@ const Header = () => {
|
||||
name={'common/leftArrowLight'}
|
||||
w={6}
|
||||
cursor={'pointer'}
|
||||
onClick={isPublished ? onBack : onOpenBackConfirm}
|
||||
onClick={isSaved ? onBack : onOpenBackConfirm}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* app info */}
|
||||
<Box ml={1}>
|
||||
<AppCard isPublished={isPublished} showSaveStatus={isV2Workflow} />
|
||||
<AppCard isSaved={isSaved} showSaveStatus={isV2Workflow} />
|
||||
</Box>
|
||||
|
||||
{isPc && (
|
||||
@@ -250,7 +211,7 @@ const Header = () => {
|
||||
}, [
|
||||
isPc,
|
||||
currentTab,
|
||||
isPublished,
|
||||
isSaved,
|
||||
onBack,
|
||||
onOpenBackConfirm,
|
||||
isV2Workflow,
|
||||
@@ -294,14 +255,16 @@ const Header = () => {
|
||||
<Button
|
||||
isLoading={loading}
|
||||
onClick={async () => {
|
||||
await onClickSave({});
|
||||
onCloseBackConfirm();
|
||||
onBack();
|
||||
backSaveToast({
|
||||
status: 'success',
|
||||
title: t('app:saved_success'),
|
||||
position: 'top-right'
|
||||
});
|
||||
try {
|
||||
await onClickSave({});
|
||||
onCloseBackConfirm();
|
||||
onBack();
|
||||
backSaveToast({
|
||||
status: 'success',
|
||||
title: t('app:saved_success'),
|
||||
position: 'top-right'
|
||||
});
|
||||
} catch (error) {}
|
||||
}}
|
||||
>
|
||||
{t('common:common.Save_and_exit')}
|
||||
|
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { appSystemModuleTemplates } from '@fastgpt/global/core/workflow/template/constants';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import { v1Workflow2V2 } from '@/web/core/workflow/adapt';
|
||||
import WorkflowContextProvider, { WorkflowContext } from '../WorkflowComponents/context';
|
||||
import { WorkflowContext } from '../WorkflowComponents/context';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppContext, TabEnum } from '../context';
|
||||
import { useMount } from 'ahooks';
|
||||
@@ -20,7 +20,9 @@ const Logs = dynamic(() => import('../Logs/index'));
|
||||
const PublishChannel = dynamic(() => import('../Publish'));
|
||||
|
||||
const WorkflowEdit = () => {
|
||||
const { appDetail, currentTab } = useContextSelector(AppContext, (e) => e);
|
||||
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
|
||||
const currentTab = useContextSelector(AppContext, (v) => v.currentTab);
|
||||
|
||||
const isV2Workflow = appDetail?.version === 'v2';
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { Box, Flex, HStack, useDisclosure } from '@chakra-ui/react';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppContext, TabEnum } from '../context';
|
||||
import { AppContext } from '../context';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
@@ -19,18 +19,14 @@ import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
|
||||
const ImportSettings = dynamic(() => import('./Flow/ImportSettings'));
|
||||
|
||||
const AppCard = ({
|
||||
showSaveStatus,
|
||||
isPublished
|
||||
}: {
|
||||
showSaveStatus: boolean;
|
||||
isPublished: boolean;
|
||||
}) => {
|
||||
const AppCard = ({ showSaveStatus, isSaved }: { showSaveStatus: boolean; isSaved: boolean }) => {
|
||||
const { t } = useTranslation();
|
||||
const { feConfigs } = useSystemStore();
|
||||
|
||||
const { appDetail, onOpenInfoEdit, onOpenTeamTagModal, onDelApp, currentTab } =
|
||||
useContextSelector(AppContext, (v) => v);
|
||||
const { appDetail, onOpenInfoEdit, onOpenTeamTagModal, onDelApp } = useContextSelector(
|
||||
AppContext,
|
||||
(v) => v
|
||||
);
|
||||
|
||||
const { isOpen: isOpenImport, onOpen: onOpenImport, onClose: onCloseImport } = useDisclosure();
|
||||
|
||||
@@ -182,16 +178,12 @@ const AppCard = ({
|
||||
showDot
|
||||
bg={'transparent'}
|
||||
colorSchema={
|
||||
isPublished
|
||||
isSaved
|
||||
? publishStatusStyle.published.colorSchema
|
||||
: publishStatusStyle.unPublish.colorSchema
|
||||
}
|
||||
>
|
||||
{t(
|
||||
isPublished
|
||||
? publishStatusStyle.published.text
|
||||
: publishStatusStyle.unPublish.text
|
||||
)}
|
||||
{t(isSaved ? publishStatusStyle.published.text : publishStatusStyle.unPublish.text)}
|
||||
</MyTag>
|
||||
</Flex>
|
||||
)}
|
||||
@@ -205,7 +197,7 @@ const AppCard = ({
|
||||
appDetail.avatar,
|
||||
appDetail.name,
|
||||
isOpenImport,
|
||||
isPublished,
|
||||
isSaved,
|
||||
onCloseImport,
|
||||
showSaveStatus,
|
||||
t
|
||||
|
@@ -704,7 +704,7 @@ export const useWorkflow = () => {
|
||||
chatConfig: appDetail.chatConfig
|
||||
});
|
||||
},
|
||||
[nodes, edges, appDetail.chatConfig],
|
||||
[nodes, edges, appDetail.chatConfig, pushPastSnapshot],
|
||||
{ wait: 500 }
|
||||
);
|
||||
|
||||
|
@@ -36,7 +36,7 @@ import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../../../context';
|
||||
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
|
||||
|
||||
const NodeExtract = ({ data }: NodeProps<FlowNodeItemType>) => {
|
||||
const NodeExtract = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
const { inputs, outputs, nodeId } = data;
|
||||
|
||||
const { t } = useTranslation();
|
||||
@@ -143,7 +143,7 @@ const NodeExtract = ({ data }: NodeProps<FlowNodeItemType>) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<NodeCard minW={'400px'} {...data}>
|
||||
<NodeCard minW={'400px'} selected={selected} {...data}>
|
||||
{isTool && (
|
||||
<>
|
||||
<Container>
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { postWorkflowDebug } from '@/web/core/workflow/api';
|
||||
import {
|
||||
checkWorkflowNodeAndConnection,
|
||||
compareSnapshot,
|
||||
simplifyWorkflowNodes,
|
||||
storeEdgesRenderEdge,
|
||||
storeNode2FlowNode
|
||||
@@ -15,16 +16,8 @@ import { RuntimeEdgeItemType, StoreEdgeItemType } from '@fastgpt/global/core/wor
|
||||
import { FlowNodeChangeProps } from '@fastgpt/global/core/workflow/type/fe';
|
||||
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { useLocalStorageState, useMemoizedFn, useUpdateEffect } from 'ahooks';
|
||||
import React, {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState
|
||||
} from 'react';
|
||||
import { useMemoizedFn, useUpdateEffect } from 'ahooks';
|
||||
import React, { Dispatch, SetStateAction, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { Edge, Node, OnConnectStartParams, ReactFlowProvider, useReactFlow } from 'reactflow';
|
||||
import { createContext, useContextSelector } from 'use-context-selector';
|
||||
import { defaultRunningStatus } from '../constants';
|
||||
@@ -37,11 +30,12 @@ import { useDisclosure } from '@chakra-ui/react';
|
||||
import { uiWorkflow2StoreWorkflow } from '../utils';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { formatTime2YMDHMS, formatTime2YMDHMW } from '@fastgpt/global/common/string/time';
|
||||
import { cloneDeep, isEqual } from 'lodash';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { AppVersionSchemaType } from '@fastgpt/global/core/app/version';
|
||||
import WorkflowInitContextProvider, { WorkflowNodeEdgeContext } from './workflowInitContext';
|
||||
import WorkflowEventContextProvider from './workflowEventContext';
|
||||
import { getAppConfigByDiff, getAppDiffConfig } from '@/web/core/app/diff';
|
||||
import { getAppConfigByDiff } from '@/web/core/app/diff';
|
||||
import WorkflowStatusContextProvider from './workflowStatusContext';
|
||||
|
||||
/*
|
||||
Context
|
||||
@@ -61,30 +55,28 @@ export const ReactFlowCustomProvider = ({
|
||||
<ReactFlowProvider>
|
||||
<WorkflowInitContextProvider>
|
||||
<WorkflowContextProvider basicNodeTemplates={templates}>
|
||||
<WorkflowEventContextProvider>{children}</WorkflowEventContextProvider>
|
||||
<WorkflowEventContextProvider>
|
||||
<WorkflowStatusContextProvider>{children}</WorkflowStatusContextProvider>
|
||||
</WorkflowEventContextProvider>
|
||||
</WorkflowContextProvider>
|
||||
</WorkflowInitContextProvider>
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export type WorkflowSnapshotsType = {
|
||||
diff?: any;
|
||||
title: string;
|
||||
isSaved?: boolean;
|
||||
state?: WorkflowStateType;
|
||||
|
||||
// old format
|
||||
nodes?: Node[];
|
||||
edges?: Edge[];
|
||||
chatConfig?: AppChatConfigType;
|
||||
};
|
||||
|
||||
export type WorkflowStateType = {
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
chatConfig: AppChatConfigType;
|
||||
};
|
||||
export type WorkflowSnapshotsType = WorkflowStateType & {
|
||||
title: string;
|
||||
isSaved?: boolean;
|
||||
|
||||
// abandon
|
||||
state?: WorkflowStateType;
|
||||
diff?: any;
|
||||
};
|
||||
|
||||
type WorkflowContextType = {
|
||||
appId?: string;
|
||||
@@ -753,12 +745,8 @@ const WorkflowContextProvider = ({
|
||||
|
||||
/* snapshots */
|
||||
const forbiddenSaveSnapshot = useRef(false);
|
||||
const [past, setPast] = useLocalStorageState<WorkflowSnapshotsType[]>(`${appId}-past`, {
|
||||
defaultValue: []
|
||||
}) as [WorkflowSnapshotsType[], (value: SetStateAction<WorkflowSnapshotsType[]>) => void];
|
||||
const [future, setFuture] = useLocalStorageState<WorkflowSnapshotsType[]>(`${appId}-future`, {
|
||||
defaultValue: []
|
||||
}) as [WorkflowSnapshotsType[], (value: SetStateAction<WorkflowSnapshotsType[]>) => void];
|
||||
const [past, setPast] = useState<WorkflowSnapshotsType[]>([]);
|
||||
const [future, setFuture] = useState<WorkflowSnapshotsType[]>([]);
|
||||
|
||||
const resetSnapshot = useMemoizedFn((state: WorkflowStateType) => {
|
||||
setNodes(state.nodes);
|
||||
@@ -777,33 +765,32 @@ const WorkflowContextProvider = ({
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get initial state
|
||||
const lastSnapshot = past[past.length - 1];
|
||||
if (!lastSnapshot?.state) return false;
|
||||
const isPastEqual = compareSnapshot(
|
||||
{
|
||||
nodes: pastNodes,
|
||||
edges: pastEdges,
|
||||
chatConfig: chatConfig
|
||||
},
|
||||
{
|
||||
nodes: past[0]?.nodes,
|
||||
edges: past[0]?.edges,
|
||||
chatConfig: past[0]?.chatConfig
|
||||
}
|
||||
);
|
||||
|
||||
// Create current state object
|
||||
const newState = {
|
||||
nodes: simplifyWorkflowNodes(pastNodes),
|
||||
edges: pastEdges,
|
||||
chatConfig
|
||||
};
|
||||
|
||||
// Calculate diff from initial state
|
||||
const diff = getAppDiffConfig(lastSnapshot.state, newState);
|
||||
if (past[0].diff && isEqual(past[0].diff, diff)) return false;
|
||||
if (isPastEqual) return false;
|
||||
|
||||
setFuture([]);
|
||||
setPast((past) => {
|
||||
const newPast = {
|
||||
diff,
|
||||
setPast((past) => [
|
||||
{
|
||||
nodes: pastNodes,
|
||||
edges: pastEdges,
|
||||
title: customTitle || formatTime2YMDHMS(new Date()),
|
||||
chatConfig,
|
||||
isSaved
|
||||
};
|
||||
if (past.length >= 100) {
|
||||
return [newPast, ...past.slice(0, 98), lastSnapshot];
|
||||
}
|
||||
return [newPast, ...past];
|
||||
});
|
||||
},
|
||||
...past.slice(0, 99)
|
||||
]);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -814,14 +801,13 @@ const WorkflowContextProvider = ({
|
||||
const copyText = t('app:version_copy');
|
||||
const regex = new RegExp(`(${copyText}-)\\1+`, 'g');
|
||||
const title = customTitle.replace(regex, `$1`);
|
||||
const pastState = getAppConfigByDiff(past[past.length - 1].state, params.diff);
|
||||
|
||||
resetSnapshot(pastState);
|
||||
resetSnapshot(params);
|
||||
|
||||
return pushPastSnapshot({
|
||||
pastNodes: pastState.nodes,
|
||||
pastEdges: pastState.edges,
|
||||
chatConfig: pastState.chatConfig,
|
||||
pastNodes: params.nodes,
|
||||
pastEdges: params.edges,
|
||||
chatConfig: params.chatConfig,
|
||||
customTitle: title
|
||||
});
|
||||
});
|
||||
@@ -844,38 +830,30 @@ const WorkflowContextProvider = ({
|
||||
});
|
||||
|
||||
const undo = useMemoizedFn(() => {
|
||||
if (past[1]) {
|
||||
setFuture((future) => [past[0], ...future]);
|
||||
if (past.length > 1) {
|
||||
forbiddenSaveSnapshot.current = true;
|
||||
|
||||
const firstPast = past[0];
|
||||
resetSnapshot(firstPast);
|
||||
|
||||
setFuture((future) => [firstPast, ...future]);
|
||||
setPast((past) => past.slice(1));
|
||||
const pastState = getAppConfigByDiff(past[past.length - 1].state, past[1].diff);
|
||||
resetSnapshot(pastState);
|
||||
}
|
||||
});
|
||||
const redo = useMemoizedFn(() => {
|
||||
if (!future[0]) return;
|
||||
|
||||
const futureState = getAppConfigByDiff(past[past.length - 1].state, future[0].diff);
|
||||
const futureState = future[0];
|
||||
|
||||
if (futureState) {
|
||||
setPast((past) => [future[0], ...past]);
|
||||
forbiddenSaveSnapshot.current = true;
|
||||
setPast((past) => [futureState, ...past]);
|
||||
setFuture((future) => future.slice(1));
|
||||
|
||||
resetSnapshot(futureState);
|
||||
}
|
||||
});
|
||||
|
||||
// remove other app's snapshot
|
||||
useEffect(() => {
|
||||
const keys = Object.keys(localStorage);
|
||||
const snapshotKeys = keys.filter((key) => key.endsWith('-past') || key.endsWith('-future'));
|
||||
snapshotKeys.forEach((key) => {
|
||||
const keyAppId = key.split('-')[0];
|
||||
if (keyAppId !== appId) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
});
|
||||
}, [appId]);
|
||||
|
||||
const initData = useCallback(
|
||||
async (
|
||||
e: {
|
||||
@@ -888,18 +866,51 @@ const WorkflowContextProvider = ({
|
||||
const nodes = e.nodes?.map((item) => storeNode2FlowNode({ item, t })) || [];
|
||||
const edges = e.edges?.map((item) => storeEdgesRenderEdge({ edge: item })) || [];
|
||||
|
||||
const initialState = {
|
||||
nodes: simplifyWorkflowNodes(nodes),
|
||||
edges,
|
||||
chatConfig: e.chatConfig || appDetail.chatConfig
|
||||
};
|
||||
// Get storage snapshot,兼容旧版正在编辑的用户,刷新后会把 local 数据存到内存并删除
|
||||
const pastSnapshot = (() => {
|
||||
try {
|
||||
const pastSnapshot = localStorage.getItem(`${appId}-past`);
|
||||
return pastSnapshot ? (JSON.parse(pastSnapshot) as WorkflowSnapshotsType[]) : [];
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
})();
|
||||
if (isInit && pastSnapshot.length > 0) {
|
||||
const defaultState = pastSnapshot[pastSnapshot.length - 1].state;
|
||||
|
||||
if (isInit && past.length > 0) {
|
||||
// new format
|
||||
if (past[0].diff && past[past.length - 1].state) {
|
||||
if (pastSnapshot[0].diff && defaultState) {
|
||||
// 设置旧的历史记录
|
||||
setPast(
|
||||
pastSnapshot
|
||||
.map((item) => {
|
||||
if (item.state) {
|
||||
return {
|
||||
title: t(`app:app.version_initial`),
|
||||
isSaved: item.isSaved,
|
||||
nodes: item.state.nodes,
|
||||
edges: item.state.edges,
|
||||
chatConfig: item.state.chatConfig
|
||||
};
|
||||
}
|
||||
if (item.diff) {
|
||||
const currentState = getAppConfigByDiff(defaultState, item.diff);
|
||||
return {
|
||||
title: item.title,
|
||||
isSaved: item.isSaved,
|
||||
nodes: currentState.nodes,
|
||||
edges: currentState.edges,
|
||||
chatConfig: currentState.chatConfig
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
.filter(Boolean) as WorkflowSnapshotsType[]
|
||||
);
|
||||
|
||||
// 设置当前版本
|
||||
const targetState = getAppConfigByDiff(
|
||||
past[past.length - 1].state,
|
||||
past[0].diff
|
||||
pastSnapshot[pastSnapshot.length - 1].state,
|
||||
pastSnapshot[0].diff
|
||||
) as WorkflowStateType;
|
||||
|
||||
setNodes(targetState.nodes);
|
||||
@@ -908,48 +919,41 @@ const WorkflowContextProvider = ({
|
||||
...state,
|
||||
chatConfig: targetState.chatConfig
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// 适配旧的编辑记录(4.8.15去除)
|
||||
if (past.every((item) => item.nodes)) {
|
||||
const newPast = convertOldFormatHistory(past);
|
||||
|
||||
setPast(newPast);
|
||||
|
||||
const latestState = getAppConfigByDiff(
|
||||
newPast[newPast.length - 1].state,
|
||||
newPast[0].diff
|
||||
) as WorkflowStateType;
|
||||
|
||||
setNodes(latestState.nodes);
|
||||
setEdges(latestState.edges);
|
||||
setAppDetail((state) => ({
|
||||
...state,
|
||||
chatConfig: latestState.chatConfig
|
||||
}));
|
||||
localStorage.removeItem(`${appId}-past`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setNodes(nodes);
|
||||
setEdges(edges);
|
||||
if (e.chatConfig) {
|
||||
setAppDetail((state) => ({ ...state, chatConfig: e.chatConfig as AppChatConfigType }));
|
||||
// 有历史记录,直接用历史记录覆盖
|
||||
if (isInit && past.length > 0) {
|
||||
const firstPast = past[0];
|
||||
setNodes(firstPast.nodes);
|
||||
setEdges(firstPast.edges);
|
||||
setAppDetail((state) => ({ ...state, chatConfig: firstPast.chatConfig }));
|
||||
return;
|
||||
}
|
||||
|
||||
// 初始化一个历史记录
|
||||
if (isInit && past.length === 0) {
|
||||
setPast([
|
||||
{
|
||||
title: t(`app:app.version_initial`),
|
||||
isSaved: true,
|
||||
state: initialState
|
||||
nodes: simplifyWorkflowNodes(nodes),
|
||||
edges,
|
||||
chatConfig: e.chatConfig || appDetail.chatConfig
|
||||
}
|
||||
]);
|
||||
forbiddenSaveSnapshot.current = true;
|
||||
}
|
||||
|
||||
// Init memory data
|
||||
setNodes(nodes);
|
||||
setEdges(edges);
|
||||
if (e.chatConfig) {
|
||||
setAppDetail((state) => ({ ...state, chatConfig: e.chatConfig as AppChatConfigType }));
|
||||
}
|
||||
},
|
||||
[appDetail.chatConfig, past, setAppDetail, setEdges, setNodes, setPast, t]
|
||||
[appDetail.chatConfig, appId, past, setAppDetail, setEdges, setNodes, t]
|
||||
);
|
||||
|
||||
const value = useMemo(
|
||||
@@ -1035,36 +1039,3 @@ const WorkflowContextProvider = ({
|
||||
);
|
||||
};
|
||||
export default React.memo(WorkflowContextProvider);
|
||||
|
||||
// Convert old history format to new format
|
||||
const convertOldFormatHistory = (past: WorkflowSnapshotsType[]) => {
|
||||
const baseState = {
|
||||
nodes: past[past.length - 1].state?.nodes || [],
|
||||
edges: past[past.length - 1].state?.edges || [],
|
||||
chatConfig: past[past.length - 1].state?.chatConfig || {}
|
||||
};
|
||||
|
||||
return past.map((item, index) => {
|
||||
if (index === past.length - 1) {
|
||||
return {
|
||||
title: item.title,
|
||||
isSaved: item.isSaved,
|
||||
state: baseState
|
||||
};
|
||||
}
|
||||
|
||||
const currentState = {
|
||||
nodes: item.nodes || [],
|
||||
edges: item.edges || [],
|
||||
chatConfig: item.chatConfig || {}
|
||||
};
|
||||
|
||||
const diff = getAppDiffConfig(baseState, currentState);
|
||||
|
||||
return {
|
||||
title: item.title || formatTime2YMDHMS(new Date()),
|
||||
isSaved: item.isSaved,
|
||||
diff
|
||||
};
|
||||
});
|
||||
};
|
||||
|
@@ -1,42 +1,9 @@
|
||||
import { createContext } from 'use-context-selector';
|
||||
import { postWorkflowDebug } from '@/web/core/workflow/api';
|
||||
import {
|
||||
checkWorkflowNodeAndConnection,
|
||||
compareSnapshot,
|
||||
storeEdgesRenderEdge,
|
||||
storeNode2FlowNode
|
||||
} from '@/web/core/workflow/utils';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type';
|
||||
import { FlowNodeItemType, StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
|
||||
import type { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/node';
|
||||
import { RuntimeEdgeItemType, StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
|
||||
import { FlowNodeChangeProps } from '@fastgpt/global/core/workflow/type/fe';
|
||||
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { useDebounceEffect, useLocalStorageState, useMemoizedFn, useUpdateEffect } from 'ahooks';
|
||||
import React, {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState
|
||||
} from 'react';
|
||||
import {
|
||||
Edge,
|
||||
EdgeChange,
|
||||
Node,
|
||||
NodeChange,
|
||||
OnConnectStartParams,
|
||||
useEdgesState,
|
||||
useNodesState,
|
||||
useReactFlow
|
||||
} from 'reactflow';
|
||||
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
|
||||
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import React, { Dispatch, SetStateAction, ReactNode, useEffect, useMemo } from 'react';
|
||||
import { Edge, EdgeChange, Node, NodeChange, useEdgesState, useNodesState } from 'reactflow';
|
||||
|
||||
type OnChange<ChangesType> = (changes: ChangesType[]) => void;
|
||||
|
||||
|
@@ -0,0 +1,89 @@
|
||||
import { useDebounceEffect } from 'ahooks';
|
||||
import React, { ReactNode, useMemo, useRef, useState } from 'react';
|
||||
import { createContext, useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowInitContext, WorkflowNodeEdgeContext } from './workflowInitContext';
|
||||
import { WorkflowContext } from '.';
|
||||
import { AppContext } from '../../context';
|
||||
import { compareSnapshot } from '@/web/core/workflow/utils';
|
||||
import { useBeforeunload } from '@fastgpt/web/hooks/useBeforeunload';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
type WorkflowStatusContextType = {
|
||||
isSaved: boolean;
|
||||
leaveSaveSign: React.MutableRefObject<boolean>;
|
||||
};
|
||||
|
||||
export const WorkflowStatusContext = createContext<WorkflowStatusContextType>({
|
||||
isSaved: false,
|
||||
// @ts-ignore
|
||||
leaveSaveSign: undefined
|
||||
});
|
||||
|
||||
const WorkflowStatusContextProvider = ({ children }: { children: ReactNode }) => {
|
||||
const { t } = useTranslation();
|
||||
const nodes = useContextSelector(WorkflowInitContext, (v) => v.nodes);
|
||||
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
|
||||
const past = useContextSelector(WorkflowContext, (v) => v.past);
|
||||
const future = useContextSelector(WorkflowContext, (v) => v.future);
|
||||
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
|
||||
|
||||
const [isSaved, setIsPublished] = useState(false);
|
||||
useDebounceEffect(
|
||||
() => {
|
||||
const savedSnapshot =
|
||||
[...future].reverse().find((snapshot) => snapshot.isSaved) ||
|
||||
past.find((snapshot) => snapshot.isSaved);
|
||||
|
||||
const val = compareSnapshot(
|
||||
{
|
||||
nodes: savedSnapshot?.nodes,
|
||||
edges: savedSnapshot?.edges,
|
||||
chatConfig: savedSnapshot?.chatConfig
|
||||
},
|
||||
{
|
||||
nodes,
|
||||
edges,
|
||||
chatConfig: appDetail.chatConfig
|
||||
}
|
||||
);
|
||||
setIsPublished(val);
|
||||
},
|
||||
[future, past, nodes, edges, appDetail.chatConfig],
|
||||
{
|
||||
wait: 500
|
||||
}
|
||||
);
|
||||
|
||||
const leaveSaveSign = useRef(true);
|
||||
|
||||
// Lead check before unload
|
||||
const flowData2StoreData = useContextSelector(WorkflowContext, (v) => v.flowData2StoreData);
|
||||
const onSaveApp = useContextSelector(AppContext, (v) => v.onSaveApp);
|
||||
useBeforeunload({
|
||||
tip: t('common:core.common.tip.leave page'),
|
||||
callback: async () => {
|
||||
if (isSaved || !leaveSaveSign.current) return;
|
||||
console.log('Leave auto save');
|
||||
const data = flowData2StoreData();
|
||||
if (!data) return;
|
||||
await onSaveApp({
|
||||
...data,
|
||||
isPublish: false,
|
||||
versionName: t('app:unusual_leave_auto_save'),
|
||||
chatConfig: appDetail.chatConfig
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const contextValue = useMemo(() => {
|
||||
return {
|
||||
isSaved,
|
||||
leaveSaveSign
|
||||
};
|
||||
}, [isSaved]);
|
||||
return (
|
||||
<WorkflowStatusContext.Provider value={contextValue}>{children}</WorkflowStatusContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkflowStatusContextProvider;
|
@@ -8,7 +8,7 @@ import { DatasetPageContext } from '@/web/core/dataset/context/datasetPageContex
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { getApiDatasetFileList, getApiDatasetFileListExistId } from '@/web/core/dataset/api';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
|
||||
import FolderPath from '@/components/common/folder/Path';
|
||||
import { getSourceNameIcon } from '@fastgpt/global/core/dataset/utils';
|
||||
|
@@ -3,7 +3,7 @@ import { Box, Flex, HStack, ModalBody } from '@chakra-ui/react';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import Markdown from '@/components/Markdown';
|
||||
|
@@ -6,7 +6,7 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useMemo, useState } from 'react';
|
||||
import PluginCard from './components/PluginCard';
|
||||
import { i18nT } from '@fastgpt/web/i18n/utils';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useRouter } from 'next/router';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
|
@@ -550,12 +550,12 @@ export const getLatestNodeTemplate = (
|
||||
export const compareSnapshot = (
|
||||
snapshot1: {
|
||||
nodes?: Node[];
|
||||
edges: Edge<any>[] | undefined;
|
||||
edges?: Edge<any>[] | undefined;
|
||||
chatConfig?: AppChatConfigType;
|
||||
},
|
||||
snapshot2: {
|
||||
nodes?: Node[];
|
||||
edges: Edge<any>[];
|
||||
edges?: Edge<any>[];
|
||||
chatConfig?: AppChatConfigType;
|
||||
}
|
||||
) => {
|
||||
@@ -563,6 +563,8 @@ export const compareSnapshot = (
|
||||
const clone2 = cloneDeep(snapshot2);
|
||||
|
||||
if (!clone1.nodes || !clone2.nodes) return false;
|
||||
if (!clone1.edges || !clone2.edges) return false;
|
||||
|
||||
const formatEdge = (edges: Edge[] | undefined) => {
|
||||
if (!edges) return [];
|
||||
return edges.map((edge) => ({
|
||||
|
Reference in New Issue
Block a user