mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-21 11:43:56 +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:
BIN
docSite/assets/imgs/image-20.png
Normal file
BIN
docSite/assets/imgs/image-20.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 156 KiB |
BIN
docSite/assets/imgs/image-21.png
Normal file
BIN
docSite/assets/imgs/image-21.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 166 KiB |
BIN
docSite/assets/imgs/image-22.png
Normal file
BIN
docSite/assets/imgs/image-22.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 85 KiB |
BIN
docSite/assets/imgs/image-23.png
Normal file
BIN
docSite/assets/imgs/image-23.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 113 KiB |
BIN
docSite/assets/imgs/image-24.png
Normal file
BIN
docSite/assets/imgs/image-24.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 72 KiB |
@@ -7,6 +7,26 @@ toc: true
|
||||
weight: 809
|
||||
---
|
||||
|
||||
## 新功能预览
|
||||
|
||||
### API 知识库
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
|  |  |
|
||||
|
||||
### HTML 渲染
|
||||
|
||||
| 源码模式 | 预览模式 | 全屏模式 |
|
||||
| --- | --- | --- |
|
||||
|  |  |  |
|
||||
|
||||
## 升级指南
|
||||
|
||||
- 更新 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. 修复 - 工作流编辑记录存储上限,去掉本地存储,增加异常离开时,强制自动保存。
|
@@ -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';
|
||||
|
@@ -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