Commit 90d7d2a1 by Archer Committed by GitHub

perf: workflow snapshots;fix: extrat node selected (#3334)

* doc

* perf: workflow snapshots

* fix: extrat node selected

* refresh page reset snapshot
parent 0c308fcf
......@@ -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
\ No newline at end of file
15. 修复 - 插件应用知识库引用上限始终为 3000
16. 修复 - 工作流编辑记录存储上限,去掉本地存储,增加异常离开时,强制自动保存。
\ No newline at end of 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.",
......
......@@ -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. 通过【变量更新】节点进行赋值。",
......
......@@ -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. 透過【變數更新】節點進行賦值。",
......
......@@ -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';
......
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,39 +61,10 @@ const Header = () => {
(v) => v.setShowHistoryModal
);
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 isSaved = useContextSelector(WorkflowStatusContext, (v) => v.isSaved);
const leaveSaveSign = useContextSelector(WorkflowStatusContext, (v) => v.leaveSaveSign);
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 { lastAppListRouteType } = useSystemStore();
const { runAsync: onClickSave, loading } = useRequest2(
async ({
......@@ -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')}
......
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
......
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
)}
......
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 />}
......
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;
// 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;
const pastState = past[0];
const isPastEqual = compareSimpleAppSnapshot(pastState?.appForm, appForm);
if (isPastEqual) 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 };
};
......
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,38 +65,10 @@ const Header = () => {
(v) => v.setShowHistoryModal
);
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 isSaved = useContextSelector(WorkflowStatusContext, (v) => v.isSaved);
const leaveSaveSign = useContextSelector(WorkflowStatusContext, (v) => v.leaveSaveSign);
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 { lastAppListRouteType } = useSystemStore();
const { runAsync: onClickSave, loading } = useRequest2(
async ({
......@@ -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();
......
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>
......
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;
......
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) => ({
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment