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:
Archer
2024-12-06 18:08:52 +08:00
committed by GitHub
parent 0c308fcf8b
commit 90d7d2a164
27 changed files with 447 additions and 524 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View File

@@ -7,6 +7,26 @@ toc: true
weight: 809
---
## 新功能预览
### API 知识库
| | |
| --- | --- |
| ![alt text](/imgs/image-20.png) | ![alt text](/imgs/image-21.png) |
### HTML 渲染
| 源码模式 | 预览模式 | 全屏模式 |
| --- | --- | --- |
| ![alt text](/imgs/image-22.png) | ![alt text](/imgs/image-23.png) | ![alt text](/imgs/image-24.png) |
## 升级指南
- 更新 FastGPT 镜像 tag: v4.8.15-alpha
- 更新 FastGPT 商业版镜像 tag: v4.8.15-alpha fastgpt-pro镜像
- Sandbox 镜像,可以不更新
## 完整更新内容
@@ -24,4 +44,5 @@ weight: 809
12. 修复 - 分享链接点赞鉴权问题。
13. 修复 - 对话页面切换自动执行应用时,会误触发非自动执行应用。
14. 修复 - 语言播放鉴权问题。
15. 修复 - 插件应用知识库引用上限始终为 3000
15. 修复 - 插件应用知识库引用上限始终为 3000
16. 修复 - 工作流编辑记录存储上限,去掉本地存储,增加异常离开时,强制自动保存。

View File

@@ -17,6 +17,7 @@
"auto_execute": "Automatic execution",
"auto_execute_default_prompt_placeholder": "Default questions sent when executing automatically",
"auto_execute_tip": "After turning it on, the workflow will be automatically triggered when the user enters the conversation interface. \nExecution order: 1. Dialogue starter; 2. Global variables; 3. Automatic execution.",
"auto_save": "Auto save",
"chat_debug": "Chat Preview",
"chat_logs": "Conversation Logs",
"chat_logs_tips": "Logs will record the online, shared, and API (requires chatId) conversation records of this app.",
@@ -135,6 +136,7 @@
"type.Plugin": "Plugin",
"type.Simple bot": "Simple App",
"type.Workflow bot": "Workflow",
"unusual_leave_auto_save": "Abnormal exit, automatic saving",
"upload_file_max_amount": "Maximum File Quantity",
"upload_file_max_amount_tip": "Maximum number of files uploaded in a single round of conversation",
"variable.select type_desc": "You can define a global variable that does not need to be filled in by the user.\n\nThe value of this variable can come from the API interface, the Query of the shared link, or assigned through the [Variable Update] module.",

View File

@@ -17,6 +17,7 @@
"auto_execute": "自动执行",
"auto_execute_default_prompt_placeholder": "自动执行时,发送的默认问题",
"auto_execute_tip": "开启后用户进入对话界面将自动触发工作流。执行顺序1、对话开场白2、全局变量3、自动执行。",
"auto_save": "自动保存",
"chat_debug": "调试预览",
"chat_logs": "对话日志",
"chat_logs_tips": "日志会记录该应用的在线、分享和 API需填写 chatId对话记录",
@@ -135,6 +136,7 @@
"type.Plugin": "插件",
"type.Simple bot": "简易应用",
"type.Workflow bot": "工作流",
"unusual_leave_auto_save": "异常离开,自动保存",
"upload_file_max_amount": "最大文件数量",
"upload_file_max_amount_tip": "单轮对话中最大上传文件数量",
"variable.select type_desc": "可以为工作流定义全局变量,常用临时缓存。赋值的方式包括:\n1. 从对话页面的 query 参数获取。\n2. 通过 API 的 variables 对象传递。\n3. 通过【变量更新】节点进行赋值。",

View File

@@ -17,6 +17,7 @@
"auto_execute": "自動執行",
"auto_execute_default_prompt_placeholder": "自動執行時,發送的預設問題",
"auto_execute_tip": "開啟後,使用者進入對話式介面將自動觸發工作流程。\n執行順序1、對話開場白2、全域變數3、自動執行。",
"auto_save": "自動儲存",
"chat_debug": "聊天預覽",
"chat_logs": "對話紀錄",
"chat_logs_tips": "紀錄會記錄此應用程式的線上、分享和 API需填寫 chatId對話紀錄",
@@ -135,6 +136,7 @@
"type.Plugin": "外掛",
"type.Simple bot": "簡易應用程式",
"type.Workflow bot": "工作流程",
"unusual_leave_auto_save": "異常離開,自動儲存",
"upload_file_max_amount": "最大檔案數量",
"upload_file_max_amount_tip": "單輪對話中最大上傳檔案數量",
"variable.select type_desc": "可以為工作流程定義全域變數,常用於暫存。賦值的方式包括:\n1. 從對話頁面的 query 參數取得。\n2. 透過 API 的 variables 物件傳遞。\n3. 透過【變數更新】節點進行賦值。",

View File

@@ -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';

View File

@@ -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')}

View File

@@ -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

View File

@@ -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
)}

View File

@@ -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 />}

View File

@@ -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 };
};

View File

@@ -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')}

View File

@@ -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();

View File

@@ -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

View File

@@ -704,7 +704,7 @@ export const useWorkflow = () => {
chatConfig: appDetail.chatConfig
});
},
[nodes, edges, appDetail.chatConfig],
[nodes, edges, appDetail.chatConfig, pushPastSnapshot],
{ wait: 500 }
);

View File

@@ -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>

View File

@@ -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
};
});
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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) => ({