Concat plugin to app (#1799)

This commit is contained in:
Archer
2024-06-19 14:38:21 +08:00
committed by GitHub
parent b17d14bb7d
commit 565bfc8486
220 changed files with 5018 additions and 4667 deletions

View File

@@ -1,6 +1,6 @@
import type { AppProps } from 'next/app';
import Script from 'next/script';
import Head from 'next/head';
import Layout from '@/components/Layout';
import { appWithTranslation } from 'next-i18next';

View File

@@ -2,10 +2,15 @@ import React from 'react';
import { useTranslation } from 'next-i18next';
import ApiKeyTable from '@/components/support/apikey/Table';
import { useI18n } from '@/web/context/I18n';
import { Box } from '@chakra-ui/react';
const ApiKey = () => {
const { publishT } = useI18n();
return <ApiKeyTable tips={publishT('key tips')}></ApiKeyTable>;
return (
<Box px={[4, 8]} py={[4, 6]}>
<ApiKeyTable tips={publishT('key tips')}></ApiKeyTable>
</Box>
);
};
export default ApiKey;

View File

@@ -0,0 +1,163 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { MongoPlugin } from '@fastgpt/service/core/plugin/schema';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { NextAPI } from '@/service/middleware/entry';
import { PluginTypeEnum } from '@fastgpt/global/core/plugin/constants';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
/*
1. 先读取 HTTP plugin 内容,并找到所有的子plugin,然后事务批量创建,最后修改 inited
2. 读取剩下未 inited 的plugin,逐一创建
*/
let success = 0;
async function handler(req: NextApiRequest, res: NextApiResponse) {
await authCert({ req, authRoot: true });
const total = await MongoPlugin.countDocuments({
inited: { $ne: true }
});
console.log('Total plugin', total);
await initHttp();
await initPlugin();
}
async function initHttp(): Promise<any> {
/* 读取http插件和他的children */
const plugin = await MongoPlugin.findOne({
type: PluginTypeEnum.folder,
inited: { $ne: true }
}).lean();
if (!plugin) return;
const children = await MongoPlugin.find({
teamId: plugin.teamId,
parentId: plugin._id,
inited: { $ne: true }
}).lean();
await mongoSessionRun(async (session) => {
/* 创建HTTP插件作为目录 */
const [{ _id }] = await MongoApp.create(
[
{
teamId: plugin.teamId,
tmbId: plugin.tmbId,
type: AppTypeEnum.httpPlugin,
name: plugin.name,
avatar: plugin.avatar,
intro: plugin.intro,
metadata: plugin.metadata,
version: 'v2',
pluginData: {
apiSchemaStr: plugin.metadata?.apiSchemaStr,
customHeaders: plugin.metadata?.customHeaders
}
}
],
{ session }
);
/* 批量创建子插件 */
for await (const item of children) {
await MongoApp.create(
[
{
parentId: _id,
teamId: item.teamId,
tmbId: item.tmbId,
type: AppTypeEnum.plugin,
name: item.name,
avatar: item.avatar,
intro: item.intro,
version: 'v2',
modules: item.modules,
edges: item.edges,
pluginData: {
nodeVersion: item.nodeVersion,
pluginUniId: plugin.metadata?.pluginUid
}
}
],
{ session }
);
}
/* 更新插件信息 */
await MongoPlugin.findOneAndUpdate(
{
_id: plugin._id
},
{
$set: { inited: true }
},
{ session }
);
await MongoPlugin.updateMany(
{
teamId: plugin.teamId,
parentId: plugin._id
},
{
$set: { inited: true }
},
{ session }
);
success += children.length + 1;
console.log(success);
});
return initHttp();
}
async function initPlugin(): Promise<any> {
const plugin = await MongoPlugin.findOne({
type: PluginTypeEnum.custom,
inited: { $ne: true }
}).lean();
if (!plugin) return;
await mongoSessionRun(async (session) => {
await MongoApp.create(
[
{
teamId: plugin.teamId,
tmbId: plugin.tmbId,
type: AppTypeEnum.plugin,
name: plugin.name,
avatar: plugin.avatar,
intro: plugin.intro,
version: 'v2',
modules: plugin.modules,
edges: plugin.edges,
pluginData: {
nodeVersion: plugin.nodeVersion
}
}
],
{ session }
);
await MongoPlugin.findOneAndUpdate(
{
_id: plugin._id
},
{
$set: { inited: true }
},
{ session }
);
success++;
console.log(success);
});
return initPlugin();
}
export default NextAPI(handler);

View File

@@ -12,6 +12,8 @@ import type { AppSchema } from '@fastgpt/global/core/app/type';
import { ApiRequestProps } from '@fastgpt/service/type/next';
import type { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils';
import { defaultNodeVersion } from '@fastgpt/global/core/workflow/node/constant';
import { ClientSession } from '@fastgpt/service/common/mongo';
export type CreateAppBody = {
parentId?: ParentIdType;
@@ -35,37 +37,16 @@ async function handler(req: ApiRequestProps<CreateAppBody>, res: NextApiResponse
// 上限校验
await checkTeamAppLimit(teamId);
// 创建模型
const appId = await mongoSessionRun(async (session) => {
const [{ _id: appId }] = await MongoApp.create(
[
{
...parseParentIdInMongo(parentId),
avatar,
name,
teamId,
tmbId,
modules,
edges,
type,
version: 'v2'
}
],
{ session }
);
await MongoAppVersion.create(
[
{
appId,
nodes: modules,
edges
}
],
{ session }
);
return appId;
// 创建app
const appId = await onCreateApp({
parentId,
name,
avatar,
type,
modules,
edges,
teamId,
tmbId
});
jsonRes(res, {
@@ -74,3 +55,72 @@ async function handler(req: ApiRequestProps<CreateAppBody>, res: NextApiResponse
}
export default NextAPI(handler);
export const onCreateApp = async ({
parentId,
name,
intro,
avatar,
type,
modules,
edges,
teamId,
tmbId,
pluginData,
session
}: {
parentId?: ParentIdType;
name?: string;
avatar?: string;
type?: AppTypeEnum;
modules?: AppSchema['modules'];
edges?: AppSchema['edges'];
intro?: string;
teamId: string;
tmbId: string;
pluginData?: AppSchema['pluginData'];
session?: ClientSession;
}) => {
const create = async (session: ClientSession) => {
const [{ _id: appId }] = await MongoApp.create(
[
{
...parseParentIdInMongo(parentId),
avatar,
name,
intro,
teamId,
tmbId,
modules,
edges,
type,
version: 'v2',
pluginData,
...(type === AppTypeEnum.plugin && { 'pluginData.nodeVersion': defaultNodeVersion })
}
],
{ session }
);
if (type !== AppTypeEnum.folder && type !== AppTypeEnum.httpPlugin) {
await MongoAppVersion.create(
[
{
appId,
nodes: modules,
edges
}
],
{ session }
);
}
return appId;
};
if (session) {
return create(session);
} else {
return await mongoSessionRun(create);
}
};

View File

@@ -14,6 +14,7 @@ import {
} from '@fastgpt/global/support/permission/constant';
import { findAppAndAllChildren } from '@fastgpt/service/core/app/controller';
import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema';
import { ClientSession } from '@fastgpt/service/common/mongo';
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const { appId } = req.query as { appId: string };
@@ -25,13 +26,30 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
// Auth owner (folder owner, can delete all apps in the folder)
const { teamId } = await authApp({ req, authToken: true, appId, per: OwnerPermissionVal });
await onDelOneApp({
teamId,
appId
});
}
export default NextAPI(handler);
export const onDelOneApp = async ({
teamId,
appId,
session
}: {
teamId: string;
appId: string;
session?: ClientSession;
}) => {
const apps = await findAppAndAllChildren({
teamId,
appId,
fields: '_id'
});
await mongoSessionRun(async (session) => {
const del = async (session: ClientSession) => {
for await (const app of apps) {
const appId = app._id;
// Chats
@@ -83,7 +101,11 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
{ session }
);
}
});
}
};
export default NextAPI(handler);
if (session) {
return del(session);
}
return mongoSessionRun(del);
};

View File

@@ -0,0 +1,68 @@
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { httpApiSchema2Plugins } from '@fastgpt/global/core/app/httpPlugin/utils';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { onCreateApp, type CreateAppBody } from '../create';
import { AppSchema } from '@fastgpt/global/core/app/type';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
export type createHttpPluginQuery = {};
export type createHttpPluginBody = Omit<CreateAppBody, 'type' | 'modules' | 'edges'> & {
intro?: string;
pluginData: AppSchema['pluginData'];
};
export type createHttpPluginResponse = {};
async function handler(
req: ApiRequestProps<createHttpPluginBody, createHttpPluginQuery>,
res: ApiResponseType<any>
): Promise<createHttpPluginResponse> {
const { parentId, name, intro, avatar, pluginData } = req.body;
if (!name || !pluginData) {
return Promise.reject('缺少参数');
}
const { teamId, tmbId } = await authUserPer({ req, authToken: true, per: WritePermissionVal });
await mongoSessionRun(async (session) => {
// create http plugin folder
const httpPluginIid = await onCreateApp({
parentId,
name,
avatar,
intro,
teamId,
tmbId,
type: AppTypeEnum.httpPlugin,
pluginData,
session
});
// compute children plugins
const childrenPlugins = await httpApiSchema2Plugins({
parentId: httpPluginIid,
apiSchemaStr: pluginData.apiSchemaStr,
customHeader: pluginData.customHeaders
});
// create children plugins
for await (const item of childrenPlugins) {
await onCreateApp({
...item,
teamId,
tmbId,
session
});
}
});
return {};
}
export default NextAPI(handler);

View File

@@ -0,0 +1,127 @@
import type { NextApiResponse } from 'next';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { ClientSession } from '@fastgpt/service/common/mongo';
import { httpApiSchema2Plugins } from '@fastgpt/global/core/app/httpPlugin/utils';
import { NextAPI } from '@/service/middleware/entry';
import { AppSchema } from '@fastgpt/global/core/app/type';
import { ApiRequestProps } from '@fastgpt/service/type/next';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { ManagePermissionVal } from '@fastgpt/global/support/permission/constant';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { isEqual } from 'lodash';
import { onCreateApp } from '../create';
import { onDelOneApp } from '../del';
export type UpdateHttpPluginBody = {
appId: string;
name: string;
avatar?: string;
intro?: string;
pluginData: AppSchema['pluginData'];
};
async function handler(req: ApiRequestProps<UpdateHttpPluginBody>, res: NextApiResponse<any>) {
const { appId, name, avatar, intro, pluginData } = req.body;
const { app } = await authApp({ req, authToken: true, appId, per: ManagePermissionVal });
const storeData = {
apiSchemaStr: app.pluginData?.apiSchemaStr,
customHeaders: app.pluginData?.customHeaders
};
const updateData = {
apiSchemaStr: pluginData?.apiSchemaStr,
customHeaders: pluginData?.customHeaders
};
await mongoSessionRun(async (session) => {
// update children
if (!isEqual(storeData, updateData)) {
await updateHttpChildrenPlugin({
teamId: app.teamId,
tmbId: app.tmbId,
parentId: app._id,
pluginData,
session
});
}
await MongoApp.findByIdAndUpdate(
appId,
{
name,
avatar,
intro,
pluginData
},
{ session }
);
});
}
export default NextAPI(handler);
const updateHttpChildrenPlugin = async ({
teamId,
tmbId,
parentId,
pluginData,
session
}: {
teamId: string;
tmbId: string;
parentId: string;
pluginData?: AppSchema['pluginData'];
session: ClientSession;
}) => {
if (!pluginData?.apiSchemaStr) return;
const dbPlugins = await MongoApp.find({
parentId,
teamId
}).select({
pluginData: 1
});
const schemaPlugins = await httpApiSchema2Plugins({
parentId,
apiSchemaStr: pluginData?.apiSchemaStr,
customHeader: pluginData?.customHeaders
});
// 数据库中存在schema不存在删除
for await (const plugin of dbPlugins) {
if (!schemaPlugins.find((p) => p.name === plugin.pluginData?.pluginUniId)) {
await onDelOneApp({
teamId,
appId: plugin._id,
session
});
}
}
// 数据库中不存在schema存在新增
for await (const plugin of schemaPlugins) {
if (!dbPlugins.find((p) => p.pluginData?.pluginUniId === plugin.name)) {
await onCreateApp({
...plugin,
teamId,
tmbId,
session
});
}
}
// 数据库中存在schema存在更新
for await (const plugin of schemaPlugins) {
const dbPlugin = dbPlugins.find((p) => plugin.name === p.pluginData?.pluginUniId);
if (dbPlugin) {
await MongoApp.findByIdAndUpdate(
dbPlugin._id,
{
...plugin,
version: 'v2'
},
{ session }
);
}
}
};

View File

@@ -17,8 +17,9 @@ import { AppDefaultPermissionVal } from '@fastgpt/global/support/permission/app/
export type ListAppBody = {
parentId?: ParentIdType;
type?: AppTypeEnum;
type?: AppTypeEnum | AppTypeEnum[];
getRecentlyChat?: boolean;
searchKey?: string;
};
async function handler(
@@ -36,26 +37,43 @@ async function handler(
per: ReadPermissionVal
});
const { parentId, type, getRecentlyChat } = req.body;
const { parentId, type, getRecentlyChat, searchKey } = req.body;
const findAppsQuery = getRecentlyChat
? {
const findAppsQuery = (() => {
const searchMatch = searchKey
? {
$or: [
{ name: { $regex: searchKey, $options: 'i' } },
{ intro: { $regex: searchKey, $options: 'i' } }
]
}
: {};
if (getRecentlyChat) {
return {
// get all chat app
teamId,
type: { $in: [AppTypeEnum.advanced, AppTypeEnum.simple] }
}
: {
teamId,
...(type && { type }),
...parseParentIdInMongo(parentId)
type: { $in: [AppTypeEnum.workflow, AppTypeEnum.simple] },
...searchMatch
};
}
return {
teamId,
...(type && Array.isArray(type) && { type: { $in: type } }),
...(type && { type }),
...parseParentIdInMongo(parentId),
...searchMatch
};
})();
/* temp: get all apps and per */
const [myApps, rpList] = await Promise.all([
MongoApp.find(findAppsQuery, '_id avatar type name intro tmbId defaultPermission')
MongoApp.find(findAppsQuery, '_id avatar type name intro tmbId pluginData defaultPermission')
.sort({
updateTime: -1
})
.limit(searchKey ? 20 : 1000)
.lean(),
MongoResourcePermission.find({
resourceType: PerResourceTypeEnum.app,
@@ -88,7 +106,8 @@ async function handler(
name: app.name,
intro: app.intro,
permission: app.permission,
defaultPermission: app.defaultPermission || AppDefaultPermissionVal
defaultPermission: app.defaultPermission || AppDefaultPermissionVal,
pluginData: app.pluginData
}));
}

View File

@@ -0,0 +1,33 @@
/*
get plugin preview modules
*/
import type { NextApiResponse } from 'next';
import {
getPluginPreviewNode,
splitCombinePluginId
} from '@fastgpt/service/core/app/plugin/controller';
import { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/index.d';
import { NextAPI } from '@/service/middleware/entry';
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 { PluginSourceEnum } from '@fastgpt/global/core/plugin/constants';
export type GetPreviewNodeQuery = { appId: string };
async function handler(
req: ApiRequestProps<{}, GetPreviewNodeQuery>,
res: NextApiResponse<any>
): Promise<FlowNodeTemplateType> {
const { appId } = req.query;
const { source } = await splitCombinePluginId(appId);
if (source === PluginSourceEnum.personal) {
await authApp({ req, authToken: true, appId, per: WritePermissionVal });
}
return getPluginPreviewNode({ id: appId });
}
export default NextAPI(handler);

View File

@@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { FlowNodeTypeEnum, defaultNodeVersion } from '@fastgpt/global/core/workflow/node/constant';
import { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/index.d';
import { FlowNodeTemplateTypeEnum } from '@fastgpt/global/core/workflow/constants';
@@ -22,7 +22,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
intro: plugin.intro,
showStatus: true,
isTool: plugin.isTool,
version: '481',
version: defaultNodeVersion,
inputs: [],
outputs: []
})) || [];

View File

@@ -0,0 +1,53 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { OwnerPermissionVal } from '@fastgpt/global/support/permission/constant';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { onCreateApp } from './create';
export type transitionWorkflowQuery = {};
export type transitionWorkflowBody = {
appId: string;
createNew?: boolean;
};
export type transitionWorkflowResponse = {
id?: string;
};
async function handler(
req: ApiRequestProps<transitionWorkflowBody, transitionWorkflowQuery>,
res: ApiResponseType<any>
): Promise<transitionWorkflowResponse> {
const { appId, createNew } = req.body;
const { app, tmbId } = await authApp({
req,
appId,
authToken: true,
per: OwnerPermissionVal
});
if (createNew) {
const appId = await onCreateApp({
parentId: app.parentId,
name: app.name + ' Copy',
avatar: app.avatar,
type: AppTypeEnum.workflow,
modules: app.modules,
edges: app.edges,
teamId: app.teamId,
tmbId
});
return { id: appId };
} else {
await MongoApp.findByIdAndUpdate(appId, { type: AppTypeEnum.workflow });
}
return {};
}
export default NextAPI(handler);

View File

@@ -6,8 +6,7 @@ import { beforeUpdateAppFormat } from '@fastgpt/service/core/app/controller';
import { NextAPI } from '@/service/middleware/entry';
import {
ManagePermissionVal,
WritePermissionVal,
OwnerPermissionVal
WritePermissionVal
} from '@fastgpt/global/support/permission/constant';
import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils';
@@ -22,7 +21,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
nodes,
edges,
chatConfig,
permission,
teamTags,
defaultPermission
} = req.body as AppUpdateParams;
@@ -33,9 +31,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
}
// 凭证校验
if (permission) {
await authApp({ req, authToken: true, appId, per: OwnerPermissionVal });
} else if (defaultPermission) {
if (defaultPermission) {
await authApp({ req, authToken: true, appId, per: ManagePermissionVal });
} else {
await authApp({ req, authToken: true, appId, per: WritePermissionVal });
@@ -56,7 +52,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
type,
avatar,
intro,
permission,
defaultPermission,
...(teamTags && teamTags),
...(formatNodes && {

View File

@@ -0,0 +1,36 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { getAppLatestVersion } from '@fastgpt/service/core/app/controller';
import { AppChatConfigType } from '@fastgpt/global/core/app/type';
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type';
export type getLatestVersionQuery = {
appId: string;
};
export type getLatestVersionBody = {};
export type getLatestVersionResponse = {
nodes: StoreNodeItemType[];
edges: StoreEdgeItemType[];
chatConfig: AppChatConfigType;
};
async function handler(
req: ApiRequestProps<getLatestVersionBody, getLatestVersionQuery>,
res: ApiResponseType<any>
): Promise<getLatestVersionResponse> {
const { app } = await authApp({
req,
authToken: true,
appId: req.query.appId,
per: WritePermissionVal
});
return getAppLatestVersion(req.query.appId, app);
}
export default NextAPI(handler);

View File

@@ -8,6 +8,7 @@ import { beforeUpdateAppFormat } from '@fastgpt/service/core/app/controller';
import { getNextTimeByCronStringAndTimezone } from '@fastgpt/global/common/string/time';
import { PostPublishAppProps } from '@/global/core/app/api';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
type Response = {};
@@ -15,13 +16,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>): Promise<
const { appId } = req.query as { appId: string };
const { nodes = [], edges = [], chatConfig, type } = req.body as PostPublishAppProps;
await authApp({ appId, req, per: WritePermissionVal, authToken: true });
const { app } = await authApp({ appId, req, per: WritePermissionVal, authToken: true });
const { nodes: formatNodes } = beforeUpdateAppFormat({ nodes });
await mongoSessionRun(async (session) => {
// create version histories
await MongoAppVersion.create(
const [{ _id }] = await MongoAppVersion.create(
[
{
appId,
@@ -34,18 +35,25 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>): Promise<
);
// update app
await MongoApp.findByIdAndUpdate(appId, {
modules: formatNodes,
edges,
chatConfig,
updateTime: new Date(),
version: 'v2',
type,
scheduledTriggerConfig: chatConfig?.scheduledTriggerConfig,
scheduledTriggerNextTime: chatConfig?.scheduledTriggerConfig
? getNextTimeByCronStringAndTimezone(chatConfig.scheduledTriggerConfig)
: null
});
await MongoApp.findByIdAndUpdate(
appId,
{
modules: formatNodes,
edges,
chatConfig,
updateTime: new Date(),
version: 'v2',
type,
scheduledTriggerConfig: chatConfig?.scheduledTriggerConfig,
scheduledTriggerNextTime: chatConfig?.scheduledTriggerConfig?.cronString
? getNextTimeByCronStringAndTimezone(chatConfig.scheduledTriggerConfig)
: null,
...(app.type === AppTypeEnum.plugin && { 'pluginData.nodeVersion': _id })
},
{
session
}
);
});
return {};

View File

@@ -8,14 +8,20 @@ import { beforeUpdateAppFormat } from '@fastgpt/service/core/app/controller';
import { getNextTimeByCronStringAndTimezone } from '@fastgpt/global/common/string/time';
import { PostRevertAppProps } from '@/global/core/app/api';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
type Response = {};
async function handler(req: NextApiRequest, res: NextApiResponse<any>): Promise<{}> {
const { appId } = req.query as { appId: string };
const { editNodes = [], editEdges = [], versionId } = req.body as PostRevertAppProps;
const {
editNodes = [],
editEdges = [],
editChatConfig,
versionId
} = req.body as PostRevertAppProps;
await authApp({ appId, req, per: WritePermissionVal, authToken: true });
const { app } = await authApp({ appId, req, per: WritePermissionVal, authToken: true });
const version = await MongoAppVersion.findOne({
_id: versionId,
@@ -37,19 +43,21 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>): Promise<
{
appId,
nodes: formatEditNodes,
edges: editEdges
edges: editEdges,
chatConfig: editChatConfig
}
],
{ session }
);
// 为历史版本再创建一个版本
await MongoAppVersion.create(
const [{ _id }] = await MongoAppVersion.create(
[
{
appId,
nodes: version.nodes,
edges: version.edges
edges: version.edges,
chatConfig: version.chatConfig
}
],
{ session }
@@ -59,11 +67,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>): Promise<
await MongoApp.findByIdAndUpdate(appId, {
modules: version.nodes,
edges: version.edges,
chatConfig: version.chatConfig,
updateTime: new Date(),
scheduledTriggerConfig,
scheduledTriggerNextTime: scheduledTriggerConfig
scheduledTriggerNextTime: scheduledTriggerConfig?.cronString
? getNextTimeByCronStringAndTimezone(scheduledTriggerConfig)
: null
: null,
...(app.type === AppTypeEnum.plugin && { 'pluginData.nodeVersion': _id })
});
});

View File

@@ -6,6 +6,7 @@ import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
import { authChatCert } from '@/service/support/permission/auth/chat';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { AppErrEnum } from '@fastgpt/global/common/error/code/app';
import { replaceRegChars } from '@fastgpt/global/common/string/tools';
export type QueryChatInputGuideBody = OutLinkChatAuthProps & {
appId: string;
@@ -28,7 +29,7 @@ async function handler(
const params = {
appId,
...(searchKey && { text: { $regex: new RegExp(searchKey, 'i') } })
...(searchKey && { text: { $regex: new RegExp(`${replaceRegChars(searchKey)}`, 'i') } })
};
const result = await MongoChatInputGuide.find(params).sort({ _id: -1 }).limit(6);

View File

@@ -1,78 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import type { CreateOnePluginParams } from '@fastgpt/global/core/plugin/controller';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { MongoPlugin } from '@fastgpt/service/core/plugin/schema';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { httpApiSchema2Plugins } from '@fastgpt/global/core/plugin/httpPlugin/utils';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { teamId, tmbId } = await authUserPer({ req, authToken: true, per: WritePermissionVal });
const body = req.body as CreateOnePluginParams;
// await checkTeamPluginLimit(teamId);
// create parent plugin and child plugin
if (body.metadata?.apiSchemaStr) {
const parentId = await mongoSessionRun(async (session) => {
const [{ _id: parentId }] = await MongoPlugin.create(
[
{
...body,
parentId: null,
teamId,
tmbId,
version: 'v2'
}
],
{ session }
);
const childrenPlugins = await httpApiSchema2Plugins({
parentId,
apiSchemaStr: body.metadata?.apiSchemaStr,
customHeader: body.metadata?.customHeaders
});
await MongoPlugin.create(
childrenPlugins.map((item) => ({
...item,
metadata: {
pluginUid: item.name
},
teamId,
tmbId,
version: 'v2'
})),
{
session
}
);
return parentId;
});
jsonRes(res, {
data: parentId
});
} else {
const { _id } = await MongoPlugin.create({
...body,
teamId,
tmbId,
version: 'v2'
});
jsonRes(res, {
data: _id
});
}
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -1,41 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { MongoPlugin } from '@fastgpt/service/core/plugin/schema';
import { authPluginCrud } from '@fastgpt/service/support/permission/auth/plugin';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { teamId } = await authUserPer({ req, authToken: true, per: WritePermissionVal });
const { pluginId } = req.query as { pluginId: string };
if (!pluginId) {
throw new Error('缺少参数');
}
await authPluginCrud({ req, authToken: true, pluginId, per: 'owner' });
await mongoSessionRun(async (session) => {
await MongoPlugin.deleteMany(
{
teamId,
parentId: pluginId
},
{
session
}
);
await MongoPlugin.findByIdAndDelete(pluginId, { session });
});
jsonRes(res, {});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -1,21 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { authPluginCrud } from '@fastgpt/service/support/permission/auth/plugin';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { id } = req.query as { id: string };
await connectToDatabase();
const { plugin } = await authPluginCrud({ req, authToken: true, pluginId: id, per: 'r' });
jsonRes(res, {
data: plugin
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -1,28 +0,0 @@
/*
get plugin preview modules
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { getPluginPreviewNode } from '@fastgpt/service/core/plugin/controller';
import { authPluginCanUse } from '@fastgpt/service/support/permission/auth/plugin';
import { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/index.d';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { id } = req.query as { id: string };
await connectToDatabase();
const { teamId, tmbId } = await authCert({ req, authToken: true });
await authPluginCanUse({ id, teamId, tmbId });
jsonRes<FlowNodeTemplateType>(res, {
data: await getPluginPreviewNode({ id })
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -1,36 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { MongoPlugin } from '@fastgpt/service/core/plugin/schema';
import { PluginListItemType } from '@fastgpt/global/core/plugin/controller';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { parentId, type } = req.query as { parentId?: string; type?: DatasetTypeEnum };
const { teamId } = await authCert({ req, authToken: true });
const plugins = await MongoPlugin.find(
{
teamId,
...(parentId !== undefined && { parentId: parentId || null }),
...(type && { type })
},
'_id parentId type name avatar intro metadata'
)
.sort({ updateTime: -1 })
.lean();
jsonRes<PluginListItemType[]>(res, {
data: plugins
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -1,46 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import type { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type.d';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { MongoPlugin } from '@fastgpt/service/core/plugin/schema';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { parentId } = req.query as { parentId: string };
if (!parentId) {
return jsonRes(res, {
data: []
});
}
await authCert({ req, authToken: true });
jsonRes<ParentTreePathItemType[]>(res, {
data: await getParents(parentId)
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
async function getParents(parentId?: string): Promise<ParentTreePathItemType[]> {
if (!parentId) {
return [];
}
const parent = await MongoPlugin.findById(parentId, 'name parentId');
if (!parent) return [];
const paths = await getParents(parent.parentId);
paths.push({ parentId, parentName: parent.name });
return paths;
}

View File

@@ -1,64 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { MongoPlugin } from '@fastgpt/service/core/plugin/schema';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/index.d';
import { FlowNodeTemplateTypeEnum } from '@fastgpt/global/core/workflow/constants';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { parentId, searchKey } = req.query as { parentId?: string; searchKey?: string };
const { teamId } = await authCert({ req, authToken: true });
const userPlugins = await (async () => {
if (searchKey) {
return MongoPlugin.find({
teamId,
// search name or intro
$or: [
{ name: { $regex: searchKey, $options: 'i' } },
{ intro: { $regex: searchKey, $options: 'i' } }
]
})
.sort({
updateTime: -1
})
.lean();
} else {
return MongoPlugin.find({ teamId, parentId: parentId || null })
.sort({
updateTime: -1
})
.lean();
}
})();
const data: FlowNodeTemplateType[] = userPlugins.map((plugin) => ({
id: String(plugin._id),
parentId: String(plugin.parentId),
pluginId: String(plugin._id),
pluginType: plugin.type,
templateType: FlowNodeTemplateTypeEnum.personalPlugin,
flowNodeType: FlowNodeTypeEnum.pluginModule,
avatar: plugin.avatar,
name: plugin.name,
intro: plugin.intro,
showStatus: false,
version: '481',
inputs: [],
outputs: []
}));
jsonRes<FlowNodeTemplateType[]>(res, {
data
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -1,152 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import type { UpdatePluginParams } from '@fastgpt/global/core/plugin/controller';
import { authPluginCrud } from '@fastgpt/service/support/permission/auth/plugin';
import { MongoPlugin } from '@fastgpt/service/core/plugin/schema';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { ClientSession } from '@fastgpt/service/common/mongo';
import { httpApiSchema2Plugins } from '@fastgpt/global/core/plugin/httpPlugin/utils';
import { isEqual } from 'lodash';
import { nanoid } from 'nanoid';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const body = req.body as UpdatePluginParams;
const { id, modules, edges, ...props } = body;
const { teamId, tmbId } = await authPluginCrud({
req,
authToken: true,
pluginId: id,
per: 'owner'
});
const originPlugin = await MongoPlugin.findById(id);
let updateData = {
name: props.name,
intro: props.intro,
avatar: props.avatar,
parentId: props.parentId,
version: 'v2',
...(modules?.length && {
modules: modules
}),
...(edges?.length && { edges }),
metadata: props.metadata,
nodeVersion: originPlugin?.nodeVersion
};
const isNodeVersionEqual =
isEqual(
originPlugin?.modules.map((module) => {
return { ...module, position: undefined };
}),
updateData.modules?.map((module) => {
return { ...module, position: undefined };
})
) && isEqual(originPlugin?.edges, updateData.edges);
if (!isNodeVersionEqual) {
updateData = {
...updateData,
nodeVersion: nanoid(6)
};
}
if (props.metadata?.apiSchemaStr) {
await mongoSessionRun(async (session) => {
// update children
await updateHttpChildrenPlugin({
teamId,
tmbId,
parent: body,
session
});
await MongoPlugin.findByIdAndUpdate(id, updateData, { session });
});
jsonRes(res, {});
} else {
jsonRes(res, {
data: await MongoPlugin.findByIdAndUpdate(id, updateData)
});
}
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
const updateHttpChildrenPlugin = async ({
teamId,
tmbId,
parent,
session
}: {
teamId: string;
tmbId: string;
parent: UpdatePluginParams;
session: ClientSession;
}) => {
if (!parent.metadata?.apiSchemaStr) return;
const dbPlugins = await MongoPlugin.find(
{
parentId: parent.id,
teamId
},
'_id metadata'
);
const schemaPlugins = await httpApiSchema2Plugins({
parentId: parent.id,
apiSchemaStr: parent.metadata?.apiSchemaStr,
customHeader: parent.metadata?.customHeaders
});
// 数据库中存在schema不存在删除
for await (const plugin of dbPlugins) {
if (!schemaPlugins.find((p) => p.name === plugin.metadata?.pluginUid)) {
await MongoPlugin.deleteOne({ _id: plugin._id }, { session });
}
}
// 数据库中不存在schema存在新增
for await (const plugin of schemaPlugins) {
if (!dbPlugins.find((p) => p.metadata?.pluginUid === plugin.name)) {
await MongoPlugin.create(
[
{
...plugin,
metadata: {
pluginUid: plugin.name
},
teamId,
tmbId,
version: 'v2'
}
],
{
session
}
);
}
}
// 数据库中存在schema存在更新
for await (const plugin of schemaPlugins) {
const dbPlugin = dbPlugins.find((p) => plugin.name === p.metadata?.pluginUid);
if (dbPlugin) {
await MongoPlugin.findByIdAndUpdate(
dbPlugin._id,
{
...plugin,
version: 'v2'
},
{ session }
);
}
}
};

View File

@@ -6,7 +6,6 @@ import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { getUserChatInfoAndAuthTeamPoints } from '@/service/support/permission/auth/team';
import { PostWorkflowDebugProps, PostWorkflowDebugResponse } from '@/global/core/workflow/api';
import { authPluginCrud } from '@fastgpt/service/support/permission/auth/plugin';
import { NextAPI } from '@/service/middleware/entry';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { defaultApp } from '@/web/core/app/constants';
@@ -15,13 +14,7 @@ async function handler(
req: NextApiRequest,
res: NextApiResponse
): Promise<PostWorkflowDebugResponse> {
const {
nodes = [],
edges = [],
variables = {},
appId,
pluginId
} = req.body as PostWorkflowDebugProps;
const { nodes = [], edges = [], variables = {}, appId } = req.body as PostWorkflowDebugProps;
if (!nodes) {
throw new Error('Prams Error');
@@ -39,8 +32,7 @@ async function handler(
req,
authToken: true
}),
appId && authApp({ req, authToken: true, appId, per: ReadPermissionVal }),
pluginId && authPluginCrud({ req, authToken: true, pluginId, per: 'r' })
authApp({ req, authToken: true, appId, per: ReadPermissionVal })
]);
// auth balance

View File

@@ -1,375 +0,0 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Box, Flex, IconButton, useTheme, useDisclosure, Button } from '@chakra-ui/react';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { useTranslation } from 'next-i18next';
import { useCopyData } from '@/web/common/hooks/useCopyData';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import dynamic from 'next/dynamic';
import MyIcon from '@fastgpt/web/components/common/Icon';
import ChatTest, { type ChatTestComponentRef } from '@/components/core/workflow/Flow/ChatTest';
import { uiWorkflow2StoreWorkflow } from '@/components/core/workflow/utils';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { getErrText } from '@fastgpt/global/common/error/utils';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import {
checkWorkflowNodeAndConnection,
filterSensitiveNodesData
} from '@/web/core/workflow/utils';
import { useBeforeunload } from '@fastgpt/web/hooks/useBeforeunload';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { formatTime2HM } from '@fastgpt/global/common/string/time';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext, getWorkflowStore } from '@/components/core/workflow/context';
import { useInterval, useUpdateEffect } from 'ahooks';
import { useI18n } from '@/web/context/I18n';
import { AppContext } from '@/web/core/app/context/appContext';
const ImportSettings = dynamic(() => import('@/components/core/workflow/Flow/ImportSettings'));
const PublishHistories = dynamic(
() => import('@/components/core/workflow/components/PublishHistoriesSlider')
);
type Props = { onClose: () => void };
const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
ChatTestRef,
setWorkflowTestData,
onClose
}: Props & {
ChatTestRef: React.RefObject<ChatTestComponentRef>;
setWorkflowTestData: React.Dispatch<
React.SetStateAction<
| {
nodes: StoreNodeItemType[];
edges: StoreEdgeItemType[];
}
| undefined
>
>;
}) {
const { appDetail } = useContextSelector(AppContext, (v) => v);
const isV2Workflow = appDetail?.version === 'v2';
const theme = useTheme();
const { toast } = useToast();
const { t } = useTranslation();
const { appT } = useI18n();
const { copyData } = useCopyData();
const { openConfirm: openConfigPublish, ConfirmModal } = useConfirm({
content: t('core.app.Publish Confirm')
});
const { publishApp, updateAppDetail } = useContextSelector(AppContext, (v) => v);
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
const [isSaving, setIsSaving] = useState(false);
const [saveLabel, setSaveLabel] = useState(t('core.app.Onclick to save'));
const onUpdateNodeError = useContextSelector(WorkflowContext, (v) => v.onUpdateNodeError);
const { isOpen: isOpenImport, onOpen: onOpenImport, onClose: onCloseImport } = useDisclosure();
const isShowVersionHistories = useContextSelector(
WorkflowContext,
(v) => v.isShowVersionHistories
);
const setIsShowVersionHistories = useContextSelector(
WorkflowContext,
(v) => v.setIsShowVersionHistories
);
const workflowDebugData = useContextSelector(WorkflowContext, (v) => v.workflowDebugData);
const flowData2StoreDataAndCheck = useCallback(async () => {
const { nodes } = await getWorkflowStore();
const checkResults = checkWorkflowNodeAndConnection({ nodes, edges });
if (!checkResults) {
const storeNodes = uiWorkflow2StoreWorkflow({ nodes, edges });
return storeNodes;
} else {
checkResults.forEach((nodeId) => onUpdateNodeError(nodeId, true));
toast({
status: 'warning',
title: t('core.workflow.Check Failed')
});
}
}, [edges, onUpdateNodeError, t, toast]);
const onclickSave = useCallback(
async (forbid?: boolean) => {
// version preview / debug mode, not save
if (!isV2Workflow || isShowVersionHistories || forbid) return;
const { nodes } = await getWorkflowStore();
if (nodes.length === 0) return null;
setIsSaving(true);
const storeWorkflow = uiWorkflow2StoreWorkflow({ nodes, edges });
try {
await updateAppDetail({
...storeWorkflow,
type: AppTypeEnum.advanced,
chatConfig: appDetail.chatConfig,
//@ts-ignore
version: 'v2'
});
setSaveLabel(
t('core.app.Auto Save time', {
time: formatTime2HM()
})
);
// ChatTestRef.current?.resetChatTest();
} catch (error) {}
setIsSaving(false);
return null;
},
[isV2Workflow, isShowVersionHistories, edges, updateAppDetail, appDetail.chatConfig, t]
);
const onclickPublish = useCallback(async () => {
setIsSaving(true);
const data = await flowData2StoreDataAndCheck();
if (data) {
try {
await publishApp({
...data,
type: AppTypeEnum.advanced,
chatConfig: appDetail.chatConfig,
//@ts-ignore
version: 'v2'
});
toast({
status: 'success',
title: t('core.app.Publish Success')
});
ChatTestRef.current?.resetChatTest();
} catch (error) {
toast({
status: 'warning',
title: getErrText(error, t('core.app.Publish Failed'))
});
}
}
setIsSaving(false);
}, [flowData2StoreDataAndCheck, publishApp, appDetail.chatConfig, toast, t, ChatTestRef]);
const saveAndBack = useCallback(async () => {
try {
await onclickSave();
onClose();
} catch (error) {}
}, [onClose, onclickSave]);
const onExportWorkflow = useCallback(async () => {
const data = await flowData2StoreDataAndCheck();
if (data) {
copyData(
JSON.stringify(
{
nodes: filterSensitiveNodesData(data.nodes),
edges: data.edges,
chatConfig: appDetail.chatConfig
},
null,
2
),
appT('Export Config Successful')
);
}
}, [appDetail.chatConfig, appT, copyData, flowData2StoreDataAndCheck]);
// effect
useBeforeunload({
callback: onclickSave,
tip: t('core.common.tip.leave page')
});
useInterval(() => {
if (!appDetail._id) return;
onclickSave(!!workflowDebugData);
}, 20000);
const Render = useMemo(() => {
return (
<>
<Flex
py={3}
px={[2, 5, 8]}
borderBottom={theme.borders.base}
alignItems={'center'}
userSelect={'none'}
bg={'myGray.25'}
h={'67px'}
>
<IconButton
size={'smSquare'}
icon={<MyIcon name={'common/backFill'} w={'14px'} />}
borderRadius={'50%'}
w={'26px'}
h={'26px'}
borderColor={'myGray.300'}
variant={'whiteBase'}
aria-label={''}
isLoading={isSaving}
onClick={saveAndBack}
/>
<Box ml={[2, 4]}>
<Box fontSize={'md'} fontWeight={'bold'}>
{appDetail.name}
</Box>
{!isShowVersionHistories && isV2Workflow && (
<MyTooltip label={t('core.app.Onclick to save')}>
<Box
fontSize={'xs'}
mt={1}
display={'inline-block'}
borderRadius={'xs'}
cursor={'pointer'}
onClick={() => onclickSave()}
color={'myGray.500'}
>
{saveLabel}
</Box>
</MyTooltip>
)}
</Box>
<Box flex={1} />
{!isShowVersionHistories && (
<>
<MyMenu
Button={
<IconButton
mr={[2, 4]}
icon={<MyIcon name={'more'} w={'14px'} p={2} />}
aria-label={''}
size={'sm'}
variant={'whitePrimary'}
/>
}
menuList={[
{
children: [
{
label: appT('Import Configs'),
icon: 'common/importLight',
onClick: onOpenImport
},
{
label: appT('Export Configs'),
icon: 'export',
onClick: onExportWorkflow
}
]
}
]}
/>
<IconButton
mr={[2, 4]}
icon={<MyIcon name={'history'} w={'18px'} />}
aria-label={''}
size={'sm'}
w={'30px'}
variant={'whitePrimary'}
onClick={() => setIsShowVersionHistories(true)}
/>
</>
)}
<Button
size={'sm'}
leftIcon={<MyIcon name={'core/workflow/debug'} w={['14px', '16px']} />}
variant={'whitePrimary'}
onClick={async () => {
const data = await flowData2StoreDataAndCheck();
if (data) {
setWorkflowTestData(data);
}
}}
>
{t('core.workflow.Debug')}
</Button>
{!isShowVersionHistories && (
<Button
ml={[2, 4]}
size={'sm'}
isLoading={isSaving}
leftIcon={<MyIcon name={'common/publishFill'} w={['14px', '16px']} />}
onClick={openConfigPublish(onclickPublish)}
>
{t('core.app.Publish')}
</Button>
)}
</Flex>
<ConfirmModal confirmText={t('core.app.Publish')} />
</>
);
}, [
theme.borders.base,
isSaving,
saveAndBack,
appDetail.name,
isShowVersionHistories,
isV2Workflow,
t,
saveLabel,
appT,
onOpenImport,
onExportWorkflow,
openConfigPublish,
onclickPublish,
ConfirmModal,
onclickSave,
setIsShowVersionHistories,
flowData2StoreDataAndCheck,
setWorkflowTestData
]);
return (
<>
{Render}
{isOpenImport && <ImportSettings onClose={onCloseImport} />}
{isShowVersionHistories && <PublishHistories />}
</>
);
});
const Header = (props: Props) => {
const ChatTestRef = useRef<ChatTestComponentRef>(null);
const [workflowTestData, setWorkflowTestData] = useState<{
nodes: StoreNodeItemType[];
edges: StoreEdgeItemType[];
}>();
const { isOpen: isOpenTest, onOpen: onOpenTest, onClose: onCloseTest } = useDisclosure();
useUpdateEffect(() => {
onOpenTest();
}, [workflowTestData]);
return (
<>
<RenderHeaderContainer
{...props}
ChatTestRef={ChatTestRef}
setWorkflowTestData={setWorkflowTestData}
/>
<ChatTest ref={ChatTestRef} isOpen={isOpenTest} {...workflowTestData} onClose={onCloseTest} />
</>
);
};
export default React.memo(Header);

View File

@@ -27,7 +27,7 @@ import {
getCollaboratorList
} from '@/web/core/app/api/collaborator';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '@/web/core/app/context/appContext';
import { AppContext } from '@/pages/app/detail/components/context';
import {
AppDefaultPermissionVal,
AppPermissionList

View File

@@ -11,7 +11,8 @@ import {
Tbody,
useTheme,
useDisclosure,
ModalBody
ModalBody,
HStack
} from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
@@ -35,13 +36,17 @@ import { formatChatValue2InputType } from '@/components/ChatBox/utils';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { useI18n } from '@/web/context/I18n';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '../context';
import { cardStyles } from '../constants';
const Logs = ({ appId }: { appId: string }) => {
const Logs = () => {
const { t } = useTranslation();
const { appT } = useI18n();
const { isPc } = useSystemStore();
const appId = useContextSelector(AppContext, (v) => v.appId);
const [dateRange, setDateRange] = useState<DateRangeType>({
from: addDays(new Date(), -7),
to: new Date()
@@ -72,8 +77,8 @@ const Logs = ({ appId }: { appId: string }) => {
const [detailLogsId, setDetailLogsId] = useState<string>();
return (
<Flex flexDirection={'column'} h={'100%'} pt={[1, 5]} position={'relative'}>
<Box px={[4, 8]}>
<>
<Box {...cardStyles} boxShadow={2} px={[4, 8]} py={[4, 6]}>
{isPc && (
<>
<Box fontWeight={'bold'} fontSize={['md', 'lg']} mb={2}>
@@ -96,97 +101,106 @@ const Logs = ({ appId }: { appId: string }) => {
</Box>
{/* table */}
<TableContainer mt={[0, 3]} flex={'1 0 0'} h={0} overflowY={'auto'} px={[4, 8]}>
<Table variant={'simple'} fontSize={'sm'}>
<Thead>
<Tr>
<Th>{t('core.app.logs.Source And Time')}</Th>
<Th>{appT('Logs Title')}</Th>
<Th>{appT('Logs Message Total')}</Th>
<Th>{appT('Feedback Count')}</Th>
<Th>{t('core.app.feedback.Custom feedback')}</Th>
<Th>{appT('Mark Count')}</Th>
</Tr>
</Thead>
<Tbody fontSize={'xs'}>
{logs.map((item) => (
<Tr
key={item._id}
_hover={{ bg: 'myWhite.600' }}
cursor={'pointer'}
title={'点击查看对话详情'}
onClick={() => setDetailLogsId(item.id)}
>
<Td>
<Box>{t(ChatSourceMap[item.source]?.name || 'UnKnow')}</Box>
<Box color={'myGray.500'}>{dayjs(item.time).format('YYYY/MM/DD HH:mm')}</Box>
</Td>
<Td className="textEllipsis" maxW={'250px'}>
{item.title}
</Td>
<Td>{item.messageCount}</Td>
<Td w={'100px'}>
{!!item?.userGoodFeedbackCount && (
<Flex
mb={item?.userGoodFeedbackCount ? 1 : 0}
bg={'green.100'}
color={'green.600'}
px={3}
py={1}
alignItems={'center'}
justifyContent={'center'}
borderRadius={'md'}
fontWeight={'bold'}
>
<MyIcon
mr={1}
name={'core/chat/feedback/goodLight'}
color={'green.600'}
w={'14px'}
/>
{item.userGoodFeedbackCount}
</Flex>
)}
{!!item?.userBadFeedbackCount && (
<Flex
bg={'#FFF2EC'}
color={'#C96330'}
px={3}
py={1}
alignItems={'center'}
justifyContent={'center'}
borderRadius={'md'}
fontWeight={'bold'}
>
<MyIcon
mr={1}
name={'core/chat/feedback/badLight'}
color={'#C96330'}
w={'14px'}
/>
{item.userBadFeedbackCount}
</Flex>
)}
{!item?.userGoodFeedbackCount && !item?.userBadFeedbackCount && <>-</>}
</Td>
<Td>{item.customFeedbacksCount || '-'}</Td>
<Td>{item.markCount}</Td>
<Flex
flexDirection={'column'}
{...cardStyles}
boxShadow={3.5}
mt={4}
px={[4, 8]}
py={[4, 6]}
flex={'1 0 0'}
>
<TableContainer mt={[0, 3]} flex={'1 0 0'} h={0} overflowY={'auto'}>
<Table variant={'simple'} fontSize={'sm'}>
<Thead>
<Tr>
<Th>{t('core.app.logs.Source And Time')}</Th>
<Th>{appT('Logs Title')}</Th>
<Th>{appT('Logs Message Total')}</Th>
<Th>{appT('Feedback Count')}</Th>
<Th>{t('core.app.feedback.Custom feedback')}</Th>
<Th>{appT('Mark Count')}</Th>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
{logs.length === 0 && !isLoading && <EmptyTip text={appT('Logs Empty')}></EmptyTip>}
<Flex w={'100%'} p={4} alignItems={'center'} justifyContent={'flex-end'}>
<DateRangePicker
defaultDate={dateRange}
position="top"
onChange={setDateRange}
onSuccess={() => getData(1)}
/>
<Box ml={3}>
</Thead>
<Tbody fontSize={'xs'}>
{logs.map((item) => (
<Tr
key={item._id}
_hover={{ bg: 'myWhite.600' }}
cursor={'pointer'}
title={'点击查看对话详情'}
onClick={() => setDetailLogsId(item.id)}
>
<Td>
<Box>{t(ChatSourceMap[item.source]?.name || 'UnKnow')}</Box>
<Box color={'myGray.500'}>{dayjs(item.time).format('YYYY/MM/DD HH:mm')}</Box>
</Td>
<Td className="textEllipsis" maxW={'250px'}>
{item.title}
</Td>
<Td>{item.messageCount}</Td>
<Td w={'100px'}>
{!!item?.userGoodFeedbackCount && (
<Flex
mb={item?.userGoodFeedbackCount ? 1 : 0}
bg={'green.100'}
color={'green.600'}
px={3}
py={1}
alignItems={'center'}
justifyContent={'center'}
borderRadius={'md'}
fontWeight={'bold'}
>
<MyIcon
mr={1}
name={'core/chat/feedback/goodLight'}
color={'green.600'}
w={'14px'}
/>
{item.userGoodFeedbackCount}
</Flex>
)}
{!!item?.userBadFeedbackCount && (
<Flex
bg={'#FFF2EC'}
color={'#C96330'}
px={3}
py={1}
alignItems={'center'}
justifyContent={'center'}
borderRadius={'md'}
fontWeight={'bold'}
>
<MyIcon
mr={1}
name={'core/chat/feedback/badLight'}
color={'#C96330'}
w={'14px'}
/>
{item.userBadFeedbackCount}
</Flex>
)}
{!item?.userGoodFeedbackCount && !item?.userBadFeedbackCount && <>-</>}
</Td>
<Td>{item.customFeedbacksCount || '-'}</Td>
<Td>{item.markCount}</Td>
</Tr>
))}
</Tbody>
</Table>
{logs.length === 0 && !isLoading && <EmptyTip text={appT('Logs Empty')}></EmptyTip>}
</TableContainer>
<HStack w={'100%'} mt={3} justifyContent={'flex-end'}>
<DateRangePicker
defaultDate={dateRange}
position="top"
onChange={setDateRange}
onSuccess={() => getData(1)}
/>
<Pagination />
</Box>
</HStack>
</Flex>
{!!detailLogsId && (
@@ -206,7 +220,7 @@ const Logs = ({ appId }: { appId: string }) => {
>
<ModalBody whiteSpace={'pre-wrap'}>{t('core.chat.Mark Description')}</ModalBody>
</MyModal>
</Flex>
</>
);
};

View File

@@ -0,0 +1,191 @@
import React, { useCallback, useMemo } from 'react';
import { Box, Flex, Button, IconButton } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import dynamic from 'next/dynamic';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useBeforeunload } from '@fastgpt/web/hooks/useBeforeunload';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext, getWorkflowStore } from '../WorkflowComponents/context';
import { useInterval } from 'ahooks';
import { AppContext, TabEnum } from '../context';
import PopoverConfirm from '@fastgpt/web/components/common/MyPopover/PopoverConfirm';
import { useRouter } from 'next/router';
import AppCard from '../WorkflowComponents/AppCard';
import { uiWorkflow2StoreWorkflow } from '../WorkflowComponents/utils';
const PublishHistories = dynamic(() => import('../PublishHistoriesSlider'));
const Header = () => {
const { t } = useTranslation();
const router = useRouter();
const { appDetail, onPublish, currentTab } = useContextSelector(AppContext, (v) => v);
const isV2Workflow = appDetail?.version === 'v2';
const {
flowData2StoreDataAndCheck,
onSaveWorkflow,
setHistoriesDefaultData,
historiesDefaultData,
initData
} = useContextSelector(WorkflowContext, (v) => v);
const onclickPublish = useCallback(async () => {
const data = flowData2StoreDataAndCheck();
if (data) {
await onPublish({
...data,
chatConfig: appDetail.chatConfig,
//@ts-ignore
version: 'v2'
});
}
}, [flowData2StoreDataAndCheck, onPublish, appDetail.chatConfig]);
const saveAndBack = useCallback(async () => {
try {
await onSaveWorkflow();
router.push('/app/list');
} catch (error) {}
}, [onSaveWorkflow, router]);
// effect
useBeforeunload({
callback: onSaveWorkflow,
tip: t('core.common.tip.leave page')
});
useInterval(() => {
if (!appDetail._id) return;
onSaveWorkflow();
}, 40000);
const Render = useMemo(() => {
return (
<>
{/* {!isPc && (
<Flex pt={2} justifyContent={'center'}>
<RouteTab />
</Flex>
)} */}
<Flex
mt={[2, 0]}
py={3}
pl={[2, 4]}
pr={[2, 6]}
borderBottom={'base'}
alignItems={'center'}
userSelect={'none'}
h={'67px'}
{...(currentTab === TabEnum.appEdit
? {
bg: 'myGray.25'
}
: {
bg: 'transparent',
borderBottomColor: 'transparent'
})}
>
{/* back */}
<MyIcon
name={'common/leftArrowLight'}
w={'1.75rem'}
cursor={'pointer'}
onClick={saveAndBack}
/>
{/* app info */}
<Box ml={1}>
<AppCard
showSaveStatus={
!historiesDefaultData && isV2Workflow && currentTab === TabEnum.appEdit
}
/>
</Box>
{/* {isPc && (
<Box position={'absolute'} left={'50%'} transform={'translateX(-50%)'}>
<RouteTab />
</Box>
)} */}
<Box flex={1} />
{currentTab === TabEnum.appEdit && (
<>
{!historiesDefaultData && (
<IconButton
// mr={[2, 4]}
icon={<MyIcon name={'history'} w={'18px'} />}
aria-label={''}
size={'sm'}
w={'30px'}
variant={'whitePrimary'}
onClick={async () => {
const { nodes, edges } = uiWorkflow2StoreWorkflow(await getWorkflowStore());
setHistoriesDefaultData({
nodes,
edges,
chatConfig: appDetail.chatConfig
});
}}
/>
)}
{/* <Button
size={'sm'}
leftIcon={<MyIcon name={'core/workflow/debug'} w={['14px', '16px']} />}
variant={'whitePrimary'}
onClick={async () => {
const data = flowData2StoreDataAndCheck();
if (data) {
setWorkflowTestData(data);
}
}}
>
{t('core.workflow.Debug')}
</Button> */}
{!historiesDefaultData && (
<PopoverConfirm
showCancel
content={t('core.app.Publish Confirm')}
Trigger={
<Button
ml={[2, 4]}
size={'sm'}
leftIcon={<MyIcon name={'common/publishFill'} w={['14px', '16px']} />}
>
{t('core.app.Publish')}
</Button>
}
onConfirm={() => onclickPublish()}
/>
)}
</>
)}
</Flex>
{historiesDefaultData && (
<PublishHistories
initData={initData}
onClose={() => {
setHistoriesDefaultData(undefined);
}}
defaultData={historiesDefaultData}
/>
)}
</>
);
}, [
appDetail.chatConfig,
currentTab,
historiesDefaultData,
initData,
isV2Workflow,
onclickPublish,
saveAndBack,
setHistoriesDefaultData,
t
]);
return Render;
};
export default React.memo(Header);

View File

@@ -0,0 +1,72 @@
import React from 'react';
import { pluginSystemModuleTemplates } from '@fastgpt/global/core/workflow/template/constants';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { v1Workflow2V2 } from '@/web/core/workflow/adapt';
import WorkflowContextProvider, { WorkflowContext } from '../WorkflowComponents/context';
import { useContextSelector } from 'use-context-selector';
import { AppContext, TabEnum } from '../context';
import { useMount } from 'ahooks';
import Header from './Header';
import { Flex } from '@chakra-ui/react';
import { workflowBoxStyles } from '../constants';
import dynamic from 'next/dynamic';
import { cloneDeep } from 'lodash';
import Flow from '../WorkflowComponents/Flow';
const Logs = dynamic(() => import('../Logs/index'));
const PublishChannel = dynamic(() => import('../Publish'));
const WorkflowEdit = () => {
const { appDetail, currentTab } = useContextSelector(AppContext, (e) => e);
const isV2Workflow = appDetail?.version === 'v2';
const { openConfirm, ConfirmModal } = useConfirm({
showCancel: false,
content:
'检测到您的高级编排为旧版,系统将为您自动格式化成新版工作流。\n\n由于版本差异较大会导致一些工作流无法正常排布请重新手动连接工作流。如仍异常可尝试删除对应节点后重新添加。\n\n你可以直接点击调试进行工作流测试调试完毕后点击发布。直到你点击发布新工作流才会真正保存生效。\n\n在你发布新工作流前自动保存不会生效。'
});
const initData = useContextSelector(WorkflowContext, (v) => v.initData);
useMount(() => {
if (!isV2Workflow) {
openConfirm(() => {
initData(JSON.parse(JSON.stringify(v1Workflow2V2((appDetail.modules || []) as any))));
})();
} else {
initData(
cloneDeep({
nodes: appDetail.modules || [],
edges: appDetail.edges || []
})
);
}
});
return (
<Flex {...workflowBoxStyles}>
<Header />
{currentTab === TabEnum.appEdit ? (
<Flow />
) : (
<Flex flexDirection={'column'} h={'100%'} px={4} pb={4}>
{currentTab === TabEnum.publish && <PublishChannel />}
{currentTab === TabEnum.logs && <Logs />}
</Flex>
)}
{!isV2Workflow && <ConfirmModal countDown={0} />}
</Flex>
);
};
const Render = () => {
return (
<WorkflowContextProvider basicNodeTemplates={pluginSystemModuleTemplates}>
<WorkflowEdit />
</WorkflowContextProvider>
);
};
export default React.memo(Render);

View File

@@ -6,11 +6,7 @@ import { useI18n } from '@/web/context/I18n';
const API = ({ appId }: { appId: string }) => {
const { publishT } = useI18n();
return (
<Box pt={3}>
<ApiKeyTable tips={publishT('app key tips')} appId={appId} />
</Box>
);
return <ApiKeyTable tips={publishT('app key tips')} appId={appId} />;
};
export default API;

View File

@@ -47,6 +47,7 @@ import { useI18n } from '@/web/context/I18n';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import MyBox from '@fastgpt/web/components/common/MyBox';
const SelectUsingWayModal = dynamic(() => import('./SelectUsingWayModal'));
@@ -72,7 +73,7 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => {
);
return (
<Box position={'relative'} pt={3} px={5} minH={'50vh'}>
<MyBox h={'100%'} isLoading={isFetching} position={'relative'}>
<Flex justifyContent={'space-between'}>
<HStack>
<Box color={'myGray.900'} fontSize={'lg'}>
@@ -241,8 +242,7 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => {
/>
)}
<ConfirmModal />
<Loading loading={isFetching} fixed={false} />
</Box>
</MyBox>
);
};

View File

@@ -1,5 +1,5 @@
import React, { useRef, useState } from 'react';
import { Box, useTheme } from '@chakra-ui/react';
import { Box, Flex, useTheme } from '@chakra-ui/react';
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
import dynamic from 'next/dynamic';
@@ -7,13 +7,20 @@ import dynamic from 'next/dynamic';
import MyRadio from '@/components/common/MyRadio';
import { useTranslation } from 'next-i18next';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '../context';
import { cardStyles } from '../constants';
import Link from './Link';
const API = dynamic(() => import('./API'));
const FeiShu = dynamic(() => import('./FeiShu'));
const OutLink = ({ appId }: { appId: string }) => {
const OutLink = () => {
const { t } = useTranslation();
const theme = useTheme();
const appId = useContextSelector(AppContext, (v) => v.appId);
const publishList = useRef([
{
icon: '/imgs/modal/shareFill.svg',
@@ -38,11 +45,8 @@ const OutLink = ({ appId }: { appId: string }) => {
const [linkType, setLinkType] = useState<PublishChannelEnum>(PublishChannelEnum.share);
return (
<Box pt={[1, 5]}>
<Box color={'myGray.900'} fontSize={'lg'} mb={2} px={[4, 8]}>
{t('core.app.navbar.Publish app')}
</Box>
<Box pb={[5, 7]} px={[4, 8]} borderBottom={theme.borders.base}>
<>
<Box {...cardStyles} boxShadow={2} px={[4, 8]} py={[4, 6]}>
<MyRadio
gridTemplateColumns={['repeat(1,1fr)', 'repeat(auto-fill, minmax(0, 400px))']}
iconSize={'20px'}
@@ -52,12 +56,22 @@ const OutLink = ({ appId }: { appId: string }) => {
/>
</Box>
{linkType === PublishChannelEnum.share && (
<Link appId={appId} type={PublishChannelEnum.share} />
)}
{linkType === PublishChannelEnum.apikey && <API appId={appId} />}
{linkType === PublishChannelEnum.feishu && <FeiShu appId={appId} />}
</Box>
<Flex
flexDirection={'column'}
{...cardStyles}
boxShadow={3.5}
mt={4}
px={[4, 8]}
py={[4, 6]}
flex={'1 0 0'}
>
{linkType === PublishChannelEnum.share && (
<Link appId={appId} type={PublishChannelEnum.share} />
)}
{linkType === PublishChannelEnum.apikey && <API appId={appId} />}
{linkType === PublishChannelEnum.feishu && <FeiShu appId={appId} />}
</Flex>
</>
);
};

View File

@@ -0,0 +1,198 @@
import React, { useCallback, useState } from 'react';
import { getPublishList, postRevertVersion } from '@/web/core/app/api/version';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import CustomRightDrawer from '@fastgpt/web/components/common/MyDrawer/CustomRightDrawer';
import { useTranslation } from 'next-i18next';
import { useMemoizedFn } from 'ahooks';
import { Box, Button, Flex } from '@chakra-ui/react';
import { formatTime2YMDHM } from '@fastgpt/global/common/string/time';
import { useContextSelector } from 'use-context-selector';
import { AppVersionSchemaType } from '@fastgpt/global/core/app/version';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { AppContext } from './context';
import { useI18n } from '@/web/context/I18n';
import { AppSchema } from '@fastgpt/global/core/app/type';
export type InitProps = {
nodes: AppSchema['modules'];
edges: AppSchema['edges'];
chatConfig: AppSchema['chatConfig'];
};
const PublishHistoriesSlider = ({
onClose,
initData,
defaultData
}: {
onClose: () => void;
initData: (data: InitProps) => void;
defaultData: InitProps;
}) => {
const { t } = useTranslation();
const { appT } = useI18n();
const { openConfirm, ConfirmModal } = useConfirm({
content: t('core.workflow.publish.OnRevert version confirm')
});
const { appDetail, setAppDetail } = useContextSelector(AppContext, (v) => v);
const appId = appDetail._id;
const [selectedHistoryId, setSelectedHistoryId] = useState<string>();
const { list, ScrollList, isLoading } = useScrollPagination(getPublishList, {
itemHeight: 49,
overscan: 20,
pageSize: 30,
defaultParams: {
appId
}
});
const onPreview = useCallback(
(data: AppVersionSchemaType) => {
setSelectedHistoryId(data._id);
initData({
nodes: data.nodes,
edges: data.edges,
chatConfig: data.chatConfig
});
},
[initData]
);
const onCloseSlider = useCallback(
(data: InitProps) => {
setSelectedHistoryId(undefined);
initData(data);
onClose();
},
[initData, onClose]
);
const { mutate: onRevert, isLoading: isReverting } = useRequest({
mutationFn: async (data: AppVersionSchemaType) => {
if (!appId) return;
await postRevertVersion(appId, {
versionId: data._id,
editNodes: defaultData.nodes, // old workflow
editEdges: defaultData.edges,
editChatConfig: defaultData.chatConfig
});
setAppDetail((state) => ({
...state,
modules: data.nodes,
edges: data.edges
}));
onCloseSlider(data);
}
});
const showLoading = isLoading || isReverting;
return (
<>
<CustomRightDrawer
onClose={() =>
onCloseSlider({
nodes: defaultData.nodes,
edges: defaultData.edges,
chatConfig: defaultData.chatConfig
})
}
iconSrc="core/workflow/versionHistories"
title={t('core.workflow.publish.histories')}
maxW={'300px'}
px={0}
showMask={false}
top={'60px'}
overflow={'unset'}
>
<Button
mx={'20px'}
variant={'whitePrimary'}
mb={2}
isDisabled={!selectedHistoryId}
onClick={() => {
setSelectedHistoryId(undefined);
initData({
nodes: defaultData.nodes,
edges: defaultData.edges,
chatConfig: defaultData.chatConfig
});
}}
>
{appT('Current settings')}
</Button>
<ScrollList isLoading={showLoading} flex={'1 0 0'} px={5}>
{list.map((data, index) => {
const item = data.data;
return (
<Flex
key={data.index}
alignItems={'center'}
py={3}
px={3}
borderRadius={'md'}
cursor={'pointer'}
fontWeight={500}
_hover={{
bg: 'primary.50'
}}
{...(selectedHistoryId === item._id && {
color: 'primary.600'
})}
onClick={() => onPreview(item)}
>
<Box
w={'12px'}
h={'12px'}
borderWidth={'2px'}
borderColor={'primary.600'}
borderRadius={'50%'}
position={'relative'}
{...(index !== list.length - 1 && {
_after: {
content: '""',
height: '40px',
width: '2px',
bgColor: 'myGray.250',
position: 'absolute',
top: '10px',
left: '3px'
}
})}
></Box>
<Box ml={3} flex={'1 0 0'}>
{formatTime2YMDHM(item.time)}
</Box>
{item._id === selectedHistoryId && (
<MyTooltip label={t('core.workflow.publish.OnRevert version')}>
<MyIcon
name={'core/workflow/revertVersion'}
w={'20px'}
color={'primary.600'}
onClick={(e) => {
e.stopPropagation();
openConfirm(() => onRevert(item))();
}}
/>
</MyTooltip>
)}
</Flex>
);
})}
</ScrollList>
</CustomRightDrawer>
<ConfirmModal />
</>
);
};
export default React.memo(PublishHistoriesSlider);

View File

@@ -0,0 +1,75 @@
import { Box, HStack } from '@chakra-ui/react';
import React, { useCallback, useMemo } from 'react';
import { AppContext, TabEnum } from './context';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import { useI18n } from '@/web/context/I18n';
import { useContextSelector } from 'use-context-selector';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
const RouteTab = () => {
const { t } = useTranslation();
const { appT } = useI18n();
const router = useRouter();
const { appDetail, currentTab } = useContextSelector(AppContext, (v) => v);
const setCurrentTab = useCallback(
(tab: TabEnum) => {
router.push({
query: {
...router.query,
currentTab: tab
}
});
},
[router]
);
const tabList = useMemo(
() => [
{
label: appDetail.type === AppTypeEnum.plugin ? appT('Setting plugin') : appT('Setting app'),
id: TabEnum.appEdit
},
...(appDetail.permission.hasManagePer
? [
{
label: appT('Publish channel'),
id: TabEnum.publish
},
{ label: appT('Chat logs'), id: TabEnum.logs }
]
: [])
],
[appDetail.permission.hasManagePer, appT]
);
return (
<HStack spacing={4} whiteSpace={'nowrap'}>
{tabList.map((tab) => (
<Box
key={tab.id}
px={2}
py={0.5}
{...(currentTab === tab.id
? {
color: 'primary.700'
}
: {
color: 'myGray.600',
cursor: 'pointer',
_hover: {
bg: 'myGray.200',
borderRadius: 'md'
},
onClick: () => setCurrentTab(tab.id)
})}
>
{tab.label}
</Box>
))}
</HStack>
);
};
export default RouteTab;

View File

@@ -0,0 +1,187 @@
import React, { useState } from 'react';
import {
Box,
Flex,
Button,
IconButton,
HStack,
Modal,
ModalBody,
Checkbox,
ModalFooter
} from '@chakra-ui/react';
import { DragHandleIcon } from '@chakra-ui/icons';
import { useRouter } from 'next/router';
import { AppSchema } from '@fastgpt/global/core/app/type.d';
import { useTranslation } from 'next-i18next';
import Avatar from '@/components/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import TagsEditModal from '../TagsEditModal';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { AppContext } from '@/pages/app/detail/components/context';
import { useContextSelector } from 'use-context-selector';
import PermissionIconText from '@/components/support/permission/IconText';
import MyTag from '@fastgpt/web/components/common/Tag/index';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { useI18n } from '@/web/context/I18n';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { postTransition2Workflow } from '@/web/core/app/api/app';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
const AppCard = () => {
const router = useRouter();
const { t } = useTranslation();
const { appT } = useI18n();
const { appDetail, setAppDetail, onOpenInfoEdit, onDelApp } = useContextSelector(
AppContext,
(v) => v
);
const appId = appDetail._id;
const { feConfigs } = useSystemStore();
const [TeamTagsSet, setTeamTagsSet] = useState<AppSchema>();
// transition to workflow
const [transitionCreateNew, setTransitionCreateNew] = useState<boolean>();
const { runAsync: onTransition, loading: transiting } = useRequest2(
() => postTransition2Workflow({ appId, createNew: transitionCreateNew }),
{
onSuccess: ({ id }) => {
if (id) {
router.replace({
query: {
appId: id
}
});
} else {
setAppDetail((state) => ({
...state,
type: AppTypeEnum.workflow
}));
}
},
successToast: t('common.Success')
}
);
return (
<>
{/* basic info */}
<Box px={6} py={4} position={'relative'}>
<Flex alignItems={'center'}>
<Avatar src={appDetail.avatar} borderRadius={'md'} w={'28px'} />
<Box ml={3} fontWeight={'bold'} fontSize={'md'} flex={'1 0 0'}>
{appDetail.name}
</Box>
</Flex>
<Box
flex={1}
mt={3}
mb={4}
className={'textEllipsis3'}
wordBreak={'break-all'}
color={'myGray.600'}
fontSize={'xs'}
minH={'46px'}
>
{appDetail.intro || t('core.app.tip.Add a intro to app')}
</Box>
<HStack>
<Button
size={['sm', 'md']}
variant={'whitePrimary'}
leftIcon={<MyIcon name={'core/chat/chatLight'} w={'16px'} />}
onClick={() => router.push(`/chat?appId=${appId}`)}
>
{t('core.Chat')}
</Button>
{appDetail.permission.hasWritePer && feConfigs?.show_team_chat && (
<Button
mr={3}
size={['sm', 'md']}
variant={'whitePrimary'}
leftIcon={<DragHandleIcon w={'16px'} />}
onClick={() => setTeamTagsSet(appDetail)}
>
{t('common.Team Tags Set')}
</Button>
)}
{appDetail.permission.hasManagePer && (
<Button
size={['sm', 'md']}
variant={'whitePrimary'}
leftIcon={<MyIcon name={'common/settingLight'} w={'16px'} />}
onClick={onOpenInfoEdit}
>
{t('common.Setting')}
</Button>
)}
{appDetail.permission.isOwner && (
<MyMenu
Button={
<IconButton
variant={'whiteBase'}
size={['smSquare', 'mdSquare']}
icon={<MyIcon name={'more'} w={'1rem'} />}
aria-label={''}
/>
}
menuList={[
{
children: [
{
icon: 'core/app/type/workflow',
label: appT('Transition to workflow'),
onClick: () => setTransitionCreateNew(true)
}
]
},
{
children: [
{
icon: 'delete',
type: 'danger',
label: t('common.Delete'),
onClick: onDelApp
}
]
}
]}
/>
)}
<Box flex={1} />
<MyTag
type="borderFill"
colorSchema="gray"
onClick={() => (appDetail.permission.hasManagePer ? onOpenInfoEdit() : undefined)}
>
<PermissionIconText defaultPermission={appDetail.defaultPermission} fontSize={'md'} />
</MyTag>
</HStack>
</Box>
{TeamTagsSet && <TagsEditModal onClose={() => setTeamTagsSet(undefined)} />}
{transitionCreateNew !== undefined && (
<MyModal isOpen title={appT('Transition to workflow')} iconSrc="core/app/type/workflow">
<ModalBody>
<Box mb={3}>{appT('Transition to workflow create new tip')}</Box>
<HStack cursor={'pointer'} onClick={() => setTransitionCreateNew((state) => !state)}>
<Checkbox isChecked={transitionCreateNew} />
<Box>{appT('Transition to workflow create new placeholder')}</Box>
</HStack>
</ModalBody>
<ModalFooter>
<Button variant={'whiteBase'} onClick={() => setTransitionCreateNew(undefined)} mr={3}>
{t('common.Close')}
</Button>
<Button variant={'dangerFill'} isLoading={transiting} onClick={() => onTransition()}>
{t('common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
)}
</>
);
};
export default React.memo(AppCard);

View File

@@ -0,0 +1,63 @@
import { Box, Flex, IconButton } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import React, { useEffect } from 'react';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useSafeState } from 'ahooks';
import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
import { form2AppWorkflow } from '@/web/core/app/utils';
import { useI18n } from '@/web/context/I18n';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '../context';
import { useChatTest } from '../useChatTest';
const ChatTest = ({ appForm }: { appForm: AppSimpleEditFormType }) => {
const { t } = useTranslation();
const { appT } = useI18n();
const { appDetail } = useContextSelector(AppContext, (v) => v);
const [workflowData, setWorkflowData] = useSafeState({
nodes: appDetail.modules || [],
edges: appDetail.edges || []
});
useEffect(() => {
const { nodes, edges } = form2AppWorkflow(appForm);
setWorkflowData({ nodes, edges });
}, [appForm, setWorkflowData]);
const { resetChatBox, ChatBox } = useChatTest({
...workflowData,
chatConfig: appForm.chatConfig
});
return (
<Flex position={'relative'} flexDirection={'column'} h={'100%'} py={4}>
<Flex px={[2, 5]}>
<Box fontSize={['md', 'lg']} fontWeight={'bold'} flex={1}>
{appT('Chat Debug')}
</Box>
<MyTooltip label={t('core.chat.Restart')}>
<IconButton
className="chat"
size={'smSquare'}
icon={<MyIcon name={'common/clearLight'} w={'14px'} />}
variant={'whiteDanger'}
borderRadius={'md'}
aria-label={'delete'}
onClick={(e) => {
e.stopPropagation();
resetChatBox();
}}
/>
</MyTooltip>
</Flex>
<Box flex={1}>
<ChatBox />
</Box>
</Flex>
);
};
export default React.memo(ChatTest);

View File

@@ -0,0 +1,80 @@
import React from 'react';
import { Box } from '@chakra-ui/react';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useMount } from 'ahooks';
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
import { appWorkflow2Form } from '@fastgpt/global/core/app/utils';
import ChatTest from './ChatTest';
import AppCard from './AppCard';
import EditForm from './EditForm';
import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
import { v1Workflow2V2 } from '@/web/core/workflow/adapt';
import { AppContext } from '@/pages/app/detail/components/context';
import { useContextSelector } from 'use-context-selector';
import { cardStyles } from '../constants';
import styles from './styles.module.scss';
const Edit = ({
appForm,
setAppForm
}: {
appForm: AppSimpleEditFormType;
setAppForm: React.Dispatch<React.SetStateAction<AppSimpleEditFormType>>;
}) => {
const { isPc } = useSystemStore();
const { loadAllDatasets } = useDatasetStore();
const { appDetail } = useContextSelector(AppContext, (v) => v);
// show selected dataset
useMount(() => {
loadAllDatasets();
setAppForm(
appWorkflow2Form({
nodes: appDetail.modules,
chatConfig: appDetail.chatConfig
})
);
if (appDetail.version !== 'v2') {
setAppForm(
appWorkflow2Form({
nodes: v1Workflow2V2((appDetail.modules || []) as any)?.nodes,
chatConfig: appDetail.chatConfig
})
);
}
});
return (
<Box
display={['block', 'flex']}
flex={'1 0 0'}
h={0}
pt={[2, 1.5]}
pl={[2, 1]}
gap={1}
borderRadius={'lg'}
overflowY={['auto', 'unset']}
>
<Box className={styles.EditAppBox} pr={[0, 1]} overflowY={'auto'} minW={'580px'} flex={'1'}>
<Box {...cardStyles} boxShadow={'2'}>
<AppCard />
</Box>
<Box mt={4} {...cardStyles} boxShadow={'3.5'} w={'auto'}>
<EditForm appForm={appForm} setAppForm={setAppForm} />
</Box>
</Box>
{isPc && (
<Box {...cardStyles} boxShadow={'3'} flex={'2 0 0'} w={0}>
<ChatTest appForm={appForm} />
</Box>
)}
</Box>
);
};
export default React.memo(Edit);

View File

@@ -0,0 +1,480 @@
import React, { useEffect, useMemo, useTransition } from 'react';
import {
Box,
Flex,
Grid,
BoxProps,
useTheme,
useDisclosure,
Button,
HStack
} from '@chakra-ui/react';
import { AddIcon, SmallAddIcon } from '@chakra-ui/icons';
import { useFieldArray, UseFormReturn } from 'react-hook-form';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import type { AppSimpleEditFormType } from '@fastgpt/global/core/app/type.d';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
import dynamic from 'next/dynamic';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import Avatar from '@/components/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import VariableEdit from '@/components/core/app/VariableEdit';
import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor';
import { formatEditorVariablePickerIcon } from '@fastgpt/global/core/workflow/utils';
import SearchParamsTip from '@/components/core/dataset/SearchParamsTip';
import SettingLLMModel from '@/components/core/ai/SettingLLMModel';
import type { SettingAIDataType } from '@fastgpt/global/core/app/type.d';
import DeleteIcon, { hoverDeleteStyles } from '@fastgpt/web/components/common/Icon/delete';
import { TTSTypeEnum } from '@/web/core/app/constants';
import { getSystemVariables } from '@/web/core/app/utils';
import { useUpdate } from 'ahooks';
import { useI18n } from '@/web/context/I18n';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '@/pages/app/detail/components/context';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
const DatasetSelectModal = dynamic(() => import('@/components/core/app/DatasetSelectModal'));
const DatasetParamsModal = dynamic(() => import('@/components/core/app/DatasetParamsModal'));
const ToolSelectModal = dynamic(() => import('./components/ToolSelectModal'));
const TTSSelect = dynamic(() => import('@/components/core/app/TTSSelect'));
const QGSwitch = dynamic(() => import('@/components/core/app/QGSwitch'));
const WhisperConfig = dynamic(() => import('@/components/core/app/WhisperConfig'));
const InputGuideConfig = dynamic(() => import('@/components/core/app/InputGuideConfig'));
const ScheduledTriggerConfig = dynamic(
() => import('@/components/core/app/ScheduledTriggerConfig')
);
const WelcomeTextConfig = dynamic(() => import('@/components/core/app/WelcomeTextConfig'));
const BoxStyles: BoxProps = {
px: 5,
py: '16px',
borderBottomWidth: '1px',
borderBottomColor: 'borderColor.low'
};
const LabelStyles: BoxProps = {
w: ['60px', '100px'],
flexShrink: 0,
fontSize: 'xs'
};
const EditForm = ({
appForm,
setAppForm
}: {
appForm: AppSimpleEditFormType;
setAppForm: React.Dispatch<React.SetStateAction<AppSimpleEditFormType>>;
}) => {
const theme = useTheme();
const router = useRouter();
const { t } = useTranslation();
const { appT } = useI18n();
const { appDetail } = useContextSelector(AppContext, (v) => v);
const { allDatasets } = useDatasetStore();
const { llmModelList } = useSystemStore();
const [, startTst] = useTransition();
const selectDatasets = useMemo(
() =>
allDatasets.filter((item) =>
appForm.dataset?.datasets.find((dataset) => dataset.datasetId === item._id)
),
[allDatasets, appForm?.dataset?.datasets]
);
const {
isOpen: isOpenDatasetSelect,
onOpen: onOpenKbSelect,
onClose: onCloseKbSelect
} = useDisclosure();
const {
isOpen: isOpenDatasetParams,
onOpen: onOpenDatasetParams,
onClose: onCloseDatasetParams
} = useDisclosure();
const {
isOpen: isOpenToolsSelect,
onOpen: onOpenToolsSelect,
onClose: onCloseToolsSelect
} = useDisclosure();
const formatVariables: any = useMemo(
() =>
formatEditorVariablePickerIcon([
...getSystemVariables(t),
...(appForm.chatConfig.variables || [])
]),
[appForm.chatConfig.variables, t]
);
const tokenLimit = useMemo(() => {
return (
llmModelList.find((item) => item.model === appForm.aiSettings.model)?.quoteMaxToken || 3000
);
}, [llmModelList, appForm.aiSettings.model]);
return (
<>
<Box>
{/* ai */}
<Box {...BoxStyles}>
<Flex alignItems={'center'}>
<MyIcon name={'core/app/simpleMode/ai'} w={'20px'} />
<FormLabel ml={2} flex={1}>
{appT('AI Settings')}
</FormLabel>
</Flex>
<Flex alignItems={'center'} mt={5}>
<Box {...LabelStyles}>{t('core.ai.Model')}</Box>
<Box flex={'1 0 0'}>
<SettingLLMModel
llmModelType={'all'}
defaultData={{
model: appForm.aiSettings.model,
temperature: appForm.aiSettings.temperature,
maxToken: appForm.aiSettings.maxToken,
maxHistories: appForm.aiSettings.maxHistories
}}
onChange={({ model, temperature, maxToken, maxHistories }: SettingAIDataType) => {
setAppForm((state) => ({
...state,
aiSettings: {
...state.aiSettings,
model,
temperature,
maxToken,
maxHistories: maxHistories ?? 6
}
}));
}}
/>
</Box>
</Flex>
<Box mt={3}>
<HStack {...LabelStyles}>
<Box>{t('core.ai.Prompt')}</Box>
<QuestionTip label={t('core.app.tip.chatNodeSystemPromptTip')} />
</HStack>
<Box mt={1}>
<PromptEditor
value={appForm.aiSettings.systemPrompt}
onChange={(text) => {
startTst(() => {
setAppForm((state) => ({
...state,
aiSettings: {
...state.aiSettings,
systemPrompt: text
}
}));
});
}}
variables={formatVariables}
placeholder={t('core.app.tip.chatNodeSystemPromptTip')}
title={t('core.ai.Prompt')}
/>
</Box>
</Box>
</Box>
{/* dataset */}
<Box {...BoxStyles}>
<Flex alignItems={'center'}>
<Flex alignItems={'center'} flex={1}>
<MyIcon name={'core/app/simpleMode/dataset'} w={'20px'} />
<FormLabel ml={2}>{t('core.dataset.Choose Dataset')}</FormLabel>
</Flex>
<Button
variant={'transparentBase'}
leftIcon={<MyIcon name="common/addLight" w={'0.8rem'} />}
iconSpacing={1}
size={'sm'}
fontSize={'sm'}
onClick={onOpenKbSelect}
>
{t('common.Choose')}
</Button>
<Button
variant={'transparentBase'}
leftIcon={<MyIcon name={'edit'} w={'14px'} />}
iconSpacing={1}
size={'sm'}
fontSize={'sm'}
onClick={onOpenDatasetParams}
>
{t('common.Params')}
</Button>
</Flex>
{appForm.dataset.datasets?.length > 0 && (
<Box my={3}>
<SearchParamsTip
searchMode={appForm.dataset.searchMode}
similarity={appForm.dataset.similarity}
limit={appForm.dataset.limit}
usingReRank={appForm.dataset.usingReRank}
queryExtensionModel={appForm.dataset.datasetSearchExtensionModel}
/>
</Box>
)}
<Grid gridTemplateColumns={'repeat(2, minmax(0, 1fr))'} gridGap={[2, 4]}>
{selectDatasets.map((item) => (
<MyTooltip key={item._id} label={t('core.dataset.Read Dataset')}>
<Flex
overflow={'hidden'}
alignItems={'center'}
p={2}
bg={'white'}
boxShadow={'0 4px 8px -2px rgba(16,24,40,.1),0 2px 4px -2px rgba(16,24,40,.06)'}
borderRadius={'md'}
border={theme.borders.base}
cursor={'pointer'}
onClick={() =>
router.push({
pathname: '/dataset/detail',
query: {
datasetId: item._id
}
})
}
>
<Avatar src={item.avatar} w={'18px'} mr={1} />
<Box flex={'1 0 0'} w={0} className={'textEllipsis'} fontSize={'sm'}>
{item.name}
</Box>
</Flex>
</MyTooltip>
))}
</Grid>
</Box>
{/* tool choice */}
<Box {...BoxStyles}>
<Flex alignItems={'center'}>
<Flex alignItems={'center'} flex={1}>
<MyIcon name={'core/app/toolCall'} w={'20px'} />
<FormLabel ml={2}>{t('core.app.Tool call')}()</FormLabel>
<QuestionTip ml={1} label={t('core.app.Tool call tip')} />
</Flex>
<Button
variant={'transparentBase'}
leftIcon={<SmallAddIcon />}
iconSpacing={1}
mr={'-5px'}
size={'sm'}
fontSize={'sm'}
onClick={onOpenToolsSelect}
>
{t('common.Choose')}
</Button>
</Flex>
<Grid
mt={appForm.selectedTools.length > 0 ? 2 : 0}
gridTemplateColumns={'repeat(2, minmax(0, 1fr))'}
gridGap={[2, 4]}
>
{appForm.selectedTools.map((item) => (
<MyTooltip key={item.id} label={item.intro}>
<Flex
overflow={'hidden'}
alignItems={'center'}
p={2.5}
bg={'white'}
boxShadow={'0 4px 8px -2px rgba(16,24,40,.1),0 2px 4px -2px rgba(16,24,40,.06)'}
borderRadius={'md'}
border={theme.borders.base}
_hover={{
...hoverDeleteStyles,
borderColor: 'primary.300'
}}
>
<Avatar src={item.avatar} w={'1rem'} mr={1} />
<Box flex={'1 0 0'} w={0} className={'textEllipsis'} fontSize={'sm'}>
{item.name}
</Box>
<DeleteIcon
onClick={() => {
setAppForm((state) => ({
...state,
selectedTools: state.selectedTools.filter((tool) => tool.id !== item.id)
}));
}}
/>
</Flex>
</MyTooltip>
))}
</Grid>
</Box>
{/* variable */}
<Box {...BoxStyles}>
<VariableEdit
variables={appForm.chatConfig.variables}
onChange={(e) => {
appForm.chatConfig.variables = e;
}}
/>
</Box>
{/* welcome */}
<Box {...BoxStyles}>
<WelcomeTextConfig
value={appForm.chatConfig.welcomeText}
onChange={(e) => {
setAppForm((state) => ({
...state,
chatConfig: {
...state.chatConfig,
welcomeText: e.target.value
}
}));
}}
/>
</Box>
{/* tts */}
<Box {...BoxStyles}>
<TTSSelect
value={appForm.chatConfig.ttsConfig}
onChange={(e) => {
setAppForm((state) => ({
...state,
chatConfig: {
...state.chatConfig,
ttsConfig: e
}
}));
}}
/>
</Box>
{/* whisper */}
<Box {...BoxStyles}>
<WhisperConfig
isOpenAudio={appForm.chatConfig.ttsConfig?.type !== TTSTypeEnum.none}
value={appForm.chatConfig.whisperConfig}
onChange={(e) => {
setAppForm((state) => ({
...state,
chatConfig: {
...state.chatConfig,
whisperConfig: e
}
}));
}}
/>
</Box>
{/* question guide */}
<Box {...BoxStyles}>
<QGSwitch
isChecked={appForm.chatConfig.questionGuide}
onChange={(e) => {
setAppForm((state) => ({
...state,
chatConfig: {
...state.chatConfig,
questionGuide: e.target.checked
}
}));
}}
/>
</Box>
{/* question tips */}
<Box {...BoxStyles}>
<InputGuideConfig
appId={appDetail._id}
value={appForm.chatConfig.chatInputGuide}
onChange={(e) => {
setAppForm((state) => ({
...state,
chatConfig: {
...state.chatConfig,
chatInputGuide: e
}
}));
}}
/>
</Box>
{/* timer trigger */}
<Box {...BoxStyles} borderBottom={'none'}>
<ScheduledTriggerConfig
value={appForm.chatConfig.scheduledTriggerConfig}
onChange={(e) => {
setAppForm((state) => ({
...state,
chatConfig: {
...state.chatConfig,
scheduledTriggerConfig: e
}
}));
}}
/>
</Box>
</Box>
{isOpenDatasetSelect && (
<DatasetSelectModal
isOpen={isOpenDatasetSelect}
defaultSelectedDatasets={selectDatasets.map((item) => ({
datasetId: item._id,
vectorModel: item.vectorModel
}))}
onClose={onCloseKbSelect}
onChange={(e) => {
setAppForm((state) => ({
...state,
dataset: {
...state.dataset,
datasets: e
}
}));
}}
/>
)}
{isOpenDatasetParams && (
<DatasetParamsModal
{...appForm.dataset}
maxTokens={tokenLimit}
onClose={onCloseDatasetParams}
onSuccess={(e) => {
setAppForm((state) => ({
...state,
dataset: {
...state.dataset,
...e
}
}));
}}
/>
)}
{isOpenToolsSelect && (
<ToolSelectModal
selectedTools={appForm.selectedTools}
onAddTool={(e) => {
setAppForm((state) => ({
...state,
selectedTools: [...state.selectedTools, e]
}));
}}
onRemoveTool={(e) => {
setAppForm((state) => ({
...state,
selectedTools: state.selectedTools.filter((item) => item.id !== e.id)
}));
}}
onClose={onCloseToolsSelect}
/>
)}
</>
);
};
export default React.memo(EditForm);

View File

@@ -0,0 +1,164 @@
import React, { useCallback, useMemo, useState } from 'react';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '../context';
import FolderPath from '@/components/common/folder/Path';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { getAppFolderPath } from '@/web/core/app/api/app';
import { Box, Button, Flex, IconButton } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import RouteTab from '../RouteTab';
import { useTranslation } from 'next-i18next';
import PopoverConfirm from '@fastgpt/web/components/common/MyPopover/PopoverConfirm';
import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { form2AppWorkflow } from '@/web/core/app/utils';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { TabEnum } from '../context';
import PublishHistoriesSlider, { type InitProps } from '../PublishHistoriesSlider';
import { appWorkflow2Form } from '@fastgpt/global/core/app/utils';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { compareWorkflow } from '@/web/core/workflow/utils';
import MyTag from '@fastgpt/web/components/common/Tag/index';
import { publishStatusStyle } from '../constants';
const Header = ({
appForm,
setAppForm
}: {
appForm: AppSimpleEditFormType;
setAppForm: React.Dispatch<React.SetStateAction<AppSimpleEditFormType>>;
}) => {
const { t } = useTranslation();
const { isPc } = useSystemStore();
const router = useRouter();
const { appId, appDetail, onPublish, currentTab } = useContextSelector(AppContext, (v) => v);
const { data: paths = [] } = useRequest2(() => getAppFolderPath(appId), {
manual: false,
refreshDeps: [appId]
});
const onclickRoute = useCallback(
(parentId: string) => {
router.push({
pathname: '/app/list',
query: {
parentId
}
});
},
[router]
);
const isPublished = useMemo(() => {
const data = form2AppWorkflow(appForm);
return compareWorkflow(
{
nodes: appDetail.modules,
edges: [],
chatConfig: appDetail.chatConfig
},
{
nodes: data.nodes,
edges: [],
chatConfig: data.chatConfig
}
);
}, [appDetail.chatConfig, appDetail.modules, appForm]);
const onSubmitPublish = useCallback(
async (data: AppSimpleEditFormType) => {
const { nodes, edges } = form2AppWorkflow(data);
await onPublish({
nodes,
edges,
chatConfig: data.chatConfig,
type: AppTypeEnum.simple
});
},
[onPublish]
);
const [historiesDefaultData, setHistoriesDefaultData] = useState<InitProps>();
return (
<Box>
{!isPc && (
<Flex pt={2} justifyContent={'center'}>
<RouteTab />
</Flex>
)}
<Flex pl={2} pt={[2, 3]} alignItems={'flex-start'} position={'relative'}>
<Box flex={'1'}>
<FolderPath paths={paths} hoverStyle={{ color: 'primary.600' }} onClick={onclickRoute} />
</Box>
{isPc && (
<Box position={'absolute'} left={'50%'} transform={'translateX(-50%)'}>
<RouteTab />
</Box>
)}
{currentTab === TabEnum.appEdit && (
<Flex alignItems={'center'}>
{!historiesDefaultData && (
<>
<MyTag
mr={3}
type={'borderFill'}
showDot
colorSchema={
isPublished
? publishStatusStyle.published.colorSchema
: publishStatusStyle.unPublish.colorSchema
}
>
{isPublished
? publishStatusStyle.published.text
: publishStatusStyle.unPublish.text}
</MyTag>
<IconButton
mr={[2, 4]}
icon={<MyIcon name={'history'} w={'18px'} />}
aria-label={''}
size={'sm'}
w={'30px'}
variant={'whitePrimary'}
onClick={() => {
const { nodes, edges } = form2AppWorkflow(appForm);
setHistoriesDefaultData({
nodes,
edges,
chatConfig: appForm.chatConfig
});
}}
/>
<PopoverConfirm
showCancel
content={t('core.app.Publish Confirm')}
Trigger={<Button isDisabled={isPublished}>{t('core.app.Publish')}</Button>}
onConfirm={() => onSubmitPublish(appForm)}
/>
</>
)}
</Flex>
)}
</Flex>
{!!historiesDefaultData && (
<PublishHistoriesSlider
initData={({ nodes, chatConfig }) => {
setAppForm(
appWorkflow2Form({
nodes,
chatConfig
})
);
}}
onClose={() => setHistoriesDefaultData(undefined)}
defaultData={historiesDefaultData}
/>
)}
</Box>
);
};
export default Header;

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
@@ -20,24 +20,25 @@ import {
Textarea
} from '@chakra-ui/react';
import RowTabs from '@fastgpt/web/components/common/Tabs/RowTabs';
import { useWorkflowStore } from '@/web/core/workflow/store/workflow';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { useQuery } from '@tanstack/react-query';
import { useRequest, useRequest2 } from '@fastgpt/web/hooks/useRequest';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/index.d';
import Avatar from '@/components/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { AddIcon } from '@chakra-ui/icons';
import { getPreviewPluginModule } from '@/web/core/plugin/api';
import { getPreviewPluginNode, getSystemPlugTemplates } from '@/web/core/app/api/plugin';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import ParentPaths from '@/components/common/ParentPaths';
import { PluginTypeEnum } from '@fastgpt/global/core/plugin/constants';
import { debounce } from 'lodash';
import { useForm } from 'react-hook-form';
import JsonEditor from '@fastgpt/web/components/common/Textarea/JsonEditor';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { getTeamPlugTemplates } from '@/web/core/app/api/plugin';
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { getAppFolderPath } from '@/web/core/app/api/app';
import FolderPath from '@/components/common/folder/Path';
type Props = {
selectedTools: FlowNodeTemplateType[];
@@ -52,55 +53,38 @@ enum TemplateTypeEnum {
const ToolSelectModal = ({ onClose, ...props }: Props & { onClose: () => void }) => {
const { t } = useTranslation();
const {
systemNodeTemplates,
loadSystemNodeTemplates,
teamPluginNodeTemplates,
loadTeamPluginNodeTemplates
} = useWorkflowStore();
const [templateType, setTemplateType] = useState(TemplateTypeEnum.teamPlugin);
const [currentParent, setCurrentParent] = useState<{
parentId: string;
parentName: string;
}>();
const [parentId, setParentId] = useState<ParentIdType>('');
const [searchKey, setSearchKey] = useState('');
const templates = useMemo(() => {
const map = {
[TemplateTypeEnum.systemPlugin]: systemNodeTemplates.filter(
(item) => item.isTool && item.name.toLowerCase().includes(searchKey.toLowerCase())
),
[TemplateTypeEnum.teamPlugin]: teamPluginNodeTemplates.filter((item) =>
searchKey ? item.pluginType !== PluginTypeEnum.folder : true
)
};
return map[templateType];
}, [searchKey, systemNodeTemplates, teamPluginNodeTemplates, templateType]);
const { mutate: onChangeTab } = useRequest({
mutationFn: async (e: any) => {
const val = e as TemplateTypeEnum;
if (val === TemplateTypeEnum.systemPlugin) {
await loadSystemNodeTemplates();
} else if (val === TemplateTypeEnum.teamPlugin) {
await loadTeamPluginNodeTemplates({
parentId: currentParent?.parentId
const { data: templates = [], loading: isLoading } = useRequest2(
async () => {
if (templateType === TemplateTypeEnum.systemPlugin) {
return (await getSystemPlugTemplates()).filter(
(item) => item.isTool && item.name.toLowerCase().includes(searchKey.toLowerCase())
);
} else if (templateType === TemplateTypeEnum.teamPlugin) {
return getTeamPlugTemplates({
parentId,
searchKey,
type: [AppTypeEnum.folder, AppTypeEnum.httpPlugin, AppTypeEnum.plugin]
});
}
setTemplateType(val);
},
errorToast: t('core.module.templates.Load plugin error')
});
const { isLoading } = useQuery(['teamNodeTemplate', currentParent?.parentId, searchKey], () =>
loadTeamPluginNodeTemplates({
parentId: currentParent?.parentId,
searchKey,
init: true
})
{
manual: false,
throttleWait: 300,
refreshDeps: [templateType, searchKey, parentId],
errorToast: t('core.module.templates.Load plugin error')
}
);
const { data: paths = [] } = useRequest2(() => getAppFolderPath(parentId), {
manual: false,
refreshDeps: [parentId]
});
return (
<MyModal
isOpen
@@ -129,7 +113,7 @@ const ToolSelectModal = ({ onClose, ...props }: Props & { onClose: () => void })
py={'5px'}
px={'15px'}
value={templateType}
onChange={onChangeTab}
onChange={(e) => setTemplateType(e as TemplateTypeEnum)}
/>
<InputGroup w={300}>
<InputLeftElement h={'full'} alignItems={'center'} display={'flex'}>
@@ -138,20 +122,19 @@ const ToolSelectModal = ({ onClose, ...props }: Props & { onClose: () => void })
<Input
bg={'myGray.50'}
placeholder={t('plugin.Search plugin')}
onChange={debounce((e) => setSearchKey(e.target.value), 200)}
onChange={(e) => setSearchKey(e.target.value)}
/>
</InputGroup>
</Box>
{/* route components */}
{templateType === TemplateTypeEnum.teamPlugin && !searchKey && currentParent && (
{templateType === TemplateTypeEnum.teamPlugin && !searchKey && parentId && (
<Flex mt={2} px={[3, 6]}>
<ParentPaths
paths={[currentParent]}
<FolderPath
paths={paths}
FirstPathDom={null}
onClick={() => {
setCurrentParent(undefined);
setParentId(null);
}}
fontSize="md"
/>
</Flex>
)}
@@ -159,7 +142,7 @@ const ToolSelectModal = ({ onClose, ...props }: Props & { onClose: () => void })
<RenderList
templates={templates}
isLoadingData={isLoading}
setCurrentParent={setCurrentParent}
setParentId={setParentId}
{...props}
/>
</MyBox>
@@ -175,11 +158,11 @@ const RenderList = React.memo(function RenderList({
isLoadingData,
onAddTool,
onRemoveTool,
setCurrentParent
setParentId
}: Props & {
templates: FlowNodeTemplateType[];
isLoadingData: boolean;
setCurrentParent: (e: { parentId: string; parentName: string }) => void;
setParentId: React.Dispatch<React.SetStateAction<ParentIdType>>;
}) {
const { t } = useTranslation();
const [configTool, setConfigTool] = useState<FlowNodeTemplateType>();
@@ -204,7 +187,7 @@ const RenderList = React.memo(function RenderList({
const { mutate: onClickAdd, isLoading } = useRequest({
mutationFn: async (template: FlowNodeTemplateType) => {
const res = await getPreviewPluginModule(template.id);
const res = await getPreviewPluginNode({ appId: template.id });
if (!checkToolInputValid(res)) {
return Promise.reject(t('core.app.ToolCall.This plugin cannot be called as a tool'));
@@ -250,7 +233,7 @@ const RenderList = React.memo(function RenderList({
<Box ml={5} flex={'1 0 0'}>
<Box color={'black'}>{t(item.name)}</Box>
{item.intro && (
<Box className="textEllipsis3" color={'myGray.500'} fontSize={['xs', 'sm']}>
<Box className="textEllipsis3" color={'myGray.500'} fontSize={'xs'}>
{t(item.intro)}
</Box>
)}
@@ -264,12 +247,9 @@ const RenderList = React.memo(function RenderList({
>
{t('common.Remove')}
</Button>
) : item.pluginType === PluginTypeEnum.folder ? (
<Button
size={'sm'}
variant={'whiteBase'}
onClick={() => setCurrentParent({ parentId: item.id, parentName: item.name })}
>
) : item.pluginType === PluginTypeEnum.folder ||
item.pluginType === AppTypeEnum.httpPlugin ? (
<Button size={'sm'} variant={'whiteBase'} onClick={() => setParentId(item.id)}>
{t('common.Open')}
</Button>
) : (

View File

@@ -0,0 +1,34 @@
import React, { useState } from 'react';
import { getDefaultAppForm } from '@fastgpt/global/core/app/utils';
import Header from './Header';
import Edit from './Edit';
import { useContextSelector } from 'use-context-selector';
import { AppContext, TabEnum } from '../context';
import dynamic from 'next/dynamic';
import { Flex } from '@chakra-ui/react';
const Logs = dynamic(() => import('../Logs/index'));
const PublishChannel = dynamic(() => import('../Publish'));
const SimpleEdit = () => {
const { currentTab } = useContextSelector(AppContext, (v) => v);
const [appForm, setAppForm] = useState(getDefaultAppForm());
return (
<Flex h={'100%'} flexDirection={'column'} pr={3} pb={3}>
<Header appForm={appForm} setAppForm={setAppForm} />
{currentTab === TabEnum.appEdit ? (
<Edit appForm={appForm} setAppForm={setAppForm} />
) : (
<Flex h={'100%'} flexDirection={'column'} mt={4}>
{currentTab === TabEnum.publish && <PublishChannel />}
{currentTab === TabEnum.logs && <Logs />}
</Flex>
)}
</Flex>
);
};
export default React.memo(SimpleEdit);

View File

@@ -0,0 +1,10 @@
.EditAppBox {
&::-webkit-scrollbar-thumb {
background: #dfe2ea !important;
transition: background 1s;
}
&::-webkit-scrollbar-thumb:hover {
background: var(--chakra-colors-gray-300) !important;
}
}

View File

@@ -1,175 +0,0 @@
import React, { useState } from 'react';
import { Box, Flex, Button, IconButton, useDisclosure } from '@chakra-ui/react';
import { DragHandleIcon } from '@chakra-ui/icons';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useRouter } from 'next/router';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { AppSchema } from '@fastgpt/global/core/app/type.d';
import { delAppById } from '@/web/core/app/api';
import { useTranslation } from 'next-i18next';
import PermissionIconText from '@/components/support/permission/IconText';
import dynamic from 'next/dynamic';
import Avatar from '@/components/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import TagsEditModal from './TagsEditModal';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useI18n } from '@/web/context/I18n';
import { AppContext } from '@/web/core/app/context/appContext';
import { useContextSelector } from 'use-context-selector';
const InfoModal = dynamic(() => import('../InfoModal'));
const AppCard = () => {
const router = useRouter();
const { t } = useTranslation();
const { appT } = useI18n();
const { toast } = useToast();
const { appDetail } = useContextSelector(AppContext, (v) => v);
const appId = appDetail._id;
const { feConfigs } = useSystemStore();
const [TeamTagsSet, setTeamTagsSet] = useState<AppSchema>();
const {
isOpen: isOpenInfoEdit,
onOpen: onOpenInfoEdit,
onClose: onCloseInfoEdit
} = useDisclosure();
const { openConfirm: openConfirmDel, ConfirmModal: ConfirmDelModal } = useConfirm({
content: appT('Confirm Del App Tip'),
type: 'delete'
});
/* 点击删除 */
const { mutate: handleDelModel, isLoading } = useRequest({
mutationFn: async () => {
if (!appDetail) return null;
await delAppById(appDetail._id);
return 'success';
},
onSuccess(res) {
if (!res) return;
toast({
title: t('common.Delete Success'),
status: 'success'
});
router.replace(`/app/list`);
},
errorToast: t('common.Delete Failed')
});
return (
<>
<Box px={4}>
<Flex alignItems={'center'} justifyContent={'space-between'}>
<Box fontWeight={'bold'}>
<PermissionIconText defaultPermission={appDetail.defaultPermission} fontSize={'md'} />
</Box>
<Box color={'myGray.500'} fontSize={'xs'}>
AppId:{' '}
<Box as={'span'} userSelect={'all'}>
{appId}
</Box>
</Box>
</Flex>
{/* basic info */}
<Box
borderWidth={'1px'}
borderColor={'primary.1'}
borderRadius={'md'}
mt={2}
px={5}
py={4}
bg={'primary.50'}
position={'relative'}
>
<Flex alignItems={'center'}>
<Avatar src={appDetail.avatar} borderRadius={'md'} w={'28px'} />
<Box ml={3} fontWeight={'bold'} fontSize={'md'}>
{appDetail.name}
</Box>
{appDetail.permission.isOwner && (
<IconButton
className="delete"
position={'absolute'}
top={4}
right={4}
size={'smSquare'}
icon={<MyIcon name={'delete'} w={'14px'} />}
variant={'whiteDanger'}
borderRadius={'md'}
aria-label={'delete'}
isLoading={isLoading}
onClick={openConfirmDel(handleDelModel)}
/>
)}
</Flex>
<Box
flex={1}
mt={3}
mb={4}
className={'textEllipsis3'}
wordBreak={'break-all'}
color={'myGray.600'}
fontSize={'xs'}
>
{appDetail.intro || t('core.app.tip.Add a intro to app')}
</Box>
<Flex>
<Button
size={['sm', 'md']}
variant={'whitePrimary'}
leftIcon={<MyIcon name={'core/chat/chatLight'} w={'16px'} />}
onClick={() => router.push(`/chat?appId=${appId}`)}
>
{t('core.Chat')}
</Button>
<Button
mx={3}
size={['sm', 'md']}
variant={'whitePrimary'}
leftIcon={<MyIcon name={'support/outlink/shareLight'} w={'16px'} />}
onClick={() => {
router.replace({
query: {
appId,
currentTab: 'publish'
}
});
}}
>
{t('core.app.navbar.Publish')}
</Button>
{appDetail.permission.hasWritePer && feConfigs?.show_team_chat && (
<Button
mr={3}
size={['sm', 'md']}
variant={'whitePrimary'}
leftIcon={<DragHandleIcon w={'16px'} />}
onClick={() => setTeamTagsSet(appDetail)}
>
{t('common.Team Tags Set')}
</Button>
)}
{appDetail.permission.hasManagePer && (
<Button
size={['sm', 'md']}
variant={'whitePrimary'}
leftIcon={<MyIcon name={'common/settingLight'} w={'16px'} />}
onClick={onOpenInfoEdit}
>
{t('common.Setting')}
</Button>
)}
</Flex>
</Box>
</Box>
<ConfirmDelModal />
{isOpenInfoEdit && <InfoModal onClose={onCloseInfoEdit} />}
{TeamTagsSet && <TagsEditModal onClose={() => setTeamTagsSet(undefined)} />}
</>
);
};
export default React.memo(AppCard);

View File

@@ -1,160 +0,0 @@
import { useUserStore } from '@/web/support/user/useUserStore';
import { Box, Flex, IconButton } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import React, { useCallback, useEffect, useRef } from 'react';
import ChatBox from '@/components/ChatBox';
import type { ComponentRef, StartChatFnProps } from '@/components/ChatBox/type.d';
import { streamFetch } from '@/web/common/api/fetch';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { checkChatSupportSelectFileByModules } from '@/web/core/chat/utils';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import {
getDefaultEntryNodeIds,
getMaxHistoryLimitFromNodes,
initWorkflowEdgeStatus,
storeNodes2RuntimeNodes
} from '@fastgpt/global/core/workflow/runtime/utils';
import { useMemoizedFn, useSafeState } from 'ahooks';
import { UseFormReturn } from 'react-hook-form';
import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
import { form2AppWorkflow } from '@/web/core/app/utils';
import { useI18n } from '@/web/context/I18n';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '@/web/core/app/context/appContext';
const ChatTest = ({
editForm,
appId
}: {
editForm: UseFormReturn<AppSimpleEditFormType, any>;
appId: string;
}) => {
const { t } = useTranslation();
const { appT } = useI18n();
const { userInfo } = useUserStore();
const ChatBoxRef = useRef<ComponentRef>(null);
const { appDetail } = useContextSelector(AppContext, (v) => v);
const { watch } = editForm;
const chatConfig = watch('chatConfig');
const [workflowData, setWorkflowData] = useSafeState({
nodes: appDetail.modules || [],
edges: appDetail.edges || []
});
const startChat = useMemoizedFn(
async ({ chatList, controller, generatingMessage, variables }: StartChatFnProps) => {
if (!workflowData) return Promise.reject('workflowData is empty');
/* get histories */
let historyMaxLen = getMaxHistoryLimitFromNodes(workflowData.nodes);
const history = chatList.slice(-historyMaxLen - 2, -2);
// 流请求,获取数据
const { responseText, responseData } = await streamFetch({
url: '/api/core/chat/chatTest',
data: {
history,
prompt: chatList[chatList.length - 2].value,
nodes: storeNodes2RuntimeNodes(
workflowData.nodes,
getDefaultEntryNodeIds(workflowData.nodes)
),
edges: initWorkflowEdgeStatus(workflowData.edges),
variables,
appId,
appName: `调试-${appDetail.name}`
},
onMessage: generatingMessage,
abortCtrl: controller
});
return { responseText, responseData };
}
);
const resetChatBox = useCallback(() => {
ChatBoxRef.current?.resetHistory([]);
ChatBoxRef.current?.resetVariables();
}, []);
useEffect(() => {
const wat = watch((data) => {
const { nodes, edges } = form2AppWorkflow(data as AppSimpleEditFormType);
setWorkflowData({ nodes, edges });
});
return () => {
wat.unsubscribe();
};
}, [setWorkflowData, watch]);
return (
<Flex
position={'relative'}
flexDirection={'column'}
h={'100%'}
py={4}
overflowX={'auto'}
bg={'white'}
>
<Flex px={[2, 5]}>
<Box fontSize={['md', 'lg']} fontWeight={'bold'} flex={1}>
{appT('Chat Debug')}
</Box>
<MyTooltip label={t('core.chat.Restart')}>
<IconButton
className="chat"
size={'smSquare'}
icon={<MyIcon name={'common/clearLight'} w={'14px'} />}
variant={'whiteDanger'}
borderRadius={'md'}
aria-label={'delete'}
onClick={(e) => {
e.stopPropagation();
resetChatBox();
}}
/>
</MyTooltip>
</Flex>
<Box flex={1}>
<ChatBox
ref={ChatBoxRef}
appId={appDetail._id}
appAvatar={appDetail.avatar}
userAvatar={userInfo?.avatar}
showMarkIcon
chatConfig={chatConfig}
showFileSelector={checkChatSupportSelectFileByModules(workflowData.nodes)}
onStartChat={startChat}
onDelMessage={() => {}}
/>
</Box>
{appDetail.type !== AppTypeEnum.simple && (
<Flex
position={'absolute'}
top={0}
right={0}
left={0}
bottom={0}
bg={'rgba(255,255,255,0.7)'}
alignItems={'center'}
justifyContent={'center'}
flexDirection={'column'}
fontSize={'lg'}
color={'black'}
whiteSpace={'pre-wrap'}
textAlign={'center'}
>
<Box fontSize={'md'}>{appT('Advance App TestTip')}</Box>
</Flex>
)}
</Flex>
);
};
export default React.memo(ChatTest);

View File

@@ -1,521 +0,0 @@
import React, { useEffect, useMemo, useTransition } from 'react';
import {
Box,
Flex,
Grid,
BoxProps,
useTheme,
useDisclosure,
Button,
HStack
} from '@chakra-ui/react';
import { AddIcon, SmallAddIcon } from '@chakra-ui/icons';
import { useFieldArray, UseFormReturn } from 'react-hook-form';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import type { AppSimpleEditFormType } from '@fastgpt/global/core/app/type.d';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
import { form2AppWorkflow } from '@/web/core/app/utils';
import dynamic from 'next/dynamic';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import Avatar from '@/components/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import VariableEdit from '@/components/core/app/VariableEdit';
import MyTextarea from '@/components/common/Textarea/MyTextarea/index';
import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor';
import { formatEditorVariablePickerIcon } from '@fastgpt/global/core/workflow/utils';
import SearchParamsTip from '@/components/core/dataset/SearchParamsTip';
import SettingLLMModel from '@/components/core/ai/SettingLLMModel';
import type { SettingAIDataType } from '@fastgpt/global/core/app/type.d';
import DeleteIcon, { hoverDeleteStyles } from '@fastgpt/web/components/common/Icon/delete';
import { TTSTypeEnum } from '@/web/core/app/constants';
import { getSystemVariables } from '@/web/core/app/utils';
import { useUpdate } from 'ahooks';
import { useI18n } from '@/web/context/I18n';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '@/web/core/app/context/appContext';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
const DatasetSelectModal = dynamic(() => import('@/components/core/app/DatasetSelectModal'));
const DatasetParamsModal = dynamic(() => import('@/components/core/app/DatasetParamsModal'));
const ToolSelectModal = dynamic(() => import('./ToolSelectModal'));
const TTSSelect = dynamic(() => import('@/components/core/app/TTSSelect'));
const QGSwitch = dynamic(() => import('@/components/core/app/QGSwitch'));
const WhisperConfig = dynamic(() => import('@/components/core/app/WhisperConfig'));
const InputGuideConfig = dynamic(() => import('@/components/core/app/InputGuideConfig'));
const ScheduledTriggerConfig = dynamic(
() => import('@/components/core/app/ScheduledTriggerConfig')
);
const WelcomeTextConfig = dynamic(() => import('@/components/core/app/WelcomeTextConfig'));
const BoxStyles: BoxProps = {
px: 5,
py: '16px',
borderBottomWidth: '1px',
borderBottomColor: 'borderColor.low'
};
const LabelStyles: BoxProps = {
w: ['60px', '100px'],
flexShrink: 0,
fontSize: 'xs'
};
const EditForm = ({
editForm,
divRef,
isSticky
}: {
editForm: UseFormReturn<AppSimpleEditFormType, any>;
divRef: React.RefObject<HTMLDivElement>;
isSticky: boolean;
}) => {
const theme = useTheme();
const router = useRouter();
const { t } = useTranslation();
const { appT } = useI18n();
const { appDetail, publishApp } = useContextSelector(AppContext, (v) => v);
const { allDatasets } = useDatasetStore();
const { llmModelList } = useSystemStore();
const [, startTst] = useTransition();
const refresh = useUpdate();
const { setValue, getValues, handleSubmit, control, watch } = editForm;
const { fields: datasets, replace: replaceDatasetList } = useFieldArray({
control,
name: 'dataset.datasets'
});
const selectDatasets = useMemo(
() => allDatasets.filter((item) => datasets.find((dataset) => dataset.datasetId === item._id)),
[allDatasets, datasets]
);
// useEffect(() => {
// if (selectDatasets.length !== datasets.length) {
// replaceDatasetList(
// selectDatasets.map((item) => ({
// datasetId: item._id
// }))
// );
// }
// }, [datasets, replaceDatasetList, selectDatasets]);
const {
isOpen: isOpenDatasetSelect,
onOpen: onOpenKbSelect,
onClose: onCloseKbSelect
} = useDisclosure();
const {
isOpen: isOpenDatasetParams,
onOpen: onOpenDatasetParams,
onClose: onCloseDatasetParams
} = useDisclosure();
const {
isOpen: isOpenToolsSelect,
onOpen: onOpenToolsSelect,
onClose: onCloseToolsSelect
} = useDisclosure();
const { openConfirm: openConfirmSave, ConfirmModal: ConfirmSaveModal } = useConfirm({
content: t('core.app.edit.Confirm Save App Tip')
});
const aiSystemPrompt = watch('aiSettings.systemPrompt');
const selectLLMModel = watch('aiSettings.model');
const datasetSearchSetting = watch('dataset');
const variables = watch('chatConfig.variables');
const formatVariables: any = useMemo(
() => formatEditorVariablePickerIcon([...getSystemVariables(t), ...(variables || [])]),
[t, variables]
);
const tts = getValues('chatConfig.ttsConfig');
const whisperConfig = getValues('chatConfig.whisperConfig');
const postQuestionGuide = getValues('chatConfig.questionGuide');
const selectedTools = watch('selectedTools');
const inputGuideConfig = watch('chatConfig.chatInputGuide');
const scheduledTriggerConfig = watch('chatConfig.scheduledTriggerConfig');
const searchMode = watch('dataset.searchMode');
const tokenLimit = useMemo(() => {
return llmModelList.find((item) => item.model === selectLLMModel)?.quoteMaxToken || 3000;
}, [selectLLMModel, llmModelList]);
/* on save app */
const { mutate: onSubmitPublish, isLoading: isSaving } = useRequest({
mutationFn: async (data: AppSimpleEditFormType) => {
const { nodes, edges } = form2AppWorkflow(data);
await publishApp({
nodes,
edges,
chatConfig: data.chatConfig,
type: AppTypeEnum.simple
});
},
successToast: t('common.Save Success'),
errorToast: t('common.Save Failed')
});
useEffect(() => {
const wat = watch((data) => {
refresh();
});
return () => {
wat.unsubscribe();
};
}, []);
return (
<Box>
{/* title */}
<Flex
ref={divRef}
position={'sticky'}
top={-4}
bg={'myGray.25'}
py={4}
justifyContent={'space-between'}
alignItems={'center'}
zIndex={100}
px={4}
{...(isSticky && {
borderBottom: theme.borders.base,
boxShadow: '0 2px 10px rgba(0,0,0,0.12)'
})}
>
<HStack>
<Box color={'myGray.900'}>{t('core.app.App params config')}</Box>
<QuestionTip label={t('core.app.Simple Config Tip')} />
</HStack>
<Button
isLoading={isSaving}
size={['sm', 'md']}
leftIcon={
appDetail.type === AppTypeEnum.simple ? (
<MyIcon name={'common/publishFill'} w={['14px', '16px']} />
) : undefined
}
variant={appDetail.type === AppTypeEnum.simple ? 'primary' : 'whitePrimary'}
onClick={() => {
if (appDetail.type !== AppTypeEnum.simple) {
openConfirmSave(handleSubmit((data) => onSubmitPublish(data)))();
} else {
handleSubmit((data) => onSubmitPublish(data))();
}
}}
>
{appDetail.type !== AppTypeEnum.simple
? t('core.app.Change to simple mode')
: t('core.app.Publish')}
</Button>
</Flex>
<Box px={4}>
<Box bg={'white'} borderRadius={'md'} borderWidth={'1px'} borderColor={'borderColor.base'}>
{/* ai */}
<Box {...BoxStyles}>
<Flex alignItems={'center'}>
<MyIcon name={'core/app/simpleMode/ai'} w={'20px'} />
<FormLabel ml={2} flex={1}>
{appT('AI Settings')}
</FormLabel>
</Flex>
<Flex alignItems={'center'} mt={5}>
<Box {...LabelStyles}>{t('core.ai.Model')}</Box>
<Box flex={'1 0 0'}>
<SettingLLMModel
llmModelType={'all'}
defaultData={{
model: getValues('aiSettings.model'),
temperature: getValues('aiSettings.temperature'),
maxToken: getValues('aiSettings.maxToken'),
maxHistories: getValues('aiSettings.maxHistories')
}}
onChange={({ model, temperature, maxToken, maxHistories }: SettingAIDataType) => {
setValue('aiSettings.model', model);
setValue('aiSettings.maxToken', maxToken);
setValue('aiSettings.temperature', temperature);
setValue('aiSettings.maxHistories', maxHistories ?? 6);
}}
/>
</Box>
</Flex>
<Box mt={3}>
<HStack {...LabelStyles}>
<Box>{t('core.ai.Prompt')}</Box>
<QuestionTip label={t('core.app.tip.chatNodeSystemPromptTip')} />
</HStack>
<Box mt={1}>
<PromptEditor
value={aiSystemPrompt}
onChange={(text) => {
startTst(() => {
setValue('aiSettings.systemPrompt', text);
});
}}
variables={formatVariables}
placeholder={t('core.app.tip.chatNodeSystemPromptTip')}
title={t('core.ai.Prompt')}
/>
</Box>
</Box>
</Box>
{/* dataset */}
<Box {...BoxStyles}>
<Flex alignItems={'center'}>
<Flex alignItems={'center'} flex={1}>
<MyIcon name={'core/app/simpleMode/dataset'} w={'20px'} />
<FormLabel ml={2}>{t('core.dataset.Choose Dataset')}</FormLabel>
</Flex>
<Button
variant={'transparentBase'}
leftIcon={<SmallAddIcon />}
iconSpacing={1}
size={'sm'}
fontSize={'sm'}
onClick={onOpenKbSelect}
>
{t('common.Choose')}
</Button>
<Button
variant={'transparentBase'}
leftIcon={<MyIcon name={'edit'} w={'14px'} />}
iconSpacing={1}
size={'sm'}
fontSize={'sm'}
onClick={onOpenDatasetParams}
>
{t('common.Params')}
</Button>
</Flex>
{datasetSearchSetting.datasets?.length > 0 && (
<Box my={3}>
<SearchParamsTip
searchMode={searchMode}
similarity={getValues('dataset.similarity')}
limit={getValues('dataset.limit')}
usingReRank={getValues('dataset.usingReRank')}
queryExtensionModel={getValues('dataset.datasetSearchExtensionModel')}
/>
</Box>
)}
<Grid
gridTemplateColumns={['repeat(2, minmax(0, 1fr))', 'repeat(3, minmax(0, 1fr))']}
gridGap={[2, 4]}
>
{selectDatasets.map((item) => (
<MyTooltip key={item._id} label={t('core.dataset.Read Dataset')}>
<Flex
overflow={'hidden'}
alignItems={'center'}
p={2}
bg={'white'}
boxShadow={'0 4px 8px -2px rgba(16,24,40,.1),0 2px 4px -2px rgba(16,24,40,.06)'}
borderRadius={'md'}
border={theme.borders.base}
cursor={'pointer'}
onClick={() =>
router.push({
pathname: '/dataset/detail',
query: {
datasetId: item._id
}
})
}
>
<Avatar src={item.avatar} w={'18px'} mr={1} />
<Box flex={'1 0 0'} w={0} className={'textEllipsis'} fontSize={'sm'}>
{item.name}
</Box>
</Flex>
</MyTooltip>
))}
</Grid>
</Box>
{/* tool choice */}
<Box {...BoxStyles}>
<Flex alignItems={'center'}>
<Flex alignItems={'center'} flex={1}>
<MyIcon name={'core/app/toolCall'} w={'20px'} />
<FormLabel ml={2}>{t('core.app.Tool call')}()</FormLabel>
<QuestionTip ml={1} label={t('core.app.Tool call tip')} />
</Flex>
<Button
variant={'transparentBase'}
leftIcon={<SmallAddIcon />}
iconSpacing={1}
mr={'-5px'}
size={'sm'}
fontSize={'sm'}
onClick={onOpenToolsSelect}
>
{t('common.Choose')}
</Button>
</Flex>
<Grid
mt={selectedTools.length > 0 ? 2 : 0}
gridTemplateColumns={'repeat(2, minmax(0, 1fr))'}
gridGap={[2, 4]}
>
{selectedTools.map((item) => (
<Flex
key={item.id}
overflow={'hidden'}
alignItems={'center'}
p={2}
bg={'white'}
boxShadow={'0 4px 8px -2px rgba(16,24,40,.1),0 2px 4px -2px rgba(16,24,40,.06)'}
borderRadius={'md'}
border={theme.borders.base}
_hover={{
...hoverDeleteStyles,
borderColor: 'primary.300'
}}
>
<Avatar src={item.avatar} w={'18px'} mr={1} />
<Box flex={'1 0 0'} w={0} className={'textEllipsis'} fontSize={'sm'}>
{item.name}
</Box>
<DeleteIcon
onClick={() => {
setValue(
'selectedTools',
selectedTools.filter((tool) => tool.id !== item.id)
);
}}
/>
</Flex>
))}
</Grid>
</Box>
{/* variable */}
<Box {...BoxStyles}>
<VariableEdit
variables={variables}
onChange={(e) => {
setValue('chatConfig.variables', e);
}}
/>
</Box>
{/* welcome */}
<Box {...BoxStyles}>
<WelcomeTextConfig
defaultValue={getValues('chatConfig.welcomeText')}
onBlur={(e) => {
setValue('chatConfig.welcomeText', e.target.value || '');
}}
/>
</Box>
{/* tts */}
<Box {...BoxStyles}>
<TTSSelect
value={tts}
onChange={(e) => {
setValue('chatConfig.ttsConfig', e);
}}
/>
</Box>
{/* whisper */}
<Box {...BoxStyles}>
<WhisperConfig
isOpenAudio={tts?.type !== TTSTypeEnum.none}
value={whisperConfig}
onChange={(e) => {
setValue('chatConfig.whisperConfig', e);
}}
/>
</Box>
{/* question guide */}
<Box {...BoxStyles}>
<QGSwitch
isChecked={postQuestionGuide}
onChange={(e) => {
setValue('chatConfig.questionGuide', e.target.checked);
}}
/>
</Box>
{/* question tips */}
<Box {...BoxStyles}>
<InputGuideConfig
appId={appDetail._id}
value={inputGuideConfig}
onChange={(e) => {
setValue('chatConfig.chatInputGuide', e);
}}
/>
</Box>
{/* timer trigger */}
<Box {...BoxStyles} borderBottom={'none'}>
<ScheduledTriggerConfig
value={scheduledTriggerConfig}
onChange={(e) => {
setValue('chatConfig.scheduledTriggerConfig', e);
}}
/>
</Box>
</Box>
</Box>
<ConfirmSaveModal bg={appDetail.type === AppTypeEnum.simple ? '' : 'red.600'} countDown={5} />
{isOpenDatasetSelect && (
<DatasetSelectModal
isOpen={isOpenDatasetSelect}
defaultSelectedDatasets={selectDatasets.map((item) => ({
datasetId: item._id,
vectorModel: item.vectorModel
}))}
onClose={onCloseKbSelect}
onChange={replaceDatasetList}
/>
)}
{isOpenDatasetParams && (
<DatasetParamsModal
{...datasetSearchSetting}
maxTokens={tokenLimit}
onClose={onCloseDatasetParams}
onSuccess={(e) => {
setValue('dataset', {
...getValues('dataset'),
...e
});
}}
/>
)}
{isOpenToolsSelect && (
<ToolSelectModal
selectedTools={selectedTools}
onAddTool={(e) => {
setValue('selectedTools', [...selectedTools, e]);
}}
onRemoveTool={(e) => {
setValue(
'selectedTools',
selectedTools.filter((item) => item.pluginId !== e.pluginId)
);
}}
onClose={onCloseToolsSelect}
/>
)}
</Box>
);
};
export default React.memo(EditForm);

View File

@@ -1,70 +0,0 @@
import React from 'react';
import { Box, Grid } from '@chakra-ui/react';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useSticky } from '@/web/common/hooks/useSticky';
import { useMount } from 'ahooks';
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
import { useForm } from 'react-hook-form';
import { appWorkflow2Form, getDefaultAppForm } from '@fastgpt/global/core/app/utils';
import ChatTest from './ChatTest';
import AppCard from './AppCard';
import EditForm from './EditForm';
import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
import { v1Workflow2V2 } from '@/web/core/workflow/adapt';
import { AppContext } from '@/web/core/app/context/appContext';
import { useContextSelector } from 'use-context-selector';
const SimpleEdit = ({ appId }: { appId: string }) => {
const { isPc } = useSystemStore();
const { parentRef, divRef, isSticky } = useSticky();
const { loadAllDatasets } = useDatasetStore();
const { appDetail } = useContextSelector(AppContext, (v) => v);
const editForm = useForm<AppSimpleEditFormType>({
defaultValues: getDefaultAppForm()
});
// show selected dataset
useMount(() => {
loadAllDatasets();
editForm.reset(
appWorkflow2Form({
nodes: appDetail.modules,
chatConfig: appDetail.chatConfig
})
);
if (appDetail.version !== 'v2') {
editForm.reset(
appWorkflow2Form({
nodes: v1Workflow2V2((appDetail.modules || []) as any)?.nodes,
chatConfig: appDetail.chatConfig
})
);
}
});
return (
<Grid gridTemplateColumns={['1fr', '560px 1fr']} h={'100%'}>
<Box
ref={parentRef}
h={'100%'}
borderRight={'1.5px solid'}
borderColor={'myGray.200'}
pt={[0, 4]}
pb={10}
overflow={'overlay'}
>
<AppCard />
<Box mt={2}>
<EditForm editForm={editForm} divRef={divRef} isSticky={isSticky} />
</Box>
</Box>
{isPc && <ChatTest editForm={editForm} appId={appId} />}
</Grid>
);
};
export default React.memo(SimpleEdit);

View File

@@ -23,7 +23,7 @@ import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { getTeamsTags } from '@/web/support/user/team/api';
import { useQuery } from '@tanstack/react-query';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '@/web/core/app/context/appContext';
import { AppContext } from '@/pages/app/detail/components/context';
const TagsEditModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();

View File

@@ -0,0 +1,198 @@
import React, { useCallback, useMemo } from 'react';
import { Box, Flex, Button, IconButton } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import dynamic from 'next/dynamic';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useBeforeunload } from '@fastgpt/web/hooks/useBeforeunload';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext, getWorkflowStore } from '../WorkflowComponents/context';
import { useInterval } from 'ahooks';
import { AppContext, TabEnum } from '../context';
import RouteTab from '../RouteTab';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import PopoverConfirm from '@fastgpt/web/components/common/MyPopover/PopoverConfirm';
import { useRouter } from 'next/router';
import AppCard from '../WorkflowComponents/AppCard';
import { uiWorkflow2StoreWorkflow } from '../WorkflowComponents/utils';
const PublishHistories = dynamic(() => import('../PublishHistoriesSlider'));
const Header = () => {
const { t } = useTranslation();
const { isPc } = useSystemStore();
const router = useRouter();
const { appDetail, onPublish, currentTab } = useContextSelector(AppContext, (v) => v);
const isV2Workflow = appDetail?.version === 'v2';
const {
flowData2StoreDataAndCheck,
setWorkflowTestData,
onSaveWorkflow,
setHistoriesDefaultData,
historiesDefaultData,
initData
} = useContextSelector(WorkflowContext, (v) => v);
const onclickPublish = useCallback(async () => {
const data = flowData2StoreDataAndCheck();
if (data) {
await onPublish({
...data,
chatConfig: appDetail.chatConfig,
//@ts-ignore
version: 'v2'
});
}
}, [flowData2StoreDataAndCheck, onPublish, appDetail.chatConfig]);
const saveAndBack = useCallback(async () => {
try {
await onSaveWorkflow();
router.push('/app/list');
} catch (error) {}
}, [onSaveWorkflow, router]);
// effect
useBeforeunload({
callback: onSaveWorkflow,
tip: t('core.common.tip.leave page')
});
useInterval(() => {
if (!appDetail._id) return;
onSaveWorkflow();
}, 40000);
const Render = useMemo(() => {
return (
<>
{!isPc && (
<Flex pt={2} justifyContent={'center'}>
<RouteTab />
</Flex>
)}
<Flex
mt={[2, 0]}
py={3}
pl={[2, 4]}
pr={[2, 6]}
borderBottom={'base'}
alignItems={'center'}
userSelect={'none'}
h={'67px'}
{...(currentTab === TabEnum.appEdit
? {
bg: 'myGray.25'
}
: {
bg: 'transparent',
borderBottomColor: 'transparent'
})}
>
{/* back */}
<MyIcon
name={'common/leftArrowLight'}
w={'1.75rem'}
cursor={'pointer'}
onClick={saveAndBack}
/>
{/* app info */}
<Box ml={1}>
<AppCard
showSaveStatus={
!historiesDefaultData && isV2Workflow && currentTab === TabEnum.appEdit
}
/>
</Box>
{isPc && (
<Box position={'absolute'} left={'50%'} transform={'translateX(-50%)'}>
<RouteTab />
</Box>
)}
<Box flex={1} />
{currentTab === TabEnum.appEdit && (
<>
{!historiesDefaultData && (
<IconButton
mr={[2, 4]}
icon={<MyIcon name={'history'} w={'18px'} />}
aria-label={''}
size={'sm'}
w={'30px'}
variant={'whitePrimary'}
onClick={async () => {
const { nodes, edges } = uiWorkflow2StoreWorkflow(await getWorkflowStore());
setHistoriesDefaultData({
nodes,
edges,
chatConfig: appDetail.chatConfig
});
}}
/>
)}
<Button
size={'sm'}
leftIcon={<MyIcon name={'core/workflow/debug'} w={['14px', '16px']} />}
variant={'whitePrimary'}
onClick={async () => {
const data = flowData2StoreDataAndCheck();
if (data) {
setWorkflowTestData(data);
}
}}
>
{t('core.workflow.Debug')}
</Button>
{!historiesDefaultData && (
<PopoverConfirm
showCancel
content={t('core.app.Publish Confirm')}
Trigger={
<Button
ml={[2, 4]}
size={'sm'}
leftIcon={<MyIcon name={'common/publishFill'} w={['14px', '16px']} />}
>
{t('core.app.Publish')}
</Button>
}
onConfirm={() => onclickPublish()}
/>
)}
</>
)}
</Flex>
{historiesDefaultData && (
<PublishHistories
initData={initData}
onClose={() => {
setHistoriesDefaultData(undefined);
}}
defaultData={historiesDefaultData}
/>
)}
</>
);
}, [
isPc,
currentTab,
saveAndBack,
historiesDefaultData,
isV2Workflow,
t,
initData,
setHistoriesDefaultData,
appDetail.chatConfig,
flowData2StoreDataAndCheck,
setWorkflowTestData,
onclickPublish
]);
return Render;
};
export default React.memo(Header);

View File

@@ -1,21 +1,25 @@
import React, { useEffect, useMemo } from 'react';
import { AppSchema } from '@fastgpt/global/core/app/type.d';
import Header from './Header';
import Flow from '@/components/core/workflow/Flow';
import React from 'react';
import { appSystemModuleTemplates } from '@fastgpt/global/core/workflow/template/constants';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { v1Workflow2V2 } from '@/web/core/workflow/adapt';
import WorkflowContextProvider, { WorkflowContext } from '@/components/core/workflow/context';
import WorkflowContextProvider, { WorkflowContext } from '../WorkflowComponents/context';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '@/web/core/app/context/appContext';
import { AppContext, TabEnum } from '../context';
import { useMount } from 'ahooks';
import Header from './Header';
import { Flex } from '@chakra-ui/react';
import { workflowBoxStyles } from '../constants';
import dynamic from 'next/dynamic';
import { cloneDeep } from 'lodash';
type Props = { onClose: () => void };
const Render = ({ onClose }: Props) => {
const appDetail = useContextSelector(AppContext, (e) => e.appDetail);
import Flow from '../WorkflowComponents/Flow';
const Logs = dynamic(() => import('../Logs/index'));
const PublishChannel = dynamic(() => import('../Publish'));
const WorkflowEdit = () => {
const { appDetail, currentTab } = useContextSelector(AppContext, (e) => e);
const isV2Workflow = appDetail?.version === 'v2';
const { openConfirm, ConfirmModal } = useConfirm({
showCancel: false,
content:
@@ -24,47 +28,45 @@ const Render = ({ onClose }: Props) => {
const initData = useContextSelector(WorkflowContext, (v) => v.initData);
const workflowStringData = JSON.stringify({
nodes: appDetail.modules || [],
edges: appDetail.edges || []
});
useMount(() => {
if (!isV2Workflow) {
openConfirm(() => {
initData(JSON.parse(JSON.stringify(v1Workflow2V2((appDetail.modules || []) as any))));
})();
} else {
initData(JSON.parse(workflowStringData));
initData(
cloneDeep({
nodes: appDetail.modules || [],
edges: appDetail.edges || []
})
);
}
});
const memoRender = useMemo(() => {
return <Flow Header={<Header onClose={onClose} />} />;
}, [onClose]);
return (
<>
{memoRender}
<Flex {...workflowBoxStyles}>
<Header />
{currentTab === TabEnum.appEdit ? (
<Flow />
) : (
<Flex flexDirection={'column'} h={'100%'} px={4} pb={4}>
{currentTab === TabEnum.publish && <PublishChannel />}
{currentTab === TabEnum.logs && <Logs />}
</Flex>
)}
{!isV2Workflow && <ConfirmModal countDown={0} />}
</>
</Flex>
);
};
export default React.memo(function FlowEdit(props: Props) {
const appDetail = useContextSelector(AppContext, (e) => e.appDetail);
const filterAppIds = useMemo(() => [appDetail._id], [appDetail._id]);
const Render = () => {
return (
<WorkflowContextProvider
value={{
appId: appDetail._id,
mode: 'app',
filterAppIds,
basicNodeTemplates: appSystemModuleTemplates
}}
>
<Render {...props} />
<WorkflowContextProvider basicNodeTemplates={appSystemModuleTemplates}>
<WorkflowEdit />
</WorkflowContextProvider>
);
});
};
export default React.memo(Render);

View File

@@ -0,0 +1,222 @@
import React, { useCallback, useMemo } from 'react';
import { Box, Flex, HStack, useDisclosure } from '@chakra-ui/react';
import { useContextSelector } from 'use-context-selector';
import { AppContext, TabEnum } from '../context';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useTranslation } from 'next-i18next';
import Avatar from '@/components/Avatar';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useI18n } from '@/web/context/I18n';
import { WorkflowContext } from './context';
import { compareWorkflow, filterSensitiveNodesData } from '@/web/core/workflow/utils';
import dynamic from 'next/dynamic';
import { useCopyData } from '@/web/common/hooks/useCopyData';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import MyTag from '@fastgpt/web/components/common/Tag/index';
import { publishStatusStyle } from '../constants';
const ImportSettings = dynamic(() => import('./Flow/ImportSettings'));
const AppCard = ({ showSaveStatus }: { showSaveStatus: boolean }) => {
const { t } = useTranslation();
const { appT } = useI18n();
const { copyData } = useCopyData();
const { feConfigs } = useSystemStore();
const { appDetail, appLatestVersion, onOpenInfoEdit, onOpenTeamTagModal, onDelApp, currentTab } =
useContextSelector(AppContext, (v) => v);
const { historiesDefaultData, flowData2StoreDataAndCheck, onSaveWorkflow, isSaving, saveLabel } =
useContextSelector(WorkflowContext, (v) => v);
const { isOpen: isOpenImport, onOpen: onOpenImport, onClose: onCloseImport } = useDisclosure();
const onExportWorkflow = useCallback(async () => {
const data = flowData2StoreDataAndCheck();
if (data) {
copyData(
JSON.stringify(
{
nodes: filterSensitiveNodesData(data.nodes),
edges: data.edges,
chatConfig: appDetail.chatConfig
},
null,
2
),
appT('Export Config Successful')
);
}
}, [appDetail.chatConfig, appT, copyData, flowData2StoreDataAndCheck]);
const isPublished = (() => {
const data = flowData2StoreDataAndCheck(true);
if (!appLatestVersion) return true;
if (data) {
return compareWorkflow(
{
nodes: appLatestVersion.nodes,
edges: appLatestVersion.edges,
chatConfig: appLatestVersion.chatConfig
},
{
nodes: data.nodes,
edges: data.edges,
chatConfig: appDetail.chatConfig
}
);
}
return false;
})();
const InfoMenu = useCallback(
({ children }: { children: React.ReactNode }) => {
return (
<MyMenu
width={150}
Button={children}
menuList={[
{
children: [
{
icon: 'edit',
label: appT('Edit info'),
onClick: onOpenInfoEdit
},
{
icon: 'support/team/key',
label: t('common.Role'),
onClick: onOpenInfoEdit
}
]
},
...(!historiesDefaultData && currentTab === TabEnum.appEdit
? [
{
children: [
{
label: appT('Import Configs'),
icon: 'common/importLight',
onClick: onOpenImport
},
{
label: appT('Export Configs'),
icon: 'export',
onClick: onExportWorkflow
}
]
}
]
: []),
...(appDetail.permission.hasWritePer && feConfigs?.show_team_chat
? [
{
children: [
{
icon: 'support/team/memberLight',
label: t('common.Team Tags Set'),
onClick: onOpenTeamTagModal
}
]
}
]
: []),
...(appDetail.permission.isOwner
? [
{
children: [
{
type: 'danger' as 'danger',
icon: 'delete',
label: t('common.Delete'),
onClick: onDelApp
}
]
}
]
: [])
]}
/>
);
},
[
appDetail.permission.hasWritePer,
appDetail.permission.isOwner,
appT,
currentTab,
feConfigs?.show_team_chat,
historiesDefaultData,
onDelApp,
onExportWorkflow,
onOpenImport,
onOpenInfoEdit,
onOpenTeamTagModal,
t
]
);
const Render = useMemo(() => {
return (
<HStack>
<InfoMenu>
<Avatar src={appDetail.avatar} w={'1.75rem'} />
</InfoMenu>
<Box>
<InfoMenu>
<HStack spacing={1} cursor={'pointer'}>
<Box color={'myGray.900'}>{appDetail.name}</Box>
<MyIcon name={'common/select'} w={'1rem'} />
</HStack>
</InfoMenu>
{showSaveStatus && (
<MyTooltip label={t('core.app.Onclick to save')}>
<Flex
alignItems={'center'}
h={'20px'}
cursor={'pointer'}
fontSize={'mini'}
onClick={onSaveWorkflow}
lineHeight={1}
>
{isSaving && <MyIcon name={'common/loading'} w={'0.8rem'} mr={0.5} />}
<Box color={'myGray.500'}>{saveLabel}</Box>
<MyTag
py={0}
showDot
bg={'transparent'}
colorSchema={
isPublished
? publishStatusStyle.published.colorSchema
: publishStatusStyle.unPublish.colorSchema
}
>
{isPublished
? publishStatusStyle.published.text
: publishStatusStyle.unPublish.text}
</MyTag>
</Flex>
</MyTooltip>
)}
</Box>
{isOpenImport && <ImportSettings onClose={onCloseImport} />}
</HStack>
);
}, [
InfoMenu,
appDetail.avatar,
appDetail.name,
isOpenImport,
isPublished,
isSaving,
onCloseImport,
onSaveWorkflow,
saveLabel,
showSaveStatus,
t
]);
return Render;
};
export default AppCard;

View File

@@ -0,0 +1,112 @@
import type { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import React, { useRef, forwardRef, ForwardedRef, useImperativeHandle } from 'react';
import { SmallCloseIcon } from '@chakra-ui/icons';
import { Box, Flex, IconButton } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useUserStore } from '@/web/support/user/useUserStore';
import type { ComponentRef } from '@/components/ChatBox/type.d';
import { useTranslation } from 'next-i18next';
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '@/pages/app/detail/components/context';
import { useChatTest } from '@/pages/app/detail/components/useChatTest';
export type ChatTestComponentRef = {
resetChatTest: () => void;
};
const ChatTest = (
{
isOpen,
nodes = [],
edges = [],
onClose
}: {
isOpen: boolean;
nodes?: StoreNodeItemType[];
edges?: StoreEdgeItemType[];
onClose: () => void;
},
ref: ForwardedRef<ChatTestComponentRef>
) => {
const { t } = useTranslation();
const ChatBoxRef = useRef<ComponentRef>(null);
const { appDetail } = useContextSelector(AppContext, (v) => v);
const { resetChatBox, ChatBox } = useChatTest({
nodes,
edges,
chatConfig: appDetail.chatConfig
});
useImperativeHandle(ref, () => ({
resetChatTest: resetChatBox
}));
return (
<>
<Box
zIndex={300}
display={isOpen ? 'block' : 'none'}
position={'fixed'}
top={0}
left={0}
bottom={0}
right={0}
onClick={onClose}
/>
<Flex
zIndex={300}
flexDirection={'column'}
position={'absolute'}
top={5}
right={0}
h={isOpen ? '95%' : '0'}
w={isOpen ? ['100%', '460px'] : '0'}
bg={'white'}
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
borderRadius={'md'}
overflow={'hidden'}
transition={'.2s ease'}
>
<Flex py={4} px={5} whiteSpace={'nowrap'}>
<Box fontSize={'lg'} fontWeight={'bold'} flex={1}>
{t('core.chat.Debug test')}
</Box>
<MyTooltip label={t('core.chat.Restart')}>
<IconButton
className="chat"
size={'smSquare'}
icon={<MyIcon name={'common/clearLight'} w={'14px'} />}
variant={'whiteDanger'}
borderRadius={'md'}
aria-label={'delete'}
onClick={(e) => {
e.stopPropagation();
ChatBoxRef.current?.resetHistory([]);
ChatBoxRef.current?.resetVariables();
}}
/>
</MyTooltip>
<MyTooltip label={t('common.Close')}>
<IconButton
ml={3}
icon={<SmallCloseIcon fontSize={'22px'} />}
variant={'grayBase'}
size={'smSquare'}
aria-label={''}
onClick={onClose}
/>
</MyTooltip>
</Flex>
<Box flex={1}>
<ChatBox />
</Box>
</Flex>
</>
);
};
export default React.memo(forwardRef(ChatTest));

View File

@@ -0,0 +1,60 @@
import React, { useState } from 'react';
import { Textarea, Button, ModalBody, ModalFooter } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../context';
import { useI18n } from '@/web/context/I18n';
type Props = {
onClose: () => void;
};
const ImportSettings = ({ onClose }: Props) => {
const { appT } = useI18n();
const { toast } = useToast();
const initData = useContextSelector(WorkflowContext, (v) => v.initData);
const [value, setValue] = useState('');
return (
<MyModal
isOpen
w={'600px'}
onClose={onClose}
iconSrc="/imgs/modal/params.svg"
title={appT('Import Configs')}
>
<ModalBody>
<Textarea
placeholder={appT('Paste Config')}
defaultValue={value}
rows={16}
onChange={(e) => setValue(e.target.value)}
/>
</ModalBody>
<ModalFooter>
<Button
variant="whiteBase"
onClick={() => {
if (!value) {
return onClose();
}
try {
const data = JSON.parse(value);
initData(data);
onClose();
} catch (error) {
toast({
title: appT('Import Configs Failed')
});
}
}}
>
</Button>
</ModalFooter>
</MyModal>
);
};
export default React.memo(ImportSettings);

View File

@@ -0,0 +1,407 @@
import React, { useCallback, useMemo, useState } from 'react';
import { Box, Flex, IconButton, Input, InputGroup, InputLeftElement, css } from '@chakra-ui/react';
import type {
FlowNodeTemplateType,
nodeTemplateListType
} from '@fastgpt/global/core/workflow/type/index.d';
import { useViewport, XYPosition } from 'reactflow';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import Avatar from '@/components/Avatar';
import { nodeTemplate2FlowNode } from '@/web/core/workflow/utils';
import { useTranslation } from 'next-i18next';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { getPreviewPluginNode, getSystemPlugTemplates } from '@/web/core/app/api/plugin';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { workflowNodeTemplateList } from '@fastgpt/web/core/workflow/constants';
import RowTabs from '@fastgpt/web/components/common/Tabs/RowTabs';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useRouter } from 'next/router';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../context';
import { useI18n } from '@/web/context/I18n';
import { getTeamPlugTemplates } from '@/web/core/app/api/plugin';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import MyBox from '@fastgpt/web/components/common/MyBox';
import FolderPath from '@/components/common/folder/Path';
import { getAppFolderPath } from '@/web/core/app/api/app';
type ModuleTemplateListProps = {
isOpen: boolean;
onClose: () => void;
};
type RenderListProps = {
templates: FlowNodeTemplateType[];
onClose: () => void;
parentId: ParentIdType;
setParentId: React.Dispatch<React.SetStateAction<ParentIdType>>;
};
enum TemplateTypeEnum {
'basic' = 'basic',
'systemPlugin' = 'systemPlugin',
'teamPlugin' = 'teamPlugin'
}
const sliderWidth = 390;
const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
const { t } = useTranslation();
const router = useRouter();
const [parentId, setParentId] = useState<ParentIdType>('');
const [searchKey, setSearchKey] = useState('');
const { feConfigs } = useSystemStore();
const { basicNodeTemplates, hasToolNode, nodeList } = useContextSelector(
WorkflowContext,
(v) => v
);
const [templateType, setTemplateType] = useState(TemplateTypeEnum.basic);
const { data: templates = [], loading } = useRequest2(
async () => {
if (templateType === TemplateTypeEnum.basic) {
return basicNodeTemplates.filter((item) => {
// unique node filter
if (item.unique) {
const nodeExist = nodeList.some((node) => node.flowNodeType === item.flowNodeType);
if (nodeExist) {
return false;
}
}
// special node filter
if (item.flowNodeType === FlowNodeTypeEnum.lafModule && !feConfigs.lafEnv) {
return false;
}
// tool stop
if (!hasToolNode && item.flowNodeType === FlowNodeTypeEnum.stopTool) {
return false;
}
return true;
});
}
if (templateType === TemplateTypeEnum.systemPlugin) {
return getSystemPlugTemplates();
}
if (templateType === TemplateTypeEnum.teamPlugin) {
return getTeamPlugTemplates({
parentId,
searchKey,
type: [AppTypeEnum.folder, AppTypeEnum.httpPlugin, AppTypeEnum.plugin]
});
}
return [];
},
{
manual: false,
throttleWait: 300,
refreshDeps: [basicNodeTemplates, nodeList, hasToolNode, templateType, searchKey, parentId]
}
);
const { data: paths = [] } = useRequest2(() => getAppFolderPath(parentId), {
manual: false,
refreshDeps: [parentId]
});
const Render = useMemo(() => {
return (
<>
<Box
zIndex={2}
display={isOpen ? 'block' : 'none'}
position={'absolute'}
top={0}
left={0}
bottom={0}
w={`${sliderWidth}px`}
onClick={onClose}
fontSize={'sm'}
/>
<MyBox
isLoading={loading}
display={'flex'}
zIndex={3}
flexDirection={'column'}
position={'absolute'}
top={'10px'}
left={0}
pt={'20px'}
pb={4}
h={isOpen ? 'calc(100% - 20px)' : '0'}
w={isOpen ? ['100%', `${sliderWidth}px`] : '0'}
bg={'white'}
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
borderRadius={'0 20px 20px 0'}
transition={'.2s ease'}
userSelect={'none'}
overflow={isOpen ? 'none' : 'hidden'}
>
<Box pl={'20px'} mb={3} pr={'10px'} whiteSpace={'nowrap'} overflow={'hidden'}>
<Flex flex={'1 0 0'} alignItems={'center'} gap={3}>
<RowTabs
list={[
{
icon: 'core/modules/basicNode',
label: t('core.module.template.Basic Node'),
value: TemplateTypeEnum.basic
},
{
icon: 'core/modules/systemPlugin',
label: t('core.module.template.System Plugin'),
value: TemplateTypeEnum.systemPlugin
},
{
icon: 'core/modules/teamPlugin',
label: t('core.module.template.Team Plugin'),
value: TemplateTypeEnum.teamPlugin
}
]}
py={'5px'}
value={templateType}
onChange={(e) => setTemplateType(e as TemplateTypeEnum)}
/>
{/* close icon */}
<IconButton
size={'sm'}
icon={<MyIcon name={'common/backFill'} w={'14px'} color={'myGray.700'} />}
borderColor={'myGray.300'}
variant={'grayBase'}
aria-label={''}
onClick={onClose}
/>
</Flex>
{templateType === TemplateTypeEnum.teamPlugin && (
<Flex mt={2} alignItems={'center'} h={10}>
<InputGroup mr={4} h={'full'}>
<InputLeftElement h={'full'} alignItems={'center'} display={'flex'}>
<MyIcon name={'common/searchLight'} w={'16px'} color={'myGray.500'} ml={3} />
</InputLeftElement>
<Input
h={'full'}
bg={'myGray.50'}
placeholder={t('plugin.Search plugin')}
onChange={(e) => setSearchKey(e.target.value)}
/>
</InputGroup>
<Box flex={1} />
<Flex
alignItems={'center'}
cursor={'pointer'}
_hover={{
color: 'primary.600'
}}
fontSize={'sm'}
onClick={() => router.push('/app/list')}
>
<Box></Box>
<MyIcon name={'common/rightArrowLight'} w={'14px'} />
</Flex>
</Flex>
)}
{templateType === TemplateTypeEnum.teamPlugin && !searchKey && parentId && (
<Flex alignItems={'center'} mt={2}>
<FolderPath paths={paths} FirstPathDom={null} onClick={setParentId} />
</Flex>
)}
</Box>
<RenderList
templates={templates}
onClose={onClose}
parentId={parentId}
setParentId={setParentId}
/>
</MyBox>
</>
);
}, [isOpen, onClose, loading, t, templateType, searchKey, parentId, paths, templates, router]);
return Render;
};
export default React.memo(NodeTemplatesModal);
const RenderList = React.memo(function RenderList({
templates,
onClose,
parentId,
setParentId
}: RenderListProps) {
const { t } = useTranslation();
const { appT } = useI18n();
const { isPc } = useSystemStore();
const { x, y, zoom } = useViewport();
const { setLoading } = useSystemStore();
const { toast } = useToast();
const reactFlowWrapper = useContextSelector(WorkflowContext, (v) => v.reactFlowWrapper);
const setNodes = useContextSelector(WorkflowContext, (v) => v.setNodes);
const formatTemplates = useMemo<nodeTemplateListType>(() => {
const copy: nodeTemplateListType = JSON.parse(JSON.stringify(workflowNodeTemplateList(t)));
templates.forEach((item) => {
const index = copy.findIndex((template) => template.type === item.templateType);
if (index === -1) return;
copy[index].list.push(item);
});
return copy.filter((item) => item.list.length > 0);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [templates, parentId]);
const onAddNode = useCallback(
async ({ template, position }: { template: FlowNodeTemplateType; position: XYPosition }) => {
if (!reactFlowWrapper?.current) return;
const templateNode = await (async () => {
try {
// get plugin preview module
if (template.flowNodeType === FlowNodeTypeEnum.pluginModule) {
setLoading(true);
const res = await getPreviewPluginNode({ appId: template.id });
setLoading(false);
return res;
}
return { ...template };
} catch (e) {
toast({
status: 'error',
title: getErrText(e, t('core.plugin.Get Plugin Module Detail Failed'))
});
setLoading(false);
return Promise.reject(e);
}
})();
const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect();
const mouseX = (position.x - reactFlowBounds.left - x) / zoom - 100;
const mouseY = (position.y - reactFlowBounds.top - y) / zoom;
const node = nodeTemplate2FlowNode({
template: {
...templateNode,
name: t(templateNode.name),
intro: t(templateNode.intro || '')
},
position: { x: mouseX, y: mouseY - 20 },
selected: true
});
setNodes((state) =>
state
.map((node) => ({
...node,
selected: false
}))
// @ts-ignore
.concat(node)
);
},
[reactFlowWrapper, setLoading, setNodes, t, toast, x, y, zoom]
);
const Render = useMemo(() => {
return templates.length === 0 ? (
<EmptyTip text={appT('module.No Modules')} />
) : (
<Box flex={'1 0 0'} overflow={'overlay'} px={'20px'}>
<Box mx={'auto'}>
{formatTemplates.map((item, i) => (
<Box
key={item.type}
css={css({
span: {
display: 'block'
}
})}
>
{item.label && (
<Flex>
<Box fontSize={'sm'} fontWeight={'bold'} flex={1}>
{t(item.label)}
</Box>
</Flex>
)}
<>
{item.list.map((template) => (
<MyTooltip
key={template.id}
placement={'right'}
label={
<Box>
<Flex alignItems={'center'}>
<Avatar
src={template.avatar}
w={'24px'}
objectFit={'contain'}
borderRadius={'0'}
/>
<Box fontWeight={'bold'} ml={3}>
{t(template.name)}
</Box>
</Flex>
<Box mt={2}>{t(template.intro || 'core.workflow.Not intro')}</Box>
</Box>
}
>
<Flex
alignItems={'center'}
p={5}
cursor={'pointer'}
_hover={{ bg: 'myWhite.600' }}
borderRadius={'sm'}
draggable={template.pluginType !== AppTypeEnum.folder}
onDragEnd={(e) => {
if (e.clientX < sliderWidth) return;
onAddNode({
template,
position: { x: e.clientX, y: e.clientY }
});
}}
onClick={(e) => {
if (
template.pluginType === AppTypeEnum.folder ||
template.pluginType === AppTypeEnum.httpPlugin
) {
return setParentId(template.id);
}
if (isPc) {
return onAddNode({
template,
position: { x: sliderWidth * 1.5, y: 200 }
});
}
onAddNode({
template: template,
position: { x: e.clientX, y: e.clientY }
});
onClose();
}}
>
<Avatar
src={template.avatar}
w={'1.7rem'}
objectFit={'contain'}
borderRadius={'0'}
/>
<Box color={'black'} fontSize={'sm'} ml={5} flex={'1 0 0'}>
{t(template.name)}
</Box>
</Flex>
</MyTooltip>
))}
</>
</Box>
))}
</Box>
</Box>
);
}, [appT, formatTemplates, isPc, onAddNode, onClose, setParentId, t, templates.length]);
return Render;
});

View File

@@ -0,0 +1,80 @@
import React, { useCallback, useState } from 'react';
import { ModalBody, ModalFooter, Button } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import type { SelectAppItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { useTranslation } from 'next-i18next';
import SelectOneResource from '@/components/common/folder/SelectOneResource';
import {
GetResourceFolderListProps,
GetResourceListItemResponse
} from '@fastgpt/global/common/parentFolder/type';
import { getMyApps } from '@/web/core/app/api';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
const SelectAppModal = ({
value,
filterAppIds = [],
onClose,
onSuccess
}: {
value?: SelectAppItemType;
filterAppIds?: string[];
onClose: () => void;
onSuccess: (e: SelectAppItemType) => void;
}) => {
const { t } = useTranslation();
const [selectedApp, setSelectedApp] = useState<SelectAppItemType | undefined>(value);
const getAppList = useCallback(
async ({ parentId }: GetResourceFolderListProps) => {
return getMyApps({ parentId }).then((res) =>
res
.filter((item) => !filterAppIds.includes(item._id))
.map<GetResourceListItemResponse>((item) => ({
id: item._id,
name: item.name,
avatar: item.avatar,
isFolder: item.type === AppTypeEnum.folder
}))
);
},
[filterAppIds]
);
return (
<MyModal
isOpen
title={`选择应用`}
iconSrc="/imgs/workflow/ai.svg"
onClose={onClose}
position={'relative'}
w={'600px'}
>
<ModalBody flex={'1 0 0'} overflow={'auto'} minH={'400px'} position={'relative'}>
<SelectOneResource
value={selectedApp?.id}
onSelect={(id) => setSelectedApp(id ? { id } : undefined)}
server={getAppList}
/>
</ModalBody>
<ModalFooter>
<Button variant={'whiteBase'} onClick={onClose}>
{t('common.Cancel')}
</Button>
<Button
ml={2}
isDisabled={!selectedApp}
onClick={() => {
if (!selectedApp) return;
onSuccess(selectedApp);
onClose();
}}
>
{t('common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
);
};
export default React.memo(SelectAppModal);

View File

@@ -0,0 +1,235 @@
import React, { useCallback, useMemo } from 'react';
import { BezierEdge, getBezierPath, EdgeLabelRenderer, EdgeProps } from 'reactflow';
import { Flex } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { NodeOutputKeyEnum, RuntimeEdgeStatusEnum } from '@fastgpt/global/core/workflow/constants';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
const ButtonEdge = (props: EdgeProps) => {
const { nodes, setEdges, workflowDebugData, hoverEdgeId } = useContextSelector(
WorkflowContext,
(v) => v
);
const {
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
selected,
source,
sourceHandleId,
target,
targetHandleId,
style
} = props;
const onDelConnect = useCallback(
(id: string) => {
setEdges((state) => state.filter((item) => item.id !== id));
},
[setEdges]
);
const highlightEdge = useMemo(() => {
const connectNode = nodes.find((node) => {
return node.selected && (node.id === props.source || node.id === props.target);
});
return !!(connectNode || selected);
}, [nodes, props.source, props.target, selected]);
const [, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition
});
const isToolEdge = sourceHandleId === NodeOutputKeyEnum.selectedTools;
const isHover = hoverEdgeId === id;
const { newTargetX, newTargetY } = useMemo(() => {
if (targetPosition === 'left') {
return {
newTargetX: targetX - 3,
newTargetY: targetY
};
}
if (targetPosition === 'right') {
return {
newTargetX: targetX + 3,
newTargetY: targetY
};
}
if (targetPosition === 'bottom') {
return {
newTargetX: targetX,
newTargetY: targetY + 3
};
}
if (targetPosition === 'top') {
return {
newTargetX: targetX,
newTargetY: targetY - 3
};
}
return {
newTargetX: targetX,
newTargetY: targetY
};
}, [targetPosition, targetX, targetY]);
const edgeColor = useMemo(() => {
const targetEdge = workflowDebugData?.runtimeEdges.find(
(edge) => edge.sourceHandle === sourceHandleId && edge.targetHandle === targetHandleId
);
if (!targetEdge) {
if (highlightEdge) return '#3370ff';
return '#94B5FF';
}
// debug mode
const colorMap = {
[RuntimeEdgeStatusEnum.active]: '#39CC83',
[RuntimeEdgeStatusEnum.waiting]: '#5E8FFF',
[RuntimeEdgeStatusEnum.skipped]: '#8A95A7'
};
return colorMap[targetEdge.status];
}, [highlightEdge, sourceHandleId, targetHandleId, workflowDebugData?.runtimeEdges]);
const memoEdgeLabel = useMemo(() => {
const arrowTransform = (() => {
if (targetPosition === 'left') {
return `translate(-85%, -47%) translate(${newTargetX}px,${newTargetY}px) rotate(0deg)`;
}
if (targetPosition === 'right') {
return `translate(-10%, -50%) translate(${newTargetX}px,${newTargetY}px) rotate(-180deg)`;
}
if (targetPosition === 'bottom') {
return `translate(-50%, -20%) translate(${newTargetX}px,${newTargetY}px) rotate(-90deg)`;
}
if (targetPosition === 'top') {
return `translate(-50%, -90%) translate(${newTargetX}px,${newTargetY}px) rotate(90deg)`;
}
})();
return (
<EdgeLabelRenderer>
<Flex
display={isHover || highlightEdge ? 'flex' : 'none'}
alignItems={'center'}
justifyContent={'center'}
position={'absolute'}
transform={`translate(-55%, -50%) translate(${labelX}px,${labelY}px)`}
pointerEvents={'all'}
w={'17px'}
h={'17px'}
bg={'white'}
borderRadius={'17px'}
cursor={'pointer'}
zIndex={1000}
onClick={() => onDelConnect(id)}
>
<MyIcon name={'core/workflow/closeEdge'} w={'100%'}></MyIcon>
</Flex>
{!isToolEdge && (
<Flex
alignItems={'center'}
justifyContent={'center'}
position={'absolute'}
transform={arrowTransform}
pointerEvents={'all'}
w={highlightEdge ? '14px' : '10px'}
h={highlightEdge ? '14px' : '10px'}
// bg={'white'}
zIndex={highlightEdge ? 1000 : 0}
>
<MyIcon
name={'core/workflow/edgeArrow'}
w={'100%'}
color={edgeColor}
{...(highlightEdge
? {
fontWeight: 'bold'
}
: {})}
></MyIcon>
</Flex>
)}
</EdgeLabelRenderer>
);
}, [
isHover,
highlightEdge,
labelX,
labelY,
isToolEdge,
edgeColor,
targetPosition,
newTargetX,
newTargetY,
onDelConnect,
id
]);
const memoBezierEdge = useMemo(() => {
const targetEdge = workflowDebugData?.runtimeEdges.find(
(edge) => edge.source === source && edge.target === target
);
const edgeStyle: React.CSSProperties = (() => {
if (!targetEdge) {
return {
...style,
...(highlightEdge
? {
strokeWidth: 5
}
: { strokeWidth: 3, zIndex: 2 })
};
}
return {
...style,
strokeWidth: 3,
zIndex: 2
};
})();
return (
<BezierEdge
{...props}
targetX={newTargetX}
targetY={newTargetY}
style={{
...edgeStyle,
stroke: edgeColor
}}
/>
);
}, [
workflowDebugData?.runtimeEdges,
props,
newTargetX,
newTargetY,
edgeColor,
source,
target,
style,
highlightEdge
]);
return (
<>
{memoBezierEdge}
{memoEdgeLabel}
</>
);
};
export default React.memo(ButtonEdge);

View File

@@ -0,0 +1,23 @@
import React from 'react';
import { Box } from '@chakra-ui/react';
import { BoxProps } from '@chakra-ui/react';
const Container = ({ children, ...props }: BoxProps) => {
return (
<Box
px={4}
mx={2}
mb={2}
py={'10px'}
position={'relative'}
bg={'myGray.50'}
border={'1px solid #F0F1F6'}
borderRadius={'md'}
{...props}
>
{children}
</Box>
);
};
export default React.memo(Container);

View File

@@ -0,0 +1,35 @@
import React from 'react';
import { Box, useTheme } from '@chakra-ui/react';
const Divider = ({
text,
showBorderBottom = true,
icon
}: {
text?: 'Input' | 'Output' | string;
showBorderBottom?: boolean;
icon?: React.ReactNode;
}) => {
const theme = useTheme();
const isDivider = !text;
return (
<Box
alignItems={'center'}
display={'flex'}
justifyContent={'center'}
bg={'myGray.25'}
py={isDivider ? '0' : 2}
borderTop={theme.borders.base}
borderBottom={showBorderBottom ? theme.borders.base : 0}
fontWeight={'medium'}
>
{icon}
{icon && <Box w={1} />}
{text}
</Box>
);
};
export default React.memo(Divider);

View File

@@ -0,0 +1,13 @@
import React from 'react';
import { Box, Flex } from '@chakra-ui/react';
const IOTitle = ({ text }: { text?: 'Input' | 'Output' | string }) => {
return (
<Flex fontSize={'md'} alignItems={'center'} fontWeight={'medium'} mb={3}>
<Box w={'3px'} h={'14px'} borderRadius={'13px'} bg={'primary.600'} mr={1.5} />
{text}
</Flex>
);
};
export default React.memo(IOTitle);

View File

@@ -0,0 +1,278 @@
import { storeNodes2RuntimeNodes } from '@fastgpt/global/core/workflow/runtime/utils';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type';
import { RuntimeEdgeItemType, StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { useCallback, useState } from 'react';
import { checkWorkflowNodeAndConnection } from '@/web/core/workflow/utils';
import { useTranslation } from 'next-i18next';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { uiWorkflow2StoreWorkflow } from '../../utils';
import { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type';
import dynamic from 'next/dynamic';
import {
Box,
Button,
Flex,
Textarea,
NumberDecrementStepper,
NumberIncrementStepper,
NumberInput,
NumberInputField,
NumberInputStepper,
Switch
} from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { checkInputIsReference } from '@fastgpt/global/core/workflow/utils';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext, getWorkflowStore } from '../../context';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
const MyRightDrawer = dynamic(
() => import('@fastgpt/web/components/common/MyDrawer/MyRightDrawer')
);
const JsonEditor = dynamic(() => import('@fastgpt/web/components/common/Textarea/JsonEditor'));
export const useDebug = () => {
const { t } = useTranslation();
const { toast } = useToast();
const setNodes = useContextSelector(WorkflowContext, (v) => v.setNodes);
const onUpdateNodeError = useContextSelector(WorkflowContext, (v) => v.onUpdateNodeError);
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
const onStartNodeDebug = useContextSelector(WorkflowContext, (v) => v.onStartNodeDebug);
const [runtimeNodeId, setRuntimeNodeId] = useState<string>();
const [runtimeNodes, setRuntimeNodes] = useState<RuntimeNodeItemType[]>();
const [runtimeEdges, setRuntimeEdges] = useState<RuntimeEdgeItemType[]>();
const flowData2StoreDataAndCheck = useCallback(async () => {
const { nodes } = await getWorkflowStore();
const checkResults = checkWorkflowNodeAndConnection({ nodes, edges });
if (!checkResults) {
const storeNodes = uiWorkflow2StoreWorkflow({ nodes, edges });
return JSON.stringify(storeNodes);
} else {
checkResults.forEach((nodeId) => onUpdateNodeError(nodeId, true));
toast({
status: 'warning',
title: t('core.workflow.Check Failed')
});
return Promise.reject();
}
}, [edges, onUpdateNodeError, t, toast]);
const openDebugNode = useCallback(
async ({ entryNodeId }: { entryNodeId: string }) => {
setNodes((state) =>
state.map((node) => ({
...node,
data: {
...node.data,
debugResult: undefined
}
}))
);
const {
nodes,
edges
}: {
nodes: StoreNodeItemType[];
edges: StoreEdgeItemType[];
} = JSON.parse(await flowData2StoreDataAndCheck());
const runtimeNodes = storeNodes2RuntimeNodes(nodes, [entryNodeId]);
const runtimeEdges: RuntimeEdgeItemType[] = edges.map((edge) =>
edge.target === entryNodeId
? {
...edge,
status: 'active'
}
: {
...edge,
status: 'waiting'
}
);
setRuntimeNodeId(entryNodeId);
setRuntimeNodes(runtimeNodes);
setRuntimeEdges(runtimeEdges);
},
[flowData2StoreDataAndCheck, setNodes]
);
const DebugInputModal = useCallback(() => {
if (!runtimeNodes || !runtimeEdges) return <></>;
const runtimeNode = runtimeNodes.find((node) => node.nodeId === runtimeNodeId);
if (!runtimeNode) return <></>;
const renderInputs = runtimeNode.inputs.filter((input) => {
if (runtimeNode.flowNodeType === FlowNodeTypeEnum.pluginInput) return true;
if (checkInputIsReference(input)) return true;
if (input.required && !input.value) return true;
});
const { register, getValues, setValue, handleSubmit } = useForm<Record<string, any>>({
defaultValues: renderInputs.reduce((acc: Record<string, any>, input) => {
const isReference = checkInputIsReference(input);
if (isReference) {
acc[input.key] = undefined;
} else if (typeof input.value === 'object') {
acc[input.key] = JSON.stringify(input.value, null, 2);
} else {
acc[input.key] = input.value;
}
return acc;
}, {})
});
const onClose = () => {
setRuntimeNodeId(undefined);
setRuntimeNodes(undefined);
setRuntimeEdges(undefined);
};
const onclickRun = (data: Record<string, any>) => {
onStartNodeDebug({
entryNodeId: runtimeNode.nodeId,
runtimeNodes: runtimeNodes.map((node) =>
node.nodeId === runtimeNode.nodeId
? {
...runtimeNode,
inputs: runtimeNode.inputs.map((input) => {
let parseValue = (() => {
try {
if (
input.valueType === WorkflowIOValueTypeEnum.string ||
input.valueType === WorkflowIOValueTypeEnum.number ||
input.valueType === WorkflowIOValueTypeEnum.boolean
)
return data[input.key];
return JSON.parse(data[input.key]);
} catch (e) {
return data[input.key];
}
})();
return {
...input,
value: parseValue ?? input.value
};
})
}
: node
),
runtimeEdges: runtimeEdges
});
onClose();
};
return (
<MyRightDrawer
onClose={onClose}
iconSrc="core/workflow/debugBlue"
title={t('core.workflow.Debug Node')}
maxW={['90vw', '35vw']}
px={0}
>
<Box flex={'1 0 0'} overflow={'auto'} px={6}>
{renderInputs.map((input) => {
const required = input.required || false;
const RenderInput = (() => {
if (input.valueType === WorkflowIOValueTypeEnum.string) {
return (
<Textarea
{...register(input.key, {
required
})}
placeholder={t(input.placeholder || '')}
bg={'myGray.50'}
/>
);
}
if (input.valueType === WorkflowIOValueTypeEnum.number) {
return (
<NumberInput step={input.step} min={input.min} max={input.max} bg={'myGray.50'}>
<NumberInputField
{...register(input.key, {
required: input.required,
min: input.min,
max: input.max,
valueAsNumber: true
})}
/>
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
);
}
if (input.valueType === WorkflowIOValueTypeEnum.boolean) {
return (
<Box>
<Switch {...register(input.key)} />
</Box>
);
}
let value = getValues(input.key) || '';
if (typeof value !== 'string') {
value = JSON.stringify(value, null, 2);
}
return (
<JsonEditor
bg={'myGray.50'}
placeholder={t(input.placeholder || '')}
resize
value={value}
onChange={(e) => {
setValue(input.key, e);
}}
/>
);
})();
return !!RenderInput ? (
<Box key={input.key} _notLast={{ mb: 4 }} px={1}>
<Flex alignItems={'center'} mb={1}>
<Box position={'relative'}>
{required && (
<Box position={'absolute'} right={-2} top={'-1px'} color={'red.600'}>
*
</Box>
)}
{t(input.debugLabel || input.label)}
</Box>
{input.description && <QuestionTip ml={2} label={input.description} />}
</Flex>
{RenderInput}
</Box>
) : null;
})}
</Box>
<Flex py={2} justifyContent={'flex-end'} px={6}>
<Button onClick={handleSubmit(onclickRun)}></Button>
</Flex>
</MyRightDrawer>
);
}, [onStartNodeDebug, runtimeEdges, runtimeNodeId, runtimeNodes, t]);
return {
DebugInputModal,
openDebugNode
};
};
export default function Dom() {
return <></>;
}

View File

@@ -0,0 +1,130 @@
import { useCallback, useEffect, useState } from 'react';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { useCopyData } from '@/web/common/hooks/useCopyData';
import { useTranslation } from 'next-i18next';
import { Node } from 'reactflow';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext, getWorkflowStore } from '../../context';
export const useKeyboard = () => {
const { t } = useTranslation();
const { setNodes, onSaveWorkflow } = useContextSelector(WorkflowContext, (v) => v);
const { copyData } = useCopyData();
const [isDowningCtrl, setIsDowningCtrl] = useState(false);
const hasInputtingElement = useCallback(() => {
const activeElement = document.activeElement;
if (activeElement) {
const tagName = activeElement.tagName.toLowerCase();
const className = activeElement.className.toLowerCase();
if (tagName === 'input' || tagName === 'textarea') return true;
if (className.includes('prompteditor')) return true;
}
return false;
}, []);
const onCopy = useCallback(async () => {
if (hasInputtingElement()) return;
const { nodes } = await getWorkflowStore();
const selectedNodes = nodes.filter(
(node) => node.selected && !node.data?.isError && node.data?.unique !== true
);
if (selectedNodes.length === 0) return;
copyData(JSON.stringify(selectedNodes), t('core.workflow.Copy node'));
}, [copyData, hasInputtingElement, t]);
const onParse = useCallback(async () => {
if (hasInputtingElement()) return;
const copyResult = await navigator.clipboard.readText();
try {
const parseData = JSON.parse(copyResult) as Node<FlowNodeItemType, string | undefined>[];
// check is array
if (!Array.isArray(parseData)) return;
// filter workflow data
const newNodes = parseData
.filter((item) => !!item.type && item.data?.unique !== true)
.map((item) => {
const nodeId = getNanoid();
return {
// reset id
...item,
id: nodeId,
data: {
...item.data,
nodeId
},
position: {
x: item.position.x + 100,
y: item.position.y + 100
}
};
});
setNodes((prev) =>
prev
.map((node) => ({
...node,
selected: false
}))
//@ts-ignore
.concat(newNodes)
);
} catch (error) {}
}, [hasInputtingElement, setNodes]);
const handleKeyDown = useCallback(
(event: KeyboardEvent) => {
if (event.ctrlKey || event.metaKey) {
setIsDowningCtrl(true);
switch (event.key) {
case 'c':
onCopy();
break;
case 'v':
onParse();
break;
case 's':
event.preventDefault();
onSaveWorkflow();
break;
default:
break;
}
}
},
[onCopy, onParse, onSaveWorkflow]
);
const handleKeyUp = useCallback((event: KeyboardEvent) => {
setIsDowningCtrl(false);
}, []);
useEffect(() => {
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [handleKeyDown]);
useEffect(() => {
document.addEventListener('keyup', handleKeyUp);
return () => {
document.removeEventListener('keyup', handleKeyUp);
};
}, [handleKeyUp]);
return {
isDowningCtrl
};
};
export default function Dom() {
return <></>;
}

View File

@@ -0,0 +1,127 @@
import React, { useCallback, useMemo } from 'react';
import { Connection, NodeChange, OnConnectStartParams, addEdge, EdgeChange, Edge } from 'reactflow';
import { EDGE_TYPE } from '@fastgpt/global/core/workflow/node/constant';
import 'reactflow/dist/style.css';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useTranslation } from 'next-i18next';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useKeyboard } from './useKeyboard';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
export const useWorkflow = () => {
const { toast } = useToast();
const { t } = useTranslation();
const { openConfirm: onOpenConfirmDeleteNode, ConfirmModal: ConfirmDeleteModal } = useConfirm({
content: t('core.module.Confirm Delete Node'),
type: 'delete'
});
const { isDowningCtrl } = useKeyboard();
const { setConnectingEdge, nodes, onNodesChange, setEdges, onEdgesChange, setHoverEdgeId } =
useContextSelector(WorkflowContext, (v) => v);
/* node */
const handleNodesChange = useCallback(
(changes: NodeChange[]) => {
for (const change of changes) {
if (change.type === 'remove') {
const node = nodes.find((n) => n.id === change.id);
if (node && node.data.forbidDelete) {
return toast({
status: 'warning',
title: t('core.workflow.Can not delete node')
});
} else {
return onOpenConfirmDeleteNode(() => {
onNodesChange(changes);
setEdges((state) =>
state.filter((edge) => edge.source !== change.id && edge.target !== change.id)
);
})();
}
} else if (change.type === 'select' && change.selected === false && isDowningCtrl) {
change.selected = true;
}
}
onNodesChange(changes);
},
[isDowningCtrl, nodes, onNodesChange, onOpenConfirmDeleteNode, setEdges, t, toast]
);
const handleEdgeChange = useCallback(
(changes: EdgeChange[]) => {
onEdgesChange(changes.filter((change) => change.type !== 'remove'));
},
[onEdgesChange]
);
/* connect */
const onConnectStart = useCallback(
(event: any, params: OnConnectStartParams) => {
setConnectingEdge(params);
},
[setConnectingEdge]
);
const onConnectEnd = useCallback(() => {
setConnectingEdge(undefined);
}, [setConnectingEdge]);
const onConnect = useCallback(
({ connect }: { connect: Connection }) => {
setEdges((state) =>
addEdge(
{
...connect,
type: EDGE_TYPE
},
state
)
);
},
[setEdges]
);
const customOnConnect = useCallback(
(connect: Connection) => {
if (!connect.sourceHandle || !connect.targetHandle) {
return;
}
if (connect.source === connect.target) {
return toast({
status: 'warning',
title: t('core.module.Can not connect self')
});
}
onConnect({
connect
});
},
[onConnect, t, toast]
);
/* edge */
const onEdgeMouseEnter = useCallback(
(e: any, edge: Edge) => {
setHoverEdgeId(edge.id);
},
[setHoverEdgeId]
);
const onEdgeMouseLeave = useCallback(() => {
setHoverEdgeId(undefined);
}, [setHoverEdgeId]);
return {
ConfirmDeleteModal,
handleNodesChange,
handleEdgeChange,
onConnectStart,
onConnectEnd,
onConnect,
customOnConnect,
onEdgeMouseEnter,
onEdgeMouseLeave
};
};
export default function Dom() {
return <></>;
}

View File

@@ -0,0 +1,184 @@
import React, { useMemo } from 'react';
import ReactFlow, {
Background,
Controls,
ControlButton,
MiniMap,
NodeProps,
ReactFlowProvider,
useReactFlow
} from 'reactflow';
import { Box, IconButton, useDisclosure } from '@chakra-ui/react';
import { SmallCloseIcon } from '@chakra-ui/icons';
import { EDGE_TYPE, FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import dynamic from 'next/dynamic';
import ButtonEdge from './components/ButtonEdge';
import NodeTemplatesModal from './NodeTemplatesModal';
import 'reactflow/dist/style.css';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { connectionLineStyle, defaultEdgeOptions } from '../constants';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../context';
import { useWorkflow } from './hooks/useWorkflow';
const NodeSimple = dynamic(() => import('./nodes/NodeSimple'));
const nodeTypes: Record<FlowNodeTypeEnum, any> = {
[FlowNodeTypeEnum.emptyNode]: NodeSimple,
[FlowNodeTypeEnum.globalVariable]: NodeSimple,
[FlowNodeTypeEnum.systemConfig]: dynamic(() => import('./nodes/NodeSystemConfig')),
[FlowNodeTypeEnum.workflowStart]: dynamic(() => import('./nodes/NodeWorkflowStart')),
[FlowNodeTypeEnum.chatNode]: NodeSimple,
[FlowNodeTypeEnum.datasetSearchNode]: NodeSimple,
[FlowNodeTypeEnum.datasetConcatNode]: dynamic(() => import('./nodes/NodeDatasetConcat')),
[FlowNodeTypeEnum.answerNode]: dynamic(() => import('./nodes/NodeAnswer')),
[FlowNodeTypeEnum.classifyQuestion]: dynamic(() => import('./nodes/NodeCQNode')),
[FlowNodeTypeEnum.contentExtract]: dynamic(() => import('./nodes/NodeExtract')),
[FlowNodeTypeEnum.httpRequest468]: dynamic(() => import('./nodes/NodeHttp')),
[FlowNodeTypeEnum.runApp]: NodeSimple,
[FlowNodeTypeEnum.pluginInput]: dynamic(() => import('./nodes/NodePluginInput')),
[FlowNodeTypeEnum.pluginOutput]: dynamic(() => import('./nodes/NodePluginOutput')),
[FlowNodeTypeEnum.pluginModule]: NodeSimple,
[FlowNodeTypeEnum.queryExtension]: NodeSimple,
[FlowNodeTypeEnum.tools]: dynamic(() => import('./nodes/NodeTools')),
[FlowNodeTypeEnum.stopTool]: (data: NodeProps<FlowNodeItemType>) => (
<NodeSimple {...data} minW={'100px'} maxW={'300px'} />
),
[FlowNodeTypeEnum.lafModule]: dynamic(() => import('./nodes/NodeLaf')),
[FlowNodeTypeEnum.ifElseNode]: dynamic(() => import('./nodes/NodeIfElse')),
[FlowNodeTypeEnum.variableUpdate]: dynamic(() => import('./nodes/NodeVariableUpdate')),
[FlowNodeTypeEnum.code]: dynamic(() => import('./nodes/NodeCode'))
};
const edgeTypes = {
[EDGE_TYPE]: ButtonEdge
};
const Workflow = () => {
const { nodes, edges, reactFlowWrapper } = useContextSelector(WorkflowContext, (v) => v);
const {
ConfirmDeleteModal,
handleNodesChange,
handleEdgeChange,
onConnectStart,
onConnectEnd,
customOnConnect,
onEdgeMouseEnter,
onEdgeMouseLeave
} = useWorkflow();
const {
isOpen: isOpenTemplate,
onOpen: onOpenTemplate,
onClose: onCloseTemplate
} = useDisclosure();
return (
<ReactFlowProvider>
<Box
flex={'1 0 0'}
h={0}
w={'100%'}
position={'relative'}
onContextMenu={(e) => {
e.preventDefault();
return false;
}}
>
{/* open module template */}
<>
<IconButton
position={'absolute'}
top={5}
left={5}
size={'mdSquare'}
borderRadius={'50%'}
icon={<SmallCloseIcon fontSize={'26px'} />}
transform={isOpenTemplate ? '' : 'rotate(135deg)'}
transition={'0.2s ease'}
aria-label={''}
zIndex={1}
boxShadow={'2px 2px 6px #85b1ff'}
onClick={() => {
isOpenTemplate ? onCloseTemplate() : onOpenTemplate();
}}
/>
<NodeTemplatesModal isOpen={isOpenTemplate} onClose={onCloseTemplate} />
</>
<ReactFlow
ref={reactFlowWrapper}
fitView
nodes={nodes}
edges={edges}
minZoom={0.1}
maxZoom={1.5}
defaultEdgeOptions={defaultEdgeOptions}
elevateEdgesOnSelect
connectionLineStyle={connectionLineStyle}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
onNodesChange={handleNodesChange}
onEdgesChange={handleEdgeChange}
onConnect={customOnConnect}
onConnectStart={onConnectStart}
onConnectEnd={onConnectEnd}
onEdgeMouseEnter={onEdgeMouseEnter}
onEdgeMouseLeave={onEdgeMouseLeave}
>
<FlowController />
</ReactFlow>
</Box>
<ConfirmDeleteModal />
</ReactFlowProvider>
);
};
export default React.memo(Workflow);
const FlowController = React.memo(function FlowController() {
const { fitView } = useReactFlow();
const Render = useMemo(() => {
return (
<>
<MiniMap
style={{
height: 78,
width: 126,
marginBottom: 35
}}
pannable
/>
<Controls
position={'bottom-right'}
style={{
display: 'flex',
marginBottom: 5,
background: 'white',
borderRadius: '6px',
overflow: 'hidden',
boxShadow:
'0px 0px 1px 0px rgba(19, 51, 107, 0.20), 0px 12px 16px -4px rgba(19, 51, 107, 0.20)'
}}
showInteractive={false}
showFitView={false}
>
<MyTooltip label={'页面居中'}>
<ControlButton className="custom-workflow-fix_view" onClick={() => fitView()}>
<MyIcon name={'core/modules/fixview'} w={'14px'} />
</ControlButton>
</MyTooltip>
</Controls>
<Background />
</>
);
}, [fitView]);
return Render;
});

View File

@@ -0,0 +1,36 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from './render/NodeCard';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import Container from '../components/Container';
import RenderInput from './render/RenderInput';
import RenderToolInput from './render/RenderToolInput';
import { useTranslation } from 'next-i18next';
import IOTitle from '../components/IOTitle';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
const NodeAnswer = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, inputs } = data;
const splitToolInputs = useContextSelector(WorkflowContext, (ctx) => ctx.splitToolInputs);
const { toolInputs, commonInputs } = splitToolInputs(inputs, nodeId);
return (
<NodeCard minW={'400px'} selected={selected} {...data}>
<Container>
{toolInputs.length > 0 && (
<>
<IOTitle text={t('core.module.tool.Tool input')} />
<Container>
<RenderToolInput nodeId={nodeId} inputs={toolInputs} />
</Container>
</>
)}
<RenderInput nodeId={nodeId} flowInputList={commonInputs} />
{/* <RenderOutput nodeId={nodeId} flowOutputList={outputs} /> */}
</Container>
</NodeCard>
);
};
export default React.memo(NodeAnswer);

View File

@@ -0,0 +1,141 @@
import React, { useMemo } from 'react';
import { NodeProps, Position } from 'reactflow';
import { Box, Button, Flex, Textarea } from '@chakra-ui/react';
import NodeCard from './render/NodeCard';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import Container from '../components/Container';
import RenderInput from './render/RenderInput';
import type { ClassifyQuestionAgentItemType } from '@fastgpt/global/core/workflow/type/index.d';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { useTranslation } from 'next-i18next';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { SourceHandle } from './render/Handle';
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
const NodeCQNode = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, inputs } = data;
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const CustomComponent = useMemo(
() => ({
[NodeInputKeyEnum.agents]: ({
key: agentKey,
value = [],
...props
}: FlowNodeInputItemType) => {
const agents = value as ClassifyQuestionAgentItemType[];
return (
<Box>
{agents.map((item, i) => (
<Box key={item.key} mb={4}>
<Flex alignItems={'center'}>
<MyTooltip label={t('common.Delete')}>
<MyIcon
mt={1}
mr={2}
name={'minus'}
w={'12px'}
cursor={'pointer'}
color={'myGray.600'}
_hover={{ color: 'red.600' }}
onClick={() => {
onChangeNode({
nodeId,
type: 'updateInput',
key: agentKey,
value: {
...props,
key: agentKey,
value: agents.filter((input) => input.key !== item.key)
}
});
onChangeNode({
nodeId,
type: 'delOutput',
key: item.key
});
}}
/>
</MyTooltip>
<Box flex={1} color={'myGray.600'} fontWeight={'medium'}>
{i + 1}
</Box>
</Flex>
<Box position={'relative'}>
<Textarea
rows={2}
mt={1}
defaultValue={item.value}
bg={'white'}
fontSize={'sm'}
onChange={(e) => {
const newVal = agents.map((val) =>
val.key === item.key
? {
...val,
value: e.target.value
}
: val
);
onChangeNode({
nodeId,
type: 'updateInput',
key: agentKey,
value: {
...props,
key: agentKey,
value: newVal
}
});
}}
/>
<SourceHandle
nodeId={nodeId}
handleId={getHandleId(nodeId, 'source', item.key)}
position={Position.Right}
translate={[26, 0]}
/>
</Box>
</Box>
))}
<Button
fontSize={'sm'}
onClick={() => {
const key = getNanoid();
onChangeNode({
nodeId,
type: 'updateInput',
key: agentKey,
value: {
...props,
key: agentKey,
value: agents.concat({ value: '', key })
}
});
}}
>
{t('core.module.Add question type')}
</Button>
</Box>
);
}
}),
[nodeId, onChangeNode, t]
);
return (
<NodeCard minW={'400px'} selected={selected} {...data}>
<Container>
<RenderInput nodeId={nodeId} flowInputList={inputs} CustomComponent={CustomComponent} />
</Container>
</NodeCard>
);
};
export default React.memo(NodeCQNode);

View File

@@ -0,0 +1,106 @@
import React, { useCallback, useMemo } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from './render/NodeCard';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import Container from '../components/Container';
import RenderInput from './render/RenderInput';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { useTranslation } from 'next-i18next';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
import IOTitle from '../components/IOTitle';
import RenderToolInput from './render/RenderToolInput';
import RenderOutput from './render/RenderOutput';
import CodeEditor from '@fastgpt/web/components/common/Textarea/CodeEditor';
import { Box, Flex } from '@chakra-ui/react';
import { useI18n } from '@/web/context/I18n';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { JS_TEMPLATE } from '@fastgpt/global/core/workflow/template/system/sandbox/constants';
const NodeCode = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { workflowT } = useI18n();
const { nodeId, inputs, outputs } = data;
const splitToolInputs = useContextSelector(WorkflowContext, (ctx) => ctx.splitToolInputs);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const { toolInputs, commonInputs } = splitToolInputs(inputs, nodeId);
const { ConfirmModal, openConfirm } = useConfirm({
content: workflowT('code.Reset template confirm')
});
const CustomComponent = useMemo(() => {
return {
[NodeInputKeyEnum.code]: (item: FlowNodeInputItemType) => {
return (
<Box>
<Flex mb={1} alignItems={'flex-end'}>
<Box flex={'1'}>Javascript{workflowT('Code')}</Box>
<Box
cursor={'pointer'}
color={'primary.500'}
fontSize={'xs'}
onClick={openConfirm(() => {
onChangeNode({
nodeId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: JS_TEMPLATE
}
});
})}
>
{workflowT('code.Reset template')}
</Box>
</Flex>
<CodeEditor
bg={'white'}
borderRadius={'sm'}
value={item.value}
onChange={(e) => {
onChangeNode({
nodeId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e
}
});
}}
/>
</Box>
);
}
};
}, [nodeId, onChangeNode, openConfirm, workflowT]);
return (
<NodeCard minW={'400px'} selected={selected} {...data}>
{toolInputs.length > 0 && (
<>
<Container>
<IOTitle text={t('core.module.tool.Tool input')} />
<RenderToolInput nodeId={nodeId} inputs={toolInputs} />
</Container>
</>
)}
<Container>
<IOTitle text={t('common.Input')} />
<RenderInput
nodeId={nodeId}
flowInputList={commonInputs}
CustomComponent={CustomComponent}
/>
</Container>
<Container>
<IOTitle text={t('common.Output')} />
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
</Container>
<ConfirmModal />
</NodeCard>
);
};
export default React.memo(NodeCode);

View File

@@ -0,0 +1,120 @@
import React, { useCallback, useMemo } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from './render/NodeCard';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import Container from '../components/Container';
import RenderInput from './render/RenderInput';
import { Box, Button, Flex } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { SmallAddIcon } from '@chakra-ui/icons';
import { WorkflowIOValueTypeEnum, NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { getOneQuoteInputTemplate } from '@fastgpt/global/core/workflow/template/system/datasetConcat';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import MySlider from '@/components/Slider';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
import RenderOutput from './render/RenderOutput';
import IOTitle from '../components/IOTitle';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
const NodeDatasetConcat = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { llmModelList } = useSystemStore();
const { nodeId, inputs, outputs } = data;
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const quotes = useMemo(
() => inputs.filter((item) => item.valueType === WorkflowIOValueTypeEnum.datasetQuote),
[inputs]
);
const tokenLimit = useMemo(() => {
let maxTokens = 3000;
nodeList.forEach((item) => {
if (item.flowNodeType === FlowNodeTypeEnum.chatNode) {
const model =
item.inputs.find((item) => item.key === NodeInputKeyEnum.aiModel)?.value || '';
const quoteMaxToken =
llmModelList.find((item) => item.model === model)?.quoteMaxToken || 3000;
maxTokens = Math.max(maxTokens, quoteMaxToken);
}
});
return maxTokens;
}, [llmModelList, nodeList]);
const onAddField = useCallback(() => {
onChangeNode({
nodeId,
type: 'addInput',
value: getOneQuoteInputTemplate({ index: quotes.length + 1 })
});
}, [nodeId, onChangeNode, quotes.length]);
const CustomComponent = useMemo(() => {
return {
[NodeInputKeyEnum.datasetMaxTokens]: (item: FlowNodeInputItemType) => (
<Box px={2}>
<MySlider
markList={[
{ label: '100', value: 100 },
{ label: tokenLimit, value: tokenLimit }
]}
width={'100%'}
min={100}
max={tokenLimit}
step={50}
value={item.value}
onChange={(e) => {
onChangeNode({
nodeId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e
}
});
}}
/>
</Box>
),
customComponent: (item: FlowNodeInputItemType) => (
<Flex className="nodrag" cursor={'default'} alignItems={'center'} position={'relative'}>
<Box position={'relative'} fontWeight={'medium'} color={'myGray.600'}>
{t('core.workflow.Dataset quote')}
</Box>
<Box flex={'1 0 0'} />
<Button
variant={'whitePrimary'}
leftIcon={<SmallAddIcon />}
iconSpacing={1}
size={'sm'}
mr={'-5px'}
onClick={onAddField}
>
{t('common.Add New')}
</Button>
</Flex>
)
};
}, [nodeId, onAddField, onChangeNode, t, tokenLimit]);
return (
<NodeCard minW={'400px'} selected={selected} {...data}>
<Container position={'relative'}>
<RenderInput nodeId={nodeId} flowInputList={inputs} CustomComponent={CustomComponent} />
{/* {RenderQuoteList} */}
</Container>
<Container>
<IOTitle text={t('common.Output')} />
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
</Container>
</NodeCard>
);
};
export default React.memo(NodeDatasetConcat);

View File

@@ -0,0 +1,10 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from './render/NodeCard';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
const NodeEmpty = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
return <NodeCard selected={selected} {...data}></NodeCard>;
};
export default React.memo(NodeEmpty);

View File

@@ -0,0 +1,131 @@
import React from 'react';
import {
Box,
Button,
ModalFooter,
ModalBody,
Flex,
Switch,
Input,
Textarea
} from '@chakra-ui/react';
import type { ContextExtractAgentItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { useForm } from 'react-hook-form';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import MySelect from '@fastgpt/web/components/common/MySelect';
import { fnValueTypeSelect } from '@/web/core/workflow/constants/dataType';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
export const defaultField: ContextExtractAgentItemType = {
valueType: 'string',
required: false,
defaultValue: '',
desc: '',
key: '',
enum: ''
};
const ExtractFieldModal = ({
defaultField,
onClose,
onSubmit
}: {
defaultField: ContextExtractAgentItemType;
onClose: () => void;
onSubmit: (data: ContextExtractAgentItemType) => void;
}) => {
const { t } = useTranslation();
const { register, setValue, handleSubmit, watch } = useForm<ContextExtractAgentItemType>({
defaultValues: defaultField
});
const required = watch('required');
const valueType = watch('valueType');
return (
<MyModal
isOpen={true}
iconSrc="/imgs/workflow/extract.png"
title={t('core.module.extract.Field Setting Title')}
onClose={onClose}
w={['90vw', '500px']}
>
<ModalBody>
<Flex mt={2} alignItems={'center'}>
<Flex alignItems={'center'} flex={['1 0 80px', '1 0 100px']}>
<FormLabel>{t('core.module.extract.Required')}</FormLabel>
<QuestionTip ml={1} label={t('core.module.extract.Required Description')}></QuestionTip>
</Flex>
<Switch {...register('required')} />
</Flex>
{required && (
<Flex mt={5} alignItems={'center'}>
<FormLabel flex={['0 0 80px', '0 0 100px']}>{t('core.module.Default value')}</FormLabel>
<Input
bg={'myGray.50'}
placeholder={t('core.module.Default value placeholder')}
{...register('defaultValue')}
/>
</Flex>
)}
<Flex alignItems={'center'} mt={5}>
<FormLabel flex={['0 0 80px', '0 0 100px']}>{t('core.module.Data Type')}</FormLabel>
<Box flex={'1 0 0'}>
<MySelect
list={fnValueTypeSelect}
value={valueType}
onchange={(e: any) => {
setValue('valueType', e);
}}
/>
</Box>
</Flex>
<Flex mt={5} alignItems={'center'}>
<FormLabel flex={['0 0 80px', '0 0 100px']}>{t('Field name')}</FormLabel>
<Input
bg={'myGray.50'}
placeholder="name/age/sql"
{...register('key', { required: true })}
/>
</Flex>
<Flex mt={5} alignItems={'center'}>
<FormLabel flex={['0 0 80px', '0 0 100px']}>
{t('core.module.Field Description')}
</FormLabel>
<Input
bg={'myGray.50'}
placeholder={t('core.module.extract.Field Description Placeholder')}
{...register('desc', { required: true })}
/>
</Flex>
{(valueType === 'string' || valueType === 'number') && (
<Box mt={5}>
<Flex alignItems={'center'}>
<FormLabel>
{t('core.module.extract.Enum Value')}({t('common.choosable')})
</FormLabel>
<QuestionTip ml={1} label={t('core.module.extract.Enum Description')}></QuestionTip>
</Flex>
<Textarea
rows={5}
bg={'myGray.50'}
placeholder={'apple\npeach\nwatermelon'}
{...register('enum')}
/>
</Box>
)}
</ModalBody>
<ModalFooter>
<Button onClick={handleSubmit(onSubmit)}>{t('common.Confirm')}</Button>
</ModalFooter>
</MyModal>
);
};
export default React.memo(ExtractFieldModal);

View File

@@ -0,0 +1,241 @@
import React, { useMemo, useState } from 'react';
import {
Box,
Button,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
Flex
} from '@chakra-ui/react';
import { NodeProps } from 'reactflow';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { useTranslation } from 'next-i18next';
import NodeCard from '../render/NodeCard';
import Container from '../../components/Container';
import { AddIcon } from '@chakra-ui/icons';
import RenderInput from '../render/RenderInput';
import type { ContextExtractAgentItemType } from '@fastgpt/global/core/workflow/type/index.d';
import RenderOutput from '../render/RenderOutput';
import MyIcon from '@fastgpt/web/components/common/Icon';
import ExtractFieldModal, { defaultField } from './ExtractFieldModal';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import RenderToolInput from '../render/RenderToolInput';
import {
FlowNodeInputItemType,
FlowNodeOutputItemType
} from '@fastgpt/global/core/workflow/type/io.d';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import IOTitle from '../../components/IOTitle';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../../context';
const NodeExtract = ({ data }: NodeProps<FlowNodeItemType>) => {
const { inputs, outputs, nodeId } = data;
const { t } = useTranslation();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const splitToolInputs = useContextSelector(WorkflowContext, (ctx) => ctx.splitToolInputs);
const { toolInputs, commonInputs } = splitToolInputs(inputs, nodeId);
const [editExtractFiled, setEditExtractField] = useState<ContextExtractAgentItemType>();
const CustomComponent = useMemo(
() => ({
[NodeInputKeyEnum.extractKeys]: ({
value: extractKeys = [],
...props
}: Omit<FlowNodeInputItemType, 'value'> & {
value?: ContextExtractAgentItemType[];
}) => (
<Box>
<Flex alignItems={'center'}>
<Box flex={'1 0 0'} fontWeight={'medium'} color={'myGray.600'}>
{t('core.module.extract.Target field')}
</Box>
<Button
size={'sm'}
variant={'whitePrimary'}
leftIcon={<AddIcon fontSize={'10px'} />}
onClick={() => setEditExtractField(defaultField)}
>
{t('core.module.extract.Add field')}
</Button>
</Flex>
<Box
mt={2}
borderRadius={'md'}
overflow={'hidden'}
borderWidth={'1px'}
borderBottom="none"
>
<TableContainer>
<Table bg={'white'}>
<Thead>
<Tr>
<Th bg={'myGray.50'} borderRadius={'none !important'}>
</Th>
<Th bg={'myGray.50'}></Th>
<Th bg={'myGray.50'}></Th>
<Th bg={'myGray.50'} borderRadius={'none !important'}></Th>
</Tr>
</Thead>
<Tbody>
{extractKeys.map((item, index) => (
<Tr
key={index}
position={'relative'}
whiteSpace={'pre-wrap'}
wordBreak={'break-all'}
>
<Td>{item.key}</Td>
<Td>{item.desc}</Td>
<Td>{item.required ? '✔' : ''}</Td>
<Td whiteSpace={'nowrap'}>
<MyIcon
mr={3}
name={'common/settingLight'}
w={'16px'}
cursor={'pointer'}
onClick={() => {
setEditExtractField(item);
}}
/>
<MyIcon
name={'delete'}
w={'16px'}
cursor={'pointer'}
onClick={() => {
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.extractKeys,
value: {
...props,
value: extractKeys.filter((extract) => item.key !== extract.key)
}
});
onChangeNode({
nodeId,
type: 'delOutput',
key: item.key
});
}}
/>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</Box>
</Box>
)
}),
[nodeId, onChangeNode, t]
);
return (
<NodeCard minW={'400px'} {...data}>
{toolInputs.length > 0 && (
<>
<Container>
<IOTitle text={t('core.module.tool.Tool input')} />
<RenderToolInput nodeId={nodeId} inputs={toolInputs} />
</Container>
</>
)}
<>
<Container>
<IOTitle text={t('common.Input')} />
<RenderInput
nodeId={nodeId}
flowInputList={commonInputs}
CustomComponent={CustomComponent}
/>
</Container>
</>
<>
<Container>
<IOTitle text={t('common.Output')} />
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
</Container>
</>
{!!editExtractFiled && (
<ExtractFieldModal
defaultField={editExtractFiled}
onClose={() => setEditExtractField(undefined)}
onSubmit={(data) => {
const extracts: ContextExtractAgentItemType[] =
inputs.find((item) => item.key === NodeInputKeyEnum.extractKeys)?.value || [];
const exists = extracts.find((item) => item.key === editExtractFiled.key);
const newInputs = exists
? extracts.map((item) => (item.key === editExtractFiled.key ? data : item))
: extracts.concat(data);
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.extractKeys,
value: {
...inputs.find((input) => input.key === NodeInputKeyEnum.extractKeys),
value: newInputs
}
});
const newOutput: FlowNodeOutputItemType = {
id: getNanoid(),
key: data.key,
label: `提取结果-${data.desc}`,
valueType: WorkflowIOValueTypeEnum.string,
type: FlowNodeOutputTypeEnum.static
};
if (exists) {
if (editExtractFiled.key === data.key) {
const output = outputs.find((output) => output.key === data.key);
// update
onChangeNode({
nodeId,
type: 'updateOutput',
key: data.key,
value: {
...output,
label: `提取结果-${data.desc}`
}
});
} else {
onChangeNode({
nodeId,
type: 'replaceOutput',
key: editExtractFiled.key,
value: newOutput
});
}
} else {
onChangeNode({
nodeId,
type: 'addOutput',
value: newOutput
});
}
setEditExtractField(undefined);
}}
/>
)}
</NodeCard>
);
};
export default React.memo(NodeExtract);

View File

@@ -0,0 +1,158 @@
import React from 'react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { ModalBody, Button, ModalFooter, useDisclosure, Textarea, Box } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useForm } from 'react-hook-form';
import parse from '@bany/curl-to-json';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../../context';
type RequestMethod = 'get' | 'post' | 'put' | 'delete' | 'patch';
const methodMap: { [K in RequestMethod]: string } = {
get: 'GET',
post: 'POST',
put: 'PUT',
delete: 'DELETE',
patch: 'PATCH'
};
const CurlImportModal = ({
nodeId,
inputs,
onClose
}: {
nodeId: string;
inputs: FlowNodeInputItemType[];
onClose: () => void;
}) => {
const { t } = useTranslation();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const { register, handleSubmit } = useForm({
defaultValues: {
curlContent: ''
}
});
const { toast } = useToast();
const handleFileProcessing = async (content: string) => {
try {
const requestUrl = inputs.find((item) => item.key === NodeInputKeyEnum.httpReqUrl);
const requestMethod = inputs.find((item) => item.key === NodeInputKeyEnum.httpMethod);
const params = inputs.find((item) => item.key === NodeInputKeyEnum.httpParams);
const headers = inputs.find((item) => item.key === NodeInputKeyEnum.httpHeaders);
const jsonBody = inputs.find((item) => item.key === NodeInputKeyEnum.httpJsonBody);
const parsed = parse(content);
if (!parsed.url) {
throw new Error('url not found');
}
const newParams = Object.keys(parsed.params || {}).map((key) => ({
key,
value: parsed.params?.[key],
type: 'string'
}));
const newHeaders = Object.keys(parsed.header || {}).map((key) => ({
key,
value: parsed.header?.[key],
type: 'string'
}));
const newBody = JSON.stringify(parsed.data, null, 2);
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.httpReqUrl,
value: {
...requestUrl,
value: parsed.url
}
});
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.httpMethod,
value: {
...requestMethod,
value: methodMap[parsed.method?.toLowerCase() as RequestMethod] || 'GET'
}
});
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.httpParams,
value: {
...params,
value: newParams
}
});
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.httpHeaders,
value: {
...headers,
value: newHeaders
}
});
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.httpJsonBody,
value: {
...jsonBody,
value: newBody
}
});
onClose();
toast({
title: t('common.Import success'),
status: 'success'
});
} catch (error: any) {
toast({
title: t('common.Import failed'),
description: error.message,
status: 'error'
});
console.error(error);
}
};
return (
<MyModal
isOpen
onClose={onClose}
iconSrc="modal/edit"
title={t('core.module.http.curl import')}
w={600}
>
<ModalBody>
<Textarea
rows={20}
mt={2}
{...register('curlContent')}
placeholder={t('core.module.http.curl import placeholder')}
/>
</ModalBody>
<ModalFooter>
<Button onClick={handleSubmit((data) => handleFileProcessing(data.curlContent))}>
{t('common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
);
};
export default React.memo(CurlImportModal);

View File

@@ -0,0 +1,681 @@
import React, { useCallback, useEffect, useMemo, useState, useTransition } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../render/NodeCard';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import Container from '../../components/Container';
import RenderInput from '../render/RenderInput';
import RenderOutput from '../render/RenderOutput';
import {
Box,
Flex,
Input,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
Button,
useDisclosure
} from '@chakra-ui/react';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { useTranslation } from 'next-i18next';
import Tabs from '@/components/Tabs';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
import { useToast } from '@fastgpt/web/hooks/useToast';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import JSONEditor from '@fastgpt/web/components/common/Textarea/JsonEditor';
import { formatEditorVariablePickerIcon } from '@fastgpt/global/core/workflow/utils';
import { EditorVariablePickerType } from '@fastgpt/web/components/common/Textarea/PromptEditor/type';
import HttpInput from '@fastgpt/web/components/common/Input/HttpInput';
import dynamic from 'next/dynamic';
import MySelect from '@fastgpt/web/components/common/MySelect';
import RenderToolInput from '../render/RenderToolInput';
import IOTitle from '../../components/IOTitle';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../../context';
import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils';
import { useMemoizedFn } from 'ahooks';
import { AppContext } from '@/pages/app/detail/components/context';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
const CurlImportModal = dynamic(() => import('./CurlImportModal'));
export const HttpHeaders = [
{ key: 'A-IM', label: 'A-IM' },
{ key: 'Accept', label: 'Accept' },
{ key: 'Accept-Charset', label: 'Accept-Charset' },
{ key: 'Accept-Encoding', label: 'Accept-Encoding' },
{ key: 'Accept-Language', label: 'Accept-Language' },
{ key: 'Accept-Datetime', label: 'Accept-Datetime' },
{ key: 'Access-Control-Request-Method', label: 'Access-Control-Request-Method' },
{ key: 'Access-Control-Request-Headers', label: 'Access-Control-Request-Headers' },
{ key: 'Authorization', label: 'Authorization' },
{ key: 'Cache-Control', label: 'Cache-Control' },
{ key: 'Connection', label: 'Connection' },
{ key: 'Content-Length', label: 'Content-Length' },
{ key: 'Content-Type', label: 'Content-Type' },
{ key: 'Cookie', label: 'Cookie' },
{ key: 'Date', label: 'Date' },
{ key: 'Expect', label: 'Expect' },
{ key: 'Forwarded', label: 'Forwarded' },
{ key: 'From', label: 'From' },
{ key: 'Host', label: 'Host' },
{ key: 'If-Match', label: 'If-Match' },
{ key: 'If-Modified-Since', label: 'If-Modified-Since' },
{ key: 'If-None-Match', label: 'If-None-Match' },
{ key: 'If-Range', label: 'If-Range' },
{ key: 'If-Unmodified-Since', label: 'If-Unmodified-Since' },
{ key: 'Max-Forwards', label: 'Max-Forwards' },
{ key: 'Origin', label: 'Origin' },
{ key: 'Pragma', label: 'Pragma' },
{ key: 'Proxy-Authorization', label: 'Proxy-Authorization' },
{ key: 'Range', label: 'Range' },
{ key: 'Referer', label: 'Referer' },
{ key: 'TE', label: 'TE' },
{ key: 'User-Agent', label: 'User-Agent' },
{ key: 'Upgrade', label: 'Upgrade' },
{ key: 'Via', label: 'Via' },
{ key: 'Warning', label: 'Warning' },
{ key: 'Dnt', label: 'Dnt' },
{ key: 'X-Requested-With', label: 'X-Requested-With' },
{ key: 'X-CSRF-Token', label: 'X-CSRF-Token' }
];
enum TabEnum {
params = 'params',
headers = 'headers',
body = 'body'
}
export type PropsArrType = {
key: string;
type: string;
value: string;
};
const RenderHttpMethodAndUrl = React.memo(function RenderHttpMethodAndUrl({
nodeId,
inputs
}: {
nodeId: string;
inputs: FlowNodeInputItemType[];
}) {
const { t } = useTranslation();
const { toast } = useToast();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const { isOpen: isOpenCurl, onOpen: onOpenCurl, onClose: onCloseCurl } = useDisclosure();
const requestMethods = inputs.find((item) => item.key === NodeInputKeyEnum.httpMethod);
const requestUrl = inputs.find((item) => item.key === NodeInputKeyEnum.httpReqUrl);
const onChangeUrl = (e: React.ChangeEvent<HTMLInputElement>) => {
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.httpReqUrl,
value: {
...requestUrl,
value: e.target.value
}
});
};
const onBlurUrl = (e: React.ChangeEvent<HTMLInputElement>) => {
const val = e.target.value;
// 拆分params和url
const url = val.split('?')[0];
const params = val.split('?')[1];
if (params) {
const paramsArr = params.split('&');
const paramsObj = paramsArr.reduce((acc, cur) => {
const [key, value] = cur.split('=');
return {
...acc,
[key]: value
};
}, {});
const inputParams = inputs.find((item) => item.key === NodeInputKeyEnum.httpParams);
if (!inputParams || Object.keys(paramsObj).length === 0) return;
const concatParams: PropsArrType[] = inputParams?.value || [];
Object.entries(paramsObj).forEach(([key, value]) => {
if (!concatParams.find((item) => item.key === key)) {
concatParams.push({ key, value: value as string, type: 'string' });
}
});
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.httpParams,
value: {
...inputParams,
value: concatParams
}
});
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.httpReqUrl,
value: {
...requestUrl,
value: url
}
});
toast({
status: 'success',
title: t('core.module.http.Url and params have been split')
});
}
};
return (
<Box>
<Box mb={2} display={'flex'} justifyContent={'space-between'}>
<Box fontWeight={'medium'} color={'myGray.600'}>
{t('core.module.Http request settings')}
</Box>
<Button variant={'link'} onClick={onOpenCurl}>
{t('core.module.http.curl import')}
</Button>
</Box>
<Flex alignItems={'center'} className="nodrag">
<MySelect
h={'34px'}
w={'88px'}
bg={'white'}
width={'100%'}
value={requestMethods?.value}
list={[
{
label: 'GET',
value: 'GET'
},
{
label: 'POST',
value: 'POST'
},
{
label: 'PUT',
value: 'PUT'
},
{
label: 'DELETE',
value: 'DELETE'
},
{
label: 'PATCH',
value: 'PATCH'
}
]}
onchange={(e) => {
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.httpMethod,
value: {
...requestMethods,
value: e
}
});
}}
/>
<Input
flex={'1 0 0'}
ml={2}
h={'34px'}
bg={'white'}
value={requestUrl?.value || ''}
placeholder={t('core.module.input.label.Http Request Url')}
fontSize={'xs'}
onChange={onChangeUrl}
onBlur={onBlurUrl}
/>
</Flex>
{isOpenCurl && <CurlImportModal nodeId={nodeId} inputs={inputs} onClose={onCloseCurl} />}
</Box>
);
});
export function RenderHttpProps({
nodeId,
inputs
}: {
nodeId: string;
inputs: FlowNodeInputItemType[];
}) {
const { t } = useTranslation();
const [selectedTab, setSelectedTab] = useState(TabEnum.params);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const { appDetail } = useContextSelector(AppContext, (v) => v);
const requestMethods = inputs.find((item) => item.key === NodeInputKeyEnum.httpMethod)?.value;
const params = inputs.find((item) => item.key === NodeInputKeyEnum.httpParams);
const headers = inputs.find((item) => item.key === NodeInputKeyEnum.httpHeaders);
const jsonBody = inputs.find((item) => item.key === NodeInputKeyEnum.httpJsonBody);
const paramsLength = params?.value?.length || 0;
const headersLength = headers?.value?.length || 0;
// get variable
const variables = useMemo(() => {
const globalVariables = getWorkflowGlobalVariables({
nodes: nodeList,
chatConfig: appDetail.chatConfig,
t
});
const moduleVariables = formatEditorVariablePickerIcon(
inputs
.filter((input) => input.canEdit || input.toolDescription)
.map((item) => ({
key: item.key,
label: item.label
}))
);
return [...moduleVariables, ...globalVariables];
}, [appDetail.chatConfig, inputs, nodeList, t]);
const variableText = useMemo(() => {
return variables
.map((item) => `${item.key}${item.key !== item.label ? `(${item.label})` : ''}`)
.join('\n');
}, [variables]);
const stringifyVariables = useMemo(
() =>
JSON.stringify({
params,
headers,
jsonBody,
variables
}),
[headers, jsonBody, params, variables]
);
const Render = useMemo(() => {
const { params, headers, jsonBody, variables } = JSON.parse(stringifyVariables);
return (
<Box>
<Flex alignItems={'center'} mb={2} fontWeight={'medium'} color={'myGray.600'}>
{t('core.module.Http request props')}
<QuestionTip
ml={1}
label={t('core.module.http.Props tip', { variable: variableText })}
></QuestionTip>
</Flex>
<Tabs
list={[
{ label: <RenderPropsItem text="Params" num={paramsLength} />, id: TabEnum.params },
...(!['GET', 'DELETE'].includes(requestMethods)
? [
{
label: (
<Flex alignItems={'center'}>
Body
{jsonBody?.value && <Box ml={1}></Box>}
</Flex>
),
id: TabEnum.body
}
]
: []),
{ label: <RenderPropsItem text="Headers" num={headersLength} />, id: TabEnum.headers }
]}
activeId={selectedTab}
onChange={(e) => setSelectedTab(e as any)}
/>
<Box bg={'white'} borderRadius={'md'}>
{params &&
headers &&
jsonBody &&
{
[TabEnum.params]: (
<RenderForm
nodeId={nodeId}
input={params}
variables={variables}
tabType={TabEnum.params}
/>
),
[TabEnum.body]: <RenderJson nodeId={nodeId} variables={variables} input={jsonBody} />,
[TabEnum.headers]: (
<RenderForm
nodeId={nodeId}
input={headers}
variables={variables}
tabType={TabEnum.headers}
/>
)
}[selectedTab]}
</Box>
</Box>
);
}, [
headersLength,
nodeId,
paramsLength,
requestMethods,
selectedTab,
stringifyVariables,
t,
variableText
]);
return Render;
}
const RenderForm = ({
nodeId,
input,
variables,
tabType
}: {
nodeId: string;
input: FlowNodeInputItemType;
variables: EditorVariablePickerType[];
tabType?: TabEnum;
}) => {
const { t } = useTranslation();
const { toast } = useToast();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const [list, setList] = useState<PropsArrType[]>(input.value || []);
const [updateTrigger, setUpdateTrigger] = useState(false);
const [shouldUpdateNode, setShouldUpdateNode] = useState(false);
const leftVariables = useMemo(() => {
return (tabType === TabEnum.headers ? HttpHeaders : variables).filter((variable) => {
const existVariables = list.map((item) => item.key);
return !existVariables.includes(variable.key);
});
}, [list, tabType, variables]);
useEffect(() => {
setList(input.value || []);
}, [input.value]);
useEffect(() => {
if (shouldUpdateNode) {
onChangeNode({
nodeId,
type: 'updateInput',
key: input.key,
value: {
...input,
value: list
}
});
setShouldUpdateNode(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [list]);
const handleKeyChange = useCallback(
(index: number, newKey: string) => {
setList((prevList) => {
if (!newKey) {
setUpdateTrigger((prev) => !prev);
toast({
status: 'warning',
title: t('core.module.http.Key cannot be empty')
});
return prevList;
}
const checkExist = prevList.find((item, i) => i !== index && item.key == newKey);
if (checkExist) {
setUpdateTrigger((prev) => !prev);
toast({
status: 'warning',
title: t('core.module.http.Key already exists')
});
return prevList;
}
return prevList.map((item, i) => (i === index ? { ...item, key: newKey } : item));
});
setShouldUpdateNode(true);
},
[t, toast]
);
const handleAddNewProps = useCallback(
(key: string, value: string = '') => {
setList((prevList) => {
if (!key) {
return prevList;
}
const checkExist = prevList.find((item) => item.key === key);
if (checkExist) {
setUpdateTrigger((prev) => !prev);
toast({
status: 'warning',
title: t('core.module.http.Key already exists')
});
return prevList;
}
return [...prevList, { key, type: 'string', value }];
});
setShouldUpdateNode(true);
},
[t, toast]
);
const Render = useMemo(() => {
return (
<Box mt={2} borderRadius={'md'} overflow={'hidden'} borderWidth={'1px'} borderBottom={'none'}>
<TableContainer overflowY={'visible'} overflowX={'unset'}>
<Table>
<Thead>
<Tr>
<Th px={2} borderBottomLeftRadius={'none !important'}>
{t('core.module.http.Props name')}
</Th>
<Th px={2} borderBottomRadius={'none !important'}>
{t('core.module.http.Props value')}
</Th>
</Tr>
</Thead>
<Tbody>
{list.map((item, index) => (
<Tr key={`${input.key}${index}`}>
<Td p={0} w={'150px'}>
<HttpInput
hasVariablePlugin={false}
hasDropDownPlugin={tabType === TabEnum.headers}
setDropdownValue={(value) => {
handleKeyChange(index, value);
setUpdateTrigger((prev) => !prev);
}}
placeholder={t('core.module.http.Props name')}
value={item.key}
variables={leftVariables}
onBlur={(val) => {
handleKeyChange(index, val);
}}
updateTrigger={updateTrigger}
/>
</Td>
<Td p={0}>
<Box display={'flex'} alignItems={'center'}>
<HttpInput
placeholder={t('core.module.http.Props value')}
value={item.value}
variables={variables}
onBlur={(val) => {
setList((prevList) =>
prevList.map((item, i) =>
i === index ? { ...item, value: val } : item
)
);
setShouldUpdateNode(true);
}}
/>
<MyIcon
name={'delete'}
cursor={'pointer'}
_hover={{ color: 'red.600' }}
w={'14px'}
onClick={() => {
setList((prevlist) => prevlist.filter((val) => val.key !== item.key));
setShouldUpdateNode(true);
}}
/>
</Box>
</Td>
</Tr>
))}
<Tr>
<Td p={0} w={'150px'}>
<HttpInput
hasVariablePlugin={false}
hasDropDownPlugin={tabType === TabEnum.headers}
setDropdownValue={(val) => {
handleAddNewProps(val);
setUpdateTrigger((prev) => !prev);
}}
placeholder={t('core.module.http.Add props')}
value={''}
variables={leftVariables}
updateTrigger={updateTrigger}
onBlur={(val) => {
handleAddNewProps(val);
setUpdateTrigger((prev) => !prev);
}}
/>
</Td>
<Td p={0}>
<Box display={'flex'} alignItems={'center'}>
<HttpInput />
</Box>
</Td>
</Tr>
</Tbody>
</Table>
</TableContainer>
</Box>
);
}, [
handleAddNewProps,
handleKeyChange,
input.key,
leftVariables,
list,
t,
tabType,
updateTrigger,
variables
]);
return Render;
};
const RenderJson = ({
nodeId,
input,
variables
}: {
nodeId: string;
input: FlowNodeInputItemType;
variables: EditorVariablePickerType[];
}) => {
const { t } = useTranslation();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const [_, startSts] = useTransition();
const Render = useMemo(() => {
return (
<Box mt={1}>
<JSONEditor
bg={'white'}
defaultHeight={200}
resize
value={input.value}
placeholder={t('core.module.template.http body placeholder')}
onChange={(e) => {
startSts(() => {
onChangeNode({
nodeId,
type: 'updateInput',
key: input.key,
value: {
...input,
value: e
}
});
});
}}
variables={variables}
/>
</Box>
);
}, [input, nodeId, onChangeNode, t, variables]);
return Render;
};
const RenderPropsItem = ({ text, num }: { text: string; num: number }) => {
return (
<Flex alignItems={'center'}>
<Box>{text}</Box>
{num > 0 && (
<Box ml={1} borderRadius={'50%'} bg={'myGray.200'} px={2} py={'1px'}>
{num}
</Box>
)}
</Flex>
);
};
const NodeHttp = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, inputs, outputs } = data;
const splitToolInputs = useContextSelector(WorkflowContext, (v) => v.splitToolInputs);
const { toolInputs, commonInputs, isTool } = splitToolInputs(inputs, nodeId);
const HttpMethodAndUrl = useMemoizedFn(() => (
<RenderHttpMethodAndUrl nodeId={nodeId} inputs={inputs} />
));
const Headers = useMemoizedFn(() => <RenderHttpProps nodeId={nodeId} inputs={inputs} />);
const CustomComponents = useMemo(() => {
return {
[NodeInputKeyEnum.httpMethod]: HttpMethodAndUrl,
[NodeInputKeyEnum.httpHeaders]: Headers
};
}, [Headers, HttpMethodAndUrl]);
return (
<NodeCard minW={'350px'} selected={selected} {...data}>
{isTool && (
<>
<Container>
<IOTitle text={t('core.module.tool.Tool input')} />
<RenderToolInput nodeId={nodeId} inputs={toolInputs} canEdit />
</Container>
</>
)}
<>
<Container>
<IOTitle text={t('common.Input')} />
<RenderInput
nodeId={nodeId}
flowInputList={commonInputs}
CustomComponent={CustomComponents}
/>
</Container>
</>
<>
<Container>
<IOTitle text={t('common.Output')} />
<RenderOutput flowOutputList={outputs} nodeId={nodeId} />
</Container>
</>
</NodeCard>
);
};
export default React.memo(NodeHttp);

View File

@@ -0,0 +1,470 @@
import { Box, Button, Flex } from '@chakra-ui/react';
import {
DraggableProvided,
DraggableStateSnapshot
} from '@fastgpt/web/components/common/DndDrag/index';
import Container from '../../components/Container';
import { MinusIcon, SmallAddIcon } from '@chakra-ui/icons';
import { IfElseListItemType } from '@fastgpt/global/core/workflow/template/system/ifElse/type';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { ReferenceValueProps } from '@fastgpt/global/core/workflow/type/io';
import { useTranslation } from 'next-i18next';
import { ReferSelector, useReference } from '../render/RenderInput/templates/Reference';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import {
VariableConditionEnum,
allConditionList,
arrayConditionList,
booleanConditionList,
numberConditionList,
objectConditionList,
stringConditionList
} from '@fastgpt/global/core/workflow/template/system/ifElse/constant';
import { useContextSelector } from 'use-context-selector';
import React, { useMemo } from 'react';
import { WorkflowContext } from '../../../context';
import MySelect from '@fastgpt/web/components/common/MySelect';
import MyInput from '@/components/MyInput';
import { getElseIFLabel, getHandleId } from '@fastgpt/global/core/workflow/utils';
import { SourceHandle } from '../render/Handle';
import { Position, useReactFlow } from 'reactflow';
import { getRefData } from '@/web/core/workflow/utils';
import DragIcon from '@fastgpt/web/components/common/DndDrag/DragIcon';
import { AppContext } from '@/pages/app/detail/components/context';
import { useI18n } from '@/web/context/I18n';
const ListItem = ({
provided,
snapshot,
conditionIndex,
conditionItem,
ifElseList,
onUpdateIfElseList,
nodeId
}: {
provided: DraggableProvided;
snapshot: DraggableStateSnapshot;
conditionIndex: number;
conditionItem: IfElseListItemType;
ifElseList: IfElseListItemType[];
onUpdateIfElseList: (value: IfElseListItemType[]) => void;
nodeId: string;
}) => {
const { t } = useTranslation();
const { getZoom } = useReactFlow();
const onDelEdge = useContextSelector(WorkflowContext, (v) => v.onDelEdge);
const handleId = getHandleId(nodeId, 'source', getElseIFLabel(conditionIndex));
const Render = useMemo(() => {
return (
<Flex
alignItems={'center'}
position={'relative'}
transform={snapshot.isDragging ? `scale(${getZoom()})` : ''}
transformOrigin={'top left'}
>
<Container w={snapshot.isDragging ? '' : 'full'} className="nodrag">
<Flex mb={4} alignItems={'center'}>
{ifElseList.length > 1 && <DragIcon provided={provided} />}
<Box color={'black'} fontSize={'md'} ml={2}>
{getElseIFLabel(conditionIndex)}
</Box>
{conditionItem.list?.length > 1 && (
<Flex
px={'2.5'}
color={'primary.600'}
fontWeight={'medium'}
alignItems={'center'}
cursor={'pointer'}
rounded={'md'}
onClick={() => {
onUpdateIfElseList(
ifElseList.map((ifElse, index) => {
if (index === conditionIndex) {
return {
...ifElse,
condition: ifElse.condition === 'AND' ? 'OR' : 'AND'
};
}
return ifElse;
})
);
}}
>
{conditionItem.condition}
<MyIcon ml={1} boxSize={5} name="change" />
</Flex>
)}
<Box flex={1}></Box>
{ifElseList.length > 1 && (
<MyIcon
ml={2}
boxSize={5}
name="delete"
cursor={'pointer'}
_hover={{ color: 'red.600' }}
color={'myGray.400'}
onClick={() => {
onUpdateIfElseList(ifElseList.filter((_, index) => index !== conditionIndex));
onDelEdge({
nodeId,
sourceHandle: handleId
});
}}
/>
)}
</Flex>
<Box>
{conditionItem.list?.map((item, i) => {
return (
<Box key={i}>
{/* condition list */}
<Flex gap={2} mb={2} alignItems={'center'}>
{/* variable reference */}
<Box minW={'250px'}>
<Reference
nodeId={nodeId}
variable={item.variable}
onSelect={(e) => {
onUpdateIfElseList(
ifElseList.map((ifElse, index) => {
if (index === conditionIndex) {
return {
...ifElse,
list: ifElse.list.map((item, index) => {
if (index === i) {
return {
...item,
variable: e,
condition: undefined
};
}
return item;
})
};
}
return ifElse;
})
);
}}
/>
</Box>
{/* condition select */}
<Box w={'130px'} flex={1}>
<ConditionSelect
condition={item.condition}
variable={item.variable}
onSelect={(e) => {
onUpdateIfElseList(
ifElseList.map((ifElse, index) => {
if (index === conditionIndex) {
return {
...ifElse,
list: ifElse.list.map((item, index) => {
if (index === i) {
return {
...item,
condition: e
};
}
return item;
})
};
}
return ifElse;
})
);
}}
/>
</Box>
{/* value */}
<Box w={'200px'}>
<ConditionValueInput
value={item.value}
condition={item.condition}
variable={item.variable}
onChange={(e) => {
onUpdateIfElseList(
ifElseList.map((ifElse, index) => {
return {
...ifElse,
list:
index === conditionIndex
? ifElse.list.map((item, index) => {
if (index === i) {
return {
...item,
value: e
};
}
return item;
})
: ifElse.list
};
})
);
}}
/>
</Box>
{/* delete */}
{conditionItem.list.length > 1 && (
<MinusIcon
ml={2}
boxSize={3}
name="delete"
cursor={'pointer'}
_hover={{ color: 'red.600' }}
color={'myGray.400'}
onClick={() => {
onUpdateIfElseList(
ifElseList.map((ifElse, index) => {
if (index === conditionIndex) {
return {
...ifElse,
list: ifElse.list.filter((_, index) => index !== i)
};
}
return ifElse;
})
);
}}
/>
)}
</Flex>
</Box>
);
})}
</Box>
<Button
onClick={() => {
onUpdateIfElseList(
ifElseList.map((ifElse, index) => {
if (index === conditionIndex) {
return {
...ifElse,
list: ifElse.list.concat({
variable: undefined,
condition: undefined,
value: undefined
})
};
}
return ifElse;
})
);
}}
variant={'link'}
leftIcon={<SmallAddIcon />}
my={3}
color={'primary.600'}
>
{t('core.module.input.add')}
</Button>
</Container>
{!snapshot.isDragging && (
<SourceHandle
nodeId={nodeId}
handleId={handleId}
position={Position.Right}
translate={[18, 0]}
/>
)}
</Flex>
);
}, [
conditionIndex,
conditionItem.condition,
conditionItem.list,
getZoom,
handleId,
ifElseList,
nodeId,
onDelEdge,
onUpdateIfElseList,
provided,
snapshot.isDragging,
t
]);
return (
<Box
ref={provided.innerRef}
{...provided.draggableProps}
style={{
...provided.draggableProps.style,
opacity: snapshot.isDragging ? 0.8 : 1
}}
>
{Render}
</Box>
);
};
export default React.memo(ListItem);
const Reference = ({
nodeId,
variable,
onSelect
}: {
nodeId: string;
variable?: ReferenceValueProps;
onSelect: (e: ReferenceValueProps) => void;
}) => {
const { t } = useTranslation();
const { referenceList, formatValue } = useReference({
nodeId,
valueType: WorkflowIOValueTypeEnum.any,
value: variable
});
return (
<ReferSelector
placeholder={t('选择引用变量')}
list={referenceList}
value={formatValue}
onSelect={onSelect}
/>
);
};
/* Different data types have different options */
const ConditionSelect = ({
condition,
variable,
onSelect
}: {
condition?: VariableConditionEnum;
variable?: ReferenceValueProps;
onSelect: (e: VariableConditionEnum) => void;
}) => {
const { t } = useTranslation();
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
// get condition type
const { valueType, required } = useMemo(() => {
return getRefData({
variable,
nodeList,
chatConfig: appDetail.chatConfig,
t
});
}, [appDetail.chatConfig, nodeList, t, variable]);
const conditionList = useMemo(() => {
if (valueType === WorkflowIOValueTypeEnum.string) return stringConditionList;
if (valueType === WorkflowIOValueTypeEnum.number) return numberConditionList;
if (valueType === WorkflowIOValueTypeEnum.boolean) return booleanConditionList;
if (
valueType === WorkflowIOValueTypeEnum.chatHistory ||
valueType === WorkflowIOValueTypeEnum.datasetQuote ||
valueType === WorkflowIOValueTypeEnum.dynamic ||
valueType === WorkflowIOValueTypeEnum.selectApp ||
valueType === WorkflowIOValueTypeEnum.arrayBoolean ||
valueType === WorkflowIOValueTypeEnum.arrayNumber ||
valueType === WorkflowIOValueTypeEnum.arrayObject ||
valueType === WorkflowIOValueTypeEnum.arrayString
)
return arrayConditionList;
if (valueType === WorkflowIOValueTypeEnum.object) return objectConditionList;
if (valueType === WorkflowIOValueTypeEnum.any) return allConditionList;
return [];
}, [valueType]);
const filterQuiredConditionList = useMemo(() => {
if (required) {
return conditionList.filter(
(item) =>
item.value !== VariableConditionEnum.isEmpty &&
item.value !== VariableConditionEnum.isNotEmpty
);
}
return conditionList;
}, [conditionList, required]);
return (
<MySelect
className="nowheel"
w={'100%'}
list={filterQuiredConditionList}
value={condition}
onchange={onSelect}
placeholder="选择条件"
/>
);
};
/*
Different condition can be entered differently
empty, notEmpty: forbid input
boolean type: select true/false
*/
const ConditionValueInput = ({
value = '',
variable,
condition,
onChange
}: {
value?: string;
variable?: ReferenceValueProps;
condition?: VariableConditionEnum;
onChange: (e: string) => void;
}) => {
const { workflowT } = useI18n();
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
// get value type
const valueType = useMemo(() => {
if (!variable) return;
const node = nodeList.find((node) => node.nodeId === variable[0]);
if (!node) return WorkflowIOValueTypeEnum.any;
const output = node.outputs.find((item) => item.id === variable[1]);
if (!output) return WorkflowIOValueTypeEnum.any;
return output.valueType;
}, [nodeList, variable]);
const Render = useMemo(() => {
if (valueType === WorkflowIOValueTypeEnum.boolean) {
return (
<MySelect
list={[
{ label: 'True', value: 'true' },
{ label: 'False', value: 'false' }
]}
onchange={onChange}
value={value}
placeholder={workflowT('ifelse.Select value')}
isDisabled={
condition === VariableConditionEnum.isEmpty ||
condition === VariableConditionEnum.isNotEmpty
}
/>
);
} else {
return (
<MyInput
value={value}
placeholder={
condition === VariableConditionEnum.reg
? '/^((+|00)86)?1[3-9]d{9}$/'
: workflowT('ifelse.Input value')
}
w={'100%'}
bg={'white'}
isDisabled={
condition === VariableConditionEnum.isEmpty ||
condition === VariableConditionEnum.isNotEmpty
}
onChange={(e) => onChange(e.target.value)}
/>
);
}
}, [condition, onChange, value, valueType, workflowT]);
return Render;
};

View File

@@ -0,0 +1,137 @@
import React, { useCallback, useMemo } from 'react';
import NodeCard from '../render/NodeCard';
import { useTranslation } from 'next-i18next';
import { Box, Button, Flex } from '@chakra-ui/react';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { NodeProps, Position } from 'reactflow';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type';
import { IfElseListItemType } from '@fastgpt/global/core/workflow/template/system/ifElse/type';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../../context';
import Container from '../../components/Container';
import DndDrag, { Draggable } from '@fastgpt/web/components/common/DndDrag/index';
import { SourceHandle } from '../render/Handle';
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
import ListItem from './ListItem';
import { IfElseResultEnum } from '@fastgpt/global/core/workflow/template/system/ifElse/constant';
const NodeIfElse = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, inputs = [] } = data;
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const elseHandleId = getHandleId(nodeId, 'source', IfElseResultEnum.ELSE);
const ifElseList = useMemo(
() =>
(inputs.find((input) => input.key === NodeInputKeyEnum.ifElseList)
?.value as IfElseListItemType[]) || [],
[inputs]
);
const onUpdateIfElseList = useCallback(
(value: IfElseListItemType[]) => {
const ifElseListInput = inputs.find((input) => input.key === NodeInputKeyEnum.ifElseList);
if (!ifElseListInput) return;
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.ifElseList,
value: {
...ifElseListInput,
value
}
});
},
[inputs, nodeId, onChangeNode]
);
return (
<NodeCard selected={selected} maxW={'1000px'} {...data}>
<Box px={4} cursor={'default'}>
<DndDrag<IfElseListItemType>
onDragEndCb={(list: IfElseListItemType[]) => onUpdateIfElseList(list)}
dataList={ifElseList}
renderClone={(provided, snapshot, rubric) => (
<ListItem
provided={provided}
snapshot={snapshot}
conditionItem={ifElseList[rubric.source.index]}
conditionIndex={rubric.source.index}
ifElseList={ifElseList}
onUpdateIfElseList={onUpdateIfElseList}
nodeId={nodeId}
/>
)}
>
{(provided) => (
<Box {...provided.droppableProps} ref={provided.innerRef}>
{ifElseList.map((conditionItem, conditionIndex) => (
<Draggable
key={conditionIndex}
draggableId={conditionIndex.toString()}
index={conditionIndex}
>
{(provided, snapshot) => (
<ListItem
provided={provided}
snapshot={snapshot}
conditionItem={conditionItem}
conditionIndex={conditionIndex}
ifElseList={ifElseList}
onUpdateIfElseList={onUpdateIfElseList}
nodeId={nodeId}
/>
)}
</Draggable>
))}
</Box>
)}
</DndDrag>
<Container position={'relative'}>
<Flex alignItems={'center'}>
<Box color={'black'} fontSize={'md'} ml={2}>
{IfElseResultEnum.ELSE}
</Box>
<SourceHandle
nodeId={nodeId}
handleId={elseHandleId}
position={Position.Right}
translate={[26, 0]}
/>
</Flex>
</Container>
</Box>
<Box py={3} px={6}>
<Button
variant={'whiteBase'}
w={'full'}
onClick={() => {
const ifElseListInput = inputs.find(
(input) => input.key === NodeInputKeyEnum.ifElseList
);
if (!ifElseListInput) return;
onUpdateIfElseList([
...ifElseList,
{
condition: 'AND',
list: [
{
variable: undefined,
condition: undefined,
value: undefined
}
]
}
]);
}}
>
{t('core.module.input.Add Branch')}
</Button>
</Box>
</NodeCard>
);
};
export default React.memo(NodeIfElse);

View File

@@ -0,0 +1,332 @@
import React, { useCallback, useMemo } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from './render/NodeCard';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import Container from '../components/Container';
import { Box, Button, Center, Flex, useDisclosure } from '@chakra-ui/react';
import { WorkflowIOValueTypeEnum, NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { useTranslation } from 'next-i18next';
import { getLafAppDetail } from '@/web/support/laf/api';
import MySelect from '@fastgpt/web/components/common/MySelect';
import { getApiSchemaByUrl } from '@/web/core/app/api/plugin';
import { getType, str2OpenApiSchema } from '@fastgpt/global/core/app/httpPlugin/utils';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { ChevronRightIcon } from '@chakra-ui/icons';
import { useQuery } from '@tanstack/react-query';
import dynamic from 'next/dynamic';
import {
FlowNodeInputTypeEnum,
FlowNodeOutputTypeEnum
} from '@fastgpt/global/core/workflow/node/constant';
import { useToast } from '@fastgpt/web/hooks/useToast';
import RenderToolInput from './render/RenderToolInput';
import RenderInput from './render/RenderInput';
import RenderOutput from './render/RenderOutput';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import {
FlowNodeInputItemType,
FlowNodeOutputItemType
} from '@fastgpt/global/core/workflow/type/io';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import IOTitle from '../components/IOTitle';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
import { putUpdateTeam } from '@/web/support/user/team/api';
const LafAccountModal = dynamic(() => import('@/components/support/laf/LafAccountModal'));
const NodeLaf = (props: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { toast } = useToast();
const { feConfigs } = useSystemStore();
const { data, selected } = props;
const { nodeId, inputs, outputs } = data;
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const requestUrl = inputs.find((item) => item.key === NodeInputKeyEnum.httpReqUrl);
const { userInfo, initUserInfo } = useUserStore();
const token = userInfo?.team.lafAccount?.token;
const appid = userInfo?.team.lafAccount?.appid;
const {
data: lafData,
isLoading: isLoadingFunctions,
refetch: refetchFunction
} = useQuery(
['getLafFunctionList'],
async () => {
// load laf app detail
try {
const appDetail = await getLafAppDetail(appid || '');
// load laf app functions
const schemaUrl = `https://${appDetail?.domain.domain}/_/api-docs?token=${appDetail?.openapi_token}`;
const schema = await getApiSchemaByUrl(schemaUrl);
const openApiSchema = await str2OpenApiSchema(JSON.stringify(schema));
const filterPostSchema = openApiSchema.pathData.filter((item) => item.method === 'post');
return {
lafApp: appDetail,
lafFunctions: filterPostSchema.map((item) => ({
...item,
requestUrl: `https://${appDetail?.domain.domain}${item.path}`
}))
};
} catch (err) {
await putUpdateTeam({
lafAccount: { token: '', appid: '', pat: '' }
});
initUserInfo();
}
},
{
enabled: !!token && !!appid,
onError(err) {
toast({
status: 'error',
title: getErrText(err, '获取Laf函数列表失败')
});
}
}
);
const lafFunctionSelectList = useMemo(
() =>
lafData?.lafFunctions.map((item) => {
const functionName = item.path.slice(1);
return {
alias: functionName,
label: item.description ? (
<Box>
<Box>{functionName}</Box>
<Box fontSize={'xs'} color={'gray.500'}>
{item.description}
</Box>
</Box>
) : (
functionName
),
value: item.requestUrl
};
}) || [],
[lafData?.lafFunctions]
);
const selectedFunction = useMemo(
() => lafFunctionSelectList.find((item) => item.value === requestUrl?.value)?.value,
[lafFunctionSelectList, requestUrl?.value]
);
const { mutate: onSyncParams, isLoading: isSyncing } = useRequest({
mutationFn: async () => {
await refetchFunction();
const lafFunction = lafData?.lafFunctions.find(
(item) => item.requestUrl === selectedFunction
);
if (!lafFunction) return;
// update intro
if (lafFunction.description) {
onChangeNode({
nodeId,
type: 'attr',
key: 'intro',
value: lafFunction.description
});
}
// add input variables
const bodyParams =
lafFunction?.request?.content?.['application/json']?.schema?.properties || {};
const requiredParams =
lafFunction?.request?.content?.['application/json']?.schema?.required || [];
const allParams = [
...Object.keys(bodyParams).map((key) => ({
name: key,
desc: bodyParams[key].description,
required: requiredParams?.includes(key) || false,
value: `{{${key}}}`,
type: getType(bodyParams[key])
}))
].filter((item) => !inputs.find((input) => input.key === item.name));
allParams.forEach((param) => {
const newInput: FlowNodeInputItemType = {
key: param.name,
valueType: param.type,
label: param.name,
renderTypeList: [FlowNodeInputTypeEnum.reference],
required: param.required,
description: param.desc || '',
toolDescription: param.desc || '未设置参数描述',
canEdit: true,
editField: {
key: true,
valueType: true
}
};
onChangeNode({
nodeId,
type: 'addInput',
value: newInput
});
});
/* add output variables */
const responseParams =
lafFunction?.response?.default.content?.['application/json'].schema.properties || {};
const requiredResponseParams =
lafFunction?.response?.default.content?.['application/json'].schema.required || [];
const allResponseParams = [
...Object.keys(responseParams).map((key) => ({
valueType: getType(responseParams[key]),
name: key,
desc: responseParams[key].description,
required: requiredResponseParams?.includes(key) || false
}))
].filter((item) => !outputs.find((output) => output.key === item.name));
allResponseParams.forEach((param) => {
const newOutput: FlowNodeOutputItemType = {
id: getNanoid(),
key: param.name,
valueType: param.valueType,
label: param.name,
type: FlowNodeOutputTypeEnum.dynamic,
required: param.required,
description: param.desc || ''
};
onChangeNode({
nodeId,
type: 'addOutput',
value: newOutput
});
});
},
successToast: t('common.Sync success')
});
// not config laf
if (!token || !appid) {
return (
<NodeCard minW={'350px'} selected={selected} {...data}>
<ConfigLaf />
</NodeCard>
);
} else {
return (
<NodeCard minW={'350px'} selected={selected} {...data}>
<Container>
{/* select function */}
<MySelect
isLoading={isLoadingFunctions}
list={lafFunctionSelectList}
placeholder={t('core.module.laf.Select laf function')}
onchange={(e) => {
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.httpReqUrl,
value: {
...requestUrl,
value: e
}
});
}}
value={selectedFunction}
/>
{/* auto set params and go to edit */}
{!!selectedFunction && (
<Flex justifyContent={'flex-end'} mt={2} gap={2}>
<Button isLoading={isSyncing} variant={'grayBase'} size={'sm'} onClick={onSyncParams}>
{t('core.module.Laf sync params')}
</Button>
<Button
variant={'grayBase'}
size={'sm'}
onClick={() => {
const lafFunction = lafData?.lafFunctions.find(
(item) => item.requestUrl === selectedFunction
);
if (!lafFunction) return;
const url = `${feConfigs.lafEnv}/app/${lafData?.lafApp?.appid}/function${lafFunction?.path}?templateid=FastGPT_Laf`;
window.open(url, '_blank');
}}
>
{t('plugin.go to laf')}
</Button>
</Flex>
)}
</Container>
{!!selectedFunction && <RenderIO {...props} />}
</NodeCard>
);
}
};
export default React.memo(NodeLaf);
const ConfigLaf = () => {
const { t } = useTranslation();
const { userInfo } = useUserStore();
const { feConfigs } = useSystemStore();
const {
isOpen: isOpenLafConfig,
onOpen: onOpenLafConfig,
onClose: onCloseLafConfig
} = useDisclosure();
return !!feConfigs?.lafEnv ? (
<Center minH={150}>
<Button onClick={onOpenLafConfig} variant={'whitePrimary'}>
{t('plugin.Please bind laf accout first')} <ChevronRightIcon />
</Button>
{isOpenLafConfig && feConfigs?.lafEnv && (
<LafAccountModal defaultData={userInfo?.team.lafAccount} onClose={onCloseLafConfig} />
)}
</Center>
) : (
<Box>Laf环境</Box>
);
};
const RenderIO = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, inputs, outputs } = data;
const splitToolInputs = useContextSelector(WorkflowContext, (ctx) => ctx.splitToolInputs);
const { commonInputs, toolInputs, isTool } = splitToolInputs(inputs, nodeId);
return (
<>
{isTool && (
<>
<Container>
<IOTitle text={t('core.module.tool.Tool input')} />
<RenderToolInput nodeId={nodeId} inputs={toolInputs} canEdit />
</Container>
</>
)}
<>
<Container>
<IOTitle text={t('common.Input')} />
<RenderInput nodeId={nodeId} flowInputList={commonInputs} />
</Container>
</>
<>
<Container>
<IOTitle text={t('common.Output')} />
<RenderOutput flowOutputList={outputs} nodeId={nodeId} />
</Container>
</>
</>
);
};

View File

@@ -0,0 +1,225 @@
import React, { useMemo, useState } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from './render/NodeCard';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { Box, Button, Flex } from '@chakra-ui/react';
import { SmallAddIcon } from '@chakra-ui/icons';
import {
FlowNodeInputItemType,
FlowNodeOutputItemType
} from '@fastgpt/global/core/workflow/type/io.d';
import Container from '../components/Container';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import type {
EditInputFieldMapType,
EditNodeFieldType
} from '@fastgpt/global/core/workflow/node/type.d';
import { useTranslation } from 'next-i18next';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import {
FlowNodeInputMap,
FlowNodeInputTypeEnum,
FlowNodeOutputTypeEnum
} from '@fastgpt/global/core/workflow/node/constant';
import { FlowValueTypeMap } from '@/web/core/workflow/constants/dataType';
import VariableTable from './render/VariableTable';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
const defaultCreateField: EditNodeFieldType = {
label: '',
key: '',
description: '',
inputType: FlowNodeInputTypeEnum.reference,
valueType: WorkflowIOValueTypeEnum.string,
required: true
};
const createEditField: EditInputFieldMapType = {
key: true,
description: true,
required: true,
valueType: true,
inputType: true
};
const dynamicInputEditField: EditInputFieldMapType = {
key: true
};
const NodePluginInput = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, inputs, outputs } = data;
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const [createField, setCreateField] = useState<EditNodeFieldType>();
const [editField, setEditField] = useState<EditNodeFieldType>();
const Render = useMemo(() => {
return (
<NodeCard
minW={'300px'}
selected={selected}
menuForbid={{
rename: true,
copy: true,
delete: true
}}
{...data}
>
<Container mt={1}>
<Flex className="nodrag" cursor={'default'} alignItems={'center'} mb={3}>
<Box>{t('core.workflow.Custom inputs')}</Box>
<Box flex={'1 0 0'} />
<Button
variant={'whitePrimary'}
leftIcon={<SmallAddIcon />}
iconSpacing={1}
size={'sm'}
onClick={() => setCreateField(defaultCreateField)}
>
{t('common.Add New')}
</Button>
</Flex>
<VariableTable
fieldEditType={createEditField}
keys={inputs.map((input) => input.key)}
onCloseFieldEdit={() => {
setCreateField(undefined);
setEditField(undefined);
}}
variables={inputs.map((input) => {
const inputType = input.renderTypeList[0];
return {
icon: FlowNodeInputMap[inputType]?.icon as string,
label: t(input.label),
type: input.valueType ? t(FlowValueTypeMap[input.valueType]?.label) : '-',
key: input.key
};
})}
createField={createField}
onCreate={({ data }) => {
if (!data.key || !data.inputType) {
return;
}
const newInput: FlowNodeInputItemType = {
key: data.key,
valueType: data.valueType,
label: data.label || '',
renderTypeList: [data.inputType],
required: data.required,
description: data.description,
toolDescription: data.isToolInput ? data.description : undefined,
canEdit: true,
value: data.defaultValue,
editField: dynamicInputEditField,
maxLength: data.maxLength,
max: data.max,
min: data.min,
dynamicParamDefaultValue: data.dynamicParamDefaultValue
};
onChangeNode({
nodeId,
type: 'addInput',
value: newInput
});
const newOutput: FlowNodeOutputItemType = {
id: getNanoid(),
key: data.key,
valueType: data.valueType,
label: data.label,
type: FlowNodeOutputTypeEnum.static
};
onChangeNode({
nodeId,
type: 'addOutput',
value: newOutput
});
setCreateField(undefined);
}}
editField={editField}
onStartEdit={(key) => {
const input = inputs.find((input) => input.key === key);
if (!input) return;
setEditField({
...input,
inputType: input.renderTypeList[0],
isToolInput: !!input.toolDescription
});
}}
onEdit={({ data, changeKey }) => {
if (!data.inputType || !data.key || !editField?.key) return;
const output = outputs.find((output) => output.key === editField.key);
const newInput: FlowNodeInputItemType = {
...data,
key: data.key,
label: data.label || '',
renderTypeList: [data.inputType],
toolDescription: data.isToolInput ? data.description : undefined,
canEdit: true,
value: data.defaultValue,
editField: dynamicInputEditField
};
const newOutput: FlowNodeOutputItemType = {
...(output as FlowNodeOutputItemType),
valueType: data.valueType,
key: data.key,
label: data.label
};
if (changeKey) {
onChangeNode({
nodeId,
type: 'replaceInput',
key: editField.key,
value: newInput
});
onChangeNode({
nodeId,
type: 'replaceOutput',
key: editField.key,
value: newOutput
});
} else {
onChangeNode({
nodeId,
type: 'updateInput',
key: newInput.key,
value: newInput
});
onChangeNode({
nodeId,
type: 'updateOutput',
key: newOutput.key,
value: newOutput
});
}
setEditField(undefined);
}}
onDelete={(key) => {
onChangeNode({
nodeId,
type: 'delInput',
key
});
onChangeNode({
nodeId,
type: 'delOutput',
key
});
}}
/>
</Container>
</NodeCard>
);
}, [createField, data, editField, inputs, nodeId, onChangeNode, outputs, selected, t]);
return Render;
};
export default React.memo(NodePluginInput);

View File

@@ -0,0 +1,105 @@
import React, { useState } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from './render/NodeCard';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import dynamic from 'next/dynamic';
import { Box, Button, Flex } from '@chakra-ui/react';
import { SmallAddIcon } from '@chakra-ui/icons';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import Container from '../components/Container';
import { EditInputFieldMapType, EditNodeFieldType } from '@fastgpt/global/core/workflow/node/type';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { useTranslation } from 'next-i18next';
import RenderInput from './render/RenderInput';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
const FieldEditModal = dynamic(() => import('./render/FieldEditModal'));
const defaultCreateField: EditNodeFieldType = {
inputType: FlowNodeInputTypeEnum.reference,
key: '',
description: '',
valueType: WorkflowIOValueTypeEnum.string
};
const createEditField: EditInputFieldMapType = {
key: true,
description: true,
valueType: true
};
const NodePluginOutput = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, inputs } = data;
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const [createField, setCreateField] = useState<EditNodeFieldType>();
return (
<NodeCard
minW={'300px'}
selected={selected}
menuForbid={{
debug: true,
rename: true,
copy: true,
delete: true
}}
{...data}
>
<Container mt={1}>
<Flex className="nodrag" cursor={'default'} alignItems={'center'} position={'relative'}>
<Box position={'relative'} fontWeight={'medium'}>
{t('core.workflow.Custom outputs')}
</Box>
<Box flex={'1 0 0'} />
<Button
variant={'whitePrimary'}
leftIcon={<SmallAddIcon />}
iconSpacing={1}
size={'sm'}
onClick={() => setCreateField(defaultCreateField)}
>
{t('common.Add New')}
</Button>
</Flex>
<RenderInput nodeId={nodeId} flowInputList={inputs} />
</Container>
{!!createField && (
<FieldEditModal
editField={createEditField}
defaultField={createField}
keys={inputs.map((input) => input.key)}
onClose={() => setCreateField(undefined)}
onSubmit={({ data }) => {
if (!data.key || !data.label) {
return;
}
const newInput: FlowNodeInputItemType = {
key: data.key,
valueType: data.valueType,
label: data.label,
renderTypeList: [FlowNodeInputTypeEnum.reference],
required: false,
description: data.description,
canEdit: true,
editField: createEditField
};
onChangeNode({
nodeId,
type: 'addInput',
value: newInput
});
setCreateField(undefined);
}}
/>
)}
</NodeCard>
);
};
export default React.memo(NodePluginOutput);

View File

@@ -0,0 +1,57 @@
import React, { useMemo } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from './render/NodeCard';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import Container from '../components/Container';
import RenderInput from './render/RenderInput';
import RenderOutput from './render/RenderOutput';
import RenderToolInput from './render/RenderToolInput';
import { useTranslation } from 'next-i18next';
import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import IOTitle from '../components/IOTitle';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
const NodeSimple = ({
data,
selected,
minW = '350px',
maxW
}: NodeProps<FlowNodeItemType> & { minW?: string | number; maxW?: string | number }) => {
const { t } = useTranslation();
const splitToolInputs = useContextSelector(WorkflowContext, (ctx) => ctx.splitToolInputs);
const { nodeId, inputs, outputs } = data;
const { toolInputs, commonInputs } = splitToolInputs(inputs, nodeId);
const filterHiddenInputs = useMemo(() => commonInputs.filter((item) => true), [commonInputs]);
return (
<NodeCard minW={minW} maxW={maxW} selected={selected} {...data}>
{toolInputs.length > 0 && (
<>
<Container>
<IOTitle text={t('core.module.tool.Tool input')} />
<RenderToolInput nodeId={nodeId} inputs={toolInputs} />
</Container>
</>
)}
{filterHiddenInputs.length > 0 && (
<>
<Container>
<IOTitle text={t('common.Input')} />
<RenderInput nodeId={nodeId} flowInputList={commonInputs} />
</Container>
</>
)}
{outputs.filter((output) => output.type !== FlowNodeOutputTypeEnum.hidden).length > 0 && (
<>
<Container>
<IOTitle text={t('common.Output')} />
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
</Container>
</>
)}
</NodeCard>
);
};
export default React.memo(NodeSimple);

View File

@@ -0,0 +1,221 @@
import React, { Dispatch, useMemo, useTransition } from 'react';
import { NodeProps } from 'reactflow';
import { Box, useTheme } from '@chakra-ui/react';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import QGSwitch from '@/components/core/app/QGSwitch';
import TTSSelect from '@/components/core/app/TTSSelect';
import WhisperConfig from '@/components/core/app/WhisperConfig';
import InputGuideConfig from '@/components/core/app/InputGuideConfig';
import { getAppChatConfig } from '@fastgpt/global/core/workflow/utils';
import { useTranslation } from 'next-i18next';
import { TTSTypeEnum } from '@/web/core/app/constants';
import NodeCard from './render/NodeCard';
import ScheduledTriggerConfig from '@/components/core/app/ScheduledTriggerConfig';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
import { AppChatConfigType, AppDetailType, VariableItemType } from '@fastgpt/global/core/app/type';
import { useMemoizedFn } from 'ahooks';
import VariableEdit from '@/components/core/app/VariableEdit';
import { AppContext } from '@/pages/app/detail/components/context';
import WelcomeTextConfig from '@/components/core/app/WelcomeTextConfig';
type ComponentProps = {
chatConfig: AppChatConfigType;
setAppDetail: Dispatch<React.SetStateAction<AppDetailType>>;
};
const NodeUserGuide = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const theme = useTheme();
const { appDetail, setAppDetail } = useContextSelector(AppContext, (v) => v);
const chatConfig = useMemo<AppChatConfigType>(() => {
return getAppChatConfig({
chatConfig: appDetail.chatConfig,
systemConfigNode: data,
isPublicFetch: true
});
}, [data, appDetail]);
const componentsProps = useMemo(
() => ({
chatConfig,
setAppDetail
}),
[chatConfig, setAppDetail]
);
return (
<>
<NodeCard
minW={'300px'}
selected={selected}
menuForbid={{
debug: true,
rename: true,
copy: true,
delete: true
}}
{...data}
>
<Box px={4} py={'10px'} position={'relative'} borderRadius={'md'} className="nodrag">
<WelcomeText {...componentsProps} />
<Box pt={4}>
<ChatStartVariable {...componentsProps} />
</Box>
<Box mt={3} pt={3} borderTop={theme.borders.base}>
<TTSGuide {...componentsProps} />
</Box>
<Box mt={3} pt={3} borderTop={theme.borders.base}>
<WhisperGuide {...componentsProps} />
</Box>
<Box mt={3} pt={3} borderTop={theme.borders.base}>
<QuestionGuide {...componentsProps} />
</Box>
<Box mt={3} pt={3} borderTop={theme.borders.base}>
<ScheduledTrigger {...componentsProps} />
</Box>
<Box mt={3} pt={3} borderTop={theme.borders.base}>
<QuestionInputGuide {...componentsProps} />
</Box>
</Box>
</NodeCard>
</>
);
};
export default React.memo(NodeUserGuide);
function WelcomeText({ chatConfig: { welcomeText }, setAppDetail }: ComponentProps) {
const { t } = useTranslation();
const [, startTst] = useTransition();
return (
<Box className="nodrag">
<WelcomeTextConfig
resize={'both'}
defaultValue={welcomeText}
onChange={(e) => {
startTst(() => {
setAppDetail((state) => ({
...state,
chatConfig: {
...state.chatConfig,
welcomeText: e.target.value
}
}));
});
}}
/>
</Box>
);
}
function ChatStartVariable({ chatConfig: { variables = [] }, setAppDetail }: ComponentProps) {
const updateVariables = useMemoizedFn((value: VariableItemType[]) => {
setAppDetail((state) => ({
...state,
chatConfig: {
...state.chatConfig,
variables: value
}
}));
});
return <VariableEdit variables={variables} onChange={(e) => updateVariables(e)} />;
}
function QuestionGuide({ chatConfig: { questionGuide = false }, setAppDetail }: ComponentProps) {
return (
<QGSwitch
isChecked={questionGuide}
onChange={(e) => {
const value = e.target.checked;
setAppDetail((state) => ({
...state,
chatConfig: {
...state.chatConfig,
questionGuide: value
}
}));
}}
/>
);
}
function TTSGuide({ chatConfig: { ttsConfig }, setAppDetail }: ComponentProps) {
return (
<TTSSelect
value={ttsConfig}
onChange={(e) => {
setAppDetail((state) => ({
...state,
chatConfig: {
...state.chatConfig,
ttsConfig: e
}
}));
}}
/>
);
}
function WhisperGuide({ chatConfig: { whisperConfig, ttsConfig }, setAppDetail }: ComponentProps) {
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
return (
<WhisperConfig
isOpenAudio={ttsConfig?.type !== TTSTypeEnum.none}
value={whisperConfig}
onChange={(e) => {
setAppDetail((state) => ({
...state,
chatConfig: {
...state.chatConfig,
whisperConfig: e
}
}));
}}
/>
);
}
function ScheduledTrigger({
chatConfig: { scheduledTriggerConfig },
setAppDetail
}: ComponentProps) {
return (
<ScheduledTriggerConfig
value={scheduledTriggerConfig}
onChange={(e) => {
setAppDetail((state) => ({
...state,
chatConfig: {
...state.chatConfig,
scheduledTriggerConfig: e
}
}));
}}
/>
);
}
function QuestionInputGuide({ chatConfig: { chatInputGuide }, setAppDetail }: ComponentProps) {
const appId = useContextSelector(WorkflowContext, (v) => v.appId);
return appId ? (
<InputGuideConfig
appId={appId}
value={chatInputGuide}
onChange={(e) => {
setAppDetail((state) => ({
...state,
chatConfig: {
...state.chatConfig,
chatInputGuide: e
}
}));
}}
/>
) : null;
}

View File

@@ -0,0 +1,43 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from './render/NodeCard';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import Divider from '../components/Divider';
import Container from '../components/Container';
import RenderInput from './render/RenderInput';
import { useTranslation } from 'next-i18next';
import { ToolSourceHandle } from './render/Handle/ToolHandle';
import { Box } from '@chakra-ui/react';
import IOTitle from '../components/IOTitle';
import MyIcon from '@fastgpt/web/components/common/Icon';
import RenderOutput from './render/RenderOutput';
const NodeTools = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, inputs, outputs } = data;
return (
<NodeCard minW={'350px'} selected={selected} {...data}>
<Container>
<IOTitle text={t('common.Input')} />
<RenderInput nodeId={nodeId} flowInputList={inputs} />
</Container>
<Container>
<IOTitle text={t('common.Output')} />
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
</Container>
<Box position={'relative'}>
<Box borderBottomLeftRadius={'md'} borderBottomRadius={'md'} overflow={'hidden'}>
<Divider
showBorderBottom={false}
icon={<MyIcon name="phoneTabbar/tool" w={'16px'} h={'16px'} />}
text={t('core.workflow.tool.Select Tool')}
/>
</Box>
<ToolSourceHandle nodeId={nodeId} />
</Box>
</NodeCard>
);
};
export default React.memo(NodeTools);

View File

@@ -0,0 +1,311 @@
import React, { useCallback, useMemo } from 'react';
import NodeCard from './render/NodeCard';
import { NodeProps } from 'reactflow';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type';
import { useTranslation } from 'next-i18next';
import {
Box,
Button,
Flex,
NumberDecrementStepper,
NumberIncrementStepper,
NumberInput,
NumberInputField,
NumberInputStepper,
Switch,
Textarea
} from '@chakra-ui/react';
import { TUpdateListItem } from '@fastgpt/global/core/workflow/template/system/variableUpdate/type';
import { NodeInputKeyEnum, WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
import {
FlowNodeInputMap,
FlowNodeInputTypeEnum
} from '@fastgpt/global/core/workflow/node/constant';
import Container from '../components/Container';
import MyIcon from '@fastgpt/web/components/common/Icon';
import JsonEditor from '@fastgpt/web/components/common/Textarea/JsonEditor';
import { SmallAddIcon } from '@chakra-ui/icons';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { ReferenceValueProps } from '@fastgpt/global/core/workflow/type/io';
import { ReferSelector, useReference } from './render/RenderInput/templates/Reference';
import { getRefData } from '@/web/core/workflow/utils';
import { isReferenceValue } from '@fastgpt/global/core/workflow/utils';
import { AppContext } from '@/pages/app/detail/components/context';
const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { inputs = [], nodeId } = data;
const { t } = useTranslation();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
const updateList = useMemo(
() =>
(inputs.find((input) => input.key === NodeInputKeyEnum.updateList)
?.value as TUpdateListItem[]) || [],
[inputs]
);
const onUpdateList = useCallback(
(value: TUpdateListItem[]) => {
const updateListInput = inputs.find((input) => input.key === NodeInputKeyEnum.updateList);
if (!updateListInput) return;
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.updateList,
value: {
...updateListInput,
value
}
});
},
[inputs, nodeId, onChangeNode]
);
const Render = useMemo(() => {
const menuList = [
{
renderType: FlowNodeInputTypeEnum.input,
icon: FlowNodeInputMap[FlowNodeInputTypeEnum.input].icon,
label: t('core.workflow.inputType.Manual input')
},
{
renderType: FlowNodeInputTypeEnum.reference,
icon: FlowNodeInputMap[FlowNodeInputTypeEnum.reference].icon,
label: t('core.workflow.inputType.Reference')
}
];
return (
<>
{updateList.map((updateItem, index) => {
const { valueType } = getRefData({
variable: updateItem.variable,
nodeList,
chatConfig: appDetail.chatConfig,
t
});
const renderTypeData = menuList.find((item) => item.renderType === updateItem.renderType);
const handleUpdate = (newValue: ReferenceValueProps | string) => {
if (isReferenceValue(newValue)) {
onUpdateList(
updateList.map((update, i) =>
i === index ? { ...update, value: newValue as ReferenceValueProps } : update
)
);
} else {
onUpdateList(
updateList.map((update, i) =>
i === index ? { ...update, value: ['', newValue as string] } : update
)
);
}
};
return (
<Container key={index} mt={4} w={'full'} mx={0}>
<Flex alignItems={'center'}>
<Flex w={'60px'}>{t('core.workflow.variable')}</Flex>
<Reference
nodeId={nodeId}
variable={updateItem.variable}
onSelect={(value) => {
onUpdateList(
updateList.map((update, i) => {
if (i === index) {
return {
...update,
value: ['', ''],
valueType,
variable: value
};
}
return update;
})
);
}}
/>
<Box flex={1} />
{updateList.length > 1 && (
<MyIcon
className="delete"
name={'delete'}
w={'14px'}
color={'myGray.600'}
cursor={'pointer'}
_hover={{ color: 'red.500' }}
position={'absolute'}
top={3}
right={3}
onClick={() => {
onUpdateList(updateList.filter((_, i) => i !== index));
}}
/>
)}
</Flex>
<Flex mt={2} w={'full'} alignItems={'center'} className="nodrag">
<Flex w={'60px'}>
<Box>{t('core.workflow.value')}</Box>
<MyTooltip
label={
menuList.find((item) => item.renderType === updateItem.renderType)?.label
}
>
<Button
size={'xs'}
bg={'white'}
borderRadius={'xs'}
mx={2}
onClick={() => {
onUpdateList(
updateList.map((update, i) => {
if (i === index) {
return {
...update,
value: ['', ''],
renderType:
updateItem.renderType === FlowNodeInputTypeEnum.input
? FlowNodeInputTypeEnum.reference
: FlowNodeInputTypeEnum.input
};
}
return update;
})
);
}}
>
<MyIcon name={renderTypeData?.icon as any} w={'14px'} />
</Button>
</MyTooltip>
</Flex>
{/* Render input components */}
{(() => {
if (updateItem.renderType === FlowNodeInputTypeEnum.reference) {
return (
<Reference
nodeId={nodeId}
variable={updateItem.value}
valueType={valueType}
onSelect={handleUpdate}
/>
);
}
if (valueType === WorkflowIOValueTypeEnum.string) {
return (
<Textarea
bg="white"
value={updateItem.value?.[1] || ''}
w="300px"
onChange={(e) => handleUpdate(e.target.value)}
/>
);
}
if (valueType === WorkflowIOValueTypeEnum.number) {
return (
<NumberInput value={Number(updateItem.value?.[1]) || 0}>
<NumberInputField
bg="white"
onChange={(e) => handleUpdate(e.target.value)}
/>
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
);
}
if (valueType === WorkflowIOValueTypeEnum.boolean) {
return (
<Switch
defaultChecked={updateItem.value?.[1] === 'true'}
onChange={(e) => handleUpdate(String(e.target.checked))}
/>
);
}
return (
<JsonEditor
bg="white"
resize
w="300px"
value={String(updateItem.value?.[1] || '')}
onChange={(e) => {
handleUpdate(e);
}}
/>
);
})()}
</Flex>
</Container>
);
})}
</>
);
}, [nodeId, nodeList, onUpdateList, t, updateList]);
return (
<NodeCard selected={selected} maxW={'1000px'} {...data}>
<Box px={4} pb={4}>
{Render}
<Flex className="nodrag" cursor={'default'} alignItems={'center'} position={'relative'}>
<Button
variant={'whiteBase'}
leftIcon={<SmallAddIcon />}
iconSpacing={1}
w={'full'}
size={'sm'}
onClick={() => {
onUpdateList([
...updateList,
{
variable: ['', ''],
value: ['', ''],
renderType: FlowNodeInputTypeEnum.input
}
]);
}}
>
{t('common.Add New')}
</Button>
</Flex>
</Box>
</NodeCard>
);
};
export default React.memo(NodeVariableUpdate);
const Reference = ({
nodeId,
variable,
valueType,
onSelect
}: {
nodeId: string;
variable?: ReferenceValueProps;
valueType?: WorkflowIOValueTypeEnum;
onSelect: (e: ReferenceValueProps) => void;
}) => {
const { t } = useTranslation();
const { referenceList, formatValue } = useReference({
nodeId,
valueType,
value: variable
});
return (
<ReferSelector
placeholder={t('选择引用变量')}
list={referenceList}
value={formatValue}
onSelect={onSelect}
/>
);
};

View File

@@ -0,0 +1,64 @@
import React, { useMemo } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from './render/NodeCard';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import Container from '../components/Container';
import RenderOutput from './render/RenderOutput';
import IOTitle from '../components/IOTitle';
import { useTranslation } from 'next-i18next';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
import { useCreation } from 'ahooks';
import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils';
import { FlowNodeOutputItemType } from '@fastgpt/global/core/workflow/type/io';
import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { AppContext } from '@/pages/app/detail/components/context';
const NodeStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, outputs } = data;
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const { appDetail } = useContextSelector(AppContext, (v) => v);
const variablesOutputs = useCreation(() => {
const variables = getWorkflowGlobalVariables({
nodes: nodeList,
chatConfig: appDetail.chatConfig,
t
});
return variables.map<FlowNodeOutputItemType>((item) => ({
id: item.key,
type: FlowNodeOutputTypeEnum.static,
key: item.key,
required: item.required,
valueType: item.valueType || WorkflowIOValueTypeEnum.any,
label: item.label
}));
}, [nodeList, t]);
return (
<NodeCard
minW={'240px'}
selected={selected}
menuForbid={{
rename: true,
copy: true,
delete: true
}}
{...data}
>
<Container>
<IOTitle text={t('common.Output')} />
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
</Container>
<Container>
<IOTitle text={t('core.module.Variable')} />
<RenderOutput nodeId={nodeId} flowOutputList={variablesOutputs} />
</Container>
</NodeCard>
);
};
export default React.memo(NodeStart);

View File

@@ -0,0 +1,530 @@
import React, { useCallback, useMemo } from 'react';
import {
Box,
Button,
ModalFooter,
ModalBody,
Flex,
Switch,
Input,
Textarea,
Stack
} from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { FlowValueTypeMap } from '@/web/core/workflow/constants/dataType';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import {
EditInputFieldMapType,
EditNodeFieldType
} from '@fastgpt/global/core/workflow/node/type.d';
import { useToast } from '@fastgpt/web/hooks/useToast';
import MySelect from '@fastgpt/web/components/common/MySelect';
import { NodeInputKeyEnum, WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import dynamic from 'next/dynamic';
import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput/index';
import { useI18n } from '@/web/context/I18n';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
const JsonEditor = dynamic(() => import('@fastgpt/web/components/common/Textarea/JsonEditor'));
const EmptyTip = dynamic(() => import('@fastgpt/web/components/common/EmptyTip'));
const defaultValue: EditNodeFieldType = {
inputType: FlowNodeInputTypeEnum.reference,
valueType: WorkflowIOValueTypeEnum.string,
key: '',
label: '',
description: '',
isToolInput: false,
defaultValue: '',
maxLength: undefined,
max: undefined,
min: undefined,
editField: {},
dynamicParamDefaultValue: {
inputType: FlowNodeInputTypeEnum.reference,
valueType: WorkflowIOValueTypeEnum.string,
required: true
}
};
const FieldEditModal = ({
editField = {
key: true
},
defaultField,
keys = [],
onClose,
onSubmit
}: {
editField?: EditInputFieldMapType;
defaultField: EditNodeFieldType;
keys: string[];
onClose: () => void;
onSubmit: (e: { data: EditNodeFieldType; changeKey: boolean }) => void;
}) => {
const { t } = useTranslation();
const { workflowT } = useI18n();
const { toast } = useToast();
const showDynamicInputSelect =
!keys.includes(NodeInputKeyEnum.addInputParam) ||
defaultField.key === NodeInputKeyEnum.addInputParam;
const inputTypeList = useMemo(
() => [
{
label: t('core.workflow.inputType.Reference'),
value: FlowNodeInputTypeEnum.reference,
defaultValue: {}
},
{
label: t('core.workflow.inputType.input'),
value: FlowNodeInputTypeEnum.input,
defaultValue: {
valueType: WorkflowIOValueTypeEnum.string
}
},
{
label: t('core.workflow.inputType.textarea'),
value: FlowNodeInputTypeEnum.textarea,
defaultValue: {
valueType: WorkflowIOValueTypeEnum.string
}
},
{
label: t('core.workflow.inputType.JSON Editor'),
value: FlowNodeInputTypeEnum.JSONEditor,
defaultValue: {
valueType: WorkflowIOValueTypeEnum.string
}
},
{
label: t('core.workflow.inputType.number input'),
value: FlowNodeInputTypeEnum.numberInput,
defaultValue: {
valueType: WorkflowIOValueTypeEnum.number
}
},
{
label: t('core.workflow.inputType.switch'),
value: FlowNodeInputTypeEnum.switch,
defaultValue: {
valueType: WorkflowIOValueTypeEnum.boolean
}
},
{
label: t('core.workflow.inputType.selectApp'),
value: FlowNodeInputTypeEnum.selectApp,
defaultValue: {
valueType: WorkflowIOValueTypeEnum.selectApp
}
},
{
label: t('core.workflow.inputType.selectLLMModel'),
value: FlowNodeInputTypeEnum.selectLLMModel,
defaultValue: {
valueType: WorkflowIOValueTypeEnum.string
}
},
{
label: t('core.workflow.inputType.selectDataset'),
value: FlowNodeInputTypeEnum.selectDataset,
defaultValue: {
valueType: WorkflowIOValueTypeEnum.selectDataset
}
},
...(showDynamicInputSelect
? [
{
label: t('core.workflow.inputType.dynamicTargetInput'),
value: FlowNodeInputTypeEnum.addInputParam,
defaultValue: {
label: t('core.workflow.inputType.dynamicTargetInput'),
valueType: WorkflowIOValueTypeEnum.dynamic,
key: NodeInputKeyEnum.addInputParam,
required: false
}
}
]
: [])
],
[showDynamicInputSelect, t]
);
const { register, getValues, setValue, handleSubmit, watch } = useForm<EditNodeFieldType>({
defaultValues: {
...defaultValue,
...defaultField,
valueType: defaultField.valueType ?? WorkflowIOValueTypeEnum.string
}
});
const inputType = watch('inputType');
const valueType = watch('valueType');
const isToolInput = watch('isToolInput');
const maxLength = watch('maxLength');
const max = watch('max');
const min = watch('min');
const defaultInputValueType = watch('dynamicParamDefaultValue.valueType');
const showKeyInput = useMemo(() => {
if (inputType === FlowNodeInputTypeEnum.addInputParam) return false;
return editField.key;
}, [editField.key, inputType]);
const showInputTypeSelect = useMemo(() => {
return editField.inputType;
}, [editField.inputType]);
const showDescriptionInput = useMemo(() => {
return editField.description;
}, [editField.description]);
const showValueTypeSelect = useMemo(() => {
if (!editField.valueType) return false;
if (inputType !== FlowNodeInputTypeEnum.reference) return false;
return true;
}, [editField.valueType, inputType]);
// input type config
const showToolInput = useMemo(() => {
return inputType === FlowNodeInputTypeEnum.reference;
}, [inputType]);
const showDefaultValue = useMemo(() => {
if (inputType === FlowNodeInputTypeEnum.input) return true;
if (inputType === FlowNodeInputTypeEnum.textarea) return true;
if (inputType === FlowNodeInputTypeEnum.JSONEditor) return true;
if (inputType === FlowNodeInputTypeEnum.numberInput) return true;
if (inputType === FlowNodeInputTypeEnum.switch) return true;
return false;
}, [inputType]);
const showMaxLenInput = useMemo(() => {
if (inputType === FlowNodeInputTypeEnum.input) return true;
if (inputType === FlowNodeInputTypeEnum.textarea) return true;
return false;
}, [inputType]);
const showMinMaxInput = useMemo(
() => inputType === FlowNodeInputTypeEnum.numberInput,
[inputType]
);
const showDynamicInput = useMemo(() => {
return inputType === FlowNodeInputTypeEnum.addInputParam;
}, [inputType]);
const slicedTypeMap = Object.values(FlowValueTypeMap).slice(0, -1);
const dataTypeSelectList = slicedTypeMap.map((item) => ({
label: t(item.label),
value: item.value
}));
const onSubmitSuccess = useCallback(
(data: EditNodeFieldType) => {
data.key = data?.key?.trim();
// add default value
const inputTypeConfig = inputTypeList.find((item) => item.value === data.inputType);
if (inputTypeConfig?.defaultValue) {
data.label = data.key;
for (const key in inputTypeConfig.defaultValue) {
// @ts-ignore
data[key] = inputTypeConfig.defaultValue[key];
}
}
if (!data.key) {
return toast({
status: 'warning',
title: t('core.module.edit.Field Name Cannot Be Empty')
});
}
// create check key
if (!defaultField.key && keys.includes(data.key)) {
return toast({
status: 'warning',
title: t('core.module.edit.Field Already Exist')
});
}
// edit check repeat key
if (defaultField.key && defaultField.key !== data.key && keys.includes(data.key)) {
return toast({
status: 'warning',
title: t('core.module.edit.Field Already Exist')
});
}
if (showValueTypeSelect && !data.valueType) {
return toast({
status: 'warning',
title: '数据类型不能为空'
});
}
onSubmit({
data,
changeKey: !keys.includes(data.key)
});
},
[defaultField.key, inputTypeList, keys, onSubmit, showValueTypeSelect, t, toast]
);
const onSubmitError = useCallback(
(e: Object) => {
for (const item of Object.values(e)) {
if (item.message) {
toast({
status: 'warning',
title: item.message
});
break;
}
}
},
[toast]
);
return (
<MyModal
isOpen={true}
iconSrc="/imgs/workflow/extract.png"
title={t('core.module.edit.Field Edit')}
maxW={['90vw', showInputTypeSelect ? '800px' : '400px']}
w={'100%'}
overflow={'unset'}
>
<ModalBody overflow={'visible'}>
<Flex gap={8} flexDirection={['column', 'row']}>
<Stack flex={1} gap={5}>
{showInputTypeSelect && (
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{t('core.module.Input Type')}</FormLabel>
<Box flex={1}>
<MySelect
list={inputTypeList}
value={inputType}
onchange={(e: string) => {
const type = e as FlowNodeInputTypeEnum;
setValue('inputType', type);
}}
/>
</Box>
</Flex>
)}
{showValueTypeSelect && !showInputTypeSelect && (
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{t('core.module.Data Type')}</FormLabel>
<Box flex={1}>
<MySelect
w={'full'}
list={dataTypeSelectList}
value={valueType}
onchange={(e: string) => {
const type = e as WorkflowIOValueTypeEnum;
setValue('valueType', type);
}}
/>
</Box>
</Flex>
)}
{showKeyInput && (
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{t('core.module.Field Name')}</FormLabel>
<Input
bg={'myGray.50'}
placeholder="appointment/sql"
{...register('key', {
required: true
})}
/>
</Flex>
)}
{showDescriptionInput && (
<Box alignItems={'flex-start'}>
<FormLabel flex={'0 0 70px'} mb={'1px'}>
{t('core.module.Field Description')}
</FormLabel>
<Textarea
bg={'myGray.50'}
placeholder={
isToolInput ? t('core.module.Plugin tool Description') : t('common.choosable')
}
rows={5}
{...register('description', { required: isToolInput ? true : false })}
/>
</Box>
)}
</Stack>
{/* input type config */}
{showInputTypeSelect && (
<Stack flex={1} gap={5}>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{workflowT('Field required')}</FormLabel>
<Switch {...register('required')} />
</Flex>
{showToolInput && (
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}></FormLabel>
<Switch {...register('isToolInput')} />
</Flex>
)}
{showValueTypeSelect && (
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{t('core.module.Data Type')}</FormLabel>
<Box flex={1}>
<MySelect
w={'full'}
list={dataTypeSelectList}
value={valueType}
onchange={(e: string) => {
const type = e as WorkflowIOValueTypeEnum;
setValue('valueType', type);
}}
/>
</Box>
</Flex>
)}
{showDefaultValue && (
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{t('core.module.Default Value')}</FormLabel>
{inputType === FlowNodeInputTypeEnum.numberInput && (
<Input
bg={'myGray.50'}
max={max}
min={min}
type={'number'}
{...register('defaultValue')}
/>
)}
{inputType === FlowNodeInputTypeEnum.input && (
<Input bg={'myGray.50'} maxLength={maxLength} {...register('defaultValue')} />
)}
{inputType === FlowNodeInputTypeEnum.textarea && (
<Textarea
bg={'myGray.50'}
maxLength={maxLength}
{...register('defaultValue')}
/>
)}
{inputType === FlowNodeInputTypeEnum.JSONEditor && (
<JsonEditor
resize
w={'full'}
onChange={(e) => {
setValue('defaultValue', e);
}}
defaultValue={String(getValues('defaultValue'))}
/>
)}
{inputType === FlowNodeInputTypeEnum.switch && (
<Switch {...register('defaultValue')} />
)}
</Flex>
)}
{showMaxLenInput && (
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{t('core.module.Max Length')}</FormLabel>
<MyNumberInput
flex={'1 0 0'}
bg={'myGray.50'}
placeholder={t('core.module.Max Length placeholder')}
value={maxLength}
onChange={(e) => {
// @ts-ignore
setValue('maxLength', e);
}}
// {...register('maxLength')}
/>
</Flex>
)}
{showMinMaxInput && (
<>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{t('core.module.Max Value')}</FormLabel>
<MyNumberInput
flex={'1 0 0'}
bg={'myGray.50'}
value={watch('max')}
onChange={(e) => {
// @ts-ignore
setValue('max', e);
}}
/>
</Flex>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{t('core.module.Min Value')}</FormLabel>
<MyNumberInput
flex={'1 0 0'}
bg={'myGray.50'}
value={watch('min')}
onChange={(e) => {
// @ts-ignore
setValue('min', e);
}}
/>
</Flex>
</>
)}
{showDynamicInput && (
<Stack gap={5}>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{t('core.module.Input Type')}</FormLabel>
<Box flex={1} fontWeight={'bold'}>
{t('core.workflow.inputType.Reference')}
</Box>
</Flex>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{t('core.module.Data Type')}</FormLabel>
<Box flex={1}>
<MySelect
list={dataTypeSelectList}
value={defaultInputValueType}
onchange={(e) => {
setValue(
'dynamicParamDefaultValue.valueType',
e as WorkflowIOValueTypeEnum
);
}}
/>
</Box>
</Flex>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{t('core.workflow.inputType.Required')}</FormLabel>
<Box flex={1}>
<Switch {...register('dynamicParamDefaultValue.required')} />
</Box>
</Flex>
</Stack>
)}
{!showToolInput &&
!showValueTypeSelect &&
!showDefaultValue &&
!showMaxLenInput &&
!showMinMaxInput &&
!showDynamicInput && <EmptyTip text={t('core.module.No Config Tips')} />}
</Stack>
)}
</Flex>
</ModalBody>
<ModalFooter>
<Button variant={'whiteBase'} mr={3} onClick={onClose}>
{t('common.Close')}
</Button>
<Button onClick={handleSubmit(onSubmitSuccess, onSubmitError)}>
{t('common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
);
};
export default React.memo(FieldEditModal);

View File

@@ -0,0 +1,188 @@
import React, { useMemo } from 'react';
import { Position } from 'reactflow';
import { SourceHandle, TargetHandle } from '.';
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../../../context';
export const ConnectionSourceHandle = ({ nodeId }: { nodeId: string }) => {
const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
const node = useMemo(() => nodeList.find((node) => node.nodeId === nodeId), [nodeList, nodeId]);
/* not node/not connecting node, hidden */
const showSourceHandle = useMemo(() => {
if (!node) return false;
if (connectingEdge && connectingEdge.nodeId !== nodeId) return false;
return true;
}, [connectingEdge, node, nodeId]);
const RightHandle = useMemo(() => {
const handleId = getHandleId(nodeId, 'source', Position.Right);
const rightTargetConnected = edges.some(
(edge) => edge.targetHandle === getHandleId(nodeId, 'target', Position.Right)
);
if (!node || !node?.sourceHandle?.right || rightTargetConnected) return null;
return (
<SourceHandle
nodeId={nodeId}
handleId={handleId}
position={Position.Right}
translate={[2, 0]}
/>
);
}, [edges, node, nodeId]);
const LeftHandlee = useMemo(() => {
const leftTargetConnected = edges.some(
(edge) => edge.targetHandle === getHandleId(nodeId, 'target', Position.Left)
);
if (!node || !node?.sourceHandle?.left || leftTargetConnected) return null;
const handleId = getHandleId(nodeId, 'source', Position.Left);
return (
<SourceHandle
nodeId={nodeId}
handleId={handleId}
position={Position.Left}
translate={[-6, 0]}
/>
);
}, [edges, node, nodeId]);
const TopHandlee = useMemo(() => {
if (
edges.some(
(edge) => edge.target === nodeId && edge.targetHandle === NodeOutputKeyEnum.selectedTools
)
)
return null;
const handleId = getHandleId(nodeId, 'source', Position.Top);
const topTargetConnected = edges.some(
(edge) => edge.targetHandle === getHandleId(nodeId, 'target', Position.Top)
);
if (!node || !node?.sourceHandle?.top || topTargetConnected) return null;
return (
<SourceHandle
nodeId={nodeId}
handleId={handleId}
position={Position.Top}
translate={[0, -2]}
/>
);
}, [edges, node, nodeId]);
const BottomHandlee = useMemo(() => {
const handleId = getHandleId(nodeId, 'source', Position.Bottom);
const targetConnected = edges.some(
(edge) => edge.targetHandle === getHandleId(nodeId, 'target', Position.Bottom)
);
if (!node || !node?.sourceHandle?.bottom || targetConnected) return null;
return (
<SourceHandle
nodeId={nodeId}
handleId={handleId}
position={Position.Bottom}
translate={[0, 2]}
/>
);
}, [edges, node, nodeId]);
return showSourceHandle ? (
<>
{RightHandle}
{LeftHandlee}
{TopHandlee}
{BottomHandlee}
</>
) : null;
};
export const ConnectionTargetHandle = ({ nodeId }: { nodeId: string }) => {
const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const node = useMemo(() => nodeList.find((node) => node.nodeId === nodeId), [nodeList, nodeId]);
const showHandle = useMemo(() => {
if (!node) return false;
if (connectingEdge && connectingEdge.nodeId === nodeId) return false;
return true;
}, [connectingEdge, node, nodeId]);
const LeftHandle = useMemo(() => {
if (!node || !node?.targetHandle?.left) return null;
const handleId = getHandleId(nodeId, 'target', Position.Left);
return (
<TargetHandle
nodeId={nodeId}
handleId={handleId}
position={Position.Left}
translate={[-2, 0]}
/>
);
}, [node, nodeId]);
const rightHandle = useMemo(() => {
if (!node || !node?.targetHandle?.right) return null;
const handleId = getHandleId(nodeId, 'target', Position.Right);
return (
<TargetHandle
nodeId={nodeId}
handleId={handleId}
position={Position.Right}
translate={[2, 0]}
/>
);
}, [node, nodeId]);
const topHandle = useMemo(() => {
if (!node || !node?.targetHandle?.top) return null;
const handleId = getHandleId(nodeId, 'target', Position.Top);
return (
<TargetHandle
nodeId={nodeId}
handleId={handleId}
position={Position.Top}
translate={[0, -2]}
/>
);
}, [node, nodeId]);
const bottomHandle = useMemo(() => {
if (!node || !node?.targetHandle?.bottom) return null;
const handleId = getHandleId(nodeId, 'target', Position.Bottom);
return (
<TargetHandle
nodeId={nodeId}
handleId={handleId}
position={Position.Bottom}
translate={[0, 2]}
/>
);
}, [node, nodeId]);
return showHandle ? (
<>
{LeftHandle}
{rightHandle}
{topHandle}
{bottomHandle}
</>
) : null;
};
export default function Dom() {
return <></>;
}

View File

@@ -0,0 +1,113 @@
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { Box, BoxProps } from '@chakra-ui/react';
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { useTranslation } from 'next-i18next';
import { Connection, Handle, Position } from 'reactflow';
import { useCallback, useMemo } from 'react';
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context';
const handleSize = '14px';
type ToolHandleProps = BoxProps & {
nodeId: string;
};
export const ToolTargetHandle = ({ nodeId }: ToolHandleProps) => {
const { t } = useTranslation();
const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge);
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
const handleId = NodeOutputKeyEnum.selectedTools;
const connected = edges.some((edge) => edge.target === nodeId && edge.targetHandle === handleId);
// if top handle is connected, return null
const hidden =
!connected &&
(connectingEdge?.handleId !== NodeOutputKeyEnum.selectedTools ||
edges.some((edge) => edge.targetHandle === getHandleId(nodeId, 'target', 'top')));
const Render = useMemo(() => {
return hidden ? null : (
<MyTooltip label={t('core.workflow.tool.Handle')} shouldWrapChildren={false}>
<Handle
style={{
borderRadius: '0',
backgroundColor: 'transparent',
border: 'none',
width: handleSize,
height: handleSize
}}
type="target"
id={handleId}
position={Position.Top}
>
<Box
className="flow-handle"
w={handleSize}
h={handleSize}
border={'4px solid #8774EE'}
transform={'translate(0,-30%) rotate(45deg)'}
pointerEvents={'none'}
visibility={'visible'}
/>
</Handle>
</MyTooltip>
);
}, [handleId, hidden, t]);
return Render;
};
export const ToolSourceHandle = ({ nodeId }: ToolHandleProps) => {
const { t } = useTranslation();
const setEdges = useContextSelector(WorkflowContext, (v) => v.setEdges);
/* onConnect edge, delete tool input and switch */
const onConnect = useCallback(
(e: Connection) => {
setEdges((edges) =>
edges.filter((edge) => {
if (edge.target !== e.target) return true;
if (edge.targetHandle === NodeOutputKeyEnum.selectedTools) return true;
return false;
})
);
},
[setEdges]
);
const Render = useMemo(() => {
return (
<MyTooltip label={t('core.workflow.tool.Handle')} shouldWrapChildren={false}>
<Handle
style={{
borderRadius: '0',
backgroundColor: 'transparent',
border: 'none',
width: handleSize,
height: handleSize
}}
type="source"
id={NodeOutputKeyEnum.selectedTools}
position={Position.Bottom}
onConnect={onConnect}
>
<Box
w={handleSize}
h={handleSize}
border={'4px solid #8774EE'}
transform={'translate(0,30%) rotate(45deg)'}
pointerEvents={'none'}
/>
</Handle>
</MyTooltip>
);
}, [onConnect, t]);
return Render;
};
export default function Dom() {
return <></>;
}

View File

@@ -0,0 +1,252 @@
import React, { useMemo } from 'react';
import { Handle, Position } from 'reactflow';
import { SmallAddIcon } from '@chakra-ui/icons';
import { handleHighLightStyle, sourceCommonStyle, handleConnectedStyle, handleSize } from './style';
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../../../context';
type Props = {
nodeId: string;
handleId: string;
position: Position;
translate?: [number, number];
};
const MySourceHandle = React.memo(function MySourceHandle({
nodeId,
translate,
handleId,
position,
highlightStyle,
connectedStyle
}: Props & {
highlightStyle: Record<string, any>;
connectedStyle: Record<string, any>;
}) {
const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge);
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
const nodes = useContextSelector(WorkflowContext, (v) => v.nodes);
const hoverNodeId = useContextSelector(WorkflowContext, (v) => v.hoverNodeId);
const node = useMemo(() => nodes.find((node) => node.data.nodeId === nodeId), [nodes, nodeId]);
const connected = edges.some((edge) => edge.sourceHandle === handleId);
const nodeIsHover = hoverNodeId === nodeId;
const active = useMemo(
() => nodeIsHover || node?.selected || connectingEdge?.handleId === handleId,
[nodeIsHover, node?.selected, connectingEdge, handleId]
);
const translateStr = useMemo(() => {
if (!translate) return '';
if (position === Position.Right) {
return `${active ? translate[0] + 2 : translate[0]}px, -50%`;
}
if (position === Position.Left) {
return `${active ? translate[0] + 2 : translate[0]}px, -50%`;
}
if (position === Position.Top) {
return `-50%, ${active ? translate[1] - 2 : translate[1]}px`;
}
if (position === Position.Bottom) {
return `-50%, ${active ? translate[1] + 2 : translate[1]}px`;
}
}, [active, position, translate]);
const transform = useMemo(
() => (translateStr ? `translate(${translateStr})` : ''),
[translateStr]
);
const { styles, showAddIcon } = useMemo(() => {
if (active) {
return {
styles: {
...highlightStyle,
...(translateStr && {
transform
})
},
showAddIcon: true
};
}
if (connected) {
return {
styles: {
...connectedStyle,
...(translateStr && {
transform
})
},
showAddIcon: false
};
}
return {
styles: undefined,
showAddIcon: false
};
}, [active, connected, highlightStyle, translateStr, transform, connectedStyle]);
const RenderHandle = useMemo(() => {
return (
<Handle
style={
!!styles
? styles
: {
visibility: 'hidden',
transform,
...handleSize
}
}
type="source"
id={handleId}
position={position}
isConnectableEnd={false}
>
{showAddIcon && (
<SmallAddIcon pointerEvents={'none'} color={'primary.600'} fontWeight={'bold'} />
)}
</Handle>
);
}, [handleId, position, showAddIcon, styles, transform]);
if (!node) return null;
if (connectingEdge?.handleId === NodeOutputKeyEnum.selectedTools) return null;
return <>{RenderHandle}</>;
});
export const SourceHandle = (props: Props) => {
return (
<MySourceHandle
{...props}
highlightStyle={{ ...sourceCommonStyle, ...handleHighLightStyle }}
connectedStyle={{ ...sourceCommonStyle, ...handleConnectedStyle }}
/>
);
};
const MyTargetHandle = React.memo(function MyTargetHandle({
nodeId,
handleId,
position,
translate,
highlightStyle,
connectedStyle
}: Props & {
highlightStyle: Record<string, any>;
connectedStyle: Record<string, any>;
}) {
const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge);
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const node = useMemo(() => nodeList.find((node) => node.nodeId === nodeId), [nodeList, nodeId]);
const connected = edges.some((edge) => edge.targetHandle === handleId);
const connectedEdges = edges.filter((edge) => edge.target === nodeId);
const translateStr = useMemo(() => {
if (!translate) return '';
if (position === Position.Right) {
return `${connectingEdge ? translate[0] + 2 : translate[0]}px, -50%`;
}
if (position === Position.Left) {
return `${connectingEdge ? translate[0] - 2 : translate[0]}px, -50%`;
}
if (position === Position.Top) {
return `-50%, ${connectingEdge ? translate[1] - 2 : translate[1]}px`;
}
if (position === Position.Bottom) {
return `-50%, ${connectingEdge ? translate[1] + 2 : translate[1]}px`;
}
}, [connectingEdge, position, translate]);
const transform = useMemo(
() => (translateStr ? `translate(${translateStr})` : ''),
[translateStr]
);
const styles = useMemo(() => {
if (!connectingEdge && !connected) return;
if (connectingEdge) {
return {
...highlightStyle,
transform
};
}
if (connected) {
return {
...connectedStyle,
transform
};
}
return;
}, [connected, connectingEdge, connectedStyle, highlightStyle, transform]);
const showHandle = useMemo(() => {
if (!node) return false;
// check tool connected
if (
edges.some(
(edge) => edge.target === nodeId && edge.targetHandle === NodeOutputKeyEnum.selectedTools
)
) {
return false;
}
if (connectingEdge?.handleId && !connectingEdge.handleId?.includes('source')) return false;
// From same source node and same handle
if (
connectedEdges.some(
(item) => item.sourceHandle === connectingEdge?.handleId && item.target === nodeId
)
)
return false;
return true;
}, [connectedEdges, connectingEdge?.handleId, edges, node, nodeId]);
const RenderHandle = useMemo(() => {
return (
<Handle
style={
!!styles && showHandle
? styles
: {
visibility: 'hidden',
transform,
...handleSize
}
}
type="target"
id={handleId}
position={position}
isConnectableStart={false}
></Handle>
);
}, [styles, showHandle, transform, handleId, position]);
return RenderHandle;
});
export const TargetHandle = (props: Props) => {
return (
<MyTargetHandle
{...props}
highlightStyle={{ ...sourceCommonStyle, ...handleHighLightStyle }}
connectedStyle={{ ...sourceCommonStyle, ...handleConnectedStyle }}
/>
);
};
export default function Dom() {
return <></>;
}

View File

@@ -0,0 +1,29 @@
export const primaryColor = '#3370FF';
export const lowPrimaryColor = '#94B5FF';
export const handleSize = {
width: '18px',
height: '18px'
};
export const sourceCommonStyle = {
backgroundColor: 'white',
borderWidth: '3px',
borderRadius: '50%'
};
export const handleConnectedStyle = {
borderColor: lowPrimaryColor,
width: '14px',
height: '14px'
};
export const handleHighLightStyle = {
borderColor: primaryColor,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '18px',
height: '18px'
};
export default function Dom() {
return <></>;
}

View File

@@ -0,0 +1,645 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Box, Button, Card, Flex } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Avatar from '@/components/Avatar';
import type { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { useTranslation } from 'next-i18next';
import { useEditTitle } from '@/web/common/hooks/useEditTitle';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { LOGO_ICON } from '@fastgpt/global/common/system/constants';
import { ToolTargetHandle } from './Handle/ToolHandle';
import { useEditTextarea } from '@fastgpt/web/hooks/useEditTextarea';
import { ConnectionSourceHandle, ConnectionTargetHandle } from './Handle/ConnectionHandle';
import { useDebug } from '../../hooks/useDebug';
import { ResponseBox } from '@/components/ChatBox/components/WholeResponseModal';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { getPreviewPluginNode } from '@/web/core/app/api/plugin';
import { storeNode2FlowNode, updateFlowNodeVersion } from '@/web/core/workflow/utils';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../../context';
import { useI18n } from '@/web/context/I18n';
import { moduleTemplatesFlat } from '@fastgpt/global/core/workflow/template/constants';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useMount } from 'ahooks';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
type Props = FlowNodeItemType & {
children?: React.ReactNode | React.ReactNode[] | string;
minW?: string | number;
maxW?: string | number;
selected?: boolean;
menuForbid?: {
debug?: boolean;
rename?: boolean;
copy?: boolean;
delete?: boolean;
};
};
const NodeCard = (props: Props) => {
const { t } = useTranslation();
const { appT } = useI18n();
const { toast } = useToast();
const {
children,
avatar = LOGO_ICON,
name = t('core.module.template.UnKnow Module'),
intro,
minW = '300px',
maxW = '600px',
nodeId,
flowNodeType,
selected,
menuForbid,
isTool = false,
isError = false,
debugResult,
pluginId
} = props;
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const setHoverNodeId = useContextSelector(WorkflowContext, (v) => v.setHoverNodeId);
const onUpdateNodeError = useContextSelector(WorkflowContext, (v) => v.onUpdateNodeError);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const onResetNode = useContextSelector(WorkflowContext, (v) => v.onResetNode);
// custom title edit
const { onOpenModal: onOpenCustomTitleModal, EditModal: EditTitleModal } = useEditTitle({
title: t('common.Custom Title'),
placeholder: appT('module.Custom Title Tip') || ''
});
const showToolHandle = useMemo(
() => isTool && !!nodeList.find((item) => item?.flowNodeType === FlowNodeTypeEnum.tools),
[isTool, nodeList]
);
const node = nodeList.find((node) => node.nodeId === nodeId);
const { openConfirm: onOpenConfirmSync, ConfirmModal: ConfirmSyncModal } = useConfirm({
content: appT('module.Confirm Sync')
});
const { data: newNodeVersion, runAsync: getNodeVersion } = useRequest2(
async () => {
if (node?.flowNodeType === FlowNodeTypeEnum.pluginModule) {
if (!node?.pluginId) return;
const template = await getPreviewPluginNode({ appId: node.pluginId });
return template.version;
} else {
const template = moduleTemplatesFlat.find(
(item) => item.flowNodeType === node?.flowNodeType
);
return template?.version;
}
},
{
manual: false
}
);
const hasNewVersion = newNodeVersion && newNodeVersion !== node?.version;
const template = moduleTemplatesFlat.find((item) => item.flowNodeType === node?.flowNodeType);
const onClickSyncVersion = useCallback(async () => {
try {
if (!node || !template) return;
if (node?.flowNodeType === 'pluginModule') {
if (!node.pluginId) return;
onResetNode({
id: nodeId,
node: await getPreviewPluginNode({ appId: node.pluginId })
});
} else {
onResetNode({
id: nodeId,
node: updateFlowNodeVersion(node, template)
});
}
await getNodeVersion();
} catch (error) {
console.error('Error fetching plugin module:', error);
}
}, [getNodeVersion, node, nodeId, onResetNode, template]);
/* Node header */
const Header = useMemo(() => {
return (
<Box position={'relative'}>
{/* debug */}
<Box px={4} py={3}>
{/* tool target handle */}
{showToolHandle && <ToolTargetHandle nodeId={nodeId} />}
{/* avatar and name */}
<Flex alignItems={'center'}>
<Avatar src={avatar} borderRadius={'0'} objectFit={'contain'} w={'30px'} h={'30px'} />
<Box ml={3} fontSize={'md'} fontWeight={'medium'}>
{t(name)}
</Box>
{!menuForbid?.rename && (
<MyIcon
className="controller-rename"
display={'none'}
name={'edit'}
w={'14px'}
cursor={'pointer'}
ml={1}
color={'myGray.500'}
_hover={{ color: 'primary.600' }}
onClick={() => {
onOpenCustomTitleModal({
defaultVal: name,
onSuccess: (e) => {
if (!e) {
return toast({
title: appT('modules.Title is required'),
status: 'warning'
});
}
onChangeNode({
nodeId,
type: 'attr',
key: 'name',
value: e
});
}
});
}}
/>
)}
<Box flex={1} />
{hasNewVersion && (
<MyTooltip label={appT('app.modules.click to update')}>
<Button
bg={'yellow.50'}
color={'yellow.600'}
variant={'ghost'}
h={8}
px={2}
rounded={'6px'}
fontSize={'xs'}
fontWeight={'medium'}
cursor={'pointer'}
_hover={{ bg: 'yellow.100' }}
onClick={onOpenConfirmSync(onClickSyncVersion)}
>
<Box>{appT('app.modules.has new version')}</Box>
<QuestionOutlineIcon ml={1} />
</Button>
</MyTooltip>
)}
</Flex>
<MenuRender
nodeId={nodeId}
pluginId={pluginId}
flowNodeType={flowNodeType}
menuForbid={menuForbid}
/>
<NodeIntro nodeId={nodeId} intro={intro} />
</Box>
<ConfirmSyncModal />
</Box>
);
}, [
showToolHandle,
nodeId,
avatar,
t,
name,
menuForbid,
hasNewVersion,
appT,
onOpenConfirmSync,
onClickSyncVersion,
pluginId,
flowNodeType,
intro,
ConfirmSyncModal,
onOpenCustomTitleModal,
onChangeNode,
toast
]);
return (
<Box
minW={minW}
maxW={maxW}
bg={'white'}
borderWidth={'1px'}
borderRadius={'md'}
boxShadow={'1'}
_hover={{
boxShadow: '4',
'& .controller-menu': {
display: 'flex'
},
'& .controller-debug': {
display: 'block'
},
'& .controller-rename': {
display: 'block'
}
}}
onMouseEnter={() => setHoverNodeId(nodeId)}
onMouseLeave={() => setHoverNodeId(undefined)}
{...(isError
? {
borderColor: 'red.500',
onMouseDownCapture: () => onUpdateNodeError(nodeId, false)
}
: {
borderColor: selected ? 'primary.600' : 'borderColor.base'
})}
>
<NodeDebugResponse nodeId={nodeId} debugResult={debugResult} />
{Header}
{children}
<ConnectionSourceHandle nodeId={nodeId} />
<ConnectionTargetHandle nodeId={nodeId} />
<EditTitleModal maxLength={20} />
</Box>
);
};
export default React.memo(NodeCard);
const MenuRender = React.memo(function MenuRender({
nodeId,
pluginId,
flowNodeType,
menuForbid
}: {
nodeId: string;
pluginId?: string;
flowNodeType: Props['flowNodeType'];
menuForbid?: Props['menuForbid'];
}) {
const { t } = useTranslation();
const { openDebugNode, DebugInputModal } = useDebug();
const { openConfirm: onOpenConfirmDeleteNode, ConfirmModal: ConfirmDeleteModal } = useConfirm({
content: t('core.module.Confirm Delete Node'),
type: 'delete'
});
const setNodes = useContextSelector(WorkflowContext, (v) => v.setNodes);
const setEdges = useContextSelector(WorkflowContext, (v) => v.setEdges);
const onCopyNode = useCallback(
(nodeId: string) => {
setNodes((state) => {
const node = state.find((node) => node.id === nodeId);
if (!node) return state;
const template = {
avatar: node.data.avatar,
name: node.data.name,
intro: node.data.intro,
flowNodeType: node.data.flowNodeType,
inputs: node.data.inputs,
outputs: node.data.outputs,
showStatus: node.data.showStatus,
pluginId: node.data.pluginId,
version: node.data.version
};
return state.concat(
storeNode2FlowNode({
item: {
flowNodeType: template.flowNodeType,
avatar: template.avatar,
name: template.name,
intro: template.intro,
nodeId: getNanoid(),
position: { x: node.position.x + 200, y: node.position.y + 50 },
showStatus: template.showStatus,
pluginId: template.pluginId,
inputs: template.inputs,
outputs: template.outputs,
version: template.version
}
})
);
});
},
[setNodes]
);
const onDelNode = useCallback(
(nodeId: string) => {
setNodes((state) => state.filter((item) => item.data.nodeId !== nodeId));
setEdges((state) => state.filter((edge) => edge.source !== nodeId && edge.target !== nodeId));
},
[setEdges, setNodes]
);
const Render = useMemo(() => {
const menuList = [
...(menuForbid?.debug
? []
: [
{
icon: 'core/workflow/debug',
label: t('core.workflow.Debug'),
variant: 'whiteBase',
onClick: () => openDebugNode({ entryNodeId: nodeId })
}
]),
...(menuForbid?.copy
? []
: [
{
icon: 'copy',
label: t('common.Copy'),
variant: 'whiteBase',
onClick: () => onCopyNode(nodeId)
}
]),
...(menuForbid?.delete
? []
: [
{
icon: 'delete',
label: t('common.Delete'),
variant: 'whiteDanger',
onClick: onOpenConfirmDeleteNode(() => onDelNode(nodeId))
}
])
];
return (
<>
<Box
className="nodrag controller-menu"
display={'none'}
flexDirection={'column'}
gap={3}
position={'absolute'}
top={'-20px'}
right={0}
transform={'translateX(90%)'}
pl={'20px'}
pr={'10px'}
pb={'20px'}
pt={'20px'}
>
{menuList.map((item) => (
<Box key={item.icon}>
<Button
size={'xs'}
variant={item.variant}
leftIcon={<MyIcon name={item.icon as any} w={'13px'} />}
onClick={item.onClick}
>
{item.label}
</Button>
</Box>
))}
</Box>
<ConfirmDeleteModal />
<DebugInputModal />
</>
);
}, [
menuForbid?.debug,
menuForbid?.copy,
menuForbid?.delete,
t,
onOpenConfirmDeleteNode,
ConfirmDeleteModal,
DebugInputModal,
openDebugNode,
nodeId,
onCopyNode,
onDelNode
]);
return Render;
});
const NodeIntro = React.memo(function NodeIntro({
nodeId,
intro = ''
}: {
nodeId: string;
intro?: string;
}) {
const { t } = useTranslation();
const splitToolInputs = useContextSelector(WorkflowContext, (ctx) => ctx.splitToolInputs);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const NodeIsTool = useMemo(() => {
const { isTool } = splitToolInputs([], nodeId);
return isTool;
}, [nodeId, splitToolInputs]);
// edit intro
const { onOpenModal: onOpenIntroModal, EditModal: EditIntroModal } = useEditTextarea({
title: t('core.module.Edit intro'),
tip: '调整该模块会对工具调用时机有影响。\n你可以通过精确的描述该模块功能引导模型进行工具调用。',
canEmpty: false
});
const Render = useMemo(() => {
return (
<>
<Flex alignItems={'flex-end'} py={1}>
<Box fontSize={'xs'} color={'myGray.600'} flex={'1 0 0'}>
{t(intro)}
</Box>
{NodeIsTool && (
<Button
size={'xs'}
variant={'whiteBase'}
onClick={() => {
onOpenIntroModal({
defaultVal: intro,
onSuccess(e) {
onChangeNode({
nodeId,
type: 'attr',
key: 'intro',
value: e
});
}
});
}}
>
{t('core.module.Edit intro')}
</Button>
)}
</Flex>
<EditIntroModal maxLength={500} />
</>
);
}, [EditIntroModal, intro, NodeIsTool, nodeId, onChangeNode, onOpenIntroModal, t]);
return Render;
});
const NodeDebugResponse = React.memo(function NodeDebugResponse({
nodeId,
debugResult
}: {
nodeId: string;
debugResult: FlowNodeItemType['debugResult'];
}) {
const { t } = useTranslation();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const onStopNodeDebug = useContextSelector(WorkflowContext, (v) => v.onStopNodeDebug);
const onNextNodeDebug = useContextSelector(WorkflowContext, (v) => v.onNextNodeDebug);
const workflowDebugData = useContextSelector(WorkflowContext, (v) => v.workflowDebugData);
const { openConfirm, ConfirmModal } = useConfirm({
content: t('core.workflow.Confirm stop debug')
});
const RenderStatus = useMemo(() => {
const map = {
running: {
bg: 'primary.50',
text: t('core.workflow.Running'),
icon: 'core/workflow/running'
},
success: {
bg: 'green.50',
text: t('core.workflow.Success'),
icon: 'core/workflow/runSuccess'
},
failed: {
bg: 'red.50',
text: t('core.workflow.Failed'),
icon: 'core/workflow/runError'
},
skipped: {
bg: 'myGray.50',
text: t('core.workflow.Skipped'),
icon: 'core/workflow/runSkip'
}
};
const statusData = map[debugResult?.status || 'running'];
const response = debugResult?.response;
const onStop = () => {
openConfirm(onStopNodeDebug)();
};
return !!debugResult && !!statusData ? (
<>
<Flex px={4} bg={statusData.bg} borderTopRadius={'md'} py={3}>
<MyIcon name={statusData.icon as any} w={'16px'} mr={2} />
<Box color={'myGray.900'} fontWeight={'bold'} flex={'1 0 0'}>
{statusData.text}
</Box>
{debugResult.status !== 'running' && (
<Box
color={'primary.700'}
cursor={'pointer'}
fontSize={'sm'}
onClick={() =>
onChangeNode({
nodeId,
type: 'attr',
key: 'debugResult',
value: {
...debugResult,
showResult: !debugResult.showResult
}
})
}
>
{debugResult.showResult
? t('core.workflow.debug.Hide result')
: t('core.workflow.debug.Show result')}
</Box>
)}
</Flex>
{/* result */}
{debugResult.showResult && (
<Card
className="nowheel"
position={'absolute'}
right={'-430px'}
top={0}
zIndex={10}
w={'420px'}
maxH={'100%'}
minH={'300px'}
overflowY={'auto'}
border={'base'}
>
{/* Status header */}
<Flex px={4} mb={1} py={3} alignItems={'center'} borderBottom={'base'}>
<MyIcon mr={1} name={'core/workflow/debugResult'} w={'20px'} color={'primary.600'} />
<Box fontWeight={'bold'} flex={'1'}>
{t('core.workflow.debug.Run result')}
</Box>
{workflowDebugData?.nextRunNodes.length !== 0 && (
<Button
size={'sm'}
leftIcon={<MyIcon name={'core/chat/stopSpeech'} w={'16px'} />}
variant={'whiteDanger'}
onClick={onStop}
>
{t('core.workflow.Stop debug')}
</Button>
)}
{(debugResult.status === 'success' || debugResult.status === 'skipped') &&
!debugResult.isExpired &&
workflowDebugData?.nextRunNodes &&
workflowDebugData.nextRunNodes.length > 0 && (
<Button
ml={2}
size={'sm'}
leftIcon={<MyIcon name={'core/workflow/debugNext'} w={'16px'} />}
variant={'primary'}
onClick={() => onNextNodeDebug()}
>
{t('common.Next Step')}
</Button>
)}
{workflowDebugData?.nextRunNodes && workflowDebugData?.nextRunNodes.length === 0 && (
<Button ml={2} size={'sm'} variant={'primary'} onClick={onStopNodeDebug}>
{t('core.workflow.debug.Done')}
</Button>
)}
</Flex>
{/* Show result */}
<Box maxH={'100%'} overflow={'auto'}>
{!debugResult.message && !response && (
<EmptyTip text={t('core.workflow.debug.Not result')} pt={2} pb={5} />
)}
{debugResult.message && (
<Box color={'red.600'} px={3} py={4}>
{debugResult.message}
</Box>
)}
{response && <ResponseBox response={[response]} showDetail hideTabs />}
</Box>
</Card>
)}
<ConfirmModal />
</>
) : null;
}, [
ConfirmModal,
debugResult,
nodeId,
onChangeNode,
onNextNodeDebug,
onStopNodeDebug,
openConfirm,
t,
workflowDebugData?.nextRunNodes
]);
return <>{RenderStatus}</>;
});

View File

@@ -0,0 +1,205 @@
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
import React, { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'next-i18next';
import { Box, Flex } from '@chakra-ui/react';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import NodeInputSelect from '@fastgpt/web/components/core/workflow/NodeInputSelect';
import MyIcon from '@fastgpt/web/components/common/Icon';
import dynamic from 'next/dynamic';
import { EditNodeFieldType } from '@fastgpt/global/core/workflow/node/type';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import ValueTypeLabel from '../ValueTypeLabel';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
const FieldEditModal = dynamic(() => import('../FieldEditModal'));
type Props = {
nodeId: string;
input: FlowNodeInputItemType;
};
const InputLabel = ({ nodeId, input }: Props) => {
const { t } = useTranslation();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const {
description,
toolDescription,
required,
label,
selectedTypeIndex,
renderTypeList,
valueType,
canEdit,
key
} = input;
const [editField, setEditField] = useState<EditNodeFieldType>();
const onChangeRenderType = useCallback(
(e: string) => {
const index = renderTypeList.findIndex((item) => item === e) || 0;
onChangeNode({
nodeId,
type: 'updateInput',
key: input.key,
value: {
...input,
selectedTypeIndex: index,
value: undefined
}
});
},
[input, nodeId, onChangeNode, renderTypeList]
);
const RenderLabel = useMemo(() => {
const renderType = renderTypeList?.[selectedTypeIndex || 0];
return (
<Flex className="nodrag" cursor={'default'} alignItems={'center'} position={'relative'}>
<Flex
alignItems={'center'}
position={'relative'}
fontWeight={'medium'}
color={'myGray.600'}
>
{required && (
<Box position={'absolute'} left={-2} top={-1} color={'red.600'}>
*
</Box>
)}
{t(label)}
{description && <QuestionTip ml={1} label={t(description)}></QuestionTip>}
</Flex>
{/* value type */}
{renderType === FlowNodeInputTypeEnum.reference && <ValueTypeLabel valueType={valueType} />}
{/* edit config */}
{canEdit && (
<>
{input.editField && Object.keys(input.editField).length > 0 && (
<MyIcon
name={'common/settingLight'}
w={'14px'}
cursor={'pointer'}
ml={3}
color={'myGray.600'}
_hover={{ color: 'primary.500' }}
onClick={() =>
setEditField({
...input,
inputType: renderTypeList[0],
valueType: valueType,
key,
label,
description,
isToolInput: !!toolDescription
})
}
/>
)}
<MyIcon
className="delete"
name={'delete'}
w={'14px'}
color={'myGray.600'}
cursor={'pointer'}
ml={2}
_hover={{ color: 'red.500' }}
onClick={() => {
onChangeNode({
nodeId,
type: 'delInput',
key: key
});
onChangeNode({
nodeId,
type: 'delOutput',
key: key
});
}}
/>
</>
)}
{/* input type select */}
{renderTypeList && renderTypeList.length > 1 && (
<Box ml={2}>
<NodeInputSelect
renderTypeList={renderTypeList}
renderTypeIndex={selectedTypeIndex}
onChange={onChangeRenderType}
/>
</Box>
)}
{!!editField?.key && (
<FieldEditModal
editField={input.editField}
keys={[editField.key]}
defaultField={editField}
onClose={() => setEditField(undefined)}
onSubmit={({ data, changeKey }) => {
if (!data.inputType || !data.key || !data.label || !editField.key) return;
const newInput: FlowNodeInputItemType = {
...input,
renderTypeList: [data.inputType],
valueType: data.valueType,
key: data.key,
required: data.required,
label: data.label,
description: data.description,
toolDescription: data.isToolInput ? data.description : undefined,
maxLength: data.maxLength,
value: data.defaultValue,
max: data.max,
min: data.min
};
if (changeKey) {
onChangeNode({
nodeId,
type: 'replaceInput',
key: editField.key,
value: newInput
});
} else {
onChangeNode({
nodeId,
type: 'updateInput',
key: newInput.key,
value: newInput
});
}
setEditField(undefined);
}}
/>
)}
</Flex>
);
}, [
canEdit,
description,
editField,
input,
key,
label,
nodeId,
onChangeNode,
onChangeRenderType,
renderTypeList,
required,
selectedTypeIndex,
t,
toolDescription,
valueType
]);
return RenderLabel;
};
export default React.memo(InputLabel);

View File

@@ -0,0 +1,119 @@
import React, { useMemo } from 'react';
import type { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
import { Box } from '@chakra-ui/react';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import dynamic from 'next/dynamic';
import InputLabel from './Label';
import type { RenderInputProps } from './type';
const RenderList: {
types: FlowNodeInputTypeEnum[];
Component: React.ComponentType<RenderInputProps>;
}[] = [
{
types: [FlowNodeInputTypeEnum.reference],
Component: dynamic(() => import('./templates/Reference'))
},
{
types: [FlowNodeInputTypeEnum.input],
Component: dynamic(() => import('./templates/TextInput'))
},
{
types: [FlowNodeInputTypeEnum.numberInput],
Component: dynamic(() => import('./templates/NumberInput'))
},
{
types: [FlowNodeInputTypeEnum.switch],
Component: dynamic(() => import('./templates/Switch'))
},
{
types: [FlowNodeInputTypeEnum.textarea],
Component: dynamic(() => import('./templates/Textarea'))
},
{
types: [FlowNodeInputTypeEnum.selectApp],
Component: dynamic(() => import('./templates/SelectApp'))
},
{
types: [FlowNodeInputTypeEnum.selectLLMModel],
Component: dynamic(() => import('./templates/SelectLLMModel'))
},
{
types: [FlowNodeInputTypeEnum.settingLLMModel],
Component: dynamic(() => import('./templates/SettingLLMModel'))
},
{
types: [FlowNodeInputTypeEnum.selectDataset],
Component: dynamic(() => import('./templates/SelectDataset'))
},
{
types: [FlowNodeInputTypeEnum.selectDatasetParamsModal],
Component: dynamic(() => import('./templates/SelectDatasetParams'))
},
{
types: [FlowNodeInputTypeEnum.addInputParam],
Component: dynamic(() => import('./templates/AddInputParam'))
},
{
types: [FlowNodeInputTypeEnum.JSONEditor],
Component: dynamic(() => import('./templates/JsonEditor'))
},
{
types: [FlowNodeInputTypeEnum.settingDatasetQuotePrompt],
Component: dynamic(() => import('./templates/SettingQuotePrompt'))
}
];
const hideLabelTypeList = [FlowNodeInputTypeEnum.addInputParam];
type Props = {
flowInputList: FlowNodeInputItemType[];
nodeId: string;
CustomComponent?: Record<string, (e: FlowNodeInputItemType) => React.ReactNode>;
mb?: number;
};
const RenderInput = ({ flowInputList, nodeId, CustomComponent, mb = 5 }: Props) => {
const copyInputs = useMemo(() => JSON.stringify(flowInputList), [flowInputList]);
const filterInputs = useMemo(() => {
const parseSortInputs = JSON.parse(copyInputs) as FlowNodeInputItemType[];
return parseSortInputs.filter((input) => {
return true;
});
}, [copyInputs]);
const memoCustomComponent = useMemo(() => CustomComponent || {}, [CustomComponent]);
const Render = useMemo(() => {
return filterInputs.map((input) => {
const renderType = input.renderTypeList?.[input.selectedTypeIndex || 0];
const RenderComponent = (() => {
if (renderType === FlowNodeInputTypeEnum.custom && memoCustomComponent[input.key]) {
return <>{memoCustomComponent[input.key]({ ...input })}</>;
}
const Component = RenderList.find((item) => item.types.includes(renderType))?.Component;
if (!Component) return null;
return <Component inputs={filterInputs} item={input} nodeId={nodeId} />;
})();
return renderType !== FlowNodeInputTypeEnum.hidden ? (
<Box key={input.key} _notLast={{ mb }} position={'relative'}>
{!!input.label && !hideLabelTypeList.includes(renderType) && (
<InputLabel nodeId={nodeId} input={input} />
)}
{!!RenderComponent && (
<Box mt={2} className={'nodrag'}>
{RenderComponent}
</Box>
)}
</Box>
) : null;
});
}, [filterInputs, mb, memoCustomComponent, nodeId]);
return <>{Render}</>;
};
export default React.memo(RenderInput);

View File

@@ -0,0 +1,114 @@
import React, { useCallback, useMemo, useState } from 'react';
import type { RenderInputProps } from '../type';
import { Box, Button, Flex } from '@chakra-ui/react';
import { SmallAddIcon } from '@chakra-ui/icons';
import { useTranslation } from 'next-i18next';
import { EditNodeFieldType } from '@fastgpt/global/core/workflow/node/type';
import dynamic from 'next/dynamic';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
import Reference from './Reference';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context';
import { AppContext } from '@/pages/app/detail/components/context';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
const FieldEditModal = dynamic(() => import('../../FieldEditModal'));
const AddInputParam = (props: RenderInputProps) => {
const { item, inputs, nodeId } = props;
const { t } = useTranslation();
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const inputValue = useMemo(() => (item.value || []) as FlowNodeInputItemType[], [item.value]);
const [editField, setEditField] = useState<EditNodeFieldType>();
const inputIndex = useMemo(
() => inputs?.findIndex((input) => input.key === item.key),
[inputs, item.key]
);
const onAddField = useCallback(
({ data }: { data: EditNodeFieldType }) => {
if (!data.key) return;
const newInput: FlowNodeInputItemType = {
key: data.key,
valueType: data.valueType,
label: data.label || '',
renderTypeList: [FlowNodeInputTypeEnum.reference],
required: data.required,
description: data.description,
canEdit: true,
editField: item.editField
};
onChangeNode({
nodeId,
type: 'addInput',
index: inputIndex ? inputIndex + 1 : 1,
value: newInput
});
setEditField(undefined);
},
[inputIndex, item, nodeId, onChangeNode]
);
const Render = useMemo(() => {
return (
<>
<Flex className="nodrag" cursor={'default'} alignItems={'center'} position={'relative'}>
<Flex
alignItems={'center'}
position={'relative'}
fontWeight={'medium'}
color={'myGray.600'}
>
{t('core.workflow.Custom variable')}
{item.description && <QuestionTip ml={1} label={t(item.description)} />}
</Flex>
<Box flex={'1 0 0'} />
<Button
variant={'whiteBase'}
leftIcon={<SmallAddIcon />}
iconSpacing={1}
size={'sm'}
onClick={() => setEditField(item.dynamicParamDefaultValue ?? {})}
>
{t('common.Add New')}
</Button>
</Flex>
{appDetail.type === AppTypeEnum.plugin && (
<Box mt={1}>
<Reference {...props} />
</Box>
)}
{!!editField && (
<FieldEditModal
editField={item.editField}
defaultField={editField}
keys={inputValue.map((input) => input.key)}
onClose={() => setEditField(undefined)}
onSubmit={onAddField}
/>
)}
</>
);
}, [
appDetail.type,
editField,
inputValue,
item.description,
item.dynamicParamDefaultValue,
item.editField,
onAddField,
props,
t
]);
return Render;
};
export default React.memo(AddInputParam);

View File

@@ -0,0 +1,79 @@
import React, { useCallback, useMemo } from 'react';
import type { RenderInputProps } from '../type';
import JSONEditor from '@fastgpt/web/components/common/Textarea/JsonEditor';
import { formatEditorVariablePickerIcon } from '@fastgpt/global/core/workflow/utils';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context';
import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils';
import { useCreation } from 'ahooks';
import { useTranslation } from 'next-i18next';
import { AppContext } from '@/pages/app/detail/components/context';
const JsonEditor = ({ inputs = [], item, nodeId }: RenderInputProps) => {
const { t } = useTranslation();
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const { appDetail } = useContextSelector(AppContext, (v) => v);
// get variable
const variables = useCreation(() => {
const globalVariables = getWorkflowGlobalVariables({
nodes: nodeList,
chatConfig: appDetail.chatConfig,
t
});
const moduleVariables = formatEditorVariablePickerIcon(
inputs
.filter((input) => input.canEdit)
.map((item) => ({
key: item.key,
label: item.label
}))
);
return [...globalVariables, ...moduleVariables];
}, [inputs, nodeList]);
const update = useCallback(
(value: string) => {
onChangeNode({
nodeId,
type: 'updateInput',
key: item.key,
value: {
...item,
value
}
});
},
[item, nodeId, onChangeNode]
);
const value = useMemo(() => {
if (typeof item.value === 'string') {
return item.value;
}
return JSON.stringify(item.value, null, 2);
}, [item.value]);
const Render = useMemo(() => {
return (
<JSONEditor
bg={'white'}
borderRadius={'sm'}
placeholder={item.placeholder}
resize
value={value}
onChange={(e) => {
update(e);
}}
variables={variables}
/>
);
}, [item.placeholder, update, value, variables]);
return Render;
};
export default React.memo(JsonEditor);

View File

@@ -0,0 +1,46 @@
import React, { useMemo } from 'react';
import type { RenderInputProps } from '../type';
import {
NumberDecrementStepper,
NumberIncrementStepper,
NumberInput,
NumberInputField,
NumberInputStepper
} from '@chakra-ui/react';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context';
const NumberInputRender = ({ item, nodeId }: RenderInputProps) => {
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const Render = useMemo(() => {
return (
<NumberInput
defaultValue={item.value}
min={item.min}
max={item.max}
onChange={(e) => {
onChangeNode({
nodeId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: Number(e)
}
});
}}
>
<NumberInputField bg={'white'} px={3} borderRadius={'sm'} />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
);
}, [item, nodeId, onChangeNode]);
return Render;
};
export default React.memo(NumberInputRender);

View File

@@ -0,0 +1,206 @@
import React, { useCallback, useMemo } from 'react';
import type { RenderInputProps } from '../type';
import { Flex, Box, ButtonProps } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { computedNodeInputReference } from '@/web/core/workflow/utils';
import { useTranslation } from 'next-i18next';
import {
NodeOutputKeyEnum,
VARIABLE_NODE_ID,
WorkflowIOValueTypeEnum
} from '@fastgpt/global/core/workflow/constants';
import type { ReferenceValueProps } from '@fastgpt/global/core/workflow/type/io';
import dynamic from 'next/dynamic';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { AppContext } from '@/pages/app/detail/components/context';
const MultipleRowSelect = dynamic(
() => import('@fastgpt/web/components/common/MySelect/MultipleRowSelect')
);
const Avatar = dynamic(() => import('@/components/Avatar'));
type SelectProps = {
value?: ReferenceValueProps;
placeholder?: string;
list: {
label: string | React.ReactNode;
value: string;
children: {
label: string;
value: string;
}[];
}[];
onSelect: (val: ReferenceValueProps) => void;
styles?: ButtonProps;
};
const Reference = ({ item, nodeId }: RenderInputProps) => {
const { t } = useTranslation();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const onSelect = useCallback(
(e: ReferenceValueProps) => {
const workflowStartNode = nodeList.find(
(node) => node.flowNodeType === FlowNodeTypeEnum.workflowStart
);
if (e[0] === workflowStartNode?.id && e[1] !== NodeOutputKeyEnum.userChatInput) {
onChangeNode({
nodeId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: [VARIABLE_NODE_ID, e[1]]
}
});
} else {
onChangeNode({
nodeId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e
}
});
}
},
[item, nodeId, nodeList, onChangeNode]
);
const { referenceList, formatValue } = useReference({
nodeId,
valueType: item.valueType,
value: item.value
});
return (
<ReferSelector
placeholder={t(item.referencePlaceholder || '选择引用变量')}
list={referenceList}
value={formatValue}
onSelect={onSelect}
/>
);
};
export default React.memo(Reference);
export const useReference = ({
nodeId,
valueType = WorkflowIOValueTypeEnum.any,
value
}: {
nodeId: string;
valueType?: WorkflowIOValueTypeEnum;
value?: any;
}) => {
const { t } = useTranslation();
const { appDetail } = useContextSelector(AppContext, (v) => v);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
const referenceList = useMemo(() => {
const sourceNodes = computedNodeInputReference({
nodeId,
nodes: nodeList,
edges: edges,
chatConfig: appDetail.chatConfig,
t
});
if (!sourceNodes) return [];
// 转换为 select 的数据结构
const list: SelectProps['list'] = sourceNodes
.map((node) => {
return {
label: (
<Flex alignItems={'center'}>
<Avatar mr={1} src={node.avatar} w={'14px'} borderRadius={'ms'} />
<Box>{t(node.name)}</Box>
</Flex>
),
value: node.nodeId,
children: node.outputs
.filter(
(output) =>
valueType === WorkflowIOValueTypeEnum.any ||
output.valueType === WorkflowIOValueTypeEnum.any ||
output.valueType === valueType
)
.filter((output) => output.id !== NodeOutputKeyEnum.addOutputParam)
.map((output) => {
return {
label: t(output.label || ''),
value: output.id
};
})
};
})
.filter((item) => item.children.length > 0);
return list;
}, [appDetail.chatConfig, edges, nodeId, nodeList, t, valueType]);
const formatValue = useMemo(() => {
if (
Array.isArray(value) &&
value.length === 2 &&
typeof value[0] === 'string' &&
typeof value[1] === 'string'
) {
return value as ReferenceValueProps;
}
return undefined;
}, [value]);
return {
referenceList,
formatValue
};
};
export const ReferSelector = ({ placeholder, value, list = [], onSelect }: SelectProps) => {
const selectItemLabel = useMemo(() => {
if (!value) {
return;
}
const firstColumn = list.find((item) => item.value === value[0]);
if (!firstColumn) {
return;
}
const secondColumn = firstColumn.children.find((item) => item.value === value[1]);
if (!secondColumn) {
return;
}
return [firstColumn, secondColumn];
}, [list, value]);
const Render = useMemo(() => {
return (
<MultipleRowSelect
label={
selectItemLabel ? (
<Flex alignItems={'center'}>
{selectItemLabel[0].label}
<MyIcon name={'common/rightArrowLight'} mx={1} w={'14px'}></MyIcon>
{selectItemLabel[1].label}
</Flex>
) : (
<Box>{placeholder}</Box>
)
}
value={value as any[]}
list={list}
onSelect={(e) => {
onSelect(e as ReferenceValueProps);
}}
/>
);
}, [list, onSelect, placeholder, selectItemLabel, value]);
return Render;
};

View File

@@ -0,0 +1,34 @@
import React, { useMemo } from 'react';
import type { RenderInputProps } from '../type';
import MySelect from '@fastgpt/web/components/common/MySelect';
import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context';
import { useContextSelector } from 'use-context-selector';
const SelectRender = ({ item, nodeId }: RenderInputProps) => {
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const Render = useMemo(() => {
return (
<MySelect
width={'100%'}
value={item.value}
list={item.list || []}
onchange={(e) => {
onChangeNode({
nodeId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e
}
});
}}
/>
);
}, [item, nodeId, onChangeNode]);
return Render;
};
export default React.memo(SelectRender);

View File

@@ -0,0 +1,107 @@
import React, { useMemo } from 'react';
import type { RenderInputProps } from '../type';
import { Box, Button, useDisclosure } from '@chakra-ui/react';
import { SelectAppItemType } from '@fastgpt/global/core/workflow/type/index.d';
import Avatar from '@/components/Avatar';
import SelectAppModal from '../../../../SelectAppModal';
import { useTranslation } from 'next-i18next';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { getAppDetailById } from '@/web/core/app/api';
const SelectAppRender = ({ item, nodeId }: RenderInputProps) => {
const { t } = useTranslation();
const filterAppIds = useContextSelector(WorkflowContext, (ctx) => ctx.filterAppIds);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const {
isOpen: isOpenSelectApp,
onOpen: onOpenSelectApp,
onClose: onCloseSelectApp
} = useDisclosure();
const value = item.value as SelectAppItemType | undefined;
const { data: appDetail, loading } = useRequest2(
() => {
if (value?.id) return getAppDetailById(value.id);
return Promise.resolve(null);
},
{
manual: false,
refreshDeps: [value?.id],
errorToast: 'Error',
onError() {
onChangeNode({
nodeId,
type: 'updateInput',
key: 'app',
value: {
...item,
value: undefined
}
});
}
}
);
const Render = useMemo(() => {
return (
<>
<Box onClick={onOpenSelectApp}>
{!value ? (
<Button variant={'whiteFlow'} w={'100%'}>
{t('core.module.Select app')}
</Button>
) : (
<Button
isLoading={loading}
w={'100%'}
justifyContent={loading ? 'center' : 'flex-start'}
variant={'whiteFlow'}
leftIcon={<Avatar src={appDetail?.avatar} w={6} />}
>
{appDetail?.name}
</Button>
)}
</Box>
{isOpenSelectApp && (
<SelectAppModal
value={item.value}
filterAppIds={filterAppIds}
onClose={onCloseSelectApp}
onSuccess={(e) => {
onChangeNode({
nodeId,
type: 'updateInput',
key: 'app',
value: {
...item,
value: e
}
});
}}
/>
)}
</>
);
}, [
appDetail?.avatar,
appDetail?.name,
filterAppIds,
isOpenSelectApp,
item,
loading,
nodeId,
onChangeNode,
onCloseSelectApp,
onOpenSelectApp,
t,
value
]);
return Render;
};
export default React.memo(SelectAppRender);

View File

@@ -0,0 +1,130 @@
import React, { useEffect, useMemo, useState } from 'react';
import type { RenderInputProps } from '../type';
import { Box, Button, Flex, Grid, useDisclosure, useTheme } from '@chakra-ui/react';
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
import { SelectedDatasetType } from '@fastgpt/global/core/workflow/api';
import Avatar from '@/components/Avatar';
import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'next-i18next';
import { DatasetSearchModeEnum } from '@fastgpt/global/core/dataset/constants';
import dynamic from 'next/dynamic';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context';
const DatasetSelectModal = dynamic(() => import('@/components/core/app/DatasetSelectModal'));
const SelectDatasetRender = ({ inputs = [], item, nodeId }: RenderInputProps) => {
const { t } = useTranslation();
const theme = useTheme();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const [data, setData] = useState({
searchMode: DatasetSearchModeEnum.embedding,
limit: 5,
similarity: 0.5,
usingReRank: false
});
const { allDatasets, loadAllDatasets } = useDatasetStore();
const {
isOpen: isOpenDatasetSelect,
onOpen: onOpenDatasetSelect,
onClose: onCloseDatasetSelect
} = useDisclosure();
const selectedDatasets = useMemo(() => {
const value = item.value as SelectedDatasetType;
return allDatasets.filter((dataset) => value?.find((item) => item.datasetId === dataset._id));
}, [allDatasets, item.value]);
useQuery(['loadAllDatasets'], loadAllDatasets);
useEffect(() => {
inputs.forEach((input) => {
// @ts-ignore
if (data[input.key] !== undefined) {
setData((state) => ({
...state,
[input.key]: input.value
}));
}
});
}, [inputs]);
const Render = useMemo(() => {
return (
<>
<Grid
gridTemplateColumns={'repeat(2, minmax(0, 1fr))'}
gridGap={4}
minW={'350px'}
w={'100%'}
>
<Button
h={'36px'}
leftIcon={<MyIcon name={'common/selectLight'} w={'14px'} />}
onClick={onOpenDatasetSelect}
>
{t('common.Choose')}
</Button>
{selectedDatasets.map((item) => (
<Flex
key={item._id}
alignItems={'center'}
h={'36px'}
border={theme.borders.base}
px={2}
borderRadius={'md'}
>
<Avatar src={item.avatar} w={'24px'}></Avatar>
<Box
ml={3}
flex={'1 0 0'}
w={0}
className="textEllipsis"
fontWeight={'bold'}
fontSize={['md', 'lg']}
>
{item.name}
</Box>
</Flex>
))}
</Grid>
{isOpenDatasetSelect && (
<DatasetSelectModal
isOpen={isOpenDatasetSelect}
defaultSelectedDatasets={item.value}
onChange={(e) => {
onChangeNode({
nodeId,
key: item.key,
type: 'updateInput',
value: {
...item,
value: e
}
});
}}
onClose={onCloseDatasetSelect}
/>
)}
</>
);
}, [
isOpenDatasetSelect,
item,
nodeId,
onChangeNode,
onCloseDatasetSelect,
onOpenDatasetSelect,
selectedDatasets,
t,
theme.borders.base
]);
return Render;
};
export default React.memo(SelectDatasetRender);

View File

@@ -0,0 +1,122 @@
import React, { useEffect, useMemo, useState } from 'react';
import type { RenderInputProps } from '../type';
import { Box, Button, Flex, useDisclosure } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { DatasetSearchModeEnum } from '@fastgpt/global/core/dataset/constants';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import MyIcon from '@fastgpt/web/components/common/Icon';
import DatasetParamsModal, { DatasetParamsProps } from '@/components/core/app/DatasetParamsModal';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import SearchParamsTip from '@/components/core/dataset/SearchParamsTip';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context';
const SelectDatasetParam = ({ inputs = [], nodeId }: RenderInputProps) => {
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const { t } = useTranslation();
const { llmModelList } = useSystemStore();
const [data, setData] = useState<DatasetParamsProps>({
searchMode: DatasetSearchModeEnum.embedding,
limit: 5,
similarity: 0.5,
usingReRank: false,
datasetSearchUsingExtensionQuery: true,
datasetSearchExtensionModel: llmModelList[0]?.model,
datasetSearchExtensionBg: ''
});
const tokenLimit = useMemo(() => {
let maxTokens = 3000;
nodeList.forEach((item) => {
if (item.flowNodeType === FlowNodeTypeEnum.chatNode) {
const model =
item.inputs.find((item) => item.key === NodeInputKeyEnum.aiModel)?.value || '';
const quoteMaxToken =
llmModelList.find((item) => item.model === model)?.quoteMaxToken || 3000;
maxTokens = Math.max(maxTokens, quoteMaxToken);
}
});
return maxTokens;
}, [llmModelList, nodeList]);
const { isOpen, onOpen, onClose } = useDisclosure();
useEffect(() => {
inputs.forEach((input) => {
// @ts-ignore
if (data[input.key] !== undefined) {
setData((state) => ({
...state,
[input.key]: input.value
}));
}
});
}, [inputs]);
const Render = useMemo(() => {
return (
<>
{/* label */}
<Flex alignItems={'center'} mb={3} fontWeight={'medium'} color={'myGray.600'}>
{t('core.dataset.search.Params Setting')}
<MyIcon
name={'common/settingLight'}
ml={2}
w={'16px'}
cursor={'pointer'}
_hover={{
color: 'primary.600'
}}
onClick={onOpen}
/>
</Flex>
<SearchParamsTip
searchMode={data.searchMode}
similarity={data.similarity}
limit={data.limit}
usingReRank={data.usingReRank}
queryExtensionModel={data.datasetSearchExtensionModel}
/>
</>
);
}, [data, onOpen, t]);
return (
<>
{Render}
{isOpen && (
<DatasetParamsModal
{...data}
maxTokens={tokenLimit}
onClose={onClose}
onSuccess={(e) => {
setData(e);
for (let key in e) {
const item = inputs.find((input) => input.key === key);
if (!item) continue;
onChangeNode({
nodeId,
type: 'updateInput',
key,
value: {
...item,
//@ts-ignore
value: e[key]
}
});
}
}}
/>
)}
</>
);
};
export default React.memo(SelectDatasetParam);

View File

@@ -0,0 +1,64 @@
import React, { useCallback, useEffect, useMemo } from 'react';
import type { RenderInputProps } from '../type';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { llmModelTypeFilterMap } from '@fastgpt/global/core/ai/constants';
import AIModelSelector from '@/components/Select/AIModelSelector';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context';
const SelectAiModelRender = ({ item, nodeId }: RenderInputProps) => {
const { llmModelList } = useSystemStore();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const modelList = useMemo(
() =>
llmModelList.filter((model) => {
if (!item.llmModelType) return true;
const filterField = llmModelTypeFilterMap[item.llmModelType];
if (!filterField) return true;
//@ts-ignore
return !!model[filterField];
}),
[llmModelList, item.llmModelType]
);
const onChangeModel = useCallback(
(e: string) => {
onChangeNode({
nodeId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e
}
});
},
[item, nodeId, onChangeNode]
);
useEffect(() => {
if (!item.value && modelList.length > 0) {
onChangeModel(modelList[0].model);
}
}, []);
const Render = useMemo(() => {
return (
<AIModelSelector
minW={'350px'}
width={'100%'}
value={item.value}
list={modelList.map((item) => ({
value: item.model,
label: item.name
}))}
onchange={onChangeModel}
/>
);
}, [item.value, modelList, onChangeModel]);
return Render;
};
export default React.memo(SelectAiModelRender);

Some files were not shown because too many files have changed in this diff Show More