4.8.11 test (#2794)
* perf: version list type * perf: add node default value * perf: snapshot status * fix: version detail auth * fix: export defalt
This commit is contained in:
parent
e31d6ec2c1
commit
86436d55ff
68
.vscode/nextapi.code-snippets
vendored
68
.vscode/nextapi.code-snippets
vendored
@ -50,5 +50,73 @@
|
|||||||
"export default ContextProvider"
|
"export default ContextProvider"
|
||||||
],
|
],
|
||||||
"description": "FastGPT usecontext template"
|
"description": "FastGPT usecontext template"
|
||||||
|
},
|
||||||
|
|
||||||
|
"Jest test template": {
|
||||||
|
"scope": "typescriptreact",
|
||||||
|
"prefix": "jesttest",
|
||||||
|
"body": [
|
||||||
|
"import '@/pages/api/__mocks__/base';",
|
||||||
|
"import { root } from '@/pages/api/__mocks__/db/init';",
|
||||||
|
"import { getTestRequest } from '@/test/utils';",
|
||||||
|
"import { AppErrEnum } from '@fastgpt/global/common/error/code/app';",
|
||||||
|
"import handler from './demo';",
|
||||||
|
"",
|
||||||
|
"// Import the schema",
|
||||||
|
"import { MongoOutLink } from '@fastgpt/service/support/outLink/schema';",
|
||||||
|
"",
|
||||||
|
"beforeAll(async () => {",
|
||||||
|
" // await MongoOutLink.create({",
|
||||||
|
" // shareId: 'aaa',",
|
||||||
|
" // appId: root.appId,",
|
||||||
|
" // tmbId: root.tmbId,",
|
||||||
|
" // teamId: root.teamId,",
|
||||||
|
" // type: 'share',",
|
||||||
|
" // name: 'aaa'",
|
||||||
|
" // })",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"test('Should return a list of outLink', async () => {",
|
||||||
|
" // Mock request",
|
||||||
|
" const res = (await handler(",
|
||||||
|
" ...getTestRequest({",
|
||||||
|
" query: {",
|
||||||
|
" appId: root.appId,",
|
||||||
|
" type: 'share'",
|
||||||
|
" },",
|
||||||
|
" user: root",
|
||||||
|
" })",
|
||||||
|
" )) as any;",
|
||||||
|
"",
|
||||||
|
" expect(res.code).toBe(200);",
|
||||||
|
" expect(res.data.length).toBe(2);",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"test('appId is required', async () => {",
|
||||||
|
" const res = (await handler(",
|
||||||
|
" ...getTestRequest({",
|
||||||
|
" query: {",
|
||||||
|
" type: 'share'",
|
||||||
|
" },",
|
||||||
|
" user: root",
|
||||||
|
" })",
|
||||||
|
" )) as any;",
|
||||||
|
" expect(res.code).toBe(500);",
|
||||||
|
" expect(res.error).toBe(AppErrEnum.unExist);",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"test('if type is not provided, return nothing', async () => {",
|
||||||
|
" const res = (await handler(",
|
||||||
|
" ...getTestRequest({",
|
||||||
|
" query: {",
|
||||||
|
" appId: root.appId",
|
||||||
|
" },",
|
||||||
|
" user: root",
|
||||||
|
" })",
|
||||||
|
" )) as any;",
|
||||||
|
" expect(res.code).toBe(200);",
|
||||||
|
" expect(res.data.length).toBe(0);",
|
||||||
|
"});"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
4
dev.md
4
dev.md
@ -29,6 +29,10 @@ Note: If the Node version is >= 20, you need to pass the `--no-node-snapshot` pa
|
|||||||
NODE_OPTIONS=--no-node-snapshot pnpm i
|
NODE_OPTIONS=--no-node-snapshot pnpm i
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Jest
|
||||||
|
|
||||||
|
https://fael3z0zfze.feishu.cn/docx/ZOI1dABpxoGhS7xzhkXcKPxZnDL
|
||||||
|
|
||||||
## I18N
|
## I18N
|
||||||
|
|
||||||
### Install i18n-ally Plugin
|
### Install i18n-ally Plugin
|
||||||
|
|||||||
9
packages/global/core/app/version.d.ts
vendored
9
packages/global/core/app/version.d.ts
vendored
@ -12,3 +12,12 @@ export type AppVersionSchemaType = {
|
|||||||
versionName: string;
|
versionName: string;
|
||||||
tmbId: string;
|
tmbId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type VersionListItemType = {
|
||||||
|
_id: string;
|
||||||
|
appId: string;
|
||||||
|
versionName: string;
|
||||||
|
time: Date;
|
||||||
|
isPublish: boolean | undefined;
|
||||||
|
tmbId: string;
|
||||||
|
};
|
||||||
|
|||||||
@ -5,11 +5,11 @@
|
|||||||
"app.Version name": "Version Name",
|
"app.Version name": "Version Name",
|
||||||
"app.modules.click to update": "Click to Refresh",
|
"app.modules.click to update": "Click to Refresh",
|
||||||
"app.modules.has new version": "New Version Available",
|
"app.modules.has new version": "New Version Available",
|
||||||
"app.version_back": "Revert to Original State",
|
"version_back": "Revert to Original State",
|
||||||
"app.version_copy": "Duplicate",
|
"version_copy": "Duplicate",
|
||||||
"app.version_current": "Current Version",
|
"app.version_current": "Current Version",
|
||||||
"app.version_initial": "Initial Version",
|
"app.version_initial": "Initial Version",
|
||||||
"app.version_initial_copy": "Duplicate - Original State",
|
"version_initial_copy": "Duplicate - Original State",
|
||||||
"app.version_name_tips": "Version name cannot be empty",
|
"app.version_name_tips": "Version name cannot be empty",
|
||||||
"app.version_past": "Previously Published",
|
"app.version_past": "Previously Published",
|
||||||
"app.version_publish_tips": "This version will be saved to the team cloud, synchronized with the entire team, and update the app version on all release channels.",
|
"app.version_publish_tips": "This version will be saved to the team cloud, synchronized with the entire team, and update the app version on all release channels.",
|
||||||
@ -51,6 +51,7 @@
|
|||||||
"import_configs": "Import Configurations",
|
"import_configs": "Import Configurations",
|
||||||
"import_configs_failed": "Import configuration failed, please ensure the configuration is correct!",
|
"import_configs_failed": "Import configuration failed, please ensure the configuration is correct!",
|
||||||
"import_configs_success": "Import Successful",
|
"import_configs_success": "Import Successful",
|
||||||
|
"initial_form": "initial state",
|
||||||
"interval.12_hours": "Every 12 Hours",
|
"interval.12_hours": "Every 12 Hours",
|
||||||
"interval.2_hours": "Every 2 Hours",
|
"interval.2_hours": "Every 2 Hours",
|
||||||
"interval.3_hours": "Every 3 Hours",
|
"interval.3_hours": "Every 3 Hours",
|
||||||
@ -87,11 +88,11 @@
|
|||||||
"search_app": "Search Application",
|
"search_app": "Search Application",
|
||||||
"setting_app": "Application Settings",
|
"setting_app": "Application Settings",
|
||||||
"setting_plugin": "Plugin Settings",
|
"setting_plugin": "Plugin Settings",
|
||||||
"template.simple_robot": "Simple robot",
|
|
||||||
"template.hard_strict": "Strict Q&A template",
|
"template.hard_strict": "Strict Q&A template",
|
||||||
"template.hard_strict_des": "Based on the question and answer template, stricter requirements are imposed on the model's answers.",
|
"template.hard_strict_des": "Based on the question and answer template, stricter requirements are imposed on the model's answers.",
|
||||||
"template.qa_template": "Q&A template",
|
"template.qa_template": "Q&A template",
|
||||||
"template.qa_template_des": "A knowledge base suitable for QA question and answer structure, which allows AI to answer strictly according to preset content",
|
"template.qa_template_des": "A knowledge base suitable for QA question and answer structure, which allows AI to answer strictly according to preset content",
|
||||||
|
"template.simple_robot": "Simple robot",
|
||||||
"template.standard_strict": "Standard strict template",
|
"template.standard_strict": "Standard strict template",
|
||||||
"template.standard_strict_des": "Based on the standard template, stricter requirements are imposed on the model's answers.",
|
"template.standard_strict_des": "Based on the standard template, stricter requirements are imposed on the model's answers.",
|
||||||
"template.standard_template": "Standard template",
|
"template.standard_template": "Standard template",
|
||||||
@ -154,4 +155,4 @@
|
|||||||
"workflow.user_file_input_desc": "Links to documents and images uploaded by users.",
|
"workflow.user_file_input_desc": "Links to documents and images uploaded by users.",
|
||||||
"workflow.user_select": "User Selection",
|
"workflow.user_select": "User Selection",
|
||||||
"workflow.user_select_tip": "This module can configure multiple options for selection during the dialogue. Different options can lead to different workflow branches."
|
"workflow.user_select_tip": "This module can configure multiple options for selection during the dialogue. Different options can lead to different workflow branches."
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,11 +5,11 @@
|
|||||||
"app.Version name": "版本名称",
|
"app.Version name": "版本名称",
|
||||||
"app.modules.click to update": "点击更新",
|
"app.modules.click to update": "点击更新",
|
||||||
"app.modules.has new version": "有新版本",
|
"app.modules.has new version": "有新版本",
|
||||||
"app.version_back": "回到初始状态",
|
"version_back": "回到初始状态",
|
||||||
"app.version_copy": "副本",
|
"version_copy": "副本",
|
||||||
"app.version_current": "当前版本",
|
"app.version_current": "当前版本",
|
||||||
"app.version_initial": "初始版本",
|
"app.version_initial": "初始版本",
|
||||||
"app.version_initial_copy": "副本-初始状态",
|
"version_initial_copy": "副本-初始状态",
|
||||||
"app.version_name_tips": "版本名称不能为空",
|
"app.version_name_tips": "版本名称不能为空",
|
||||||
"app.version_past": "发布过",
|
"app.version_past": "发布过",
|
||||||
"app.version_publish_tips": "该版本将被保存至团队云端,同步给整个团队,同时更新所有发布渠道的应用版本",
|
"app.version_publish_tips": "该版本将被保存至团队云端,同步给整个团队,同时更新所有发布渠道的应用版本",
|
||||||
@ -51,6 +51,7 @@
|
|||||||
"import_configs": "导入配置",
|
"import_configs": "导入配置",
|
||||||
"import_configs_failed": "导入配置失败,请确保配置正常!",
|
"import_configs_failed": "导入配置失败,请确保配置正常!",
|
||||||
"import_configs_success": "导入成功",
|
"import_configs_success": "导入成功",
|
||||||
|
"initial_form": "初始状态",
|
||||||
"interval.12_hours": "每12小时",
|
"interval.12_hours": "每12小时",
|
||||||
"interval.2_hours": "每2小时",
|
"interval.2_hours": "每2小时",
|
||||||
"interval.3_hours": "每3小时",
|
"interval.3_hours": "每3小时",
|
||||||
|
|||||||
@ -43,6 +43,4 @@ export const initMockData = async () => {
|
|||||||
root.tmbId = rootTeamMember._id;
|
root.tmbId = rootTeamMember._id;
|
||||||
root.teamId = rootTeam._id;
|
root.teamId = rootTeam._id;
|
||||||
root.appId = rootApp._id;
|
root.appId = rootApp._id;
|
||||||
|
|
||||||
await Promise.all([rootUser.save(), rootTeam.save(), rootTeamMember.save(), rootApp.save()]);
|
|
||||||
};
|
};
|
||||||
|
|||||||
61
projects/app/src/pages/api/__mocks__/demo/demo.test.ts
Normal file
61
projects/app/src/pages/api/__mocks__/demo/demo.test.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import '@/pages/api/__mocks__/base';
|
||||||
|
import { root } from '@/pages/api/__mocks__/db/init';
|
||||||
|
import { getTestRequest } from '@/test/utils';
|
||||||
|
import { AppErrEnum } from '@fastgpt/global/common/error/code/app';
|
||||||
|
import handler from './demo';
|
||||||
|
|
||||||
|
// Import the schema
|
||||||
|
import { MongoOutLink } from '@fastgpt/service/support/outLink/schema';
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
// await MongoOutLink.create({
|
||||||
|
// shareId: 'aaa',
|
||||||
|
// appId: root.appId,
|
||||||
|
// tmbId: root.tmbId,
|
||||||
|
// teamId: root.teamId,
|
||||||
|
// type: 'share',
|
||||||
|
// name: 'aaa'
|
||||||
|
// })
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should return a list of outLink', async () => {
|
||||||
|
// Mock request
|
||||||
|
const res = (await handler(
|
||||||
|
...getTestRequest({
|
||||||
|
query: {
|
||||||
|
appId: root.appId,
|
||||||
|
type: 'share'
|
||||||
|
},
|
||||||
|
user: root
|
||||||
|
})
|
||||||
|
)) as any;
|
||||||
|
|
||||||
|
expect(res.code).toBe(200);
|
||||||
|
expect(res.data.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('appId is required', async () => {
|
||||||
|
const res = (await handler(
|
||||||
|
...getTestRequest({
|
||||||
|
query: {
|
||||||
|
type: 'share'
|
||||||
|
},
|
||||||
|
user: root
|
||||||
|
})
|
||||||
|
)) as any;
|
||||||
|
expect(res.code).toBe(500);
|
||||||
|
expect(res.error).toBe(AppErrEnum.unExist);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('if type is not provided, return nothing', async () => {
|
||||||
|
const res = (await handler(
|
||||||
|
...getTestRequest({
|
||||||
|
query: {
|
||||||
|
appId: root.appId
|
||||||
|
},
|
||||||
|
user: root
|
||||||
|
})
|
||||||
|
)) as any;
|
||||||
|
expect(res.code).toBe(200);
|
||||||
|
expect(res.data.length).toBe(0);
|
||||||
|
});
|
||||||
17
projects/app/src/pages/api/__mocks__/demo/demo.ts
Normal file
17
projects/app/src/pages/api/__mocks__/demo/demo.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
|
||||||
|
import { NextAPI } from '@/service/middleware/entry';
|
||||||
|
|
||||||
|
export type demoQuery = {};
|
||||||
|
|
||||||
|
export type demoBody = {};
|
||||||
|
|
||||||
|
export type demoResponse = {};
|
||||||
|
|
||||||
|
async function handler(
|
||||||
|
req: ApiRequestProps<demoBody, demoQuery>,
|
||||||
|
res: ApiResponseType<any>
|
||||||
|
): Promise<demoResponse> {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NextAPI(handler);
|
||||||
@ -2,20 +2,32 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
|||||||
import { NextAPI } from '@/service/middleware/entry';
|
import { NextAPI } from '@/service/middleware/entry';
|
||||||
import { MongoAppVersion } from '@fastgpt/service/core/app/version/schema';
|
import { MongoAppVersion } from '@fastgpt/service/core/app/version/schema';
|
||||||
import { authApp } from '@fastgpt/service/support/permission/app/auth';
|
import { authApp } from '@fastgpt/service/support/permission/app/auth';
|
||||||
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
|
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||||
|
import { AppVersionSchemaType } from '@fastgpt/global/core/app/version';
|
||||||
|
import { formatTime2YMDHM } from '@fastgpt/global/common/string/time';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
versionId: string;
|
versionId: string;
|
||||||
appId: string;
|
appId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse<any>
|
||||||
|
): Promise<AppVersionSchemaType> {
|
||||||
const { versionId, appId } = req.query as Props;
|
const { versionId, appId } = req.query as Props;
|
||||||
|
|
||||||
await authApp({ req, authToken: true, appId, per: ReadPermissionVal });
|
await authApp({ req, authToken: true, appId, per: WritePermissionVal });
|
||||||
const result = await MongoAppVersion.findById(versionId);
|
const result = await MongoAppVersion.findById(versionId).lean();
|
||||||
|
|
||||||
return result;
|
if (!result) {
|
||||||
|
return Promise.reject('version not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
versionName: result?.versionName || formatTime2YMDHM(result?.time)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NextAPI(handler);
|
export default NextAPI(handler);
|
||||||
|
|||||||
69
projects/app/src/pages/api/core/app/version/lis.test.ts
Normal file
69
projects/app/src/pages/api/core/app/version/lis.test.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import '@/pages/api/__mocks__/base';
|
||||||
|
import { root } from '@/pages/api/__mocks__/db/init';
|
||||||
|
import { getTestRequest } from '@/test/utils';
|
||||||
|
import handler, { versionListBody, versionListResponse } from './list';
|
||||||
|
|
||||||
|
// Import the schema
|
||||||
|
import { MongoAppVersion } from '@fastgpt/service/core/app/version/schema';
|
||||||
|
|
||||||
|
const total = 22;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const arr = new Array(total).fill(0);
|
||||||
|
await MongoAppVersion.insertMany(
|
||||||
|
arr.map((_, index) => ({
|
||||||
|
appId: root.appId,
|
||||||
|
nodes: [],
|
||||||
|
edges: [],
|
||||||
|
chatConfig: {},
|
||||||
|
isPublish: index % 2 === 0,
|
||||||
|
versionName: `v` + index,
|
||||||
|
tmbId: root.tmbId,
|
||||||
|
time: new Date(index * 1000)
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Get version list and check', async () => {
|
||||||
|
const offset = 0;
|
||||||
|
const pageSize = 10;
|
||||||
|
|
||||||
|
const _res = (await handler(
|
||||||
|
...getTestRequest<{}, versionListBody>({
|
||||||
|
body: {
|
||||||
|
offset,
|
||||||
|
pageSize,
|
||||||
|
appId: root.appId
|
||||||
|
},
|
||||||
|
user: root
|
||||||
|
})
|
||||||
|
)) as any;
|
||||||
|
const res = _res.data as versionListResponse;
|
||||||
|
|
||||||
|
expect(res.total).toBe(total);
|
||||||
|
expect(res.list.length).toBe(pageSize);
|
||||||
|
expect(res.list[0].versionName).toBe('v21');
|
||||||
|
expect(res.list[9].versionName).toBe('v12');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Get version list with offset 20', async () => {
|
||||||
|
const offset = 20;
|
||||||
|
const pageSize = 10;
|
||||||
|
|
||||||
|
const _res = (await handler(
|
||||||
|
...getTestRequest<{}, versionListBody>({
|
||||||
|
body: {
|
||||||
|
offset,
|
||||||
|
pageSize,
|
||||||
|
appId: root.appId
|
||||||
|
},
|
||||||
|
user: root
|
||||||
|
})
|
||||||
|
)) as any;
|
||||||
|
const res = _res.data as versionListResponse;
|
||||||
|
|
||||||
|
expect(res.total).toBe(total);
|
||||||
|
expect(res.list.length).toBe(2);
|
||||||
|
expect(res.list[0].versionName).toBe('v1');
|
||||||
|
expect(res.list[1].versionName).toBe('v0');
|
||||||
|
});
|
||||||
@ -1,27 +1,26 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
import type { NextApiResponse } from 'next';
|
||||||
import { NextAPI } from '@/service/middleware/entry';
|
import { NextAPI } from '@/service/middleware/entry';
|
||||||
import { MongoAppVersion } from '@fastgpt/service/core/app/version/schema';
|
import { MongoAppVersion } from '@fastgpt/service/core/app/version/schema';
|
||||||
import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type';
|
import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type';
|
||||||
import { ApiRequestProps } from '@fastgpt/service/type/next';
|
import { ApiRequestProps } from '@fastgpt/service/type/next';
|
||||||
|
import { authApp } from '@fastgpt/service/support/permission/app/auth';
|
||||||
|
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||||
|
import { VersionListItemType } from '@fastgpt/global/core/app/version';
|
||||||
|
|
||||||
type Props = PaginationProps<{
|
export type versionListBody = PaginationProps<{
|
||||||
appId: string;
|
appId: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type versionListResponse = {
|
export type versionListResponse = PaginationResponse<VersionListItemType>;
|
||||||
_id: string;
|
|
||||||
appId: string;
|
|
||||||
versionName: string;
|
|
||||||
time: Date;
|
|
||||||
isPublish: boolean | undefined;
|
|
||||||
tmbId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Response = PaginationResponse<versionListResponse>;
|
async function handler(
|
||||||
|
req: ApiRequestProps<versionListBody>,
|
||||||
async function handler(req: ApiRequestProps<Props>, res: NextApiResponse<any>): Promise<Response> {
|
res: NextApiResponse<any>
|
||||||
|
): Promise<versionListResponse> {
|
||||||
const { offset, pageSize, appId } = req.body;
|
const { offset, pageSize, appId } = req.body;
|
||||||
|
|
||||||
|
await authApp({ appId, req, per: WritePermissionVal, authToken: true });
|
||||||
|
|
||||||
const [result, total] = await Promise.all([
|
const [result, total] = await Promise.all([
|
||||||
MongoAppVersion.find(
|
MongoAppVersion.find(
|
||||||
{
|
{
|
||||||
@ -10,17 +10,15 @@ import {
|
|||||||
useDisclosure
|
useDisclosure
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import dynamic from 'next/dynamic';
|
|
||||||
|
|
||||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||||
import { useContextSelector } from 'use-context-selector';
|
import { useContextSelector } from 'use-context-selector';
|
||||||
import { WorkflowContext, getWorkflowStore } from '../WorkflowComponents/context';
|
import { WorkflowContext, WorkflowSnapshotsType } from '../WorkflowComponents/context';
|
||||||
import { AppContext, TabEnum } from '../context';
|
import { AppContext, TabEnum } from '../context';
|
||||||
import RouteTab from '../RouteTab';
|
import RouteTab from '../RouteTab';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
|
|
||||||
import AppCard from '../WorkflowComponents/AppCard';
|
import AppCard from '../WorkflowComponents/AppCard';
|
||||||
import { uiWorkflow2StoreWorkflow } from '../WorkflowComponents/utils';
|
|
||||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||||
@ -30,8 +28,7 @@ import { useToast } from '@fastgpt/web/hooks/useToast';
|
|||||||
import { useDebounceEffect } from 'ahooks';
|
import { useDebounceEffect } from 'ahooks';
|
||||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||||
import SaveButton from '../Workflow/components/SaveButton';
|
import SaveButton from '../Workflow/components/SaveButton';
|
||||||
|
import PublishHistories from '../PublishHistoriesSlider';
|
||||||
const PublishHistories = dynamic(() => import('../PublishHistoriesSlider'));
|
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -51,15 +48,15 @@ const Header = () => {
|
|||||||
flowData2StoreData,
|
flowData2StoreData,
|
||||||
flowData2StoreDataAndCheck,
|
flowData2StoreDataAndCheck,
|
||||||
setWorkflowTestData,
|
setWorkflowTestData,
|
||||||
setHistoriesDefaultData,
|
setShowHistoryModal,
|
||||||
historiesDefaultData,
|
showHistoryModal,
|
||||||
nodes,
|
nodes,
|
||||||
edges,
|
edges,
|
||||||
past,
|
past,
|
||||||
future,
|
future,
|
||||||
setPast,
|
setPast,
|
||||||
saveSnapshot,
|
onSwitchTmpVersion,
|
||||||
resetSnapshot
|
onSwitchCloudVersion
|
||||||
} = useContextSelector(WorkflowContext, (v) => v);
|
} = useContextSelector(WorkflowContext, (v) => v);
|
||||||
const { lastAppListRouteType } = useSystemStore();
|
const { lastAppListRouteType } = useSystemStore();
|
||||||
|
|
||||||
@ -187,21 +184,15 @@ const Header = () => {
|
|||||||
|
|
||||||
{currentTab === TabEnum.appEdit && (
|
{currentTab === TabEnum.appEdit && (
|
||||||
<HStack flexDirection={['column', 'row']} spacing={[2, 3]}>
|
<HStack flexDirection={['column', 'row']} spacing={[2, 3]}>
|
||||||
{!historiesDefaultData && (
|
{!showHistoryModal && (
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<MyIcon name={'history'} w={'18px'} />}
|
icon={<MyIcon name={'history'} w={'18px'} />}
|
||||||
aria-label={''}
|
aria-label={''}
|
||||||
size={'sm'}
|
size={'sm'}
|
||||||
w={'30px'}
|
w={'30px'}
|
||||||
variant={'whitePrimary'}
|
variant={'whitePrimary'}
|
||||||
onClick={async () => {
|
onClick={() => {
|
||||||
const { nodes, edges } = uiWorkflow2StoreWorkflow(await getWorkflowStore());
|
setShowHistoryModal(true);
|
||||||
|
|
||||||
setHistoriesDefaultData({
|
|
||||||
nodes,
|
|
||||||
edges,
|
|
||||||
chatConfig: appDetail.chatConfig
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -218,7 +209,7 @@ const Header = () => {
|
|||||||
>
|
>
|
||||||
{t('common:core.workflow.Run')}
|
{t('common:core.workflow.Run')}
|
||||||
</Button>
|
</Button>
|
||||||
{!historiesDefaultData && (
|
{!showHistoryModal && (
|
||||||
<SaveButton
|
<SaveButton
|
||||||
isLoading={loading}
|
isLoading={loading}
|
||||||
onClickSave={onClickSave}
|
onClickSave={onClickSave}
|
||||||
@ -228,47 +219,6 @@ const Header = () => {
|
|||||||
</HStack>
|
</HStack>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
{historiesDefaultData && isV2Workflow && currentTab === TabEnum.appEdit && (
|
|
||||||
<PublishHistories
|
|
||||||
onClose={() => {
|
|
||||||
setHistoriesDefaultData(undefined);
|
|
||||||
}}
|
|
||||||
past={past}
|
|
||||||
saveSnapshot={saveSnapshot}
|
|
||||||
resetSnapshot={resetSnapshot}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<MyModal
|
|
||||||
isOpen={isOpenBackConfirm}
|
|
||||||
onClose={onCloseBackConfirm}
|
|
||||||
iconSrc="common/warn"
|
|
||||||
title={t('common:common.Exit')}
|
|
||||||
w={'400px'}
|
|
||||||
>
|
|
||||||
<ModalBody>
|
|
||||||
<Box>{t('workflow:workflow.exit_tips')}</Box>
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter gap={3}>
|
|
||||||
<Button variant={'whiteDanger'} onClick={onBack}>
|
|
||||||
{t('common:common.Exit Directly')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
isLoading={loading}
|
|
||||||
onClick={async () => {
|
|
||||||
await onClickSave({});
|
|
||||||
onCloseBackConfirm();
|
|
||||||
onBack();
|
|
||||||
toast({
|
|
||||||
status: 'success',
|
|
||||||
title: t('app:saved_success'),
|
|
||||||
position: 'top-right'
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('common:common.Save_and_exit')}
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</MyModal>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}, [
|
}, [
|
||||||
@ -276,22 +226,63 @@ const Header = () => {
|
|||||||
currentTab,
|
currentTab,
|
||||||
isPublished,
|
isPublished,
|
||||||
onBack,
|
onBack,
|
||||||
isOpenBackConfirm,
|
|
||||||
onOpenBackConfirm,
|
onOpenBackConfirm,
|
||||||
onCloseBackConfirm,
|
isV2Workflow,
|
||||||
|
showHistoryModal,
|
||||||
t,
|
t,
|
||||||
loading,
|
loading,
|
||||||
isV2Workflow,
|
|
||||||
historiesDefaultData,
|
|
||||||
onClickSave,
|
onClickSave,
|
||||||
setHistoriesDefaultData,
|
|
||||||
appDetail.chatConfig,
|
|
||||||
flowData2StoreDataAndCheck,
|
flowData2StoreDataAndCheck,
|
||||||
setWorkflowTestData,
|
setShowHistoryModal,
|
||||||
toast
|
setWorkflowTestData
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return Render;
|
return (
|
||||||
|
<>
|
||||||
|
{Render}
|
||||||
|
{showHistoryModal && isV2Workflow && currentTab === TabEnum.appEdit && (
|
||||||
|
<PublishHistories<WorkflowSnapshotsType>
|
||||||
|
onClose={() => {
|
||||||
|
setShowHistoryModal(false);
|
||||||
|
}}
|
||||||
|
past={past}
|
||||||
|
onSwitchTmpVersion={onSwitchTmpVersion}
|
||||||
|
onSwitchCloudVersion={onSwitchCloudVersion}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<MyModal
|
||||||
|
isOpen={isOpenBackConfirm}
|
||||||
|
onClose={onCloseBackConfirm}
|
||||||
|
iconSrc="common/warn"
|
||||||
|
title={t('common:common.Exit')}
|
||||||
|
w={'400px'}
|
||||||
|
>
|
||||||
|
<ModalBody>
|
||||||
|
<Box>{t('workflow:workflow.exit_tips')}</Box>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter gap={3}>
|
||||||
|
<Button variant={'whiteDanger'} onClick={onBack}>
|
||||||
|
{t('common:common.Exit Directly')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
isLoading={loading}
|
||||||
|
onClick={async () => {
|
||||||
|
await onClickSave({});
|
||||||
|
onCloseBackConfirm();
|
||||||
|
onBack();
|
||||||
|
toast({
|
||||||
|
status: 'success',
|
||||||
|
title: t('app:saved_success'),
|
||||||
|
position: 'top-right'
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('common:common.Save_and_exit')}
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</MyModal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default React.memo(Header);
|
export default React.memo(Header);
|
||||||
|
|||||||
@ -7,38 +7,36 @@ import {
|
|||||||
import { useVirtualScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
|
import { useVirtualScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
|
||||||
import CustomRightDrawer from '@fastgpt/web/components/common/MyDrawer/CustomRightDrawer';
|
import CustomRightDrawer from '@fastgpt/web/components/common/MyDrawer/CustomRightDrawer';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { Box, Button, Flex, Input } from '@chakra-ui/react';
|
import { Box, BoxProps, Button, Flex, Input } from '@chakra-ui/react';
|
||||||
import { useContextSelector } from 'use-context-selector';
|
import { useContextSelector } from 'use-context-selector';
|
||||||
import { AppContext } from './context';
|
import { AppContext } from './context';
|
||||||
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
|
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
|
||||||
import { SaveSnapshotParams, SnapshotsType } from './WorkflowComponents/context';
|
import type { WorkflowSnapshotsType } from './WorkflowComponents/context';
|
||||||
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
|
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
|
||||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||||
import Tag from '@fastgpt/web/components/common/Tag';
|
import Tag from '@fastgpt/web/components/common/Tag';
|
||||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||||
import MyPopover from '@fastgpt/web/components/common/MyPopover';
|
import MyPopover from '@fastgpt/web/components/common/MyPopover';
|
||||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||||
import { storeEdgesRenderEdge, storeNode2FlowNode } from '@/web/core/workflow/utils';
|
|
||||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||||
import type { versionListResponse } from '@/pages/api/core/app/version/list';
|
import type { AppVersionSchemaType, VersionListItemType } from '@fastgpt/global/core/app/version';
|
||||||
|
import type { SimpleAppSnapshotType } from './SimpleApp/useSnapshots';
|
||||||
|
|
||||||
const PublishHistoriesSlider = ({
|
const PublishHistoriesSlider = <T extends SimpleAppSnapshotType | WorkflowSnapshotsType>({
|
||||||
onClose,
|
onClose,
|
||||||
past,
|
past,
|
||||||
saveSnapshot,
|
onSwitchTmpVersion,
|
||||||
resetSnapshot,
|
onSwitchCloudVersion,
|
||||||
top,
|
positionStyles
|
||||||
bottom
|
|
||||||
}: {
|
}: {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
past: SnapshotsType[];
|
past: T[];
|
||||||
saveSnapshot: (params: SaveSnapshotParams) => Promise<boolean>;
|
onSwitchTmpVersion: (params: T, customTitle: string) => void;
|
||||||
resetSnapshot: (state: SnapshotsType) => void;
|
onSwitchCloudVersion: (appVersion: AppVersionSchemaType) => void;
|
||||||
top?: string | number;
|
positionStyles?: BoxProps;
|
||||||
bottom?: string | number;
|
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [currentTab, setCurrentTab] = useState<'myEdit' | 'teamCloud'>('myEdit');
|
const [currentTab, setCurrentTab] = useState<'myEdit' | 'teamCloud'>('myEdit');
|
||||||
@ -69,29 +67,26 @@ const PublishHistoriesSlider = ({
|
|||||||
px={0}
|
px={0}
|
||||||
showMask={false}
|
showMask={false}
|
||||||
overflow={'unset'}
|
overflow={'unset'}
|
||||||
top={top}
|
{...positionStyles}
|
||||||
bottom={bottom}
|
|
||||||
>
|
>
|
||||||
{currentTab === 'myEdit' ? (
|
{currentTab === 'myEdit' ? (
|
||||||
<MyEdit past={past} saveSnapshot={saveSnapshot} resetSnapshot={resetSnapshot} />
|
<MyEdit past={past} onSwitchTmpVersion={onSwitchTmpVersion} />
|
||||||
) : (
|
) : (
|
||||||
<TeamCloud saveSnapshot={saveSnapshot} resetSnapshot={resetSnapshot} />
|
<TeamCloud onSwitchCloudVersion={onSwitchCloudVersion} />
|
||||||
)}
|
)}
|
||||||
</CustomRightDrawer>
|
</CustomRightDrawer>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default React.memo(PublishHistoriesSlider);
|
export default PublishHistoriesSlider;
|
||||||
|
|
||||||
const MyEdit = ({
|
const MyEdit = <T extends SimpleAppSnapshotType | WorkflowSnapshotsType>({
|
||||||
past,
|
past,
|
||||||
saveSnapshot,
|
onSwitchTmpVersion
|
||||||
resetSnapshot
|
|
||||||
}: {
|
}: {
|
||||||
past: SnapshotsType[];
|
past: T[];
|
||||||
saveSnapshot: (params: SaveSnapshotParams) => Promise<boolean>;
|
onSwitchTmpVersion: (params: T, customTitle: string) => void;
|
||||||
resetSnapshot: (state: SnapshotsType) => void;
|
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@ -107,24 +102,14 @@ const MyEdit = ({
|
|||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const initialSnapshot = past[past.length - 1];
|
const initialSnapshot = past[past.length - 1];
|
||||||
|
|
||||||
const res = await saveSnapshot({
|
onSwitchTmpVersion(initialSnapshot, t(`app:version_initial_copy`));
|
||||||
pastNodes: initialSnapshot.nodes,
|
|
||||||
pastEdges: initialSnapshot.edges,
|
|
||||||
chatConfig: initialSnapshot.chatConfig,
|
|
||||||
customTitle: t(`app:app.version_initial_copy`)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res) {
|
|
||||||
resetSnapshot(initialSnapshot);
|
|
||||||
}
|
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: t('workflow:workflow.Switch_success'),
|
title: t('workflow:workflow.Switch_success'),
|
||||||
status: 'success'
|
status: 'success'
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('app:app.version_back')}
|
{t('app:version_back')}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
@ -142,17 +127,8 @@ const MyEdit = ({
|
|||||||
_hover={{
|
_hover={{
|
||||||
bg: 'primary.50'
|
bg: 'primary.50'
|
||||||
}}
|
}}
|
||||||
onClick={async () => {
|
onClick={() => {
|
||||||
const res = await saveSnapshot({
|
onSwitchTmpVersion(item, `${t('app:version_copy')}-${item.title}`);
|
||||||
pastNodes: item.nodes,
|
|
||||||
pastEdges: item.edges,
|
|
||||||
chatConfig: item.chatConfig,
|
|
||||||
customTitle: `${t('app:app.version_copy')}-${item.title}`
|
|
||||||
});
|
|
||||||
if (res) {
|
|
||||||
resetSnapshot(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: t('workflow:workflow.Switch_success'),
|
title: t('workflow:workflow.Switch_success'),
|
||||||
status: 'success'
|
status: 'success'
|
||||||
@ -201,18 +177,16 @@ const MyEdit = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const TeamCloud = ({
|
const TeamCloud = ({
|
||||||
saveSnapshot,
|
onSwitchCloudVersion
|
||||||
resetSnapshot
|
|
||||||
}: {
|
}: {
|
||||||
saveSnapshot: (params: SaveSnapshotParams) => Promise<boolean>;
|
onSwitchCloudVersion: (appVersion: AppVersionSchemaType) => void;
|
||||||
resetSnapshot: (state: SnapshotsType) => void;
|
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||||
const { loadAndGetTeamMembers } = useUserStore();
|
const { loadAndGetTeamMembers } = useUserStore();
|
||||||
const { feConfigs } = useSystemStore();
|
const { feConfigs } = useSystemStore();
|
||||||
|
|
||||||
const { scrollDataList, ScrollList, isLoading, fetchData } = useVirtualScrollPagination(
|
const { scrollDataList, ScrollList, isLoading, fetchData, setData } = useVirtualScrollPagination(
|
||||||
getWorkflowVersionList,
|
getWorkflowVersionList,
|
||||||
{
|
{
|
||||||
itemHeight: 40,
|
itemHeight: 40,
|
||||||
@ -230,30 +204,15 @@ const TeamCloud = ({
|
|||||||
const [editIndex, setEditIndex] = useState<number | undefined>(undefined);
|
const [editIndex, setEditIndex] = useState<number | undefined>(undefined);
|
||||||
const [hoveredIndex, setHoveredIndex] = useState<number | undefined>(undefined);
|
const [hoveredIndex, setHoveredIndex] = useState<number | undefined>(undefined);
|
||||||
|
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const { runAsync: onChangeVersion, loading: isLoadingVersion } = useRequest2(
|
const { runAsync: onChangeVersion, loading: isLoadingVersion } = useRequest2(
|
||||||
async (versionItem: versionListResponse) => {
|
async (versionItem: VersionListItemType) => {
|
||||||
const versionDetail = await getAppVersionDetail(versionItem._id, versionItem.appId);
|
const versionDetail = await getAppVersionDetail(versionItem._id, versionItem.appId);
|
||||||
|
|
||||||
if (!versionDetail) return;
|
if (!versionDetail) return;
|
||||||
|
|
||||||
const state = {
|
onSwitchCloudVersion(versionDetail);
|
||||||
nodes: versionDetail.nodes?.map((item) => storeNode2FlowNode({ item, t })),
|
|
||||||
edges: versionDetail.edges?.map((item) => storeEdgesRenderEdge({ edge: item })),
|
|
||||||
title: versionItem.versionName,
|
|
||||||
chatConfig: versionDetail.chatConfig
|
|
||||||
};
|
|
||||||
|
|
||||||
await saveSnapshot({
|
|
||||||
pastNodes: state.nodes,
|
|
||||||
pastEdges: state.edges,
|
|
||||||
chatConfig: state.chatConfig,
|
|
||||||
customTitle: `${t('app:app.version_copy')}-${state.title}`
|
|
||||||
});
|
|
||||||
|
|
||||||
resetSnapshot(state);
|
|
||||||
toast({
|
toast({
|
||||||
title: t('workflow:workflow.Switch_success'),
|
title: t('workflow:workflow.Switch_success'),
|
||||||
status: 'success'
|
status: 'success'
|
||||||
@ -261,6 +220,22 @@ const TeamCloud = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { runAsync: onUpdateVersion, loading: isEditing } = useRequest2(
|
||||||
|
async (item: VersionListItemType, name: string) => {
|
||||||
|
await updateAppVersion({
|
||||||
|
appId: item.appId,
|
||||||
|
versionName: name,
|
||||||
|
versionId: item._id
|
||||||
|
});
|
||||||
|
setData((state) =>
|
||||||
|
state.map((version) =>
|
||||||
|
version._id === item._id ? { ...version, versionName: name } : version
|
||||||
|
)
|
||||||
|
);
|
||||||
|
setEditIndex(undefined);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollList isLoading={isLoading || isLoadingVersion} flex={'1 0 0'} px={5}>
|
<ScrollList isLoading={isLoading || isLoadingVersion} flex={'1 0 0'} px={5}>
|
||||||
{scrollDataList.map((data, index) => {
|
{scrollDataList.map((data, index) => {
|
||||||
@ -361,16 +336,12 @@ const TeamCloud = ({
|
|||||||
h={8}
|
h={8}
|
||||||
defaultValue={item.versionName || formatTime2YMDHMS(item.time)}
|
defaultValue={item.versionName || formatTime2YMDHMS(item.time)}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
onBlur={async (e) => {
|
onBlur={(e) => onUpdateVersion(item, e.target.value)}
|
||||||
setIsEditing(true);
|
onKeyDown={(e) => {
|
||||||
await updateAppVersion({
|
if (e.key === 'Enter') {
|
||||||
appId: item.appId,
|
// @ts-ignore
|
||||||
versionName: e.target.value,
|
onUpdateVersion(item, e.target.value);
|
||||||
versionId: item._id
|
}
|
||||||
});
|
|
||||||
await fetchData();
|
|
||||||
setEditIndex(undefined);
|
|
||||||
setIsEditing(false);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</MyBox>
|
</MyBox>
|
||||||
|
|||||||
@ -15,11 +15,8 @@ import { cardStyles } from '../constants';
|
|||||||
|
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||||
import { storeNode2FlowNode } from '@/web/core/workflow/utils';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { uiWorkflow2StoreWorkflow } from '../WorkflowComponents/utils';
|
import { onSaveSnapshotFnType, SimpleAppSnapshotType } from './useSnapshots';
|
||||||
import { SnapshotsType } from '../WorkflowComponents/context';
|
|
||||||
import { SaveSnapshotFnType } from './useSnapshots';
|
|
||||||
|
|
||||||
const Edit = ({
|
const Edit = ({
|
||||||
appForm,
|
appForm,
|
||||||
@ -29,8 +26,8 @@ const Edit = ({
|
|||||||
}: {
|
}: {
|
||||||
appForm: AppSimpleEditFormType;
|
appForm: AppSimpleEditFormType;
|
||||||
setAppForm: React.Dispatch<React.SetStateAction<AppSimpleEditFormType>>;
|
setAppForm: React.Dispatch<React.SetStateAction<AppSimpleEditFormType>>;
|
||||||
past: SnapshotsType[];
|
past: SimpleAppSnapshotType[];
|
||||||
saveSnapshot: SaveSnapshotFnType;
|
saveSnapshot: onSaveSnapshotFnType;
|
||||||
}) => {
|
}) => {
|
||||||
const { isPc } = useSystem();
|
const { isPc } = useSystem();
|
||||||
const { loadAllDatasets } = useDatasetStore();
|
const { loadAllDatasets } = useDatasetStore();
|
||||||
@ -43,26 +40,25 @@ const Edit = ({
|
|||||||
loadAllDatasets();
|
loadAllDatasets();
|
||||||
|
|
||||||
// Get the latest snapshot
|
// Get the latest snapshot
|
||||||
if (past.length > 0) {
|
if (past?.[0]?.appForm) {
|
||||||
const storeWorkflow = uiWorkflow2StoreWorkflow(past[0]);
|
return setAppForm(past[0].appForm);
|
||||||
const currentAppForm = appWorkflow2Form({ ...storeWorkflow, chatConfig: past[0].chatConfig });
|
|
||||||
|
|
||||||
return setAppForm(currentAppForm);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the first snapshot
|
const appForm = appWorkflow2Form({
|
||||||
saveSnapshot({
|
nodes: appDetail.modules,
|
||||||
pastNodes: appDetail.modules?.map((item) => storeNode2FlowNode({ item, t })),
|
chatConfig: appDetail.chatConfig
|
||||||
chatConfig: appDetail.chatConfig,
|
|
||||||
isSaved: true
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setAppForm(
|
// Set the first snapshot
|
||||||
appWorkflow2Form({
|
if (past.length === 0) {
|
||||||
nodes: appDetail.modules,
|
saveSnapshot({
|
||||||
chatConfig: appDetail.chatConfig
|
appForm,
|
||||||
})
|
title: t('app:initial_form'),
|
||||||
);
|
isSaved: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setAppForm(appForm);
|
||||||
|
|
||||||
if (appDetail.version !== 'v2') {
|
if (appDetail.version !== 'v2') {
|
||||||
setAppForm(
|
setAppForm(
|
||||||
|
|||||||
@ -20,32 +20,30 @@ import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
|
|||||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||||
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
|
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
|
||||||
import SaveButton from '../Workflow/components/SaveButton';
|
import SaveButton from '../Workflow/components/SaveButton';
|
||||||
import dynamic from 'next/dynamic';
|
import { useBoolean, useDebounceEffect } from 'ahooks';
|
||||||
import { useDebounceEffect } from 'ahooks';
|
|
||||||
import { InitProps, SnapshotsType } from '../WorkflowComponents/context';
|
|
||||||
import { appWorkflow2Form } from '@fastgpt/global/core/app/utils';
|
import { appWorkflow2Form } from '@fastgpt/global/core/app/utils';
|
||||||
import {
|
import {
|
||||||
compareSnapshot,
|
compareSimpleAppSnapshot,
|
||||||
storeEdgesRenderEdge,
|
onSaveSnapshotFnType,
|
||||||
storeNode2FlowNode
|
SimpleAppSnapshotType
|
||||||
} from '@/web/core/workflow/utils';
|
} from './useSnapshots';
|
||||||
import { uiWorkflow2StoreWorkflow } from '../WorkflowComponents/utils';
|
import PublishHistories from '../PublishHistoriesSlider';
|
||||||
import { SaveSnapshotFnType } from './useSnapshots';
|
import { AppVersionSchemaType } from '@fastgpt/global/core/app/version';
|
||||||
|
|
||||||
const PublishHistories = dynamic(() => import('../PublishHistoriesSlider'));
|
|
||||||
|
|
||||||
const Header = ({
|
const Header = ({
|
||||||
|
forbiddenSaveSnapshot,
|
||||||
appForm,
|
appForm,
|
||||||
setAppForm,
|
setAppForm,
|
||||||
past,
|
past,
|
||||||
setPast,
|
setPast,
|
||||||
saveSnapshot
|
saveSnapshot
|
||||||
}: {
|
}: {
|
||||||
|
forbiddenSaveSnapshot: React.MutableRefObject<boolean>;
|
||||||
appForm: AppSimpleEditFormType;
|
appForm: AppSimpleEditFormType;
|
||||||
setAppForm: (form: AppSimpleEditFormType) => void;
|
setAppForm: (form: AppSimpleEditFormType) => void;
|
||||||
past: SnapshotsType[];
|
past: SimpleAppSnapshotType[];
|
||||||
setPast: (value: React.SetStateAction<SnapshotsType[]>) => void;
|
setPast: (value: React.SetStateAction<SimpleAppSnapshotType[]>) => void;
|
||||||
saveSnapshot: SaveSnapshotFnType;
|
saveSnapshot: onSaveSnapshotFnType;
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isPc } = useSystem();
|
const { isPc } = useSystem();
|
||||||
@ -101,30 +99,43 @@ const Header = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const [historiesDefaultData, setHistoriesDefaultData] = useState<InitProps>();
|
const [isShowHistories, { setTrue: setIsShowHistories, setFalse: closeHistories }] =
|
||||||
|
useBoolean(false);
|
||||||
|
|
||||||
const resetSnapshot = useCallback(
|
const onSwitchTmpVersion = useCallback(
|
||||||
(data: SnapshotsType) => {
|
(data: SimpleAppSnapshotType, customTitle: string) => {
|
||||||
const storeWorkflow = uiWorkflow2StoreWorkflow(data);
|
setAppForm(data.appForm);
|
||||||
const currentAppForm = appWorkflow2Form({ ...storeWorkflow, chatConfig: data.chatConfig });
|
|
||||||
|
|
||||||
setAppForm(currentAppForm);
|
// Remove multiple "copy-"
|
||||||
},
|
const copyText = t('app:version_copy');
|
||||||
[setAppForm]
|
const regex = new RegExp(`(${copyText}-)\\1+`, 'g');
|
||||||
);
|
const title = customTitle.replace(regex, `$1`);
|
||||||
|
|
||||||
// Save snapshot to local
|
return saveSnapshot({
|
||||||
useDebounceEffect(
|
appForm: data.appForm,
|
||||||
() => {
|
title
|
||||||
const data = form2AppWorkflow(appForm, t);
|
|
||||||
|
|
||||||
saveSnapshot({
|
|
||||||
pastNodes: data.nodes?.map((item) => storeNode2FlowNode({ item, t })),
|
|
||||||
chatConfig: data.chatConfig
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[appForm],
|
[saveSnapshot, setAppForm, t]
|
||||||
{ wait: 500 }
|
);
|
||||||
|
const onSwitchCloudVersion = useCallback(
|
||||||
|
(appVersion: AppVersionSchemaType) => {
|
||||||
|
const appForm = appWorkflow2Form({
|
||||||
|
nodes: appVersion.nodes,
|
||||||
|
chatConfig: appVersion.chatConfig
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = saveSnapshot({
|
||||||
|
appForm,
|
||||||
|
title: `${t('app:version_copy')}-${appVersion.versionName}`
|
||||||
|
});
|
||||||
|
forbiddenSaveSnapshot.current = true;
|
||||||
|
|
||||||
|
setAppForm(appForm);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
[forbiddenSaveSnapshot, saveSnapshot, setAppForm, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if the workflow is published
|
// Check if the workflow is published
|
||||||
@ -132,20 +143,7 @@ const Header = ({
|
|||||||
useDebounceEffect(
|
useDebounceEffect(
|
||||||
() => {
|
() => {
|
||||||
const savedSnapshot = past.find((snapshot) => snapshot.isSaved);
|
const savedSnapshot = past.find((snapshot) => snapshot.isSaved);
|
||||||
const editFormData = form2AppWorkflow(appForm, t);
|
const val = compareSimpleAppSnapshot(savedSnapshot?.appForm, appForm);
|
||||||
console.log(savedSnapshot?.nodes, editFormData.chatConfig);
|
|
||||||
const val = compareSnapshot(
|
|
||||||
{
|
|
||||||
nodes: savedSnapshot?.nodes,
|
|
||||||
edges: [],
|
|
||||||
chatConfig: savedSnapshot?.chatConfig
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nodes: editFormData.nodes?.map((item) => storeNode2FlowNode({ item, t })),
|
|
||||||
edges: [],
|
|
||||||
chatConfig: editFormData.chatConfig
|
|
||||||
}
|
|
||||||
);
|
|
||||||
setIsPublished(val);
|
setIsPublished(val);
|
||||||
},
|
},
|
||||||
[past, allDatasets],
|
[past, allDatasets],
|
||||||
@ -176,7 +174,7 @@ const Header = ({
|
|||||||
)}
|
)}
|
||||||
{currentTab === TabEnum.appEdit && (
|
{currentTab === TabEnum.appEdit && (
|
||||||
<Flex alignItems={'center'}>
|
<Flex alignItems={'center'}>
|
||||||
{!historiesDefaultData && (
|
{!isShowHistories && (
|
||||||
<>
|
<>
|
||||||
{isPc && (
|
{isPc && (
|
||||||
<MyTag
|
<MyTag
|
||||||
@ -204,14 +202,7 @@ const Header = ({
|
|||||||
size={'sm'}
|
size={'sm'}
|
||||||
w={'30px'}
|
w={'30px'}
|
||||||
variant={'whitePrimary'}
|
variant={'whitePrimary'}
|
||||||
onClick={() => {
|
onClick={setIsShowHistories}
|
||||||
const { nodes, edges } = form2AppWorkflow(appForm, t);
|
|
||||||
setHistoriesDefaultData({
|
|
||||||
nodes,
|
|
||||||
edges,
|
|
||||||
chatConfig: appForm.chatConfig
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<SaveButton isLoading={loading} onClickSave={onClickSave} />
|
<SaveButton isLoading={loading} onClickSave={onClickSave} />
|
||||||
</>
|
</>
|
||||||
@ -220,16 +211,16 @@ const Header = ({
|
|||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{historiesDefaultData && currentTab === TabEnum.appEdit && (
|
{isShowHistories && currentTab === TabEnum.appEdit && (
|
||||||
<PublishHistories
|
<PublishHistories<SimpleAppSnapshotType>
|
||||||
onClose={() => {
|
onClose={closeHistories}
|
||||||
setHistoriesDefaultData(undefined);
|
|
||||||
}}
|
|
||||||
past={past}
|
past={past}
|
||||||
saveSnapshot={saveSnapshot}
|
onSwitchTmpVersion={onSwitchTmpVersion}
|
||||||
resetSnapshot={resetSnapshot}
|
onSwitchCloudVersion={onSwitchCloudVersion}
|
||||||
top={14}
|
positionStyles={{
|
||||||
bottom={3}
|
top: 14,
|
||||||
|
bottom: 3
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@ -9,7 +9,8 @@ import dynamic from 'next/dynamic';
|
|||||||
import { Box, Flex } from '@chakra-ui/react';
|
import { Box, Flex } from '@chakra-ui/react';
|
||||||
import { useBeforeunload } from '@fastgpt/web/hooks/useBeforeunload';
|
import { useBeforeunload } from '@fastgpt/web/hooks/useBeforeunload';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import useSnapshots from './useSnapshots';
|
import { useSimpleAppSnapshots } from './useSnapshots';
|
||||||
|
import { useDebounceEffect } from 'ahooks';
|
||||||
|
|
||||||
const Logs = dynamic(() => import('../Logs/index'));
|
const Logs = dynamic(() => import('../Logs/index'));
|
||||||
const PublishChannel = dynamic(() => import('../Publish'));
|
const PublishChannel = dynamic(() => import('../Publish'));
|
||||||
@ -17,9 +18,21 @@ const PublishChannel = dynamic(() => import('../Publish'));
|
|||||||
const SimpleEdit = () => {
|
const SimpleEdit = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { currentTab, appDetail } = useContextSelector(AppContext, (v) => v);
|
const { currentTab, appDetail } = useContextSelector(AppContext, (v) => v);
|
||||||
const { past, setPast, saveSnapshot } = useSnapshots(appDetail._id);
|
const { forbiddenSaveSnapshot, past, setPast, saveSnapshot } = useSimpleAppSnapshots(
|
||||||
|
appDetail._id
|
||||||
|
);
|
||||||
|
|
||||||
const [appForm, setAppForm] = useState(getDefaultAppForm());
|
const [appForm, setAppForm] = useState(getDefaultAppForm());
|
||||||
|
// Save snapshot to local
|
||||||
|
useDebounceEffect(
|
||||||
|
() => {
|
||||||
|
saveSnapshot({
|
||||||
|
appForm
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[appForm],
|
||||||
|
{ wait: 500 }
|
||||||
|
);
|
||||||
|
|
||||||
useBeforeunload({
|
useBeforeunload({
|
||||||
tip: t('common:core.common.tip.leave page')
|
tip: t('common:core.common.tip.leave page')
|
||||||
@ -29,6 +42,7 @@ const SimpleEdit = () => {
|
|||||||
<Flex h={'100%'} flexDirection={'column'} px={[3, 0]} pr={[3, 3]}>
|
<Flex h={'100%'} flexDirection={'column'} px={[3, 0]} pr={[3, 3]}>
|
||||||
<Header
|
<Header
|
||||||
appForm={appForm}
|
appForm={appForm}
|
||||||
|
forbiddenSaveSnapshot={forbiddenSaveSnapshot}
|
||||||
setAppForm={setAppForm}
|
setAppForm={setAppForm}
|
||||||
past={past}
|
past={past}
|
||||||
setPast={setPast}
|
setPast={setPast}
|
||||||
|
|||||||
@ -1,56 +1,86 @@
|
|||||||
import { useLocalStorageState, useMemoizedFn } from 'ahooks';
|
import { useLocalStorageState, useMemoizedFn } from 'ahooks';
|
||||||
import { SaveSnapshotParams, SnapshotsType } from '../WorkflowComponents/context';
|
import { SetStateAction, useEffect, useRef } from 'react';
|
||||||
import { SetStateAction, useEffect } from 'react';
|
|
||||||
import { compareSnapshot } from '@/web/core/workflow/utils';
|
|
||||||
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
|
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
|
||||||
import { AppChatConfigType } from '@fastgpt/global/core/app/type';
|
import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
|
||||||
import { Node } from 'reactflow';
|
import { isEqual } from 'lodash';
|
||||||
|
|
||||||
export type SaveSnapshotFnType = (
|
export type SimpleAppSnapshotType = {
|
||||||
props: SaveSnapshotParams & {
|
appForm: AppSimpleEditFormType;
|
||||||
isSaved?: boolean;
|
title: string;
|
||||||
|
isSaved?: boolean;
|
||||||
|
};
|
||||||
|
export type onSaveSnapshotFnType = (props: {
|
||||||
|
appForm: AppSimpleEditFormType;
|
||||||
|
title?: string;
|
||||||
|
isSaved?: boolean;
|
||||||
|
}) => Promise<boolean>;
|
||||||
|
|
||||||
|
export const compareSimpleAppSnapshot = (
|
||||||
|
appForm1?: AppSimpleEditFormType,
|
||||||
|
appForm2?: AppSimpleEditFormType
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
appForm1?.chatConfig &&
|
||||||
|
appForm2?.chatConfig &&
|
||||||
|
!isEqual(
|
||||||
|
{
|
||||||
|
welcomeText: appForm1.chatConfig?.welcomeText || '',
|
||||||
|
variables: appForm1.chatConfig?.variables || [],
|
||||||
|
questionGuide: appForm1.chatConfig?.questionGuide || false,
|
||||||
|
ttsConfig: appForm1.chatConfig?.ttsConfig || undefined,
|
||||||
|
whisperConfig: appForm1.chatConfig?.whisperConfig || undefined,
|
||||||
|
scheduledTriggerConfig: appForm1.chatConfig?.scheduledTriggerConfig || undefined,
|
||||||
|
chatInputGuide: appForm1.chatConfig?.chatInputGuide || undefined,
|
||||||
|
fileSelectConfig: appForm1.chatConfig?.fileSelectConfig || undefined,
|
||||||
|
instruction: appForm1.chatConfig?.instruction || ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
welcomeText: appForm2.chatConfig?.welcomeText || '',
|
||||||
|
variables: appForm2.chatConfig?.variables || [],
|
||||||
|
questionGuide: appForm2.chatConfig?.questionGuide || false,
|
||||||
|
ttsConfig: appForm2.chatConfig?.ttsConfig || undefined,
|
||||||
|
whisperConfig: appForm2.chatConfig?.whisperConfig || undefined,
|
||||||
|
scheduledTriggerConfig: appForm2.chatConfig?.scheduledTriggerConfig || undefined,
|
||||||
|
chatInputGuide: appForm2.chatConfig?.chatInputGuide || undefined,
|
||||||
|
fileSelectConfig: appForm2.chatConfig?.fileSelectConfig || undefined,
|
||||||
|
instruction: appForm2.chatConfig?.instruction || ''
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
console.log('chatConfig not equal');
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
) => Promise<boolean>;
|
|
||||||
|
|
||||||
const useSnapshots = (appId: string) => {
|
return isEqual(appForm1, appForm2);
|
||||||
const [past, setPast] = useLocalStorageState<SnapshotsType[]>(`${appId}-past-simple`, {
|
};
|
||||||
defaultValue: [],
|
|
||||||
listenStorageChange: true
|
|
||||||
}) as [SnapshotsType[], (value: SetStateAction<SnapshotsType[]>) => void];
|
|
||||||
|
|
||||||
const saveSnapshot: SaveSnapshotFnType = useMemoizedFn(
|
export const useSimpleAppSnapshots = (appId: string) => {
|
||||||
async ({ pastNodes, chatConfig, customTitle, isSaved }) => {
|
const forbiddenSaveSnapshot = useRef(false);
|
||||||
if (!pastNodes) return false;
|
const [past, setPast] = useLocalStorageState<SimpleAppSnapshotType[]>(`${appId}-past-simple`, {
|
||||||
|
defaultValue: []
|
||||||
|
}) as [SimpleAppSnapshotType[], (value: SetStateAction<SimpleAppSnapshotType[]>) => void];
|
||||||
|
|
||||||
const pastState = past[0];
|
const saveSnapshot: onSaveSnapshotFnType = useMemoizedFn(async ({ appForm, title, isSaved }) => {
|
||||||
|
if (forbiddenSaveSnapshot.current) {
|
||||||
const isPastEqual = compareSnapshot(
|
forbiddenSaveSnapshot.current = false;
|
||||||
{
|
return false;
|
||||||
nodes: pastNodes,
|
|
||||||
edges: [],
|
|
||||||
chatConfig: chatConfig
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nodes: pastState?.nodes,
|
|
||||||
edges: pastState?.edges,
|
|
||||||
chatConfig: pastState?.chatConfig
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (isPastEqual) return false;
|
|
||||||
|
|
||||||
setPast((past) => [
|
|
||||||
{
|
|
||||||
nodes: pastNodes,
|
|
||||||
edges: [],
|
|
||||||
title: customTitle || formatTime2YMDHMS(new Date()),
|
|
||||||
chatConfig,
|
|
||||||
isSaved
|
|
||||||
},
|
|
||||||
...past.slice(0, 199)
|
|
||||||
]);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
const pastState = past[0];
|
||||||
|
|
||||||
|
const isPastEqual = compareSimpleAppSnapshot(pastState?.appForm, appForm);
|
||||||
|
if (isPastEqual) return false;
|
||||||
|
|
||||||
|
setPast((past) => [
|
||||||
|
{
|
||||||
|
appForm,
|
||||||
|
title: title || formatTime2YMDHMS(new Date()),
|
||||||
|
isSaved
|
||||||
|
},
|
||||||
|
...past.slice(0, 199)
|
||||||
|
]);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
// remove other app's snapshot
|
// remove other app's snapshot
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -64,7 +94,7 @@ const useSnapshots = (appId: string) => {
|
|||||||
});
|
});
|
||||||
}, [appId]);
|
}, [appId]);
|
||||||
|
|
||||||
return { past, setPast, saveSnapshot };
|
return { forbiddenSaveSnapshot, past, setPast, saveSnapshot };
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useSnapshots;
|
export default <></>;
|
||||||
|
|||||||
@ -10,17 +10,15 @@ import {
|
|||||||
useDisclosure
|
useDisclosure
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import dynamic from 'next/dynamic';
|
|
||||||
|
|
||||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||||
import { useContextSelector } from 'use-context-selector';
|
import { useContextSelector } from 'use-context-selector';
|
||||||
import { WorkflowContext, getWorkflowStore } from '../WorkflowComponents/context';
|
import { WorkflowContext } from '../WorkflowComponents/context';
|
||||||
import { AppContext, TabEnum } from '../context';
|
import { AppContext, TabEnum } from '../context';
|
||||||
import RouteTab from '../RouteTab';
|
import RouteTab from '../RouteTab';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
|
|
||||||
import AppCard from '../WorkflowComponents/AppCard';
|
import AppCard from '../WorkflowComponents/AppCard';
|
||||||
import { uiWorkflow2StoreWorkflow } from '../WorkflowComponents/utils';
|
|
||||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||||
@ -30,8 +28,7 @@ import { useToast } from '@fastgpt/web/hooks/useToast';
|
|||||||
import { useDebounceEffect } from 'ahooks';
|
import { useDebounceEffect } from 'ahooks';
|
||||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||||
import SaveButton from './components/SaveButton';
|
import SaveButton from './components/SaveButton';
|
||||||
|
import PublishHistories from '../PublishHistoriesSlider';
|
||||||
const PublishHistories = dynamic(() => import('../PublishHistoriesSlider'));
|
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -51,15 +48,15 @@ const Header = () => {
|
|||||||
flowData2StoreData,
|
flowData2StoreData,
|
||||||
flowData2StoreDataAndCheck,
|
flowData2StoreDataAndCheck,
|
||||||
setWorkflowTestData,
|
setWorkflowTestData,
|
||||||
setHistoriesDefaultData,
|
setShowHistoryModal,
|
||||||
historiesDefaultData,
|
showHistoryModal,
|
||||||
nodes,
|
nodes,
|
||||||
edges,
|
edges,
|
||||||
past,
|
past,
|
||||||
future,
|
future,
|
||||||
setPast,
|
setPast,
|
||||||
saveSnapshot,
|
onSwitchTmpVersion,
|
||||||
resetSnapshot
|
onSwitchCloudVersion
|
||||||
} = useContextSelector(WorkflowContext, (v) => v);
|
} = useContextSelector(WorkflowContext, (v) => v);
|
||||||
|
|
||||||
const { lastAppListRouteType } = useSystemStore();
|
const { lastAppListRouteType } = useSystemStore();
|
||||||
@ -189,21 +186,15 @@ const Header = () => {
|
|||||||
|
|
||||||
{currentTab === TabEnum.appEdit && (
|
{currentTab === TabEnum.appEdit && (
|
||||||
<HStack flexDirection={['column', 'row']} spacing={[2, 3]}>
|
<HStack flexDirection={['column', 'row']} spacing={[2, 3]}>
|
||||||
{!historiesDefaultData && (
|
{!showHistoryModal && (
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<MyIcon name={'history'} w={'18px'} />}
|
icon={<MyIcon name={'history'} w={'18px'} />}
|
||||||
aria-label={''}
|
aria-label={''}
|
||||||
size={'sm'}
|
size={'sm'}
|
||||||
w={'30px'}
|
w={'30px'}
|
||||||
variant={'whitePrimary'}
|
variant={'whitePrimary'}
|
||||||
onClick={async () => {
|
onClick={() => {
|
||||||
const { nodes, edges } = uiWorkflow2StoreWorkflow(await getWorkflowStore());
|
setShowHistoryModal(true);
|
||||||
|
|
||||||
setHistoriesDefaultData({
|
|
||||||
nodes,
|
|
||||||
edges,
|
|
||||||
chatConfig: appDetail.chatConfig
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -220,7 +211,7 @@ const Header = () => {
|
|||||||
>
|
>
|
||||||
{t('common:core.workflow.Run')}
|
{t('common:core.workflow.Run')}
|
||||||
</Button>
|
</Button>
|
||||||
{!historiesDefaultData && (
|
{!showHistoryModal && (
|
||||||
<SaveButton
|
<SaveButton
|
||||||
isLoading={loading}
|
isLoading={loading}
|
||||||
onClickSave={onClickSave}
|
onClickSave={onClickSave}
|
||||||
@ -230,48 +221,6 @@ const Header = () => {
|
|||||||
</HStack>
|
</HStack>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
{historiesDefaultData && isV2Workflow && currentTab === TabEnum.appEdit && (
|
|
||||||
<PublishHistories
|
|
||||||
onClose={() => {
|
|
||||||
setHistoriesDefaultData(undefined);
|
|
||||||
}}
|
|
||||||
past={past}
|
|
||||||
saveSnapshot={saveSnapshot}
|
|
||||||
resetSnapshot={resetSnapshot}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<MyModal
|
|
||||||
isOpen={isOpenBackConfirm}
|
|
||||||
onClose={onCloseBackConfirm}
|
|
||||||
iconSrc="common/warn"
|
|
||||||
title={t('common:common.Exit')}
|
|
||||||
w={'400px'}
|
|
||||||
>
|
|
||||||
<ModalBody>
|
|
||||||
<Box>{t('workflow:workflow.exit_tips')}</Box>
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter gap={3}>
|
|
||||||
<Button variant={'whiteDanger'} onClick={onBack}>
|
|
||||||
{t('common:common.Exit Directly')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
isLoading={loading}
|
|
||||||
onClick={async () => {
|
|
||||||
await onClickSave({});
|
|
||||||
onCloseBackConfirm();
|
|
||||||
onBack();
|
|
||||||
toast({
|
|
||||||
status: 'success',
|
|
||||||
title: t('app:saved_success'),
|
|
||||||
position: 'top-right'
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('common:common.Save_and_exit')}
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</MyModal>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}, [
|
}, [
|
||||||
@ -281,23 +230,62 @@ const Header = () => {
|
|||||||
onBack,
|
onBack,
|
||||||
onOpenBackConfirm,
|
onOpenBackConfirm,
|
||||||
isV2Workflow,
|
isV2Workflow,
|
||||||
historiesDefaultData,
|
showHistoryModal,
|
||||||
t,
|
t,
|
||||||
loading,
|
loading,
|
||||||
onClickSave,
|
onClickSave,
|
||||||
flowData2StoreDataAndCheck,
|
flowData2StoreDataAndCheck,
|
||||||
past,
|
setShowHistoryModal,
|
||||||
saveSnapshot,
|
setWorkflowTestData
|
||||||
resetSnapshot,
|
|
||||||
isOpenBackConfirm,
|
|
||||||
onCloseBackConfirm,
|
|
||||||
setHistoriesDefaultData,
|
|
||||||
appDetail.chatConfig,
|
|
||||||
setWorkflowTestData,
|
|
||||||
toast
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return Render;
|
return (
|
||||||
|
<>
|
||||||
|
{Render}
|
||||||
|
{showHistoryModal && isV2Workflow && currentTab === TabEnum.appEdit && (
|
||||||
|
<PublishHistories
|
||||||
|
onClose={() => {
|
||||||
|
setShowHistoryModal(false);
|
||||||
|
}}
|
||||||
|
past={past}
|
||||||
|
onSwitchCloudVersion={onSwitchCloudVersion}
|
||||||
|
onSwitchTmpVersion={onSwitchTmpVersion}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<MyModal
|
||||||
|
isOpen={isOpenBackConfirm}
|
||||||
|
onClose={onCloseBackConfirm}
|
||||||
|
iconSrc="common/warn"
|
||||||
|
title={t('common:common.Exit')}
|
||||||
|
w={'400px'}
|
||||||
|
>
|
||||||
|
<ModalBody>
|
||||||
|
<Box>{t('workflow:workflow.exit_tips')}</Box>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter gap={3}>
|
||||||
|
<Button variant={'whiteDanger'} onClick={onBack}>
|
||||||
|
{t('common:common.Exit Directly')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
isLoading={loading}
|
||||||
|
onClick={async () => {
|
||||||
|
await onClickSave({});
|
||||||
|
onCloseBackConfirm();
|
||||||
|
onBack();
|
||||||
|
toast({
|
||||||
|
status: 'success',
|
||||||
|
title: t('app:saved_success'),
|
||||||
|
position: 'top-right'
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('common:common.Save_and_exit')}
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</MyModal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default React.memo(Header);
|
export default React.memo(Header);
|
||||||
|
|||||||
@ -31,7 +31,7 @@ const AppCard = ({
|
|||||||
|
|
||||||
const { appDetail, onOpenInfoEdit, onOpenTeamTagModal, onDelApp, currentTab } =
|
const { appDetail, onOpenInfoEdit, onOpenTeamTagModal, onDelApp, currentTab } =
|
||||||
useContextSelector(AppContext, (v) => v);
|
useContextSelector(AppContext, (v) => v);
|
||||||
const { historiesDefaultData } = useContextSelector(WorkflowContext, (v) => v);
|
const { showHistoryModal } = useContextSelector(WorkflowContext, (v) => v);
|
||||||
|
|
||||||
const { isOpen: isOpenImport, onOpen: onOpenImport, onClose: onCloseImport } = useDisclosure();
|
const { isOpen: isOpenImport, onOpen: onOpenImport, onClose: onCloseImport } = useDisclosure();
|
||||||
|
|
||||||
@ -56,7 +56,7 @@ const AppCard = ({
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
...(!historiesDefaultData && currentTab === TabEnum.appEdit
|
...(!showHistoryModal && currentTab === TabEnum.appEdit
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
children: [
|
children: [
|
||||||
@ -117,7 +117,7 @@ const AppCard = ({
|
|||||||
appDetail.permission.isOwner,
|
appDetail.permission.isOwner,
|
||||||
currentTab,
|
currentTab,
|
||||||
feConfigs?.show_team_chat,
|
feConfigs?.show_team_chat,
|
||||||
historiesDefaultData,
|
showHistoryModal,
|
||||||
onDelApp,
|
onDelApp,
|
||||||
onOpenImport,
|
onOpenImport,
|
||||||
onOpenInfoEdit,
|
onOpenInfoEdit,
|
||||||
|
|||||||
@ -49,6 +49,7 @@ import CostTooltip from '@/components/core/app/plugin/CostTooltip';
|
|||||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||||
import { LoopStartNode } from '@fastgpt/global/core/workflow/template/system/loop/loopStart';
|
import { LoopStartNode } from '@fastgpt/global/core/workflow/template/system/loop/loopStart';
|
||||||
import { LoopEndNode } from '@fastgpt/global/core/workflow/template/system/loop/loopEnd';
|
import { LoopEndNode } from '@fastgpt/global/core/workflow/template/system/loop/loopEnd';
|
||||||
|
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||||
|
|
||||||
type ModuleTemplateListProps = {
|
type ModuleTemplateListProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -399,8 +400,7 @@ const RenderList = React.memo(function RenderList({
|
|||||||
|
|
||||||
const { screenToFlowPosition } = useReactFlow();
|
const { screenToFlowPosition } = useReactFlow();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const reactFlowWrapper = useContextSelector(WorkflowContext, (v) => v.reactFlowWrapper);
|
const { reactFlowWrapper, setNodes, nodeList } = useContextSelector(WorkflowContext, (v) => v);
|
||||||
const setNodes = useContextSelector(WorkflowContext, (v) => v.setNodes);
|
|
||||||
const { computedNewNodeName } = useWorkflowUtils();
|
const { computedNewNodeName } = useWorkflowUtils();
|
||||||
|
|
||||||
const formatTemplates = useMemo<NodeTemplateListType>(() => {
|
const formatTemplates = useMemo<NodeTemplateListType>(() => {
|
||||||
@ -424,6 +424,7 @@ const RenderList = React.memo(function RenderList({
|
|||||||
}) => {
|
}) => {
|
||||||
if (!reactFlowWrapper?.current) return;
|
if (!reactFlowWrapper?.current) return;
|
||||||
|
|
||||||
|
// Load template node
|
||||||
const templateNode = await (async () => {
|
const templateNode = await (async () => {
|
||||||
try {
|
try {
|
||||||
// get plugin preview module
|
// get plugin preview module
|
||||||
@ -458,6 +459,19 @@ const RenderList = React.memo(function RenderList({
|
|||||||
const mouseX = nodePosition.x - 100;
|
const mouseX = nodePosition.x - 100;
|
||||||
const mouseY = nodePosition.y - 20;
|
const mouseY = nodePosition.y - 20;
|
||||||
|
|
||||||
|
// Add default values to some inputs
|
||||||
|
const defaultValueMap: Record<string, any> = {
|
||||||
|
[NodeInputKeyEnum.userChatInput]: undefined
|
||||||
|
};
|
||||||
|
nodeList.forEach((node) => {
|
||||||
|
if (node.flowNodeType === FlowNodeTypeEnum.workflowStart) {
|
||||||
|
defaultValueMap[NodeInputKeyEnum.userChatInput] = [
|
||||||
|
node.nodeId,
|
||||||
|
NodeOutputKeyEnum.userChatInput
|
||||||
|
];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const newNode = nodeTemplate2FlowNode({
|
const newNode = nodeTemplate2FlowNode({
|
||||||
template: {
|
template: {
|
||||||
...templateNode,
|
...templateNode,
|
||||||
@ -469,6 +483,7 @@ const RenderList = React.memo(function RenderList({
|
|||||||
intro: t(templateNode.intro as any),
|
intro: t(templateNode.intro as any),
|
||||||
inputs: templateNode.inputs.map((input) => ({
|
inputs: templateNode.inputs.map((input) => ({
|
||||||
...input,
|
...input,
|
||||||
|
value: defaultValueMap[input.key] ?? input.value,
|
||||||
valueDesc: t(input.valueDesc as any),
|
valueDesc: t(input.valueDesc as any),
|
||||||
label: t(input.label as any),
|
label: t(input.label as any),
|
||||||
description: t(input.description as any),
|
description: t(input.description as any),
|
||||||
@ -516,7 +531,16 @@ const RenderList = React.memo(function RenderList({
|
|||||||
return newState;
|
return newState;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[computedNewNodeName, reactFlowWrapper, setLoading, setNodes, t, toast, screenToFlowPosition]
|
[
|
||||||
|
reactFlowWrapper,
|
||||||
|
screenToFlowPosition,
|
||||||
|
nodeList,
|
||||||
|
computedNewNodeName,
|
||||||
|
t,
|
||||||
|
setNodes,
|
||||||
|
setLoading,
|
||||||
|
toast
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const gridStyle = useMemo(() => {
|
const gridStyle = useMemo(() => {
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import MyTextarea from '@/components/common/Textarea/MyTextarea';
|
|||||||
import { AppContext } from '../../../../context';
|
import { AppContext } from '../../../../context';
|
||||||
import { AppChatConfigType, AppDetailType } from '@fastgpt/global/core/app/type';
|
import { AppChatConfigType, AppDetailType } from '@fastgpt/global/core/app/type';
|
||||||
import { getAppChatConfig } from '@fastgpt/global/core/workflow/utils';
|
import { getAppChatConfig } from '@fastgpt/global/core/workflow/utils';
|
||||||
import { useCreation } from 'ahooks';
|
import { useCreation, useMount } from 'ahooks';
|
||||||
import ChatFunctionTip from '@/components/core/app/Tip';
|
import ChatFunctionTip from '@/components/core/app/Tip';
|
||||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||||
import { WorkflowContext } from '../../../context';
|
import { WorkflowContext } from '../../../context';
|
||||||
@ -35,7 +35,7 @@ const NodePluginConfig = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
|||||||
});
|
});
|
||||||
}, [data, appDetail]);
|
}, [data, appDetail]);
|
||||||
|
|
||||||
useCreation(() => {
|
useMount(() => {
|
||||||
setAppDetail((state) => ({
|
setAppDetail((state) => ({
|
||||||
...state,
|
...state,
|
||||||
chatConfig: {
|
chatConfig: {
|
||||||
@ -43,7 +43,7 @@ const NodePluginConfig = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
|||||||
...chatConfig
|
...chatConfig
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}, []);
|
});
|
||||||
|
|
||||||
const componentsProps = useMemo(
|
const componentsProps = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
|||||||
@ -39,37 +39,26 @@ import { defaultRunningStatus } from './constants';
|
|||||||
import { checkNodeRunStatus } from '@fastgpt/global/core/workflow/runtime/utils';
|
import { checkNodeRunStatus } from '@fastgpt/global/core/workflow/runtime/utils';
|
||||||
import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus';
|
import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus';
|
||||||
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
|
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
|
||||||
import { AppChatConfigType, AppSchema } from '@fastgpt/global/core/app/type';
|
import { AppChatConfigType } from '@fastgpt/global/core/app/type';
|
||||||
import { AppContext } from '@/pages/app/detail/components/context';
|
import { AppContext } from '@/pages/app/detail/components/context';
|
||||||
import ChatTest from './Flow/ChatTest';
|
import ChatTest from './Flow/ChatTest';
|
||||||
import { useDisclosure } from '@chakra-ui/react';
|
import { useDisclosure } from '@chakra-ui/react';
|
||||||
import { uiWorkflow2StoreWorkflow } from './utils';
|
import { uiWorkflow2StoreWorkflow } from './utils';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
|
||||||
import { formatTime2YMDHMS, formatTime2YMDHMW } from '@fastgpt/global/common/string/time';
|
import { formatTime2YMDHMS, formatTime2YMDHMW } from '@fastgpt/global/common/string/time';
|
||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
import { SetState } from 'ahooks/lib/createUseStorageState';
|
import { SetState } from 'ahooks/lib/createUseStorageState';
|
||||||
|
import { AppVersionSchemaType } from '@fastgpt/global/core/app/version';
|
||||||
|
|
||||||
type OnChange<ChangesType> = (changes: ChangesType[]) => void;
|
type OnChange<ChangesType> = (changes: ChangesType[]) => void;
|
||||||
|
|
||||||
export type SnapshotsType = {
|
export type WorkflowSnapshotsType = {
|
||||||
nodes: Node[];
|
nodes: Node[];
|
||||||
edges: Edge[];
|
edges: Edge[];
|
||||||
title: string;
|
title: string;
|
||||||
chatConfig: AppChatConfigType;
|
chatConfig: AppChatConfigType;
|
||||||
isSaved?: boolean;
|
isSaved?: boolean;
|
||||||
};
|
};
|
||||||
export type SaveSnapshotParams = {
|
|
||||||
pastNodes?: Node[];
|
|
||||||
pastEdges?: Edge[];
|
|
||||||
customTitle?: string;
|
|
||||||
chatConfig: AppChatConfigType;
|
|
||||||
};
|
|
||||||
export type InitProps = {
|
|
||||||
nodes: AppSchema['modules'];
|
|
||||||
edges: AppSchema['edges'];
|
|
||||||
chatConfig: AppSchema['chatConfig'];
|
|
||||||
};
|
|
||||||
|
|
||||||
type WorkflowContextType = {
|
type WorkflowContextType = {
|
||||||
appId?: string;
|
appId?: string;
|
||||||
@ -103,22 +92,11 @@ type WorkflowContextType = {
|
|||||||
hoverEdgeId?: string;
|
hoverEdgeId?: string;
|
||||||
setHoverEdgeId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
setHoverEdgeId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||||
|
|
||||||
// snapshots
|
onSwitchTmpVersion: (data: WorkflowSnapshotsType, customTitle: string) => boolean;
|
||||||
saveSnapshot: ({
|
onSwitchCloudVersion: (appVersion: AppVersionSchemaType) => boolean;
|
||||||
pastNodes,
|
past: WorkflowSnapshotsType[];
|
||||||
pastEdges,
|
setPast: Dispatch<SetStateAction<WorkflowSnapshotsType[]>>;
|
||||||
customTitle,
|
future: WorkflowSnapshotsType[];
|
||||||
chatConfig
|
|
||||||
}: {
|
|
||||||
pastNodes?: Node[];
|
|
||||||
pastEdges?: Edge[];
|
|
||||||
customTitle?: string;
|
|
||||||
chatConfig?: AppChatConfigType;
|
|
||||||
}) => Promise<boolean>;
|
|
||||||
resetSnapshot: (state: SnapshotsType) => void;
|
|
||||||
past: SnapshotsType[];
|
|
||||||
setPast: Dispatch<SetStateAction<SnapshotsType[]>>;
|
|
||||||
future: SnapshotsType[];
|
|
||||||
redo: () => void;
|
redo: () => void;
|
||||||
undo: () => void;
|
undo: () => void;
|
||||||
canRedo: boolean;
|
canRedo: boolean;
|
||||||
@ -179,8 +157,8 @@ type WorkflowContextType = {
|
|||||||
onStopNodeDebug: () => void;
|
onStopNodeDebug: () => void;
|
||||||
|
|
||||||
// version history
|
// version history
|
||||||
historiesDefaultData?: InitProps;
|
showHistoryModal: boolean;
|
||||||
setHistoriesDefaultData: React.Dispatch<React.SetStateAction<undefined | InitProps>>;
|
setShowHistoryModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
|
||||||
// chat test
|
// chat test
|
||||||
setWorkflowTestData: React.Dispatch<
|
setWorkflowTestData: React.Dispatch<
|
||||||
@ -306,19 +284,13 @@ export const WorkflowContext = createContext<WorkflowContextType>({
|
|||||||
| undefined {
|
| undefined {
|
||||||
throw new Error('Function not implemented.');
|
throw new Error('Function not implemented.');
|
||||||
},
|
},
|
||||||
historiesDefaultData: undefined,
|
showHistoryModal: false,
|
||||||
setHistoriesDefaultData: function (value: React.SetStateAction<InitProps | undefined>): void {
|
setShowHistoryModal: function (value: React.SetStateAction<boolean>): void {
|
||||||
throw new Error('Function not implemented.');
|
throw new Error('Function not implemented.');
|
||||||
},
|
},
|
||||||
getNodeDynamicInputs: function (nodeId: string): FlowNodeInputItemType[] {
|
getNodeDynamicInputs: function (nodeId: string): FlowNodeInputItemType[] {
|
||||||
throw new Error('Function not implemented.');
|
throw new Error('Function not implemented.');
|
||||||
},
|
},
|
||||||
saveSnapshot: function (): Promise<boolean> {
|
|
||||||
throw new Error('Function not implemented.');
|
|
||||||
},
|
|
||||||
resetSnapshot: function (): void {
|
|
||||||
throw new Error('Function not implemented.');
|
|
||||||
},
|
|
||||||
past: [],
|
past: [],
|
||||||
setPast: function (): void {
|
setPast: function (): void {
|
||||||
throw new Error('Function not implemented.');
|
throw new Error('Function not implemented.');
|
||||||
@ -335,6 +307,12 @@ export const WorkflowContext = createContext<WorkflowContextType>({
|
|||||||
workflowControlMode: 'drag',
|
workflowControlMode: 'drag',
|
||||||
setWorkflowControlMode: function (value?: SetState<'drag' | 'select'> | undefined): void {
|
setWorkflowControlMode: function (value?: SetState<'drag' | 'select'> | undefined): void {
|
||||||
throw new Error('Function not implemented.');
|
throw new Error('Function not implemented.');
|
||||||
|
},
|
||||||
|
onSwitchTmpVersion: function (data: WorkflowSnapshotsType, customTitle: string): boolean {
|
||||||
|
throw new Error('Function not implemented.');
|
||||||
|
},
|
||||||
|
onSwitchCloudVersion: function (appVersion: AppVersionSchemaType): boolean {
|
||||||
|
throw new Error('Function not implemented.');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -792,16 +770,15 @@ const WorkflowContextProvider = ({
|
|||||||
}, [workflowTestData]);
|
}, [workflowTestData]);
|
||||||
|
|
||||||
/* snapshots */
|
/* snapshots */
|
||||||
const [past, setPast] = useLocalStorageState<SnapshotsType[]>(`${appId}-past`, {
|
const forbiddenSaveSnapshot = useRef(false);
|
||||||
defaultValue: [],
|
const [past, setPast] = useLocalStorageState<WorkflowSnapshotsType[]>(`${appId}-past`, {
|
||||||
listenStorageChange: true
|
defaultValue: []
|
||||||
}) as [SnapshotsType[], (value: SetStateAction<SnapshotsType[]>) => void];
|
}) as [WorkflowSnapshotsType[], (value: SetStateAction<WorkflowSnapshotsType[]>) => void];
|
||||||
const [future, setFuture] = useLocalStorageState<SnapshotsType[]>(`${appId}-future`, {
|
const [future, setFuture] = useLocalStorageState<WorkflowSnapshotsType[]>(`${appId}-future`, {
|
||||||
defaultValue: [],
|
defaultValue: []
|
||||||
listenStorageChange: true
|
}) as [WorkflowSnapshotsType[], (value: SetStateAction<WorkflowSnapshotsType[]>) => void];
|
||||||
}) as [SnapshotsType[], (value: SetStateAction<SnapshotsType[]>) => void];
|
|
||||||
|
|
||||||
const resetSnapshot = useMemoizedFn((state: SnapshotsType) => {
|
const resetSnapshot = useMemoizedFn((state: Omit<WorkflowSnapshotsType, 'title' | 'isSaved'>) => {
|
||||||
setNodes(state.nodes);
|
setNodes(state.nodes);
|
||||||
setEdges(state.edges);
|
setEdges(state.edges);
|
||||||
setAppDetail((detail) => ({
|
setAppDetail((detail) => ({
|
||||||
@ -809,30 +786,33 @@ const WorkflowContextProvider = ({
|
|||||||
chatConfig: state.chatConfig
|
chatConfig: state.chatConfig
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
const pushPastSnapshot = useMemoizedFn(
|
||||||
const saveSnapshot = useMemoizedFn(
|
({
|
||||||
async ({
|
|
||||||
pastNodes,
|
pastNodes,
|
||||||
pastEdges,
|
pastEdges,
|
||||||
customTitle,
|
customTitle,
|
||||||
chatConfig,
|
chatConfig,
|
||||||
isSaved
|
isSaved
|
||||||
}: {
|
}: {
|
||||||
pastNodes?: Node[];
|
pastNodes: Node[];
|
||||||
pastEdges?: Edge[];
|
pastEdges: Edge[];
|
||||||
customTitle?: string;
|
customTitle?: string;
|
||||||
chatConfig?: AppChatConfigType;
|
chatConfig: AppChatConfigType;
|
||||||
isSaved?: boolean;
|
isSaved?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
|
if (!pastNodes || !pastEdges || !chatConfig) return false;
|
||||||
|
|
||||||
|
if (forbiddenSaveSnapshot.current) {
|
||||||
|
forbiddenSaveSnapshot.current = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const pastState = past[0];
|
const pastState = past[0];
|
||||||
const currentNodes = pastNodes || nodes;
|
|
||||||
const currentEdges = pastEdges || edges;
|
|
||||||
const currentChatConfig = chatConfig || appDetail.chatConfig;
|
|
||||||
const isPastEqual = compareSnapshot(
|
const isPastEqual = compareSnapshot(
|
||||||
{
|
{
|
||||||
nodes: currentNodes,
|
nodes: pastNodes,
|
||||||
edges: currentEdges,
|
edges: pastEdges,
|
||||||
chatConfig: currentChatConfig
|
chatConfig: chatConfig
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
nodes: pastState?.nodes,
|
nodes: pastState?.nodes,
|
||||||
@ -843,28 +823,60 @@ const WorkflowContextProvider = ({
|
|||||||
|
|
||||||
if (isPastEqual) return false;
|
if (isPastEqual) return false;
|
||||||
|
|
||||||
|
setFuture([]);
|
||||||
setPast((past) => [
|
setPast((past) => [
|
||||||
{
|
{
|
||||||
nodes: currentNodes,
|
nodes: pastNodes,
|
||||||
edges: currentEdges,
|
edges: pastEdges,
|
||||||
title: customTitle || formatTime2YMDHMS(new Date()),
|
title: customTitle || formatTime2YMDHMS(new Date()),
|
||||||
chatConfig: currentChatConfig,
|
chatConfig,
|
||||||
isSaved
|
isSaved
|
||||||
},
|
},
|
||||||
...past.slice(0, 199)
|
...past.slice(0, 199)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
setFuture([]);
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
const onSwitchTmpVersion = useMemoizedFn((params: WorkflowSnapshotsType, customTitle: string) => {
|
||||||
|
// Remove multiple "copy-"
|
||||||
|
const copyText = t('app:version_copy');
|
||||||
|
const regex = new RegExp(`(${copyText}-)\\1+`, 'g');
|
||||||
|
const title = customTitle.replace(regex, `$1`);
|
||||||
|
|
||||||
|
resetSnapshot(params);
|
||||||
|
|
||||||
|
return pushPastSnapshot({
|
||||||
|
pastNodes: params.nodes,
|
||||||
|
pastEdges: params.edges,
|
||||||
|
chatConfig: params.chatConfig,
|
||||||
|
customTitle: title
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const onSwitchCloudVersion = useMemoizedFn((appVersion: AppVersionSchemaType) => {
|
||||||
|
const nodes = appVersion.nodes.map((item) => storeNode2FlowNode({ item, t }));
|
||||||
|
const edges = appVersion.edges.map((item) => storeEdgesRenderEdge({ edge: item }));
|
||||||
|
const chatConfig = appVersion.chatConfig;
|
||||||
|
|
||||||
|
resetSnapshot({
|
||||||
|
nodes,
|
||||||
|
edges,
|
||||||
|
chatConfig
|
||||||
|
});
|
||||||
|
return pushPastSnapshot({
|
||||||
|
pastNodes: nodes,
|
||||||
|
pastEdges: edges,
|
||||||
|
chatConfig,
|
||||||
|
customTitle: `${t('app:version_copy')}-${appVersion.versionName}`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Auto save snapshot
|
// Auto save snapshot
|
||||||
useDebounceEffect(
|
useDebounceEffect(
|
||||||
() => {
|
() => {
|
||||||
if (!nodes.length) return;
|
if (nodes.length === 0 || !appDetail.chatConfig) return;
|
||||||
saveSnapshot({
|
|
||||||
|
pushPastSnapshot({
|
||||||
pastNodes: nodes,
|
pastNodes: nodes,
|
||||||
pastEdges: edges,
|
pastEdges: edges,
|
||||||
customTitle: formatTime2YMDHMS(new Date()),
|
customTitle: formatTime2YMDHMS(new Date()),
|
||||||
@ -904,14 +916,23 @@ const WorkflowContextProvider = ({
|
|||||||
});
|
});
|
||||||
}, [appId]);
|
}, [appId]);
|
||||||
|
|
||||||
const initData = useMemoizedFn(
|
const initData = useCallback(
|
||||||
async (e: Parameters<WorkflowContextType['initData']>[0], isInit?: boolean) => {
|
async (e: Parameters<WorkflowContextType['initData']>[0], isInit?: boolean) => {
|
||||||
/*
|
// Refresh web page, load init
|
||||||
Refresh web page, load init
|
|
||||||
*/
|
|
||||||
if (isInit && past.length > 0) {
|
if (isInit && past.length > 0) {
|
||||||
return resetSnapshot(past[0]);
|
return resetSnapshot(past[0]);
|
||||||
}
|
}
|
||||||
|
// If it is the initial data, save the initial snapshot
|
||||||
|
if (isInit && past.length === 0) {
|
||||||
|
pushPastSnapshot({
|
||||||
|
pastNodes: e.nodes?.map((item) => storeNode2FlowNode({ item, t })) || [],
|
||||||
|
pastEdges: e.edges?.map((item) => storeEdgesRenderEdge({ edge: item })) || [],
|
||||||
|
customTitle: t(`app:app.version_initial`),
|
||||||
|
chatConfig: appDetail.chatConfig,
|
||||||
|
isSaved: true
|
||||||
|
});
|
||||||
|
forbiddenSaveSnapshot.current = true;
|
||||||
|
}
|
||||||
|
|
||||||
setNodes(e.nodes?.map((item) => storeNode2FlowNode({ item, t })) || []);
|
setNodes(e.nodes?.map((item) => storeNode2FlowNode({ item, t })) || []);
|
||||||
setEdges(e.edges?.map((item) => storeEdgesRenderEdge({ edge: item })) || []);
|
setEdges(e.edges?.map((item) => storeEdgesRenderEdge({ edge: item })) || []);
|
||||||
@ -923,22 +944,21 @@ const WorkflowContextProvider = ({
|
|||||||
chatConfig
|
chatConfig
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
},
|
||||||
// If it is the initial data, save the initial snapshot
|
[
|
||||||
if (isInit) {
|
appDetail.chatConfig,
|
||||||
saveSnapshot({
|
past,
|
||||||
pastNodes: e.nodes?.map((item) => storeNode2FlowNode({ item, t })) || [],
|
resetSnapshot,
|
||||||
pastEdges: e.edges?.map((item) => storeEdgesRenderEdge({ edge: item })) || [],
|
pushPastSnapshot,
|
||||||
customTitle: t(`app:app.version_initial`),
|
setAppDetail,
|
||||||
chatConfig: appDetail.chatConfig,
|
setEdges,
|
||||||
isSaved: true
|
setNodes,
|
||||||
});
|
t
|
||||||
}
|
]
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/* Version histories */
|
/* Version histories */
|
||||||
const [historiesDefaultData, setHistoriesDefaultData] = useState<InitProps>();
|
const [showHistoryModal, setShowHistoryModal] = useState(false);
|
||||||
|
|
||||||
/* event bus */
|
/* event bus */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -990,10 +1010,10 @@ const WorkflowContextProvider = ({
|
|||||||
future,
|
future,
|
||||||
undo,
|
undo,
|
||||||
redo,
|
redo,
|
||||||
saveSnapshot,
|
|
||||||
resetSnapshot,
|
|
||||||
canUndo: past.length > 1,
|
canUndo: past.length > 1,
|
||||||
canRedo: !!future.length,
|
canRedo: !!future.length,
|
||||||
|
onSwitchTmpVersion,
|
||||||
|
onSwitchCloudVersion,
|
||||||
|
|
||||||
// function
|
// function
|
||||||
splitToolInputs,
|
splitToolInputs,
|
||||||
@ -1008,8 +1028,8 @@ const WorkflowContextProvider = ({
|
|||||||
onStopNodeDebug,
|
onStopNodeDebug,
|
||||||
|
|
||||||
// version history
|
// version history
|
||||||
historiesDefaultData,
|
showHistoryModal,
|
||||||
setHistoriesDefaultData,
|
setShowHistoryModal,
|
||||||
|
|
||||||
// chat test
|
// chat test
|
||||||
setWorkflowTestData
|
setWorkflowTestData
|
||||||
|
|||||||
@ -5,19 +5,17 @@ import type { FastGPTConfigFileType } from '@fastgpt/global/common/system/types/
|
|||||||
import { PluginSourceEnum } from '@fastgpt/global/core/plugin/constants';
|
import { PluginSourceEnum } from '@fastgpt/global/core/plugin/constants';
|
||||||
import { getFastGPTConfigFromDB } from '@fastgpt/service/common/system/config/controller';
|
import { getFastGPTConfigFromDB } from '@fastgpt/service/common/system/config/controller';
|
||||||
import { PluginTemplateType } from '@fastgpt/global/core/plugin/type';
|
import { PluginTemplateType } from '@fastgpt/global/core/plugin/type';
|
||||||
import { FastGPTProUrl } from '@fastgpt/service/common/system/constants';
|
import { FastGPTProUrl, isProduction } from '@fastgpt/service/common/system/constants';
|
||||||
import { initFastGPTConfig } from '@fastgpt/service/common/system/tools';
|
import { initFastGPTConfig } from '@fastgpt/service/common/system/tools';
|
||||||
import json5 from 'json5';
|
import json5 from 'json5';
|
||||||
import { SystemPluginTemplateItemType } from '@fastgpt/global/core/workflow/type';
|
import { SystemPluginTemplateItemType } from '@fastgpt/global/core/workflow/type';
|
||||||
|
|
||||||
export const readConfigData = (name: string) => {
|
export const readConfigData = (name: string) => {
|
||||||
const isDev = process.env.NODE_ENV === 'development';
|
|
||||||
|
|
||||||
const splitName = name.split('.');
|
const splitName = name.split('.');
|
||||||
const devName = `${splitName[0]}.local.${splitName[1]}`;
|
const devName = `${splitName[0]}.local.${splitName[1]}`;
|
||||||
|
|
||||||
const filename = (() => {
|
const filename = (() => {
|
||||||
if (isDev) {
|
if (!isProduction) {
|
||||||
// check local file exists
|
// check local file exists
|
||||||
const hasLocalFile = existsSync(`data/${devName}`);
|
const hasLocalFile = existsSync(`data/${devName}`);
|
||||||
if (hasLocalFile) {
|
if (hasLocalFile) {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { PostPublishAppProps, PostRevertAppProps } from '@/global/core/app/api';
|
import { PostPublishAppProps } from '@/global/core/app/api';
|
||||||
import { GET, POST, DELETE, PUT } from '@/web/common/api/request';
|
import { GET, POST, DELETE, PUT } from '@/web/common/api/request';
|
||||||
import type { AppVersionSchemaType } from '@fastgpt/global/core/app/version';
|
import type { AppVersionSchemaType } from '@fastgpt/global/core/app/version';
|
||||||
import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type';
|
import { PaginationProps } from '@fastgpt/web/common/fetch/type';
|
||||||
import type {
|
import type {
|
||||||
getLatestVersionQuery,
|
getLatestVersionQuery,
|
||||||
getLatestVersionResponse
|
getLatestVersionResponse
|
||||||
@ -16,7 +16,7 @@ export const postPublishApp = (appId: string, data: PostPublishAppProps) =>
|
|||||||
POST(`/core/app/version/publish?appId=${appId}`, data);
|
POST(`/core/app/version/publish?appId=${appId}`, data);
|
||||||
|
|
||||||
export const getWorkflowVersionList = (data: PaginationProps<{ appId: string }>) =>
|
export const getWorkflowVersionList = (data: PaginationProps<{ appId: string }>) =>
|
||||||
POST<PaginationResponse<versionListResponse>>('/core/app/version/list', data);
|
POST<versionListResponse>('/core/app/version/list', data);
|
||||||
|
|
||||||
export const getAppVersionDetail = (versionId: string, appId: string) =>
|
export const getAppVersionDetail = (versionId: string, appId: string) =>
|
||||||
GET<AppVersionSchemaType>(`/core/app/version/detail?versionId=${versionId}&appId=${appId}`);
|
GET<AppVersionSchemaType>(`/core/app/version/detail?versionId=${versionId}&appId=${appId}`);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user