Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
361e255af8 | ||
|
|
2b888fb0fa | ||
|
|
2128d306ad | ||
|
|
e59816aba4 | ||
|
|
ded0383ac4 | ||
|
|
81202c53a8 | ||
|
|
e74ab643fe | ||
|
|
3b0f0a8108 | ||
|
|
165b783a95 | ||
|
|
d7b9f94270 |
3
.vscode/settings.json
vendored
@ -27,5 +27,8 @@
|
||||
},
|
||||
"markdown.copyFiles.destination": {
|
||||
"/docSite/content/**/*": "${documentWorkspaceFolder}/docSite/assets/imgs/"
|
||||
},
|
||||
"[svg]": {
|
||||
"editor.defaultFormatter": "jock.svg"
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,9 @@ import { type ErrType } from '../errorCode';
|
||||
/* dataset: 507000 */
|
||||
const startCode = 507000;
|
||||
export enum CommonErrEnum {
|
||||
methodNotAllowed = 'methodNotAllowed',
|
||||
systemError = 'systemError',
|
||||
unauthorized = 'unauthorized',
|
||||
invalidParams = 'invalidParams',
|
||||
invalidResource = 'invalidResource',
|
||||
fileNotFound = 'fileNotFound',
|
||||
@ -35,6 +38,22 @@ const datasetErr = [
|
||||
{
|
||||
statusText: CommonErrEnum.inheritPermissionError,
|
||||
message: 'error.inheritPermissionError'
|
||||
},
|
||||
{
|
||||
statusText: CommonErrEnum.methodNotAllowed,
|
||||
message: i18nT('common:code_error.error_message.405')
|
||||
},
|
||||
{
|
||||
statusText: CommonErrEnum.systemError,
|
||||
message: i18nT('common:code_error.error_message.500')
|
||||
},
|
||||
{
|
||||
statusText: CommonErrEnum.unauthorized,
|
||||
message: i18nT('common:code_error.error_message.403')
|
||||
},
|
||||
{
|
||||
statusText: CommonErrEnum.invalidParams,
|
||||
message: i18nT('common:code_error.error_message.422')
|
||||
}
|
||||
];
|
||||
export default datasetErr.reduce((acc, cur, index) => {
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
} from './type';
|
||||
|
||||
export enum AppTypeEnum {
|
||||
gate = 'gate',
|
||||
folder = 'folder',
|
||||
simple = 'simple',
|
||||
workflow = 'advanced',
|
||||
|
||||
24
packages/global/core/app/tags.d.ts
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
import { TeamMemberStatusEnum } from 'support/user/team/constant';
|
||||
import type { SourceMemberType } from 'support/user/type';
|
||||
|
||||
export type TagSchemaType = {
|
||||
_id: string;
|
||||
teamId: string;
|
||||
name: string;
|
||||
color: string;
|
||||
createTime: Date;
|
||||
};
|
||||
|
||||
export type TagWithCountType = TagSchemaType & {
|
||||
count: number;
|
||||
};
|
||||
|
||||
export type TagListItemType = {
|
||||
_id: string;
|
||||
teamId: string;
|
||||
name: string;
|
||||
color: string;
|
||||
createTime: Date;
|
||||
count?: number;
|
||||
sourceMember?: SourceMemberType;
|
||||
};
|
||||
1
packages/global/core/app/type.d.ts
vendored
@ -65,6 +65,7 @@ export type AppListItemType = {
|
||||
inheritPermission?: boolean;
|
||||
private?: boolean;
|
||||
sourceMember: SourceMemberType;
|
||||
tags?: string[];
|
||||
};
|
||||
|
||||
export type AppDetailType = AppSchema & {
|
||||
|
||||
@ -46,7 +46,7 @@ export const appWorkflow2Form = ({
|
||||
chatConfig
|
||||
}: {
|
||||
nodes: StoreNodeItemType[];
|
||||
chatConfig: AppChatConfigType;
|
||||
chatConfig?: AppChatConfigType;
|
||||
}) => {
|
||||
const defaultAppForm = getDefaultAppForm();
|
||||
const findInputValueByKey = (inputs: FlowNodeInputItemType[], key: string) => {
|
||||
@ -172,6 +172,10 @@ export const appWorkflow2Form = ({
|
||||
}
|
||||
});
|
||||
|
||||
if (chatConfig) {
|
||||
defaultAppForm.chatConfig = chatConfig;
|
||||
}
|
||||
|
||||
return defaultAppForm;
|
||||
};
|
||||
|
||||
|
||||
@ -2,7 +2,18 @@ import { NullPermission, PermissionKeyEnum, PermissionList } from '../constant';
|
||||
import { type PermissionListType } from '../type';
|
||||
import { i18nT } from '../../../../web/i18n/utils';
|
||||
export enum AppPermissionKeyEnum {}
|
||||
export const AppPermissionList: PermissionListType = {
|
||||
|
||||
export enum AppPermissionKeyEnum {
|
||||
log = 'log',
|
||||
quickGate = 'quickGate',
|
||||
featuredGate = 'featuredGate'
|
||||
}
|
||||
|
||||
export const AppLogPermission = 0b100000;
|
||||
export const GateQuickAppPermission = 0b001100;
|
||||
export const GateFeaturedAppPermission = 0b010100;
|
||||
|
||||
export const AppPermissionList: PermissionListType<AppPermissionKeyEnum> = {
|
||||
[PermissionKeyEnum.read]: {
|
||||
...PermissionList[PermissionKeyEnum.read],
|
||||
description: i18nT('app:permission.des.read')
|
||||
@ -13,8 +24,28 @@ export const AppPermissionList: PermissionListType = {
|
||||
},
|
||||
[PermissionKeyEnum.manage]: {
|
||||
...PermissionList[PermissionKeyEnum.manage],
|
||||
value: 0b111111,
|
||||
description: i18nT('app:permission.des.manage')
|
||||
},
|
||||
[AppPermissionKeyEnum.log]: {
|
||||
name: i18nT('app:permission.name.log'),
|
||||
value: AppLogPermission,
|
||||
checkBoxType: 'multiple',
|
||||
description: i18nT('app:permission.des.log')
|
||||
},
|
||||
[AppPermissionKeyEnum.quickGate]: {
|
||||
name: '门户快捷应用权限',
|
||||
description: '',
|
||||
value: GateQuickAppPermission,
|
||||
checkBoxType: 'hiden'
|
||||
},
|
||||
[AppPermissionKeyEnum.featuredGate]: {
|
||||
name: '门户推荐应用权限',
|
||||
description: '',
|
||||
value: GateFeaturedAppPermission,
|
||||
checkBoxType: 'hiden'
|
||||
}
|
||||
};
|
||||
|
||||
export const AppDefaultPermissionVal = NullPermission;
|
||||
export const AppLogPermissionVal = AppPermissionList[AppPermissionKeyEnum.log].value;
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { type PerConstructPros, Permission } from '../controller';
|
||||
import { AppDefaultPermissionVal } from './constant';
|
||||
import { AppDefaultPermissionVal, AppPermissionList } from './constant';
|
||||
|
||||
export class AppPermission extends Permission {
|
||||
hasLogPer: boolean = false;
|
||||
constructor(props?: PerConstructPros) {
|
||||
if (!props) {
|
||||
props = {
|
||||
@ -10,6 +11,13 @@ export class AppPermission extends Permission {
|
||||
} else if (!props?.per) {
|
||||
props.per = AppDefaultPermissionVal;
|
||||
}
|
||||
props.permissionList = AppPermissionList;
|
||||
super(props);
|
||||
this.setUpdatePermissionCallback(() => {
|
||||
this.hasReadPer = this.checkPer(AppPermissionList.read.value);
|
||||
this.hasWritePer = this.checkPer(AppPermissionList.write.value);
|
||||
this.hasManagePer = this.checkPer(AppPermissionList.manage.value);
|
||||
this.hasLogPer = this.checkPer(AppPermissionList.log.value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
import { type PermissionListType, type PermissionValueType } from './type';
|
||||
import { PermissionList, NullPermission, OwnerPermissionVal } from './constant';
|
||||
import {
|
||||
PermissionList,
|
||||
NullPermission,
|
||||
OwnerPermissionVal,
|
||||
ManagePermissionVal
|
||||
} from './constant';
|
||||
|
||||
export type PerConstructPros = {
|
||||
per?: PermissionValueType;
|
||||
@ -63,6 +68,7 @@ export class Permission {
|
||||
if (perm === OwnerPermissionVal) {
|
||||
return this.value === OwnerPermissionVal;
|
||||
}
|
||||
|
||||
return (this.value & perm) === perm;
|
||||
}
|
||||
|
||||
|
||||
2
packages/global/support/permission/type.d.ts
vendored
@ -18,7 +18,7 @@ export type PermissionListType<T = {}> = Record<
|
||||
name: string;
|
||||
description: string;
|
||||
value: PermissionValueType;
|
||||
checkBoxType: 'single' | 'multiple';
|
||||
checkBoxType: 'single' | 'multiple' | 'hiden';
|
||||
}
|
||||
>;
|
||||
|
||||
|
||||
@ -19,7 +19,7 @@ export const TeamPermissionList: PermissionListType<TeamPermissionKeyEnum> = {
|
||||
},
|
||||
[PermissionKeyEnum.manage]: {
|
||||
...PermissionList[PermissionKeyEnum.manage],
|
||||
value: 0b000001
|
||||
value: 0b000101
|
||||
},
|
||||
[TeamPermissionKeyEnum.appCreate]: {
|
||||
checkBoxType: 'multiple',
|
||||
|
||||
31
packages/global/support/user/team/gate/api.d.ts
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
export type putUpdateGateConfigData = {
|
||||
status?: boolean;
|
||||
tools?: GateTool[];
|
||||
slogan?: string;
|
||||
placeholderText?: string;
|
||||
};
|
||||
|
||||
export type putUpdateGateConfigResponse = {
|
||||
status?: boolean;
|
||||
tools?: string[];
|
||||
slogan?: string;
|
||||
placeholderText?: string;
|
||||
};
|
||||
|
||||
export type putUpdateGateConfigCopyRightData = {
|
||||
name?: string;
|
||||
logo?: string;
|
||||
banner?: string;
|
||||
};
|
||||
|
||||
export type putUpdateGateConfigCopyRightResponse = {
|
||||
name: string;
|
||||
logo: string;
|
||||
banner: string;
|
||||
};
|
||||
|
||||
export type getGateConfigCopyRightResponse = {
|
||||
name: string;
|
||||
logo: string;
|
||||
banner: string;
|
||||
};
|
||||
12
packages/global/support/user/team/gate/type.d.ts
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
export type GateSchemaType = {
|
||||
teamId: string;
|
||||
status: boolean;
|
||||
tools: string[];
|
||||
featuredApps: string[];
|
||||
quickApps: string[];
|
||||
slogan: string;
|
||||
placeholderText: string;
|
||||
name: string;
|
||||
logo: string;
|
||||
banner: string;
|
||||
};
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"author": "",
|
||||
"name": "博查搜索",
|
||||
"avatar": "core/workflow/template/bocha",
|
||||
"name": "网络搜索",
|
||||
"avatar": "common/searchLight",
|
||||
"intro": "使用博查AI搜索引擎进行网络搜索。",
|
||||
"showStatus": true,
|
||||
"weight": 10,
|
||||
@ -24,9 +24,7 @@
|
||||
"version": "481",
|
||||
"inputs": [
|
||||
{
|
||||
"renderTypeList": [
|
||||
"input"
|
||||
],
|
||||
"renderTypeList": ["input"],
|
||||
"selectedTypeIndex": 0,
|
||||
"valueType": "string",
|
||||
"canEdit": true,
|
||||
@ -37,10 +35,7 @@
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"renderTypeList": [
|
||||
"input",
|
||||
"reference"
|
||||
],
|
||||
"renderTypeList": ["input", "reference"],
|
||||
"selectedTypeIndex": 0,
|
||||
"valueType": "string",
|
||||
"canEdit": true,
|
||||
@ -52,10 +47,7 @@
|
||||
"toolDescription": "搜索查询词"
|
||||
},
|
||||
{
|
||||
"renderTypeList": [
|
||||
"input",
|
||||
"reference"
|
||||
],
|
||||
"renderTypeList": ["input", "reference"],
|
||||
"selectedTypeIndex": 0,
|
||||
"valueType": "string",
|
||||
"canEdit": true,
|
||||
@ -67,10 +59,7 @@
|
||||
"toolDescription": "搜索时间范围"
|
||||
},
|
||||
{
|
||||
"renderTypeList": [
|
||||
"input",
|
||||
"reference"
|
||||
],
|
||||
"renderTypeList": ["input", "reference"],
|
||||
"selectedTypeIndex": 0,
|
||||
"valueType": "boolean",
|
||||
"canEdit": true,
|
||||
@ -82,10 +71,7 @@
|
||||
"toolDescription": "是否显示文本摘要"
|
||||
},
|
||||
{
|
||||
"renderTypeList": [
|
||||
"input",
|
||||
"reference"
|
||||
],
|
||||
"renderTypeList": ["input", "reference"],
|
||||
"selectedTypeIndex": 0,
|
||||
"valueType": "string",
|
||||
"canEdit": true,
|
||||
@ -97,10 +83,7 @@
|
||||
"toolDescription": "指定搜索的site范围"
|
||||
},
|
||||
{
|
||||
"renderTypeList": [
|
||||
"input",
|
||||
"reference"
|
||||
],
|
||||
"renderTypeList": ["input", "reference"],
|
||||
"selectedTypeIndex": 0,
|
||||
"valueType": "string",
|
||||
"canEdit": true,
|
||||
@ -112,10 +95,7 @@
|
||||
"toolDescription": "排除搜索的网站范围"
|
||||
},
|
||||
{
|
||||
"renderTypeList": [
|
||||
"input",
|
||||
"reference"
|
||||
],
|
||||
"renderTypeList": ["input", "reference"],
|
||||
"selectedTypeIndex": 0,
|
||||
"valueType": "number",
|
||||
"canEdit": true,
|
||||
@ -195,19 +175,14 @@
|
||||
"version": "481",
|
||||
"inputs": [
|
||||
{
|
||||
"renderTypeList": [
|
||||
"reference"
|
||||
],
|
||||
"renderTypeList": ["reference"],
|
||||
"valueType": "object",
|
||||
"canEdit": true,
|
||||
"key": "result",
|
||||
"label": "result",
|
||||
"isToolOutput": true,
|
||||
"description": "",
|
||||
"value": [
|
||||
"nyA6oA8mF1iW",
|
||||
"httpRawResponse"
|
||||
]
|
||||
"value": ["nyA6oA8mF1iW", "httpRawResponse"]
|
||||
}
|
||||
],
|
||||
"outputs": []
|
||||
@ -241,9 +216,7 @@
|
||||
"inputs": [
|
||||
{
|
||||
"key": "system_addInputParam",
|
||||
"renderTypeList": [
|
||||
"addInputParam"
|
||||
],
|
||||
"renderTypeList": ["addInputParam"],
|
||||
"valueType": "dynamic",
|
||||
"label": "",
|
||||
"required": false,
|
||||
@ -274,9 +247,7 @@
|
||||
},
|
||||
{
|
||||
"key": "system_httpMethod",
|
||||
"renderTypeList": [
|
||||
"custom"
|
||||
],
|
||||
"renderTypeList": ["custom"],
|
||||
"valueType": "string",
|
||||
"label": "",
|
||||
"value": "POST",
|
||||
@ -286,9 +257,7 @@
|
||||
},
|
||||
{
|
||||
"key": "system_httpTimeout",
|
||||
"renderTypeList": [
|
||||
"custom"
|
||||
],
|
||||
"renderTypeList": ["custom"],
|
||||
"valueType": "number",
|
||||
"label": "",
|
||||
"value": 30,
|
||||
@ -300,9 +269,7 @@
|
||||
},
|
||||
{
|
||||
"key": "system_httpReqUrl",
|
||||
"renderTypeList": [
|
||||
"hidden"
|
||||
],
|
||||
"renderTypeList": ["hidden"],
|
||||
"valueType": "string",
|
||||
"label": "",
|
||||
"description": "common:core.module.input.description.Http Request Url",
|
||||
@ -314,9 +281,7 @@
|
||||
},
|
||||
{
|
||||
"key": "system_httpHeader",
|
||||
"renderTypeList": [
|
||||
"custom"
|
||||
],
|
||||
"renderTypeList": ["custom"],
|
||||
"valueType": "any",
|
||||
"value": [
|
||||
{
|
||||
@ -339,9 +304,7 @@
|
||||
},
|
||||
{
|
||||
"key": "system_httpParams",
|
||||
"renderTypeList": [
|
||||
"hidden"
|
||||
],
|
||||
"renderTypeList": ["hidden"],
|
||||
"valueType": "any",
|
||||
"value": [],
|
||||
"label": "",
|
||||
@ -351,9 +314,7 @@
|
||||
},
|
||||
{
|
||||
"key": "system_httpJsonBody",
|
||||
"renderTypeList": [
|
||||
"hidden"
|
||||
],
|
||||
"renderTypeList": ["hidden"],
|
||||
"valueType": "any",
|
||||
"value": "{\n \"query\": \"{{query}}\",\n \"freshness\": \"{{freshness}}\",\n \"summary\": {{summary}},\n \"include\": \"{{include}}\",\n \"exclude\": \"{{exclude}}\",\n \"count\": {{count}}\n}",
|
||||
"label": "",
|
||||
@ -363,9 +324,7 @@
|
||||
},
|
||||
{
|
||||
"key": "system_httpFormBody",
|
||||
"renderTypeList": [
|
||||
"hidden"
|
||||
],
|
||||
"renderTypeList": ["hidden"],
|
||||
"valueType": "any",
|
||||
"value": [],
|
||||
"label": "",
|
||||
@ -375,9 +334,7 @@
|
||||
},
|
||||
{
|
||||
"key": "system_httpContentType",
|
||||
"renderTypeList": [
|
||||
"hidden"
|
||||
],
|
||||
"renderTypeList": ["hidden"],
|
||||
"valueType": "string",
|
||||
"value": "json",
|
||||
"label": "",
|
||||
@ -387,9 +344,7 @@
|
||||
},
|
||||
{
|
||||
"valueType": "string",
|
||||
"renderTypeList": [
|
||||
"reference"
|
||||
],
|
||||
"renderTypeList": ["reference"],
|
||||
"key": "query",
|
||||
"label": "query",
|
||||
"toolDescription": "博查搜索检索词",
|
||||
@ -420,16 +375,11 @@
|
||||
"showDescription": false,
|
||||
"showDefaultValue": true
|
||||
},
|
||||
"value": [
|
||||
"pluginInput",
|
||||
"query"
|
||||
]
|
||||
"value": ["pluginInput", "query"]
|
||||
},
|
||||
{
|
||||
"valueType": "string",
|
||||
"renderTypeList": [
|
||||
"reference"
|
||||
],
|
||||
"renderTypeList": ["reference"],
|
||||
"key": "freshness",
|
||||
"label": "freshness",
|
||||
"toolDescription": "搜索时间范围",
|
||||
@ -460,16 +410,11 @@
|
||||
"showDescription": false,
|
||||
"showDefaultValue": true
|
||||
},
|
||||
"value": [
|
||||
"pluginInput",
|
||||
"freshness"
|
||||
]
|
||||
"value": ["pluginInput", "freshness"]
|
||||
},
|
||||
{
|
||||
"valueType": "boolean",
|
||||
"renderTypeList": [
|
||||
"reference"
|
||||
],
|
||||
"renderTypeList": ["reference"],
|
||||
"key": "summary",
|
||||
"label": "summary",
|
||||
"toolDescription": "是否显示文本摘要",
|
||||
@ -500,16 +445,11 @@
|
||||
"showDescription": false,
|
||||
"showDefaultValue": true
|
||||
},
|
||||
"value": [
|
||||
"pluginInput",
|
||||
"summary"
|
||||
]
|
||||
"value": ["pluginInput", "summary"]
|
||||
},
|
||||
{
|
||||
"valueType": "string",
|
||||
"renderTypeList": [
|
||||
"reference"
|
||||
],
|
||||
"renderTypeList": ["reference"],
|
||||
"key": "include",
|
||||
"label": "include",
|
||||
"toolDescription": "指定搜索的site范围",
|
||||
@ -540,16 +480,11 @@
|
||||
"showDescription": false,
|
||||
"showDefaultValue": true
|
||||
},
|
||||
"value": [
|
||||
"pluginInput",
|
||||
"include"
|
||||
]
|
||||
"value": ["pluginInput", "include"]
|
||||
},
|
||||
{
|
||||
"valueType": "string",
|
||||
"renderTypeList": [
|
||||
"reference"
|
||||
],
|
||||
"renderTypeList": ["reference"],
|
||||
"key": "exclude",
|
||||
"label": "exclude",
|
||||
"toolDescription": "排除搜索的网站范围",
|
||||
@ -580,16 +515,11 @@
|
||||
"showDescription": false,
|
||||
"showDefaultValue": true
|
||||
},
|
||||
"value": [
|
||||
"pluginInput",
|
||||
"exclude"
|
||||
]
|
||||
"value": ["pluginInput", "exclude"]
|
||||
},
|
||||
{
|
||||
"valueType": "number",
|
||||
"renderTypeList": [
|
||||
"reference"
|
||||
],
|
||||
"renderTypeList": ["reference"],
|
||||
"key": "count",
|
||||
"label": "count",
|
||||
"toolDescription": "返回结果条数",
|
||||
@ -620,10 +550,7 @@
|
||||
"showDescription": false,
|
||||
"showDefaultValue": true
|
||||
},
|
||||
"value": [
|
||||
"pluginInput",
|
||||
"count"
|
||||
]
|
||||
"value": ["pluginInput", "count"]
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
|
||||
@ -64,7 +64,12 @@ const AppSchema = new Schema({
|
||||
type: Date,
|
||||
default: () => new Date()
|
||||
},
|
||||
|
||||
tags: [
|
||||
{
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'app_tags'
|
||||
}
|
||||
],
|
||||
// role and auth
|
||||
teamTags: {
|
||||
type: [String]
|
||||
|
||||
242
packages/service/core/app/tags/controller.ts
Normal file
@ -0,0 +1,242 @@
|
||||
import { MongoTag } from './schema';
|
||||
import { MongoApp } from '../schema';
|
||||
import { Types } from '../../../common/mongo';
|
||||
|
||||
/**
|
||||
* 创建新标签
|
||||
*/
|
||||
export const createTag = async ({
|
||||
teamId,
|
||||
name,
|
||||
color
|
||||
}: {
|
||||
teamId: string;
|
||||
name: string;
|
||||
color?: string;
|
||||
}) => {
|
||||
const tag = await MongoTag.create({
|
||||
teamId,
|
||||
name,
|
||||
color
|
||||
});
|
||||
|
||||
return tag.toObject();
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取团队所有标签
|
||||
*/
|
||||
export const getTeamTags = async (teamId: string) => {
|
||||
const tags = await MongoTag.find({ teamId }).lean();
|
||||
return tags;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取标签使用统计
|
||||
*/
|
||||
export const getTagsWithCount = async (teamId: string) => {
|
||||
return MongoTag.aggregate([
|
||||
{ $match: { teamId: new Types.ObjectId(teamId) } },
|
||||
{
|
||||
$lookup: {
|
||||
from: 'apps',
|
||||
localField: '_id',
|
||||
foreignField: 'tags',
|
||||
as: 'apps'
|
||||
}
|
||||
},
|
||||
{
|
||||
$addFields: {
|
||||
count: { $size: '$apps' }
|
||||
}
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
apps: 0
|
||||
}
|
||||
}
|
||||
]);
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新标签
|
||||
*/
|
||||
export const updateTag = async ({
|
||||
tagId,
|
||||
teamId,
|
||||
name,
|
||||
color
|
||||
}: {
|
||||
tagId: string;
|
||||
teamId: string;
|
||||
name?: string;
|
||||
color?: string;
|
||||
}) => {
|
||||
const updateData: Record<string, any> = {};
|
||||
if (name !== undefined) updateData.name = name;
|
||||
if (color !== undefined) updateData.color = color;
|
||||
|
||||
await MongoTag.updateOne({ _id: tagId, teamId }, { $set: updateData });
|
||||
|
||||
return MongoTag.findById(tagId).lean();
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除标签
|
||||
*/
|
||||
export const deleteTag = async ({ tagId, teamId }: { tagId: string; teamId: string }) => {
|
||||
// 先从所有 app 中移除该标签
|
||||
await MongoApp.updateMany({ teamId, tags: tagId }, { $pull: { tags: tagId } });
|
||||
|
||||
// 然后删除标签
|
||||
await MongoTag.deleteOne({ _id: tagId, teamId });
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 为 app 添加标签
|
||||
*/
|
||||
export const addTagToApp = async ({
|
||||
appId,
|
||||
tagId,
|
||||
teamId
|
||||
}: {
|
||||
appId: string;
|
||||
tagId: string;
|
||||
teamId: string;
|
||||
}) => {
|
||||
// 确认标签存在且属于该团队
|
||||
const tag = await MongoTag.findOne({ _id: tagId, teamId });
|
||||
if (!tag) {
|
||||
throw new Error('Tag not found or not authorized');
|
||||
}
|
||||
|
||||
await MongoApp.updateOne({ _id: appId, teamId }, { $addToSet: { tags: tagId } });
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 从 app 移除标签
|
||||
*/
|
||||
export const removeTagFromApp = async ({
|
||||
appId,
|
||||
tagId,
|
||||
teamId
|
||||
}: {
|
||||
appId: string;
|
||||
tagId: string;
|
||||
teamId: string;
|
||||
}) => {
|
||||
await MongoApp.updateOne({ _id: appId, teamId }, { $pull: { tags: tagId } });
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 批量删除标签
|
||||
*/
|
||||
export const batchDeleteTags = async ({ tagIds, teamId }: { tagIds: string[]; teamId: string }) => {
|
||||
if (!tagIds || tagIds.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 先从所有 app 中移除这些标签
|
||||
await MongoApp.updateMany(
|
||||
{ teamId, tags: { $in: tagIds } },
|
||||
{ $pull: { tags: { $in: tagIds } } }
|
||||
);
|
||||
|
||||
// 然后删除标签
|
||||
const result = await MongoTag.deleteMany({ _id: { $in: tagIds }, teamId });
|
||||
|
||||
return { deletedCount: result.deletedCount };
|
||||
};
|
||||
|
||||
/**
|
||||
* 批量为 app 添加标签
|
||||
*/
|
||||
export const batchAddTagsToApp = async ({
|
||||
appId,
|
||||
tagIds,
|
||||
teamId
|
||||
}: {
|
||||
appId: string;
|
||||
tagIds: string[];
|
||||
teamId: string;
|
||||
}) => {
|
||||
if (!tagIds || tagIds.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 确认标签存在且属于该团队
|
||||
const tags = await MongoTag.find({ _id: { $in: tagIds }, teamId });
|
||||
if (tags.length !== tagIds.length) {
|
||||
throw new Error('Some tags not found or not authorized');
|
||||
}
|
||||
|
||||
await MongoApp.updateOne({ _id: appId, teamId }, { $addToSet: { tags: { $each: tagIds } } });
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 批量从 app 移除标签
|
||||
*/
|
||||
export const batchRemoveTagsFromApp = async ({
|
||||
appId,
|
||||
tagIds,
|
||||
teamId
|
||||
}: {
|
||||
appId: string;
|
||||
tagIds: string[];
|
||||
teamId: string;
|
||||
}) => {
|
||||
if (!tagIds || tagIds.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
await MongoApp.updateOne({ _id: appId, teamId }, { $pull: { tags: { $in: tagIds } } });
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 批量为某一标签添加 app(全量更新)
|
||||
*/
|
||||
export const batchAddAppsToTag = async ({
|
||||
tagId,
|
||||
appIds,
|
||||
teamId
|
||||
}: {
|
||||
tagId: string;
|
||||
appIds: string[];
|
||||
teamId: string;
|
||||
}) => {
|
||||
// 确认标签存在且属于该团队
|
||||
const tag = await MongoTag.findOne({ _id: tagId, teamId });
|
||||
if (!tag) {
|
||||
throw new Error('Tag not found or not authorized');
|
||||
}
|
||||
|
||||
// 如果 appIds 为空数组,则移除该标签的所有应用
|
||||
if (!appIds || appIds.length === 0) {
|
||||
await MongoApp.updateMany({ teamId, tags: tagId }, { $pull: { tags: tagId } });
|
||||
return true;
|
||||
}
|
||||
|
||||
// 确认所有 app 都存在且属于该团队
|
||||
const apps = await MongoApp.find({ _id: { $in: appIds }, teamId });
|
||||
if (apps.length !== appIds.length) {
|
||||
throw new Error('Some apps not found or not authorized');
|
||||
}
|
||||
|
||||
// 先从所有应用中移除该标签
|
||||
await MongoApp.updateMany({ teamId, tags: tagId }, { $pull: { tags: tagId } });
|
||||
|
||||
// 然后为指定的应用添加该标签
|
||||
await MongoApp.updateMany({ _id: { $in: appIds }, teamId }, { $addToSet: { tags: tagId } });
|
||||
|
||||
return true;
|
||||
};
|
||||
37
packages/service/core/app/tags/schema.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { TeamCollectionName } from '@fastgpt/global/support/user/team/constant';
|
||||
import { getMongoModel, Schema } from '../../../common/mongo';
|
||||
|
||||
export const TagCollectionName = 'app_tags';
|
||||
|
||||
export type TagSchemaType = {
|
||||
_id: string;
|
||||
teamId: string;
|
||||
name: string;
|
||||
color: string;
|
||||
createTime: Date;
|
||||
};
|
||||
|
||||
const TagSchema = new Schema({
|
||||
teamId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: TeamCollectionName,
|
||||
required: true
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: '#3370ff'
|
||||
},
|
||||
createTime: {
|
||||
type: Date,
|
||||
default: () => new Date()
|
||||
}
|
||||
});
|
||||
|
||||
// 创建复合索引:按团队和名称确保唯一性
|
||||
TagSchema.index({ teamId: 1, name: 1 }, { unique: true });
|
||||
|
||||
export const MongoTag = getMongoModel<TagSchemaType>(TagCollectionName, TagSchema);
|
||||
@ -137,7 +137,7 @@ export const authApp = async ({
|
||||
appId: ParentIdType;
|
||||
per: PermissionValueType;
|
||||
}): Promise<
|
||||
AuthResponseType & {
|
||||
AuthResponseType<AppPermission> & {
|
||||
app: AppDetailType;
|
||||
}
|
||||
> => {
|
||||
|
||||
568
packages/service/support/user/team/gate/controller.ts
Normal file
@ -0,0 +1,568 @@
|
||||
import { MongoTeamGate } from './schema';
|
||||
import { Types } from '../../../../common/mongo';
|
||||
import type { ClientSession } from '../../../../common/mongo';
|
||||
import { mongoSessionRun } from '../../../../common/mongo/sessionRun';
|
||||
import {
|
||||
GateFeaturedAppPermission,
|
||||
GateQuickAppPermission
|
||||
} from '@fastgpt/global/support/permission/app/constant';
|
||||
import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant';
|
||||
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
|
||||
import { MongoMemberGroupModel } from '../../../permission/memberGroup/memberGroupSchema';
|
||||
import { MongoResourcePermission } from '../../../permission/schema';
|
||||
|
||||
export const addGatePermission = async ({
|
||||
teamId,
|
||||
appId,
|
||||
per,
|
||||
session
|
||||
}: {
|
||||
teamId: string;
|
||||
appId: string;
|
||||
per: number;
|
||||
session?: ClientSession;
|
||||
}) => {
|
||||
// 1. 先找全员组
|
||||
const teamGroup = await MongoMemberGroupModel.findOne({
|
||||
teamId,
|
||||
name: DefaultGroupName
|
||||
});
|
||||
if (!teamGroup) {
|
||||
return Promise.reject('找不到全员组');
|
||||
}
|
||||
|
||||
// 2. 加权限
|
||||
await MongoResourcePermission.updateOne(
|
||||
{
|
||||
teamId,
|
||||
groupId: teamGroup?._id,
|
||||
resourceType: PerResourceTypeEnum.app,
|
||||
resourceId: appId
|
||||
},
|
||||
{
|
||||
permission: per
|
||||
},
|
||||
{
|
||||
session,
|
||||
upsert: true
|
||||
}
|
||||
);
|
||||
};
|
||||
export const removeGatePermission = async ({
|
||||
teamId,
|
||||
appId,
|
||||
per,
|
||||
session
|
||||
}: {
|
||||
teamId: string;
|
||||
appId: string;
|
||||
per: number;
|
||||
session?: ClientSession;
|
||||
}) => {
|
||||
// 1. 先找全员组
|
||||
const teamGroup = await MongoMemberGroupModel.findOne({
|
||||
teamId,
|
||||
name: DefaultGroupName
|
||||
});
|
||||
if (!teamGroup) {
|
||||
return Promise.reject('找不到全员组');
|
||||
}
|
||||
|
||||
await MongoResourcePermission.deleteOne(
|
||||
{
|
||||
teamId,
|
||||
groupId: teamGroup?._id,
|
||||
resourceType: PerResourceTypeEnum.app,
|
||||
resourceId: appId,
|
||||
permission: per
|
||||
},
|
||||
{
|
||||
session
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建团队门户配置
|
||||
*/
|
||||
export const createGateConfig = async ({ teamId }: { teamId: string }) => {
|
||||
const gate = await MongoTeamGate.create({
|
||||
teamId
|
||||
});
|
||||
|
||||
return gate.toObject();
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取团队门户配置
|
||||
*/
|
||||
export const getGateConfig = async (teamId: string) => {
|
||||
const gate = await MongoTeamGate.findOne({ teamId }).lean();
|
||||
return gate;
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新团队门户配置
|
||||
*/
|
||||
export const updateGateConfig = async ({
|
||||
teamId,
|
||||
status,
|
||||
name,
|
||||
banner,
|
||||
logo,
|
||||
tools,
|
||||
placeholderText
|
||||
}: {
|
||||
teamId: string;
|
||||
status?: boolean;
|
||||
name?: string;
|
||||
banner?: string;
|
||||
logo?: string;
|
||||
tools?: string[];
|
||||
placeholderText?: string;
|
||||
}) => {
|
||||
const updateData: Record<string, any> = {};
|
||||
if (status !== undefined) updateData.status = status;
|
||||
if (name !== undefined) updateData.name = name;
|
||||
if (banner !== undefined) updateData.banner = banner;
|
||||
if (logo !== undefined) updateData.logo = logo;
|
||||
if (tools !== undefined) updateData.tools = tools;
|
||||
if (placeholderText !== undefined) updateData.placeholderText = placeholderText;
|
||||
|
||||
// 使用 upsert 选项,如果不存在则创建
|
||||
await MongoTeamGate.updateOne({ teamId }, { $set: updateData }, { upsert: true });
|
||||
|
||||
return MongoTeamGate.findOne({ teamId }).lean();
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除团队门户配置
|
||||
*/
|
||||
export const deleteGateConfig = async (teamId: string) => {
|
||||
await MongoTeamGate.deleteOne({ teamId });
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 启用或禁用团队门户
|
||||
*/
|
||||
export const toggleGateStatus = async ({ teamId, status }: { teamId: string; status: boolean }) => {
|
||||
await MongoTeamGate.updateOne({ teamId }, { $set: { status } }, { upsert: true });
|
||||
|
||||
return MongoTeamGate.findOne({ teamId }).lean();
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新门户工具配置
|
||||
*/
|
||||
export const updateGateTools = async ({ teamId, tools }: { teamId: string; tools: string[] }) => {
|
||||
await MongoTeamGate.updateOne({ teamId }, { $set: { tools } }, { upsert: true });
|
||||
|
||||
return MongoTeamGate.findOne({ teamId }).lean();
|
||||
};
|
||||
|
||||
/**
|
||||
* 添加门户工具
|
||||
*/
|
||||
export const addGateTool = async ({ teamId, tool }: { teamId: string; tool: string }) => {
|
||||
await MongoTeamGate.updateOne({ teamId }, { $addToSet: { tools: tool } }, { upsert: true });
|
||||
|
||||
return MongoTeamGate.findOne({ teamId }).lean();
|
||||
};
|
||||
|
||||
/**
|
||||
* 移除门户工具
|
||||
*/
|
||||
export const removeGateTool = async ({ teamId, tool }: { teamId: string; tool: string }) => {
|
||||
await MongoTeamGate.updateOne({ teamId }, { $pull: { tools: tool } });
|
||||
|
||||
return MongoTeamGate.findOne({ teamId }).lean();
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新特色应用列表
|
||||
*/
|
||||
export const updateFeaturedApps = async ({
|
||||
teamId,
|
||||
featuredApps
|
||||
}: {
|
||||
teamId: string;
|
||||
featuredApps: string[];
|
||||
}) => {
|
||||
// 将字符串数组转换为 ObjectId 数组
|
||||
const objectIdArray = featuredApps.map((id) => new Types.ObjectId(id));
|
||||
await MongoTeamGate.updateOne(
|
||||
{ teamId },
|
||||
{ $set: { featuredApps: objectIdArray } },
|
||||
{ upsert: true }
|
||||
);
|
||||
return MongoTeamGate.findOne({ teamId }).lean();
|
||||
};
|
||||
|
||||
/**
|
||||
* 添加特色应用
|
||||
*/
|
||||
export const addFeaturedApp = async ({ teamId, appId }: { teamId: string; appId: string }) => {
|
||||
await MongoTeamGate.updateOne(
|
||||
{ teamId },
|
||||
{ $addToSet: { featuredApps: new Types.ObjectId(appId) } },
|
||||
{ upsert: true }
|
||||
);
|
||||
return MongoTeamGate.findOne({ teamId }).lean();
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除特色应用
|
||||
*/
|
||||
export const removeFeaturedApp = async ({
|
||||
teamId,
|
||||
appId,
|
||||
session
|
||||
}: {
|
||||
teamId: string;
|
||||
appId: string;
|
||||
session?: ClientSession;
|
||||
}) => {
|
||||
await MongoTeamGate.updateOne(
|
||||
{ teamId },
|
||||
{ $pull: { featuredApps: new Types.ObjectId(appId) } },
|
||||
{ session }
|
||||
);
|
||||
return MongoTeamGate.findOne({ teamId }).lean();
|
||||
};
|
||||
|
||||
/**
|
||||
* 移动特色应用位置(原子操作)
|
||||
* @param teamId 团队ID
|
||||
* @param appId 要移动的应用ID
|
||||
* @param toIndex 目标位置索引
|
||||
*/
|
||||
export const moveFeatureAppToPosition = async ({
|
||||
teamId,
|
||||
appId,
|
||||
toIndex
|
||||
}: {
|
||||
teamId: string;
|
||||
appId: string;
|
||||
toIndex: number;
|
||||
}) => {
|
||||
const objectId = new Types.ObjectId(appId);
|
||||
|
||||
// 获取当前配置
|
||||
const config = await MongoTeamGate.findOne({ teamId }).lean();
|
||||
if (!config || !config.featuredApps) {
|
||||
throw new Error('团队配置不存在');
|
||||
}
|
||||
|
||||
const apps = [...config.featuredApps];
|
||||
const currentIndex = apps.findIndex((id) => id.toString() === appId);
|
||||
|
||||
if (currentIndex === -1) {
|
||||
throw new Error('应用不在特色应用列表中');
|
||||
}
|
||||
|
||||
// 移动数组元素
|
||||
const [movedApp] = apps.splice(currentIndex, 1);
|
||||
apps.splice(toIndex, 0, movedApp);
|
||||
|
||||
// 一次性更新
|
||||
await MongoTeamGate.updateOne({ teamId }, { $set: { featuredApps: apps } });
|
||||
|
||||
return MongoTeamGate.findOne({ teamId }).lean();
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新工具排序
|
||||
* @param teamId 团队ID
|
||||
* @param orderedTools 按新顺序排列的工具数组
|
||||
*/
|
||||
export const reorderTools = async ({
|
||||
teamId,
|
||||
orderedTools
|
||||
}: {
|
||||
teamId: string;
|
||||
orderedTools: string[];
|
||||
}) => {
|
||||
await MongoTeamGate.updateOne({ teamId }, { $set: { tools: orderedTools } });
|
||||
return MongoTeamGate.findOne({ teamId }).lean();
|
||||
};
|
||||
|
||||
/**
|
||||
* 批量更新门户配置
|
||||
*/
|
||||
export const batchUpdateGateConfigs = async (
|
||||
configs: {
|
||||
teamId: string;
|
||||
status?: boolean;
|
||||
banner?: string;
|
||||
logo?: string;
|
||||
tools?: string[];
|
||||
placeholderText?: string;
|
||||
}[]
|
||||
) => {
|
||||
const operations = configs.map((config) => {
|
||||
const { teamId, ...updateData } = config;
|
||||
return {
|
||||
updateOne: {
|
||||
filter: { teamId },
|
||||
update: { $set: updateData },
|
||||
upsert: true
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
if (operations.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
await MongoTeamGate.bulkWrite(operations);
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 批量更新特色应用
|
||||
*/
|
||||
export const batchUpdateFeaturedApps = async (
|
||||
updates: {
|
||||
teamId: string;
|
||||
featuredApps: string[];
|
||||
}[]
|
||||
) => {
|
||||
const operations = updates.map((update) => {
|
||||
const { teamId, featuredApps } = update;
|
||||
// 将字符串数组转换为 ObjectId 数组
|
||||
const objectIdArray = featuredApps.map((id) => new Types.ObjectId(id));
|
||||
return {
|
||||
updateOne: {
|
||||
filter: { teamId },
|
||||
update: { $set: { featuredApps: objectIdArray } },
|
||||
upsert: true
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
if (operations.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const teamId = updates[0]?.teamId;
|
||||
|
||||
const gateConfig = await MongoTeamGate.findOne({ teamId });
|
||||
if (!gateConfig) return Promise.reject('无 gate 配置');
|
||||
|
||||
const updatedAppId = updates[0].featuredApps;
|
||||
const deleteAppId = gateConfig.featuredApps.filter((id) => !updatedAppId.includes(id));
|
||||
|
||||
await mongoSessionRun(async (session) => {
|
||||
await MongoTeamGate.bulkWrite(operations, { session });
|
||||
|
||||
for (const id of deleteAppId) {
|
||||
await removeGatePermission({
|
||||
teamId,
|
||||
appId: id,
|
||||
per: GateFeaturedAppPermission,
|
||||
session
|
||||
});
|
||||
}
|
||||
for (const id of updatedAppId) {
|
||||
await addGatePermission({
|
||||
teamId,
|
||||
appId: id,
|
||||
per: GateFeaturedAppPermission,
|
||||
session
|
||||
});
|
||||
}
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 批量更新工具排序
|
||||
*/
|
||||
export const batchUpdateToolsOrder = async (
|
||||
updates: {
|
||||
teamId: string;
|
||||
tools: string[];
|
||||
}[]
|
||||
) => {
|
||||
const operations = updates.map((update) => {
|
||||
const { teamId, tools } = update;
|
||||
return {
|
||||
updateOne: {
|
||||
filter: { teamId },
|
||||
update: { $set: { tools } },
|
||||
upsert: true
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
if (operations.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
await MongoTeamGate.bulkWrite(operations);
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 批量删除特色应用
|
||||
* @param teamId 团队ID
|
||||
* @param appIds 要删除的应用ID数组
|
||||
*/
|
||||
export const batchDeleteFeaturedApps = async ({
|
||||
teamId,
|
||||
appIds,
|
||||
session
|
||||
}: {
|
||||
teamId: string;
|
||||
appIds: string[];
|
||||
session?: ClientSession;
|
||||
}) => {
|
||||
if (!appIds || appIds.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await MongoTeamGate.updateOne(
|
||||
{ teamId },
|
||||
{ $pull: { featuredApps: { $in: appIds.map((id) => new Types.ObjectId(id)) } } },
|
||||
{
|
||||
session
|
||||
}
|
||||
);
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新快速应用列表
|
||||
*/
|
||||
export const updateQuickApps = async ({
|
||||
teamId,
|
||||
quickApps
|
||||
}: {
|
||||
teamId: string;
|
||||
quickApps: string[];
|
||||
}) => {
|
||||
await MongoTeamGate.updateOne({ teamId }, { $set: { quickApps } }, { upsert: true });
|
||||
return MongoTeamGate.findOne({ teamId }).lean();
|
||||
};
|
||||
|
||||
/**
|
||||
* 添加快速应用
|
||||
*/
|
||||
export const addQuickApp = async ({ teamId, appId }: { teamId: string; appId: string }) => {
|
||||
await MongoTeamGate.updateOne(
|
||||
{ teamId },
|
||||
{ $addToSet: { quickApps: new Types.ObjectId(appId) } },
|
||||
{ upsert: true }
|
||||
);
|
||||
return MongoTeamGate.findOne({ teamId }).lean();
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除快速应用
|
||||
*/
|
||||
export const removeQuickApp = async ({ teamId, appId }: { teamId: string; appId: string }) => {
|
||||
await MongoTeamGate.updateOne({ teamId }, { $pull: { quickApps: new Types.ObjectId(appId) } });
|
||||
return MongoTeamGate.findOne({ teamId }).lean();
|
||||
};
|
||||
|
||||
/**
|
||||
* 移动快速应用位置(原子操作)
|
||||
* @param teamId 团队ID
|
||||
* @param appId 要移动的应用ID
|
||||
* @param toIndex 目标位置索引
|
||||
*/
|
||||
export const moveQuickAppToPosition = async ({
|
||||
teamId,
|
||||
appId,
|
||||
toIndex
|
||||
}: {
|
||||
teamId: string;
|
||||
appId: string;
|
||||
toIndex: number;
|
||||
}) => {
|
||||
const objectId = new Types.ObjectId(appId);
|
||||
|
||||
// 获取当前配置
|
||||
const config = await MongoTeamGate.findOne({ teamId }).lean();
|
||||
if (!config || !config.quickApps) {
|
||||
throw new Error('团队配置不存在');
|
||||
}
|
||||
|
||||
const apps = [...config.quickApps];
|
||||
const currentIndex = apps.findIndex((id) => id.toString() === appId);
|
||||
|
||||
if (currentIndex === -1) {
|
||||
throw new Error('应用不在快速应用列表中');
|
||||
}
|
||||
|
||||
// 移动数组元素
|
||||
const [movedApp] = apps.splice(currentIndex, 1);
|
||||
apps.splice(toIndex, 0, movedApp);
|
||||
|
||||
// 一次性更新
|
||||
await MongoTeamGate.updateOne({ teamId }, { $set: { quickApps: apps } });
|
||||
|
||||
return MongoTeamGate.findOne({ teamId }).lean();
|
||||
};
|
||||
|
||||
/**
|
||||
* 批量更新快速应用
|
||||
*/
|
||||
export const batchUpdateQuickApps = async (teamId: string, quickApps: string[]) => {
|
||||
const gateConfig = await MongoTeamGate.findOne({ teamId });
|
||||
if (!gateConfig) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 计算删除的appId
|
||||
const deleteAppIds = gateConfig.quickApps.filter((id) => !quickApps.includes(id.toString()));
|
||||
|
||||
return mongoSessionRun(async (session) => {
|
||||
// 1. 删除权限
|
||||
for (const id of deleteAppIds) {
|
||||
await removeGatePermission({
|
||||
teamId,
|
||||
appId: id,
|
||||
per: GateQuickAppPermission,
|
||||
session
|
||||
});
|
||||
}
|
||||
// 加权限
|
||||
for (const id of quickApps) {
|
||||
await addGatePermission({
|
||||
teamId,
|
||||
appId: id,
|
||||
per: GateQuickAppPermission,
|
||||
session
|
||||
});
|
||||
}
|
||||
|
||||
gateConfig.quickApps = quickApps;
|
||||
await gateConfig.save({ session });
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 批量删除快速应用
|
||||
* @param teamId 团队ID
|
||||
* @param appIds 要删除的应用ID数组
|
||||
*/
|
||||
export const batchDeleteQuickApps = async ({
|
||||
teamId,
|
||||
appIds
|
||||
}: {
|
||||
teamId: string;
|
||||
appIds: string[];
|
||||
}) => {
|
||||
if (!appIds || appIds.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await MongoTeamGate.updateOne(
|
||||
{ teamId },
|
||||
{ $pull: { quickApps: { $in: appIds.map((id) => new Types.ObjectId(id)) } } }
|
||||
);
|
||||
return true;
|
||||
};
|
||||
45
packages/service/support/user/team/gate/schema.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { TeamCollectionName } from '@fastgpt/global/support/user/team/constant';
|
||||
import { Schema, getMongoModel } from '../../../../common/mongo';
|
||||
import type { GateSchemaType } from '@fastgpt/global/support/user/team/gate/type';
|
||||
|
||||
export const gateCollectionName = 'team_gate_config';
|
||||
|
||||
const GateConfigSchema = new Schema({
|
||||
teamId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: TeamCollectionName
|
||||
},
|
||||
status: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
name: {
|
||||
type: String
|
||||
},
|
||||
banner: {
|
||||
type: String
|
||||
},
|
||||
logo: {
|
||||
type: String
|
||||
},
|
||||
tools: {
|
||||
type: [String]
|
||||
},
|
||||
placeholderText: {
|
||||
type: String
|
||||
},
|
||||
featuredApps: [
|
||||
{
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'apps'
|
||||
}
|
||||
],
|
||||
quickApps: [
|
||||
{
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'apps'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
export const MongoTeamGate = getMongoModel<GateSchemaType>(gateCollectionName, GateConfigSchema);
|
||||
@ -17,6 +17,11 @@ const TeamSchema = new Schema({
|
||||
type: String,
|
||||
default: '/icon/logo.svg'
|
||||
},
|
||||
// todo :banner
|
||||
banner: {
|
||||
type: String,
|
||||
default: '/icon/banner.svg'
|
||||
},
|
||||
createTime: {
|
||||
type: Date,
|
||||
default: () => Date.now()
|
||||
|
||||
@ -474,6 +474,31 @@ export const iconPaths = {
|
||||
'support/user/userLightSmall': () => import('./icons/support/user/userLightSmall.svg'),
|
||||
'support/user/usersFill': () => import('./icons/support/user/usersFill.svg'),
|
||||
'support/user/usersLight': () => import('./icons/support/user/usersLight.svg'),
|
||||
'support/gate/gateLight': () => import('./icons/support/gate/gateLight.svg'),
|
||||
'support/gate/chat/sidebar/chatGray': () =>
|
||||
import('./icons/support/gate/chat/sidebar/chatGray.svg'),
|
||||
'support/gate/chat/historySlider/new_chat': () =>
|
||||
import('./icons/support/gate/chat/historySlider/new_chat.svg'),
|
||||
'support/gate/chat/historySlider/clear-all': () =>
|
||||
import('./icons/support/gate/chat/historySlider/clear-all.svg'),
|
||||
'support/gate/chat/historySlider/chevron-right2': () =>
|
||||
import('./icons/support/gate/chat/historySlider/chevron-right2.svg'),
|
||||
'support/gate/chat/toolkitLine': () => import('./icons/support/gate/chat/toolkitLine.svg'),
|
||||
'support/gate/chat/historySlider/chevron-left2': () =>
|
||||
import('./icons/support/gate/chat/historySlider/chevron-left2.svg'),
|
||||
'support/gate/chat/sidebar/appGray': () =>
|
||||
import('./icons/support/gate/chat/sidebar/appGray.svg'),
|
||||
'support/gate/chat/voiceGray': () => import('./icons/support/gate/chat/voiceGray.svg'),
|
||||
'support/gate/chat/fileGray': () => import('./icons/support/gate/chat/fileGray.svg'),
|
||||
'support/gate/chat/paperclip': () => import('./icons/support/gate/chat/paperclip.svg'),
|
||||
'support/gate/chat/imageGray': () => import('./icons/support/gate/chat/imageGray.svg'),
|
||||
'support/gate/chat/sidebar/CollapseButton': () =>
|
||||
import('./icons/support/gate/chat/sidebar/CollapseButton.svg'),
|
||||
'support/gate/home/savePrimary': () => import('./icons/support/gate/home/savePrimary.svg'),
|
||||
'support/gate/home/shareLight': () => import('./icons/support/gate/home/shareLight.svg'),
|
||||
'support/gate/home/sharePrimary': () => import('./icons/support/gate/home/sharePrimary.svg'),
|
||||
'support/gate/home/upload': () => import('./icons/support/gate/home/upload.svg'),
|
||||
'support/gate/home/add': () => import('./icons/support/gate/home/add.svg'),
|
||||
text: () => import('./icons/text.svg'),
|
||||
union: () => import('./icons/union.svg'),
|
||||
user: () => import('./icons/user.svg'),
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.6665 3.0263C10.5079 3.01307 10.2763 3.01095 9.84294 3.01095H7.16646C6.45348 3.01095 5.96581 3.01152 5.58819 3.04048C5.21887 3.06881 5.02616 3.12054 4.89105 3.18516C4.57275 3.3374 4.32387 3.57549 4.17134 3.85649C4.11194 3.96593 4.05991 4.12789 4.03072 4.4633C4.00053 4.81012 3.9998 5.26058 3.9998 5.93236V14.0677C3.9998 14.7394 4.00053 15.1899 4.03072 15.5367C4.05991 15.8721 4.11194 16.0341 4.17134 16.1435C4.32387 16.4245 4.57275 16.6626 4.89105 16.8149C5.02616 16.8795 5.21887 16.9312 5.58819 16.9595C5.96581 16.9885 6.45348 16.9891 7.16646 16.9891H12.4998C13.2128 16.9891 13.7005 16.9885 14.0781 16.9595C14.4474 16.9312 14.6401 16.8795 14.7752 16.8149C15.0935 16.6626 15.3424 16.4245 15.4949 16.1435C15.5543 16.0341 15.6063 15.8721 15.6355 15.5367C15.6657 15.1899 15.6665 14.7394 15.6665 14.0677V8.42632C15.6665 8.04949 15.6642 7.84193 15.654 7.70438L12.8076 7.70439C12.5963 7.70441 12.3934 7.70443 12.2221 7.69129C12.0356 7.67698 11.8167 7.64346 11.5952 7.53756C11.2863 7.38982 11.0253 7.14923 10.8582 6.84149C10.736 6.61626 10.6972 6.39184 10.681 6.2055C10.6664 6.03826 10.6664 5.84201 10.6665 5.64647C10.6665 5.63747 10.6665 5.62847 10.6665 5.61947V3.0263ZM11.8068 1.61342C11.6211 1.53551 11.4284 1.47397 11.2311 1.42951C10.8511 1.34388 10.4571 1.34404 9.92517 1.34426C9.89812 1.34427 9.87072 1.34428 9.84294 1.34428L7.13385 1.34428C6.4615 1.34427 5.90966 1.34427 5.46075 1.37869C4.99631 1.41431 4.57159 1.49047 4.17193 1.68162C3.54942 1.97936 3.03339 2.45928 2.70655 3.0614C2.49347 3.45396 2.40926 3.87156 2.37033 4.31878C2.33311 4.74646 2.33312 5.26987 2.33313 5.89642V14.1036C2.33312 14.7301 2.33311 15.2536 2.37033 15.6812C2.40926 16.1285 2.49347 16.5461 2.70655 16.9386C3.03339 17.5407 3.54942 18.0207 4.17193 18.3184C4.57159 18.5096 4.99631 18.5857 5.46075 18.6213C5.90965 18.6558 6.46147 18.6557 7.13382 18.6557H12.5324C13.2048 18.6557 13.7566 18.6558 14.2055 18.6213C14.67 18.5857 15.0947 18.5096 15.4943 18.3184C16.1168 18.0207 16.6329 17.5407 16.9597 16.9386C17.1728 16.5461 17.257 16.1285 17.2959 15.6812C17.3332 15.2536 17.3331 14.7301 17.3331 14.1036V8.42632C17.3331 8.39763 17.3331 8.3693 17.3332 8.3413C17.3335 7.85013 17.3337 7.46273 17.2381 7.08878C17.1885 6.89509 17.1202 6.70705 17.0344 6.52696C17.0287 6.51433 17.0227 6.50187 17.0163 6.4896C16.9608 6.37678 16.8983 6.26719 16.8292 6.16139C16.6188 5.83909 16.3328 5.57093 15.9611 5.22237C15.9405 5.20307 15.9197 5.18353 15.8986 5.16372L13.2417 2.66977C13.2216 2.65089 13.2018 2.63225 13.1822 2.61385C12.8076 2.26189 12.5241 1.99554 12.186 1.80108C12.0761 1.73786 11.9628 1.68091 11.8466 1.63043C11.8335 1.62442 11.8202 1.61875 11.8068 1.61342ZM12.3331 4.10281V5.61947C12.3331 5.82534 12.3337 5.94445 12.339 6.02864C12.3423 6.02893 12.3458 6.02922 12.3495 6.0295C12.4492 6.03715 12.5869 6.03772 12.8331 6.03772H14.3944L12.3331 4.10281Z" fill="#707070"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
@ -0,0 +1,7 @@
|
||||
<svg width="16" height="20" viewBox="0 0 16 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="icon/solid/chevron-right2">
|
||||
<path id="Rectangle 3101"
|
||||
d="M11.0474 14.2501C11.0474 15.735 9.25219 16.4786 8.20225 15.4286L3.95213 11.1785C3.30126 10.5276 3.30126 9.47236 3.95213 8.82149L8.20225 4.57138C9.25219 3.52143 11.0474 4.26505 11.0474 5.74989L11.0474 14.2501Z"
|
||||
fill="currentColor" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 444 B |
@ -0,0 +1,7 @@
|
||||
<svg width="16" height="20" viewBox="0 0 16 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="icon/solid/chevron-right2">
|
||||
<path id="Rectangle 3101"
|
||||
d="M4.95255 5.74989C4.95255 4.26505 6.74778 3.52143 7.79772 4.57138L12.0478 8.82149C12.6987 9.47236 12.6987 10.5276 12.0478 11.1785L7.79772 15.4286C6.74778 16.4786 4.95255 15.735 4.95255 14.2501V5.74989Z"
|
||||
fill="currentColor" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 436 B |
@ -0,0 +1,7 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="icon/line/clear-all">
|
||||
<path id="Union" fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M7.33331 2.63809C7.33331 2.49081 7.4527 2.37142 7.59997 2.37142H8.39997C8.54725 2.37142 8.66664 2.49081 8.66664 2.63809V4.2C8.66664 4.9732 9.29344 5.6 10.0666 5.6L12.861 5.6C13.0083 5.6 13.1277 5.71939 13.1277 5.86666V6.66666C13.1277 6.81394 13.0083 6.93333 12.861 6.93333L3.13891 6.93333C2.99164 6.93333 2.87224 6.81394 2.87224 6.66666V5.86666C2.87224 5.71939 2.99164 5.6 3.13891 5.6L5.93331 5.6C6.70651 5.6 7.33331 4.97319 7.33331 4.2V2.63809ZM7.59997 1.03809C6.71632 1.03809 5.99997 1.75443 5.99997 2.63809V4.2C5.99997 4.23681 5.97012 4.26666 5.93331 4.26666L3.13891 4.26666C2.25526 4.26666 1.53891 4.98301 1.53891 5.86666V6.66666C1.53891 7.41125 2.04753 8.03705 2.73629 8.21558L1.47841 12.628C1.18709 13.6499 1.9545 14.6667 3.01711 14.6667H13.0318C14.0831 14.6667 14.8487 13.6701 14.5778 12.6543L13.384 8.17923C14.0109 7.96253 14.461 7.36717 14.461 6.66666V5.86666C14.461 4.98301 13.7447 4.26666 12.861 4.26666L10.0666 4.26666C10.0298 4.26666 9.99997 4.23681 9.99997 4.2V2.63809C9.99997 1.75443 9.28363 1.03809 8.39997 1.03809H7.59997ZM4.30936 8.26696H11.8226C11.9434 8.26696 12.0491 8.34817 12.0803 8.46489L13.2895 12.9979C13.3347 13.1672 13.207 13.3333 13.0318 13.3333H10.6269C10.627 13.3291 10.6271 13.3249 10.6271 13.3206L10.6409 11.4144C10.6436 11.0462 10.3472 10.7456 9.97907 10.7429C9.61089 10.7403 9.31026 11.0366 9.30759 11.4047L9.2938 13.311C9.29374 13.3184 9.29381 13.3259 9.294 13.3333H6.87303L6.87324 13.3206L6.88704 11.4144C6.88971 11.0462 6.5934 10.7456 6.22522 10.7429C5.85704 10.7403 5.55641 11.0366 5.55374 11.4047L5.53995 13.311C5.53989 13.3184 5.53996 13.3259 5.54015 13.3333H3.01711C2.84 13.3333 2.71211 13.1639 2.76066 12.9936L4.05291 8.46052C4.08557 8.34596 4.19024 8.26696 4.30936 8.26696Z"
|
||||
fill="currentColor" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@ -0,0 +1,8 @@
|
||||
<svg width="19" height="18" viewBox="0 0 19 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M11.3834 2.86546C11.3834 3.27967 11.0476 3.61546 10.6334 3.61546H6.05212C4.39495 3.61546 3.05139 4.95892 3.05139 6.61546V10.3215C3.05158 11.5943 3.84602 12.6857 4.9696 13.1201L5.19885 13.1977L5.54646 13.3005C5.76638 13.3656 5.94445 13.5277 6.02982 13.7406L6.16492 14.0774C6.29685 14.4057 6.68973 14.54 6.99503 14.3611L8.59462 13.4243C8.70956 13.357 8.84034 13.3215 8.97353 13.3215H12.8014C14.4065 13.3215 15.7178 12.0607 15.7985 10.4761L15.8021 10.3215V7.97703C15.8021 7.56281 16.1379 7.22703 16.5521 7.22703C16.9663 7.22703 17.3021 7.56281 17.3021 7.97703V10.3215C17.3019 12.8066 15.2865 14.8215 12.8014 14.8215H9.38096C9.24764 14.8215 9.11674 14.857 9.00172 14.9245L6.57434 16.3471C6.10009 16.625 5.48906 16.4168 5.28381 15.907L4.90857 14.9729C4.82314 14.7602 4.64369 14.6019 4.42992 14.5193C2.74626 13.8685 1.55158 12.2347 1.55139 10.3215V6.61546C1.55139 4.13017 3.56684 2.11546 6.05212 2.11546H10.6334C11.0476 2.11546 11.3834 2.45124 11.3834 2.86546Z"
|
||||
fill="#3370FF" />
|
||||
<path
|
||||
d="M14.9027 1.52901C15.2541 1.52912 15.5392 1.81403 15.5392 2.16549V3.43844H16.8121C17.1636 3.43851 17.4486 3.72344 17.4486 4.07491C17.4485 4.42632 17.1635 4.71132 16.8121 4.71139H15.5392V5.98434C15.5391 6.33571 15.2541 6.62071 14.9027 6.62081C14.5513 6.62081 14.2663 6.33577 14.2662 5.98434V4.71139H12.9933C12.6418 4.71139 12.3569 4.42637 12.3568 4.07491C12.3568 3.7234 12.6418 3.43844 12.9933 3.43844H14.2662V2.16549C14.2662 1.81397 14.5512 1.52901 14.9027 1.52901Z"
|
||||
fill="#3370FF" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@ -0,0 +1,4 @@
|
||||
<svg width="18" height="16" viewBox="0 0 18 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.61372 6.52442C6.30407 6.52442 6.86372 5.96478 6.86372 5.27442C6.86372 4.58407 6.30407 4.02442 5.61372 4.02442C4.92336 4.02442 4.36371 4.58407 4.36371 5.27442C4.36371 5.96478 4.92336 6.52442 5.61372 6.52442Z" fill="#707070"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.666656 5.31489C0.666656 3.63474 0.666656 2.79466 0.993637 2.15292C1.28126 1.58843 1.7402 1.12949 2.30468 0.841873C2.94642 0.514893 3.7865 0.514893 5.46666 0.514893H12.5333C14.2135 0.514893 15.0536 0.514893 15.6953 0.841873C16.2598 1.12949 16.7187 1.58843 17.0063 2.15292C17.3333 2.79466 17.3333 3.63473 17.3333 5.31489V10.6852C17.3333 12.3654 17.3333 13.2054 17.0063 13.8472C16.7187 14.4117 16.2598 14.8706 15.6953 15.1582C15.0536 15.4852 14.2135 15.4852 12.5333 15.4852H5.46666C3.7865 15.4852 2.94642 15.4852 2.30468 15.1582C1.7402 14.8706 1.28126 14.4117 0.993637 13.8472C0.666656 13.2054 0.666656 12.3654 0.666656 10.6852V5.31489ZM5.46666 2.18156H12.5333C13.4009 2.18156 13.9514 2.18286 14.368 2.2169C14.7652 2.24935 14.8919 2.30306 14.9386 2.32688C15.1895 2.45472 15.3935 2.65869 15.5213 2.90957C15.5452 2.95633 15.5989 3.08303 15.6313 3.48022C15.6654 3.89686 15.6667 4.44731 15.6667 5.31489V10.6852C15.6667 10.6952 15.6667 10.7052 15.6667 10.7152L11.6244 6.67291C11.2989 6.34747 10.7713 6.34747 10.4459 6.67291L3.36552 13.7533C3.17216 13.7239 3.09546 13.6906 3.06134 13.6732C2.81045 13.5454 2.60648 13.3414 2.47865 13.0905C2.45483 13.0438 2.40111 12.9171 2.36866 12.5199C2.33462 12.1032 2.33332 11.5528 2.33332 10.6852V5.31489C2.33332 4.44731 2.33462 3.89686 2.36866 3.48022C2.40111 3.08303 2.45483 2.95633 2.47865 2.90957C2.60648 2.65869 2.81045 2.45472 3.06134 2.32688C3.10809 2.30306 3.23479 2.24935 3.63198 2.2169C4.04863 2.18286 4.59908 2.18156 5.46666 2.18156ZM11.0351 8.44068L5.65725 13.8185H12.5333C13.4009 13.8185 13.9514 13.8172 14.368 13.7832C14.7652 13.7508 14.8919 13.697 14.9386 13.6732C15.1895 13.5454 15.3935 13.3414 15.5213 13.0905C15.5316 13.0704 15.5474 13.0354 15.5647 12.9703L11.0351 8.44068Z" fill="#707070"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@ -0,0 +1,7 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="icon/line/paperclip">
|
||||
<path id="Icon (Stroke)" fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M13.001 3.12035C12.4123 3.12035 11.8477 3.3542 11.4315 3.77046L4.63705 10.5649C3.94333 11.2586 3.55361 12.1995 3.55361 13.1805C3.55361 14.1616 3.94333 15.1025 4.63705 15.7962C5.33077 16.4899 6.27165 16.8796 7.25271 16.8796C8.23377 16.8796 9.17465 16.4899 9.86837 15.7962L16.6628 9.00179C16.9515 8.71306 17.4196 8.71306 17.7084 9.00179C17.9971 9.29051 17.9971 9.75863 17.7084 10.0474L10.9139 16.8418C9.94292 17.8128 8.62594 18.3583 7.25271 18.3583C5.87948 18.3583 4.5625 17.8128 3.59148 16.8418C2.62046 15.8708 2.07495 14.5538 2.07495 13.1805C2.07495 11.8073 2.62046 10.4903 3.59148 9.51931L10.3859 2.72489C11.0795 2.03133 12.0201 1.64169 13.001 1.64169C13.9818 1.64169 14.9225 2.03133 15.6161 2.72489C16.3096 3.41846 16.6993 4.35913 16.6993 5.33997C16.6993 6.32082 16.3096 7.26149 15.6161 7.95506L8.81425 14.7495C8.39814 15.1656 7.83378 15.3993 7.24532 15.3993C6.65685 15.3993 6.09249 15.1656 5.67638 14.7495C5.26028 14.3334 5.02651 13.769 5.02651 13.1805C5.02651 12.5921 5.26028 12.0277 5.67638 11.6116L11.9536 5.34181C12.2425 5.05325 12.7106 5.05353 12.9991 5.34242C13.2877 5.63132 13.2874 6.09943 12.9985 6.38799L6.72195 12.6572C6.58334 12.796 6.50517 12.9844 6.50517 13.1805C6.50517 13.3768 6.58315 13.5651 6.72195 13.7039C6.86076 13.8427 7.04902 13.9207 7.24532 13.9207C7.44162 13.9207 7.62988 13.8427 7.76868 13.7039L14.5705 6.90949C14.9866 6.49326 15.2206 5.92852 15.2206 5.33997C15.2206 4.75129 14.9868 4.18672 14.5705 3.77046C14.1542 3.3542 13.5897 3.12035 13.001 3.12035Z"
|
||||
fill="currentColor" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@ -0,0 +1,8 @@
|
||||
<svg width="18" height="16" viewBox="0 0 18 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M9.91227 5.23185C10.2356 4.95244 10.724 4.98763 11.0036 5.31079C11.2829 5.63414 11.2479 6.12338 10.9246 6.40291L9.16113 7.92797L10.8774 9.51977C11.192 9.81221 11.2103 10.305 10.9181 10.62C10.6259 10.9347 10.1339 10.9531 9.81868 10.6615L7.54085 8.54972C7.37406 8.39504 7.29205 8.18359 7.29346 7.97273C7.2559 7.71942 7.34339 7.45278 7.55143 7.27286L9.91227 5.23185Z"
|
||||
fill="currentcolor" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M13.0072 0.108149C15.6489 0.242083 17.7498 2.42669 17.75 5.10164V10.8984L17.7435 11.1555C17.614 13.7122 15.5638 15.7622 13.0072 15.8918L12.75 15.8984H5.25L4.99284 15.8918C2.43607 15.7623 0.385993 13.7123 0.25651 11.1555L0.25 10.8984V5.10164C0.250186 2.42663 2.35104 0.24199 4.99284 0.108149L5.25 0.101639H12.75L13.0072 0.108149ZM5.92546 14.2317H12.75C14.5909 14.2316 16.0833 12.7392 16.0833 10.8984V5.10164C16.0831 3.26092 14.5907 1.76842 12.75 1.76831H5.92546V14.2317ZM4.39225 1.8798C3.02015 2.24419 1.99539 3.4618 1.92074 4.92993L1.91667 5.10164V10.8984C1.91667 12.4427 2.96708 13.7408 4.39225 14.1194V1.8798Z"
|
||||
fill="currentcolor" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@ -0,0 +1,14 @@
|
||||
<svg width="19" height="18" viewBox="0 0 19 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M0.10022 1.2C0.10022 0.537258 0.637478 0 1.30022 0H6.90022C7.56296 0 8.10022 0.537258 8.10022 1.2V6.8C8.10022 7.46274 7.56296 8 6.90022 8H1.30022C0.637478 8 0.10022 7.46274 0.10022 6.8V1.2ZM2.10022 6V2H6.10022V6H2.10022Z"
|
||||
fill="#8A95A7" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M10.1002 1.2C10.1002 0.537258 10.6375 0 11.3002 0H16.9002C17.563 0 18.1002 0.537258 18.1002 1.2V6.8C18.1002 7.46274 17.563 8 16.9002 8H11.3002C10.6375 8 10.1002 7.46274 10.1002 6.8V1.2ZM12.1002 6V2H16.1002V6H12.1002Z"
|
||||
fill="#8A95A7" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M11.3002 10C10.6375 10 10.1002 10.5373 10.1002 11.2V16.8C10.1002 17.4627 10.6375 18 11.3002 18H16.9002C17.563 18 18.1002 17.4627 18.1002 16.8V11.2C18.1002 10.5373 17.563 10 16.9002 10H11.3002ZM12.1002 12V16H16.1002V12H12.1002Z"
|
||||
fill="#8A95A7" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M0.10022 11.2C0.10022 10.5373 0.637478 10 1.30022 10H6.90022C7.56296 10 8.10022 10.5373 8.10022 11.2V16.8C8.10022 17.4627 7.56296 18 6.90022 18H1.30022C0.637478 18 0.10022 17.4627 0.10022 16.8V11.2ZM2.10022 16V12H6.10022V16H2.10022Z"
|
||||
fill="#8A95A7" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@ -0,0 +1,18 @@
|
||||
<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="icon/line/chat">
|
||||
<g id="Union">
|
||||
<path
|
||||
d="M6.18697 8.52243C6.77251 8.52244 7.24715 8.99732 7.24735 9.58281C7.24735 10.1685 6.77263 10.6432 6.18697 10.6432C5.60129 10.6432 5.12659 10.1685 5.12659 9.58281C5.12679 8.99731 5.60142 8.52243 6.18697 8.52243Z"
|
||||
fill="currentColor" />
|
||||
<path
|
||||
d="M10.0004 8.52243C10.586 8.52244 11.0606 8.99732 11.0608 9.58281C11.0608 10.1685 10.5861 10.6432 10.0004 10.6432C9.41477 10.6432 8.94006 10.1685 8.94006 9.58281C8.94027 8.99731 9.4149 8.52243 10.0004 8.52243Z"
|
||||
fill="currentColor" />
|
||||
<path
|
||||
d="M13.8139 8.52243C14.3994 8.52249 14.8741 8.99735 14.8743 9.58281C14.8743 10.1684 14.3995 10.6431 13.8139 10.6432C13.2282 10.6432 12.7535 10.1685 12.7535 9.58281C12.7537 8.99731 13.2284 8.52243 13.8139 8.52243Z"
|
||||
fill="currentColor" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M13.7504 2.5247C16.5117 2.52492 18.7504 4.76342 18.7504 7.5247V11.6425C18.7502 14.4037 16.5109 16.6425 13.7496 16.6425H9.94918C9.80105 16.6425 9.65542 16.6824 9.52763 16.7573L6.83069 18.3377C6.30374 18.6465 5.62482 18.4151 5.39677 17.8486L4.9801 16.811C4.88518 16.5747 4.6854 16.3983 4.44788 16.3064C2.57725 15.5833 1.24985 13.7682 1.24963 11.6425V7.5247C1.24963 4.76328 3.48902 2.5247 6.25045 2.5247H13.7504ZM6.25045 4.19137C4.40914 4.19137 2.9163 5.68411 2.9163 7.5247V11.6425C2.91651 13.0567 3.79923 14.2694 5.04765 14.7521L5.30237 14.8383L5.68892 14.9523C5.93318 15.0246 6.1312 15.2049 6.22603 15.4414L6.37577 15.8157C6.52236 16.1805 6.9586 16.3293 7.29781 16.1307L9.07515 15.0898C9.20286 15.015 9.34872 14.9759 9.4967 14.9759H13.7496C15.5331 14.9759 16.9901 13.5749 17.0797 11.8143L17.0838 11.6425V7.5247C17.0838 5.74165 15.6833 4.28501 13.9222 4.19544L13.7504 4.19137H6.25045Z"
|
||||
fill="currentColor" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M12.81 4.4538H12.8072C14.1971 4.45385 14.8993 4.45754 15.4374 4.73173C15.9173 4.97621 16.3074 5.36631 16.5518 5.84612C16.8298 6.3916 16.8298 7.10566 16.8298 8.5338V12.42C16.8298 13.8481 16.8298 14.5622 16.5518 15.1077C16.3074 15.5875 15.9173 15.9776 15.4374 16.2221C14.892 16.5 14.1779 16.5 12.7498 16.5H5.24992C3.82179 16.5 3.10772 16.5 2.56225 16.2221C2.08243 15.9776 1.69233 15.5875 1.44786 15.1077C1.16992 14.5622 1.16992 13.8481 1.16992 12.42V8.5338C1.16992 7.10566 1.16992 6.3916 1.44786 5.84612C1.69233 5.36631 2.08243 4.97621 2.56225 4.73173C3.10772 4.4538 3.82179 4.4538 5.24992 4.4538H5.36574V4.19994C5.36574 2.70877 6.57457 1.49994 8.06574 1.49994H10.11C11.6012 1.49994 12.81 2.70877 12.81 4.19994V4.4538ZM8.06574 2.99994H10.11C10.7727 2.99994 11.31 3.5372 11.31 4.19994V4.4538H6.86574V4.19994C6.86574 3.5372 7.403 2.99994 8.06574 2.99994ZM12.7498 5.9538H5.24992C4.5111 5.9538 4.0472 5.95496 3.69724 5.98356C3.36478 6.01072 3.26927 6.05497 3.24323 6.06824C3.04566 6.16891 2.88503 6.32954 2.78437 6.52711C2.7711 6.55314 2.72684 6.64865 2.69968 6.98111C2.68387 7.17461 2.67645 7.40293 2.67297 7.6933H15.3267C15.3232 7.40293 15.3158 7.17461 15.3 6.98111C15.2728 6.64865 15.2286 6.55314 15.2153 6.52711C15.1147 6.32954 14.954 6.16891 14.7565 6.06824C14.7304 6.05497 14.6349 6.01072 14.3025 5.98356C13.9525 5.95496 13.4886 5.9538 12.7498 5.9538ZM15.3298 9.0433H10.9765V11.392C10.9765 11.6405 10.775 11.842 10.5265 11.842H7.47313C7.2246 11.842 7.02313 11.6405 7.02313 11.392V9.0433H2.66992V12.42C2.66992 13.1588 2.67109 13.6227 2.69968 13.9727C2.72684 14.3052 2.7711 14.4007 2.78437 14.4267C2.88503 14.6243 3.04566 14.7849 3.24323 14.8856C3.26927 14.8988 3.36478 14.9431 3.69723 14.9702C4.0472 14.9988 4.5111 15 5.24992 15H12.7498C13.4886 15 13.9525 14.9988 14.3025 14.9702C14.6349 14.9431 14.7304 14.8988 14.7565 14.8856C14.954 14.7849 15.1147 14.6243 15.2153 14.4267C15.2286 14.4007 15.2728 14.3052 15.3 13.9727C15.3286 13.6227 15.3298 13.1588 15.3298 12.42V9.0433ZM9.62651 9.0433H8.37313V10.492H9.62651V9.0433Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@ -0,0 +1,5 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M7.91635 2.02287C8.46897 1.47025 9.21848 1.15979 10 1.15979C10.7815 1.15979 11.531 1.47025 12.0837 2.02287C12.6363 2.57549 12.9467 3.325 12.9467 4.10652V9.99999C12.9467 10.7815 12.6363 11.531 12.0837 12.0836C11.531 12.6363 10.7815 12.9467 10 12.9467C9.21848 12.9467 8.46897 12.6363 7.91635 12.0836C7.36373 11.531 7.05327 10.7815 7.05327 9.99999V4.10652C7.05327 3.325 7.36373 2.57549 7.91635 2.02287ZM10 2.63316C9.60924 2.63316 9.23448 2.78839 8.95817 3.06469C8.68186 3.341 8.52663 3.71576 8.52663 4.10652V9.99999C8.52663 10.3907 8.68186 10.7655 8.95817 11.0418C9.23448 11.3181 9.60924 11.4734 10 11.4734C10.3908 11.4734 10.7655 11.3181 11.0418 11.0418C11.3181 10.7655 11.4734 10.3907 11.4734 9.99999V4.10652C11.4734 3.71576 11.3181 3.341 11.0418 3.06469C10.7655 2.78839 10.3908 2.63316 10 2.63316ZM4.84322 7.78994C5.25008 7.78994 5.5799 8.11976 5.5799 8.52662V9.99999C5.5799 11.1723 6.04559 12.2965 6.87452 13.1255C7.70345 13.9544 8.82772 14.4201 10 14.4201C11.1723 14.4201 12.2966 13.9544 13.1255 13.1255C13.9544 12.2965 14.4201 11.1723 14.4201 9.99999V8.52662C14.4201 8.11976 14.7499 7.78994 15.1568 7.78994C15.5636 7.78994 15.8935 8.11976 15.8935 8.52662V9.99999C15.8935 11.563 15.2725 13.0621 14.1673 14.1673C13.2372 15.0974 12.0281 15.6846 10.7367 15.8472V17.3668H12.9467C13.3536 17.3668 13.6834 17.6966 13.6834 18.1035C13.6834 18.5104 13.3536 18.8402 12.9467 18.8402H7.05327C6.64641 18.8402 6.31659 18.5104 6.31659 18.1035C6.31659 17.6966 6.64641 17.3668 7.05327 17.3668H9.26332V15.8472C7.97191 15.6846 6.76285 15.0974 5.83269 14.1673C4.72745 13.0621 4.10654 11.563 4.10654 9.99999V8.52662C4.10654 8.11976 4.43636 7.78994 4.84322 7.78994Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<!-- 保持原路径不变 -->
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M9.57021 1.39896C9.85172 1.32274 10.1484 1.32274 10.4299 1.39896C10.757 1.48751 11.0321 1.70357 11.2481 1.87322C11.2689 1.88955 11.2891 1.90544 11.3088 1.92075L16.8123 6.20128C16.8337 6.21793 16.855 6.23443 16.8761 6.25081C17.1809 6.48733 17.4525 6.6981 17.655 6.97191C17.8327 7.21212 17.9651 7.48272 18.0456 7.77043C18.1374 8.09838 18.137 8.4422 18.1366 8.82801C18.1365 8.85472 18.1365 8.88163 18.1365 8.90875V14.8115C18.1365 15.2387 18.1365 15.6085 18.1116 15.9134C18.0853 16.2355 18.0271 16.5578 17.8688 16.8684C17.6333 17.3306 17.2575 17.7064 16.7954 17.9418C16.4847 18.1001 16.1625 18.1584 15.8403 18.1847C15.5354 18.2096 15.1656 18.2096 14.7385 18.2096H5.26167C4.8345 18.2096 4.4647 18.2096 4.15985 18.1847C3.83769 18.1584 3.51541 18.1001 3.20478 17.9418C2.7426 17.7064 2.36685 17.3306 2.13136 16.8684C1.97308 16.5578 1.91484 16.2355 1.88852 15.9134C1.86362 15.6085 1.86363 15.2387 1.86364 14.8115L1.86364 8.90875C1.86364 8.88163 1.86361 8.85472 1.86358 8.82801C1.86315 8.4422 1.86276 8.09838 1.95456 7.77043C2.03509 7.48272 2.16744 7.21211 2.3451 6.97191C2.54761 6.6981 2.81925 6.48733 3.12405 6.25081C3.14515 6.23443 3.16642 6.21793 3.18782 6.20128L8.69136 1.92075C8.71105 1.90544 8.73129 1.88954 8.75208 1.87322C8.96808 1.70357 9.24317 1.48751 9.57021 1.39896ZM8.39904 16.5429H11.6011V11.3715C11.6011 11.1371 11.6005 11.0071 11.5934 10.9141C11.5005 10.9071 11.3705 10.9065 11.1361 10.9065H8.86404C8.62967 10.9065 8.49969 10.9071 8.40672 10.9141C8.39966 11.0071 8.39904 11.1371 8.39904 11.3715V16.5429ZM13.2678 16.5429L13.2678 11.3451C13.2678 11.1407 13.2678 10.9405 13.2539 10.7706C13.2387 10.5838 13.2026 10.3617 13.0885 10.1379C12.9308 9.82839 12.6792 9.57677 12.3697 9.41907C12.1459 9.30502 11.9238 9.26889 11.7369 9.25362C11.5671 9.23975 11.3668 9.23977 11.1624 9.2398H8.83772C8.63333 9.23977 8.43305 9.23975 8.26321 9.25362C8.07637 9.26889 7.85429 9.30502 7.63045 9.41907C7.32096 9.57676 7.06934 9.82839 6.91165 10.1379C6.79759 10.3617 6.76146 10.5838 6.7462 10.7706C6.73232 10.9405 6.73235 11.1407 6.73237 11.3451L6.73237 16.5429H5.29363C4.82543 16.5429 4.52439 16.5422 4.29557 16.5236C4.07648 16.5057 3.99795 16.4754 3.96143 16.4568C3.81286 16.3811 3.69207 16.2603 3.61637 16.1118C3.59776 16.0753 3.56756 15.9967 3.54965 15.7776C3.53096 15.5488 3.53031 15.2478 3.53031 14.7796V8.90875C3.53031 8.39549 3.53755 8.29823 3.55954 8.21968C3.58542 8.12719 3.62797 8.0402 3.68508 7.96299C3.73358 7.89741 3.80592 7.83198 4.21106 7.51687L9.71459 3.23634C9.86132 3.12222 9.94061 3.06115 10.0001 3.02088C10.0595 3.06115 10.1388 3.12222 10.2856 3.23634L15.7891 7.51687C16.1942 7.83198 16.2666 7.89741 16.3151 7.96299C16.3722 8.0402 16.4147 8.12719 16.4406 8.21968C16.4626 8.29823 16.4698 8.39549 16.4698 8.90875V14.7796C16.4698 15.2478 16.4692 15.5488 16.4505 15.7776C16.4326 15.9967 16.4024 16.0753 16.3838 16.1118C16.3081 16.2603 16.1873 16.3811 16.0387 16.4568C16.0022 16.4754 15.9237 16.5057 15.7046 16.5236C15.4758 16.5422 15.1747 16.5429 14.7065 16.5429H13.2678Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
@ -0,0 +1,3 @@
|
||||
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.5 3.33301C10.9603 3.33301 11.3334 3.7061 11.3334 4.16634V9.16634H16.3334C16.7936 9.16634 17.1667 9.53944 17.1667 9.99967C17.1667 10.4599 16.7936 10.833 16.3334 10.833H11.3334V15.833C11.3334 16.2932 10.9603 16.6663 10.5 16.6663C10.0398 16.6663 9.66671 16.2932 9.66671 15.833V10.833H4.66671C4.20647 10.833 3.83337 10.4599 3.83337 9.99967C3.83337 9.53944 4.20647 9.16634 4.66671 9.16634H9.66671V4.16634C9.66671 3.7061 10.0398 3.33301 10.5 3.33301Z" fill="#3370FF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 619 B |
@ -0,0 +1,12 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_19520_1493)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M6.99991 2.05339C4.26775 2.05339 2.0529 4.26824 2.0529 7.0004C2.0529 9.73256 4.26775 11.9474 6.99991 11.9474C9.73207 11.9474 11.9469 9.73256 11.9469 7.0004C11.9469 4.26824 9.73207 2.05339 6.99991 2.05339ZM0.88623 7.0004C0.88623 3.62391 3.62342 0.886719 6.99991 0.886719C10.3764 0.886719 13.1136 3.62391 13.1136 7.0004C13.1136 10.3769 10.3764 13.1141 6.99991 13.1141C3.62342 13.1141 0.88623 10.3769 0.88623 7.0004ZM6.41657 4.78826C6.41657 4.46609 6.67774 4.20493 6.99991 4.20493H7.00544C7.3276 4.20493 7.58877 4.46609 7.58877 4.78826C7.58877 5.11042 7.3276 5.37159 7.00544 5.37159H6.99991C6.67774 5.37159 6.41657 5.11042 6.41657 4.78826ZM6.99991 6.41706C7.32207 6.41706 7.58324 6.67823 7.58324 7.0004V9.21253C7.58324 9.5347 7.32207 9.79587 6.99991 9.79587C6.67774 9.79587 6.41657 9.5347 6.41657 9.21253V7.0004C6.41657 6.67823 6.67774 6.41706 6.99991 6.41706Z"
|
||||
fill="#3370FF" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_19520_1493">
|
||||
<rect width="14" height="14" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@ -0,0 +1,5 @@
|
||||
<svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M11.5538 3.5208C11.4875 3.5049 11.4085 3.50008 11.0059 3.50008H6V5.30008C6 5.52247 6.00058 5.64416 6.00773 5.7316C6.00801 5.73508 6.0083 5.73838 6.00859 5.74149C6.0117 5.74178 6.01499 5.74206 6.01848 5.74235C6.10592 5.74949 6.22761 5.75008 6.45 5.75008H11.55C11.7724 5.75008 11.8941 5.74949 11.9815 5.74235C11.985 5.74206 11.9883 5.74178 11.9914 5.74149C11.9917 5.73838 11.992 5.73508 11.9923 5.7316C11.9994 5.64416 12 5.52247 12 5.30008V3.81211C11.8588 3.67386 11.8143 3.63743 11.7706 3.6106C11.7035 3.56947 11.6303 3.53917 11.5538 3.5208ZM13.2802 2.96965L13.2333 2.92269C13.2184 2.90784 13.2037 2.8931 13.1891 2.87848C12.9735 2.66249 12.7835 2.47206 12.5543 2.33164C12.353 2.20827 12.1335 2.11736 11.9039 2.06224C11.6426 1.9995 11.3735 1.99975 11.0684 2.00004C11.0477 2.00006 11.0269 2.00008 11.0059 2.00008L5.81903 2.00008C5.63224 2.00007 5.45554 2.00007 5.28855 2.00105C5.27578 2.0004 5.26293 2.00008 5.25 2.00008C5.23345 2.00008 5.21702 2.00061 5.20074 2.00167C4.86462 2.00446 4.5692 2.01214 4.31113 2.03322C3.88956 2.06767 3.50203 2.14159 3.13803 2.32706C2.57354 2.61468 2.1146 3.07362 1.82698 3.6381C1.64151 4.00211 1.56759 4.38963 1.53315 4.81121C1.49998 5.2171 1.49999 5.71537 1.5 6.3191V12.681C1.49999 13.2848 1.49998 13.783 1.53315 14.1889C1.56759 14.6105 1.64151 14.998 1.82698 15.362C2.1146 15.9265 2.57354 16.3855 3.13803 16.6731C3.50203 16.8586 3.88956 16.9325 4.31113 16.9669C4.5692 16.988 4.86462 16.9957 5.20074 16.9985C5.21702 16.9995 5.23345 17.0001 5.25 17.0001C5.26293 17.0001 5.27578 16.9997 5.28855 16.9991C5.45553 17.0001 5.63222 17.0001 5.81901 17.0001H12.181C12.3678 17.0001 12.5445 17.0001 12.7115 16.9991C12.7242 16.9997 12.7371 17.0001 12.75 17.0001C12.7666 17.0001 12.783 16.9995 12.7993 16.9985C13.1354 16.9957 13.4308 16.988 13.6889 16.9669C14.1104 16.9325 14.498 16.8586 14.862 16.6731C15.4265 16.3855 15.8854 15.9265 16.173 15.362C16.3585 14.998 16.4324 14.6105 16.4669 14.1889C16.5 13.7831 16.5 13.2848 16.5 12.6811V7.49419C16.5 7.47319 16.5 7.45236 16.5 7.4317C16.5003 7.12653 16.5006 6.85748 16.4378 6.59614C16.3827 6.36656 16.2918 6.14709 16.1684 5.94577C16.028 5.71662 15.8376 5.52655 15.6216 5.31096C15.607 5.29637 15.5922 5.28165 15.5774 5.2668L13.2802 2.96965C13.2803 2.96971 13.2802 2.96958 13.2802 2.96965ZM13.5 5.31074V5.32399C13.5 5.51328 13.5 5.69763 13.4873 5.85375C13.4733 6.02519 13.4402 6.22749 13.3365 6.43106C13.1927 6.71331 12.9632 6.94278 12.681 7.08659C12.4774 7.19031 12.2751 7.22336 12.1037 7.23737C11.9476 7.25012 11.7632 7.2501 11.5739 7.25008L6.45 7.25008C6.44202 7.25008 6.43404 7.25008 6.42608 7.25008C6.23679 7.2501 6.05245 7.25012 5.89633 7.23737C5.72488 7.22336 5.52258 7.19031 5.31902 7.08659C5.03677 6.94278 4.8073 6.7133 4.66349 6.43106C4.55977 6.22749 4.52672 6.02519 4.51271 5.85375C4.49995 5.69763 4.49998 5.51329 4.5 5.324C4.5 5.31603 4.5 5.30806 4.5 5.30008V3.52322C4.47733 3.52479 4.4551 3.52646 4.43328 3.52824C4.10447 3.5551 3.93631 3.6038 3.81902 3.66357C3.53677 3.80738 3.3073 4.03685 3.16349 4.31909C3.10372 4.43639 3.05503 4.60454 3.02816 4.93335C3.00058 5.27092 3 5.70764 3 6.35008V12.6501C3 13.2925 3.00058 13.7292 3.02816 14.0668C3.05503 14.3956 3.10372 14.5638 3.16349 14.6811C3.3073 14.9633 3.53677 15.1928 3.81902 15.3366C3.93631 15.3964 4.10447 15.445 4.43328 15.4719C4.4551 15.4737 4.47733 15.4754 4.5 15.4769L4.5 11.4262C4.49998 11.2369 4.49995 11.0525 4.51271 10.8964C4.52672 10.725 4.55977 10.5227 4.66349 10.3191C4.8073 10.0368 5.03677 9.80738 5.31902 9.66357C5.52258 9.55984 5.72488 9.52679 5.89633 9.51278C6.05245 9.50003 6.2368 9.50005 6.42609 9.50007H11.5739C11.7632 9.50005 11.9475 9.50003 12.1037 9.51278C12.2751 9.52679 12.4774 9.55984 12.681 9.66357C12.9632 9.80738 13.1927 10.0368 13.3365 10.3191C13.4402 10.5227 13.4733 10.725 13.4873 10.8964C13.5 11.0525 13.5 11.2369 13.5 11.4262L13.5 15.4769C13.5227 15.4754 13.5449 15.4737 13.5667 15.4719C13.8955 15.445 14.0637 15.3964 14.181 15.3366C14.4632 15.1928 14.6927 14.9633 14.8365 14.6811C14.8963 14.5638 14.945 14.3956 14.9718 14.0668C14.9994 13.7292 15 13.2925 15 12.6501V7.49419C15 7.0916 14.9952 7.01255 14.9793 6.94631C14.9609 6.86979 14.9306 6.79663 14.8895 6.72952C14.8539 6.67144 14.8014 6.61213 14.5167 6.32746L13.5 5.31074ZM12 15.5001V11.4501C12 11.2277 11.9994 11.106 11.9923 11.0186C11.992 11.0151 11.9917 11.0118 11.9914 11.0087C11.9883 11.0084 11.985 11.0081 11.9815 11.0078C11.8941 11.0007 11.7724 11.0001 11.55 11.0001H6.45C6.22761 11.0001 6.10592 11.0007 6.01848 11.0078C6.01499 11.0081 6.0117 11.0084 6.00859 11.0087C6.0083 11.0118 6.00801 11.0151 6.00773 11.0186C6.00058 11.106 6 11.2277 6 11.4501V15.5001H12Z"
|
||||
fill="#3370FF" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.7 KiB |
@ -0,0 +1,5 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M6.72916 1.85097L6.69716 1.85085C5.98981 1.84827 5.41301 1.84617 4.94539 1.88309C4.46228 1.92122 4.0275 2.00464 3.62407 2.21292C3.01781 2.52591 2.5263 3.01564 2.21111 3.62075C2.00154 4.02311 1.91644 4.45656 1.8765 4.93842C1.83788 5.40451 1.83788 5.97916 1.83789 6.68347V11.3274C1.83788 12.0263 1.83788 12.5969 1.87635 13.0602C1.91616 13.5396 2.001 13.9708 2.20943 14.3721C2.52252 14.9748 3.01395 15.4662 3.61667 15.7793C4.01791 15.9877 4.44916 16.0726 4.92851 16.1124C5.39183 16.1509 5.96247 16.1508 6.66137 16.1508H11.3384C12.0373 16.1508 12.6079 16.1509 13.0712 16.1124C13.5506 16.0726 13.9818 15.9877 14.3831 15.7793C14.9858 15.4662 15.4772 14.9748 15.7903 14.3721C15.9987 13.9708 16.0836 13.5396 16.1234 13.0602C16.1619 12.5969 16.1619 12.0263 16.1619 11.3274V9.50554C16.1619 9.09133 15.8261 8.75554 15.4119 8.75554C14.9976 8.75554 14.6619 9.09133 14.6619 9.50554V11.2954C14.6619 12.0341 14.6613 12.5422 14.6285 12.9361C14.5966 13.321 14.5379 13.5291 14.4592 13.6806C14.2884 14.0094 14.0204 14.2774 13.6916 14.4482C13.5402 14.5269 13.332 14.5856 12.9471 14.6175C12.5532 14.6502 12.045 14.6508 11.3064 14.6508H6.69339C5.9547 14.6508 5.44655 14.6502 5.05265 14.6175C4.66773 14.5856 4.45958 14.5269 4.30814 14.4482C3.97938 14.2774 3.71132 14.0094 3.54055 13.6806C3.46188 13.5292 3.40317 13.321 3.37121 12.9361C3.33849 12.5422 3.33789 12.034 3.33789 11.2953V6.71554C3.33789 5.97142 3.33849 5.45912 3.37138 5.06231C3.40354 4.67419 3.46264 4.46502 3.54147 4.31369C3.7137 3.98302 3.98088 3.71681 4.31217 3.54578C4.46347 3.46767 4.67333 3.40923 5.06343 3.37843C5.46198 3.34697 5.97664 3.34825 6.72372 3.35096C7.34592 3.35322 7.97166 3.35468 8.54751 3.35468C8.96173 3.35468 9.29751 3.0189 9.29751 2.60468C9.29751 2.19047 8.96173 1.85468 8.54751 1.85468C7.97404 1.85468 7.35021 1.85322 6.72916 1.85097ZM8.26254 8.66553C7.96964 8.95843 7.96964 9.4333 8.26254 9.7262C8.55543 10.0191 9.0303 10.0191 9.3232 9.72619L14.6619 4.38754V6.5054C14.6619 6.91962 14.9976 7.2554 15.4119 7.2554C15.8261 7.2554 16.1619 6.91962 16.1619 6.5054V2.6698C16.1622 2.66102 16.1623 2.6522 16.1623 2.64334C16.1623 2.22913 15.8265 1.89334 15.4123 1.89334C15.4122 1.89334 15.4124 1.89334 15.4123 1.89334H11.5503C11.136 1.89334 10.8003 2.22913 10.8003 2.64334C10.8003 3.05756 11.136 3.39334 11.5503 3.39334H13.5347L8.26254 8.66553Z"
|
||||
fill="white" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M5.47674 0.0561965L5.44118 0.0560671C4.65524 0.053204 4.01435 0.0508692 3.49477 0.0918841C2.95798 0.134258 2.47489 0.226947 2.02664 0.458364C1.35301 0.806127 0.806891 1.35027 0.456684 2.02263C0.223824 2.46969 0.129267 2.9513 0.0848958 3.48669C0.0419755 4.00458 0.0419831 4.64308 0.0419925 5.42564V10.5855C0.041983 11.3621 0.0419753 11.9961 0.0847264 12.5109C0.128958 13.0435 0.223226 13.5227 0.454811 13.9685C0.802688 14.6382 1.34873 15.1842 2.01842 15.5321C2.46424 15.7637 2.9434 15.858 3.47601 15.9022C3.99081 15.945 4.62486 15.945 5.40142 15.9449H10.5981C11.3746 15.945 12.0087 15.945 12.5235 15.9022C13.0561 15.858 13.5353 15.7637 13.9811 15.5321C14.6508 15.1842 15.1968 14.6382 15.5447 13.9685C15.7763 13.5227 15.8705 13.0435 15.9148 12.5109C15.9575 11.9962 15.9575 11.3621 15.9575 10.5856V8.56128C15.9575 8.10104 15.5844 7.72795 15.1242 7.72795C14.6639 7.72795 14.2908 8.10104 14.2908 8.56128V10.55C14.2908 11.3708 14.2902 11.9354 14.2538 12.373C14.2183 12.8007 14.1531 13.032 14.0657 13.2002C13.8759 13.5655 13.5781 13.8634 13.2128 14.0531C13.0445 14.1405 12.8132 14.2057 12.3856 14.2413C11.9479 14.2776 11.3833 14.2783 10.5625 14.2783H5.43699C4.61622 14.2783 4.05161 14.2776 3.61395 14.2413C3.18626 14.2057 2.95498 14.1405 2.78671 14.0531C2.42142 13.8634 2.12358 13.5655 1.93383 13.2002C1.84642 13.032 1.78119 12.8007 1.74568 12.373C1.70933 11.9353 1.70866 11.3707 1.70866 10.5499V5.46128C1.70866 4.63448 1.70933 4.06526 1.74587 3.62435C1.78161 3.19311 1.84727 2.9607 1.93485 2.79256C2.12623 2.42514 2.42309 2.12936 2.79119 1.93932C2.95931 1.85253 3.19248 1.7876 3.62592 1.75338C4.06876 1.71843 4.6406 1.71984 5.47069 1.72285C6.16203 1.72536 6.85729 1.72699 7.49713 1.72699C7.95737 1.72699 8.33046 1.3539 8.33046 0.893659C8.33046 0.433422 7.95737 0.0603261 7.49713 0.0603261C6.85993 0.0603261 6.1668 0.0587019 5.47674 0.0561965ZM7.18049 7.62794C6.85505 7.95338 6.85505 8.48101 7.18049 8.80645C7.50592 9.13189 8.03356 9.13189 8.359 8.80645L14.2908 2.87461V5.22779C14.2908 5.68803 14.6639 6.06113 15.1242 6.06113C15.5844 6.06113 15.9575 5.68803 15.9575 5.22779V0.966006C15.9578 0.95625 15.958 0.946452 15.958 0.936614C15.958 0.476377 15.5849 0.103281 15.1247 0.103281C15.1246 0.103281 15.1248 0.103281 15.1247 0.103281H10.8335C10.3733 0.103281 10.0002 0.476377 10.0002 0.936614C10.0002 1.39685 10.3733 1.76995 10.8335 1.76995H13.0385L7.18049 7.62794Z"
|
||||
fill="#3370FF" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
@ -0,0 +1,7 @@
|
||||
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="icon/line/export">
|
||||
<path id="Icon (Stroke)" fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M11.2928 2.58684C11.6834 2.19631 12.3165 2.19631 12.707 2.58684L16.707 6.58684C17.0976 6.97736 17.0976 7.61053 16.707 8.00105C16.3165 8.39158 15.6834 8.39158 15.2928 8.00105L12.9999 5.70816V15.2939C12.9999 15.8462 12.5522 16.2939 11.9999 16.2939C11.4477 16.2939 10.9999 15.8462 10.9999 15.2939V5.70816L8.70705 8.00105C8.31652 8.39158 7.68336 8.39158 7.29283 8.00105C6.90231 7.61053 6.90231 6.97736 7.29283 6.58684L11.2928 2.58684ZM2.99994 11.2939C3.55222 11.2939 3.99994 11.7417 3.99994 12.2939V16.4939C3.99994 17.3505 4.00072 17.9328 4.03749 18.3829C4.07331 18.8213 4.13824 19.0455 4.21793 19.2019C4.40967 19.5783 4.71564 19.8842 5.09196 20.076C5.24836 20.1556 5.47256 20.2206 5.91098 20.2564C6.36107 20.2932 6.94336 20.2939 7.79994 20.2939H16.1999C17.0565 20.2939 17.6388 20.2932 18.0889 20.2564C18.5273 20.2206 18.7515 20.1556 18.9079 20.076C19.2842 19.8842 19.5902 19.5783 19.782 19.2019C19.8616 19.0455 19.9266 18.8213 19.9624 18.3829C19.9992 17.9328 19.9999 17.3505 19.9999 16.4939V12.2939C19.9999 11.7417 20.4477 11.2939 20.9999 11.2939C21.5522 11.2939 21.9999 11.7417 21.9999 12.2939V16.5353C22 17.3402 22 18.0046 21.9557 18.5458C21.9098 19.1079 21.8113 19.6246 21.564 20.1099C21.1805 20.8626 20.5686 21.4745 19.8159 21.858C19.3306 22.1053 18.8139 22.2038 18.2518 22.2498C17.7106 22.294 17.0462 22.294 16.2413 22.2939H7.75862C6.95366 22.294 6.2893 22.294 5.74811 22.2498C5.18602 22.2038 4.66931 22.1053 4.18398 21.858C3.43133 21.4745 2.81941 20.8626 2.43591 20.1099C2.18862 19.6246 2.09006 19.1079 2.04413 18.5458C1.99992 18.0046 1.99993 17.3402 1.99994 16.5352L1.99994 12.2939C1.99994 11.7417 2.44766 11.2939 2.99994 11.2939Z"
|
||||
fill="#3370FF" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
289
packages/web/components/common/MySelect/GateSelect.tsx
Normal file
@ -0,0 +1,289 @@
|
||||
import type { ForwardedRef } from 'react';
|
||||
import React, {
|
||||
useRef,
|
||||
forwardRef,
|
||||
useMemo,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useState
|
||||
} from 'react';
|
||||
import {
|
||||
Menu,
|
||||
MenuList,
|
||||
MenuItem,
|
||||
Button,
|
||||
useDisclosure,
|
||||
MenuButton,
|
||||
Box,
|
||||
Flex,
|
||||
Input,
|
||||
Tag,
|
||||
HStack
|
||||
} from '@chakra-ui/react';
|
||||
import type { ButtonProps, MenuItemProps } from '@chakra-ui/react';
|
||||
import MyIcon from '../Icon';
|
||||
import { useRequest2 } from '../../../hooks/useRequest';
|
||||
import MyDivider from '../MyDivider';
|
||||
import type { useScrollPagination } from '../../../hooks/useScrollPagination';
|
||||
import Avatar from '../Avatar';
|
||||
|
||||
/** 选择组件 Props 类型
|
||||
* value: 选中的值
|
||||
* placeholder: 占位符
|
||||
* list: 列表数据
|
||||
* isLoading: 是否加载中
|
||||
* ScrollData: 分页滚动数据控制器 [useScrollPagination] 的返回值
|
||||
* */
|
||||
export type GateSelectProps<T = any> = Omit<ButtonProps, 'onChange'> & {
|
||||
value?: T;
|
||||
placeholder?: string;
|
||||
isSearch?: boolean;
|
||||
list: {
|
||||
alias?: string;
|
||||
icon?: string;
|
||||
iconSize?: string;
|
||||
label: string | React.ReactNode;
|
||||
description?: string;
|
||||
value: T;
|
||||
showBorder?: boolean;
|
||||
tagColor?: string;
|
||||
tagText?: string;
|
||||
}[];
|
||||
isLoading?: boolean;
|
||||
onChange?: (val: T) => any | Promise<any>;
|
||||
ScrollData?: ReturnType<typeof useScrollPagination>['ScrollData'];
|
||||
};
|
||||
|
||||
const GateSelect = <T = any,>(
|
||||
{
|
||||
placeholder,
|
||||
value,
|
||||
isSearch = false,
|
||||
width = '100%',
|
||||
list = [],
|
||||
onChange,
|
||||
isLoading = false,
|
||||
ScrollData,
|
||||
...props
|
||||
}: GateSelectProps<T>,
|
||||
ref: ForwardedRef<{
|
||||
focus: () => void;
|
||||
}>
|
||||
) => {
|
||||
const ButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const MenuListRef = useRef<HTMLDivElement>(null);
|
||||
const SelectedItemRef = useRef<HTMLDivElement>(null);
|
||||
const SearchInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const menuItemStyles: MenuItemProps = {
|
||||
borderRadius: 'sm',
|
||||
py: 2,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
_hover: {
|
||||
backgroundColor: 'myGray.100'
|
||||
},
|
||||
_notLast: {
|
||||
mb: 1
|
||||
}
|
||||
};
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const selectItem = useMemo(() => list.find((item) => item.value === value), [list, value]);
|
||||
|
||||
const [search, setSearch] = useState('');
|
||||
const filterList = useMemo(() => {
|
||||
if (!isSearch || !search) {
|
||||
return list;
|
||||
}
|
||||
return list.filter((item) => {
|
||||
const text = `${item.label?.toString()}${item.alias}${item.value}`;
|
||||
const regx = new RegExp(search, 'gi');
|
||||
return regx.test(text);
|
||||
});
|
||||
}, [list, search, isSearch]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus() {
|
||||
onOpen();
|
||||
}
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && MenuListRef.current && SelectedItemRef.current) {
|
||||
const menu = MenuListRef.current;
|
||||
const selectedItem = SelectedItemRef.current;
|
||||
menu.scrollTop = selectedItem.offsetTop - menu.offsetTop - 100;
|
||||
|
||||
if (isSearch) {
|
||||
setSearch('');
|
||||
}
|
||||
}
|
||||
}, [isSearch, isOpen]);
|
||||
|
||||
const { runAsync: onclickChange, loading } = useRequest2((val: T) => onChange?.(val));
|
||||
|
||||
const ListRender = useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
{filterList.map((item, i) => (
|
||||
<Box key={i}>
|
||||
<MenuItem
|
||||
{...menuItemStyles}
|
||||
{...(value === item.value
|
||||
? {
|
||||
ref: SelectedItemRef,
|
||||
color: 'primary.700',
|
||||
bg: 'myGray.100',
|
||||
fontWeight: '600'
|
||||
}
|
||||
: {
|
||||
color: 'myGray.900'
|
||||
})}
|
||||
onClick={() => {
|
||||
if (value !== item.value) {
|
||||
onclickChange(item.value);
|
||||
}
|
||||
}}
|
||||
whiteSpace={'pre-wrap'}
|
||||
fontSize={'sm'}
|
||||
display={'block'}
|
||||
mb={0.5}
|
||||
>
|
||||
<Flex alignItems={'center'} justifyContent="space-between" width="100%">
|
||||
<Flex alignItems={'center'}>
|
||||
{item.icon && (
|
||||
<Avatar mr={2} src={item.icon as any} w={item.iconSize ?? '1rem'} />
|
||||
)}
|
||||
{item.label}
|
||||
</Flex>
|
||||
{item.tagText && (
|
||||
<Tag size="sm" colorScheme={item.tagColor || 'gray'} ml={2}>
|
||||
{item.tagText}
|
||||
</Tag>
|
||||
)}
|
||||
</Flex>
|
||||
{item.description && (
|
||||
<Box color={'myGray.500'} fontSize={'xs'}>
|
||||
{item.description}
|
||||
</Box>
|
||||
)}
|
||||
</MenuItem>
|
||||
{item.showBorder && <MyDivider my={2} />}
|
||||
</Box>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}, [filterList, value]);
|
||||
|
||||
const isSelecting = loading || isLoading;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Menu
|
||||
autoSelect={false}
|
||||
isOpen={isOpen && !isSelecting}
|
||||
onOpen={onOpen}
|
||||
onClose={onClose}
|
||||
strategy={'fixed'}
|
||||
>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
ref={ButtonRef}
|
||||
width={width}
|
||||
px={3}
|
||||
rightIcon={<MyIcon name={'core/chat/chevronDown'} w={4} color={'myGray.500'} />}
|
||||
variant={'whitePrimaryOutline'}
|
||||
size={'md'}
|
||||
fontSize={'sm'}
|
||||
textAlign={'left'}
|
||||
h={'auto'}
|
||||
whiteSpace={'pre-wrap'}
|
||||
wordBreak={'break-word'}
|
||||
_active={{
|
||||
transform: 'none'
|
||||
}}
|
||||
_hover={{
|
||||
borderRadius: '10px',
|
||||
border: '0.5px solid var(--Gray-Iron-250, #E0E0E0)',
|
||||
background: 'var(--Gray-Iron-150, #F3F3F3)'
|
||||
}}
|
||||
{...(isOpen
|
||||
? {
|
||||
borderColor: 'primary.600',
|
||||
color: 'primary.700'
|
||||
}
|
||||
: {})}
|
||||
{...props}
|
||||
>
|
||||
<Flex alignItems={'center'} justifyContent="space-between" width="100%">
|
||||
<Flex alignItems={'center'}>
|
||||
{isSelecting && <MyIcon mr={2} name={'common/loading'} w={'1rem'} />}
|
||||
{isSearch && isOpen ? (
|
||||
<Input
|
||||
ref={SearchInputRef}
|
||||
autoFocus
|
||||
variant={'unstyled'}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder={
|
||||
selectItem?.alias ||
|
||||
(typeof selectItem?.label === 'string' ? selectItem?.label : placeholder)
|
||||
}
|
||||
size={'sm'}
|
||||
w={'100%'}
|
||||
color={'myGray.700'}
|
||||
onBlur={() => {
|
||||
setTimeout(() => {
|
||||
SearchInputRef?.current?.focus();
|
||||
}, 0);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Flex alignItems="center">
|
||||
{selectItem?.icon && (
|
||||
<Avatar mr={2} src={selectItem.icon as any} w={selectItem.iconSize ?? '1rem'} />
|
||||
)}
|
||||
{selectItem?.alias || selectItem?.label || placeholder}
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
{selectItem?.tagText && (
|
||||
<Tag size="sm" colorScheme={selectItem.tagColor || 'gray'} ml={2}>
|
||||
{selectItem.tagText}
|
||||
</Tag>
|
||||
)}
|
||||
</Flex>
|
||||
</MenuButton>
|
||||
|
||||
<MenuList
|
||||
ref={MenuListRef}
|
||||
className={props.className}
|
||||
w={(() => {
|
||||
const w = ButtonRef.current?.clientWidth;
|
||||
if (w) {
|
||||
return `${w}px !important`;
|
||||
}
|
||||
return Array.isArray(width)
|
||||
? width.map((item) => `${item} !important`)
|
||||
: `${width} !important`;
|
||||
})()}
|
||||
px={'6px'}
|
||||
py={'6px'}
|
||||
border={'1px solid #fff'}
|
||||
boxShadow={
|
||||
'0px 2px 4px rgba(161, 167, 179, 0.25), 0px 0px 1px rgba(121, 141, 159, 0.25);'
|
||||
}
|
||||
zIndex={99}
|
||||
maxH={'40vh'}
|
||||
overflowY={'auto'}
|
||||
>
|
||||
{ScrollData ? <ScrollData>{ListRender}</ScrollData> : ListRender}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(GateSelect) as <T>(
|
||||
props: GateSelectProps<T> & { ref?: React.Ref<HTMLSelectElement> }
|
||||
) => JSX.Element;
|
||||
@ -50,6 +50,7 @@ export type SelectProps<T = any> = Omit<ButtonProps, 'onChange'> & {
|
||||
showBorder?: boolean;
|
||||
}[];
|
||||
isLoading?: boolean;
|
||||
showAvatar?: boolean;
|
||||
onChange?: (val: T) => any | Promise<any>;
|
||||
ScrollData?: ReturnType<typeof useScrollPagination>['ScrollData'];
|
||||
customOnOpen?: () => void;
|
||||
@ -79,6 +80,7 @@ const MySelect = <T = any,>(
|
||||
list = [],
|
||||
onChange,
|
||||
isLoading = false,
|
||||
showAvatar = true,
|
||||
ScrollData,
|
||||
customOnOpen,
|
||||
customOnClose,
|
||||
@ -255,7 +257,7 @@ const MySelect = <T = any,>(
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{selectItem?.icon && (
|
||||
{selectItem?.icon && showAvatar && (
|
||||
<Avatar
|
||||
mr={2}
|
||||
src={selectItem.icon as any}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { forwardRef } from 'react';
|
||||
import React, { forwardRef, useRef, useState, useEffect } from 'react';
|
||||
import { Flex, Box, type BoxProps, HStack } from '@chakra-ui/react';
|
||||
import MyIcon from '../Icon';
|
||||
|
||||
@ -26,8 +26,41 @@ const FillRowTabs = ({
|
||||
iconGap = 2,
|
||||
...props
|
||||
}: Props) => {
|
||||
const tabsRef = useRef<HTMLDivElement>(null);
|
||||
const itemsRef = useRef<Map<any, HTMLDivElement>>(new Map());
|
||||
const [sliderStyle, setSliderStyle] = useState({
|
||||
width: 0,
|
||||
left: 0,
|
||||
opacity: 0
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const updateSlider = () => {
|
||||
const activeItem = itemsRef.current.get(value);
|
||||
if (activeItem && tabsRef.current) {
|
||||
const tabsRect = tabsRef.current.getBoundingClientRect();
|
||||
const itemRect = activeItem.getBoundingClientRect();
|
||||
|
||||
setSliderStyle({
|
||||
width: itemRect.width,
|
||||
left: itemRect.left - tabsRect.left,
|
||||
opacity: 1
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
updateSlider();
|
||||
window.addEventListener('resize', updateSlider);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', updateSlider);
|
||||
};
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={tabsRef}
|
||||
position="relative"
|
||||
display={'inline-flex'}
|
||||
px={'3px'}
|
||||
py={'3px'}
|
||||
@ -40,9 +73,29 @@ const FillRowTabs = ({
|
||||
fontWeight={'medium'}
|
||||
{...props}
|
||||
>
|
||||
{/* 滑动背景元素 */}
|
||||
<Box
|
||||
position="absolute"
|
||||
height="calc(100% - 6px)"
|
||||
top="3px"
|
||||
borderRadius={'xs'}
|
||||
bg="white"
|
||||
boxShadow="1.5"
|
||||
transition="all 0.14s ease-in-out"
|
||||
pointerEvents="none"
|
||||
style={{
|
||||
width: `${sliderStyle.width}px`,
|
||||
left: `${sliderStyle.left}px`,
|
||||
opacity: sliderStyle.opacity
|
||||
}}
|
||||
/>
|
||||
|
||||
{list.map((item) => (
|
||||
<HStack
|
||||
key={item.value}
|
||||
ref={(el) => {
|
||||
if (el) itemsRef.current.set(item.value, el);
|
||||
}}
|
||||
flex={'1 0 0'}
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
@ -53,19 +106,14 @@ const FillRowTabs = ({
|
||||
userSelect={'none'}
|
||||
whiteSpace={'noWrap'}
|
||||
gap={iconGap}
|
||||
{...(value === item.value
|
||||
? {
|
||||
bg: 'white',
|
||||
boxShadow: '1.5',
|
||||
zIndex={1}
|
||||
position="relative"
|
||||
transition="color 0.25s ease"
|
||||
onClick={() => onChange(item.value)}
|
||||
color={value === item.value ? 'primary.600' : 'myGray.500'}
|
||||
_hover={{
|
||||
color: 'primary.600'
|
||||
}
|
||||
: {
|
||||
color: 'myGray.500',
|
||||
_hover: {
|
||||
color: 'primary.600'
|
||||
},
|
||||
onClick: () => onChange(item.value)
|
||||
})}
|
||||
}}
|
||||
>
|
||||
{item.icon && <MyIcon name={item.icon as any} w={iconSize} />}
|
||||
<Box fontSize={labelSize}>{item.label}</Box>
|
||||
|
||||
@ -4,6 +4,9 @@
|
||||
"api_key": "API key",
|
||||
"bills_and_invoices": "Bills",
|
||||
"channel": "Channel",
|
||||
"config_app": "Featured Applications",
|
||||
"config_copyright": "Application configuration",
|
||||
"config_home": "Home page configuration",
|
||||
"config_model": "Model configuration",
|
||||
"confirm_logout": "Confirm to log out?",
|
||||
"create_channel": "Add new channel",
|
||||
@ -11,7 +14,12 @@
|
||||
"custom_model": "custom model",
|
||||
"default_model": "Default model",
|
||||
"default_model_config": "Default model configuration",
|
||||
"gateway.cname_tip": "Please go to your domain name service provider, such as adding the domain name, and parsing the CNAME to Ixjgiwggswmb.sealoshzh.site. After the resolution takes effect, you can bind the custom domain name.",
|
||||
"gateway.save_config": "save",
|
||||
"gateway.share": "share",
|
||||
"gateways": "Gate Management",
|
||||
"logout": "Sign out",
|
||||
"logs": "Homepage log",
|
||||
"model.active": "Active",
|
||||
"model.alias": "Alias",
|
||||
"model.alias_tip": "The name of the model displayed in the system is convenient for users to understand.",
|
||||
|
||||
31
packages/web/i18n/en/account_gate.json
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"Gate": "Gate",
|
||||
"Gate List": "Gate List",
|
||||
"Gate app avatar updated": "Gate app icon update",
|
||||
"Gate app created successfully": "The gate application was created successfully",
|
||||
"No Gates Available": "No Gates Available",
|
||||
"Operation failed": "Operation failed",
|
||||
"available_tools": "Available tools",
|
||||
"confirm_delete_gate": "Confirm deletion of the gate",
|
||||
"deep_thinking": "Deep thinking",
|
||||
"delete_gate": "Delete the gate",
|
||||
"dialog_prompt_text": "Dialog prompt text",
|
||||
"disabled": "closure",
|
||||
"enabled": "Enable",
|
||||
"example": "Schematic diagram",
|
||||
"file_upload": "File upload",
|
||||
"gate_list": "Portal list",
|
||||
"gate_logo": "LOGO preview",
|
||||
"image_upload": "Image upload",
|
||||
"no_gate_available": "No portal available",
|
||||
"no_gate_to_delete": "There is no gate to delete",
|
||||
"quick_app": "Quick Application",
|
||||
"slogan": "slogan",
|
||||
"status": "state",
|
||||
"suggestion_ratio_1_1": "Suggested ratio 1:1",
|
||||
"suggestion_ratio_4_1": "Suggested ratio 4:1",
|
||||
"team_name": "Team name",
|
||||
"upload": "Upload",
|
||||
"voice_input": "Voice input",
|
||||
"web_search": "Search online"
|
||||
}
|
||||
@ -48,6 +48,7 @@
|
||||
"create_by_template": "By template",
|
||||
"create_copy_success": "Duplicate Created Successfully",
|
||||
"create_empty_app": "Create Default App",
|
||||
"create_empty_gate": "Create a blank gate",
|
||||
"create_empty_plugin": "Create Default Plugin",
|
||||
"create_empty_workflow": "Create Default Workflow",
|
||||
"cron.every_day": "Run Daily",
|
||||
@ -123,6 +124,8 @@
|
||||
"permission.des.manage": "Based on write permissions, you can configure publishing channels, view conversation logs, and assign permissions to the application.",
|
||||
"permission.des.read": "Use the app to have conversations",
|
||||
"permission.des.write": "Can view and edit apps",
|
||||
"permission.des.log": "Can view conversation logs",
|
||||
"permission.name.log": "View logs",
|
||||
"plugin.Instructions": "Instructions",
|
||||
"plugin_cost_by_token": "Charged based on token usage",
|
||||
"plugin_cost_per_times": "{{cost}} points/time",
|
||||
@ -148,6 +151,7 @@
|
||||
"team_tags_set": "Team tags",
|
||||
"temperature": "Temperature",
|
||||
"temperature_tip": "Range 0~10. \nThe larger the value, the more divergent the model’s answer is; the smaller the value, the more rigorous the answer.",
|
||||
"template.gate": "Gate",
|
||||
"template.hard_strict": "Strict Q&A template",
|
||||
"template.hard_strict_des": "Based on the question and answer template, stricter requirements are imposed on the model's answers.",
|
||||
"template.qa_template": "Q&A template",
|
||||
@ -181,6 +185,8 @@
|
||||
"tts_browser": "Browser's own (free)",
|
||||
"tts_close": "Close",
|
||||
"type.All": "All",
|
||||
"type.Create gate": "Create a gate",
|
||||
"type.Create gate tip": "The gate should not be created here",
|
||||
"type.Create http plugin tip": "Batch create plugins through OpenAPI Schema, compatible with GPTs format.",
|
||||
"type.Create mcp tools tip": "Automatically parse and batch create callable MCP tools by entering the MCP address",
|
||||
"type.Create one plugin tip": "Customizable input and output workflows, usually used to encapsulate reusable workflows.",
|
||||
@ -189,6 +195,7 @@
|
||||
"type.Create simple bot tip": "Create a simple AI app by filling out a form, suitable for beginners.",
|
||||
"type.Create workflow bot": "Create Workflow",
|
||||
"type.Create workflow tip": "Build complex multi-turn dialogue AI applications through low-code methods, recommended for advanced users.",
|
||||
"type.Gate": "Gate",
|
||||
"type.Http plugin": "HTTP Plugin",
|
||||
"type.Import from json": "Import JSON",
|
||||
"type.Import from json tip": "Create applications directly through JSON configuration files",
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
"chat.quote.No Data": "The file cannot be found",
|
||||
"chat.quote.deleted": "This data has been deleted ~",
|
||||
"chat.waiting_for_response": "Please wait for the conversation to complete",
|
||||
"chat_gate_app": "Portal homepage",
|
||||
"chat_history": "Conversation History",
|
||||
"chat_input_guide_lexicon_is_empty": "Lexicon not configured yet",
|
||||
"chat_test_app": "Debug-{{name}}",
|
||||
|
||||
@ -13,8 +13,10 @@
|
||||
"Confirm": "Confirm",
|
||||
"Continue_Adding": "Continue adding",
|
||||
"Copy": "Copy",
|
||||
"Create Success": "Created successfully",
|
||||
"Creating": "Creating",
|
||||
"Delete": "Delete",
|
||||
"Delete Success": "Delete successfully",
|
||||
"Detail": "Detail",
|
||||
"Documents": "Documents",
|
||||
"Done": "Done",
|
||||
@ -44,12 +46,14 @@
|
||||
"Folder": "Folder",
|
||||
"FullScreen": "FullScreen",
|
||||
"FullScreenLight": "FullScreenLight",
|
||||
"Gate.service.is.unavailable": "The Gate is not available",
|
||||
"Import": "Import",
|
||||
"Input": "Input",
|
||||
"Instructions": "Instruction",
|
||||
"Intro": "Introduction",
|
||||
"Loading": "Loading...",
|
||||
"Login": "Login",
|
||||
"Manage tags": "Management Tags",
|
||||
"More": "More",
|
||||
"Move": "Move",
|
||||
"Name": "Name",
|
||||
@ -74,22 +78,29 @@
|
||||
"Run": "Run",
|
||||
"Running": "Running",
|
||||
"Save": "Save",
|
||||
"Save Failed": "Saving failed",
|
||||
"Save Success": "Save successfully",
|
||||
"Save_and_exit": "Save and Exit",
|
||||
"Search": "Search",
|
||||
"Select tags": "Select a tag",
|
||||
"Select_all": "Select all",
|
||||
"Setting": "Setting",
|
||||
"Status": "Status",
|
||||
"Submit": "Submit",
|
||||
"Success": "Success",
|
||||
"Tag already added": "The tag has been added",
|
||||
"Tags": "Label",
|
||||
"Team": "Team",
|
||||
"UnKnow": "Unknown",
|
||||
"Unlimited": "Unlimited",
|
||||
"Update": "Update",
|
||||
"Update Success": "Update successfully",
|
||||
"Username": "Username",
|
||||
"Waiting": "Waiting",
|
||||
"Warning": "Warning",
|
||||
"Website": "Website",
|
||||
"action_confirm": "Confirm",
|
||||
"add_app": "Added apps",
|
||||
"add_new": "add_new",
|
||||
"add_new_param": "Add new param",
|
||||
"add_success": "Added Successfully",
|
||||
@ -135,10 +146,14 @@
|
||||
"code_error.error_code.504": "Gateway Timeout",
|
||||
"code_error.error_code[429]": "Requests are too frequent",
|
||||
"code_error.error_message.403": "Credential Error",
|
||||
"code_error.error_message.405": "methodNotAllowed",
|
||||
"code_error.error_message.510": "Insufficient Account Balance",
|
||||
"code_error.error_message.511": "Unauthorized to Operate This Model",
|
||||
"code_error.error_message.513": "Unauthorized to Read This File",
|
||||
"code_error.error_message.514": "Invalid API Key",
|
||||
"code_error.error_message[405]": "Method not allowed",
|
||||
"code_error.error_message[422]": "Params illegal",
|
||||
"code_error.error_message[500]": "System Error",
|
||||
"code_error.openapi_error.api_key_not_exist": "API Key Does Not Exist",
|
||||
"code_error.openapi_error.exceed_limit": "Up to 10 API Keys",
|
||||
"code_error.openapi_error.un_auth": "Unauthorized to Operate This API Key",
|
||||
@ -829,10 +844,12 @@
|
||||
"folder.open_dataset": "Open Dataset",
|
||||
"folder_description": "Folder Description",
|
||||
"free": "Free",
|
||||
"gate.placeholder": "You can ask me any questions",
|
||||
"get_QR_failed": "Failed to Get QR Code",
|
||||
"get_app_failed": "Failed to Retrieve App",
|
||||
"get_laf_failed": "Failed to Retrieve Laf Function List",
|
||||
"has_verification": "Verified, Click to Unbind",
|
||||
"have_a_try": "Give it a try",
|
||||
"have_done": "Completed",
|
||||
"import_failed": "Import Failed",
|
||||
"import_success": "Imported Successfully",
|
||||
@ -911,11 +928,14 @@
|
||||
"next_step": "Next",
|
||||
"no": "No",
|
||||
"no_child_folder": "No Subdirectories, Place Here",
|
||||
"no_data_available": "No valid data",
|
||||
"no_intro": "No Introduction Available",
|
||||
"no_laf_env": "System Not Configured with Laf Environment",
|
||||
"no_matching_apps_found": "No matching app found",
|
||||
"no_more_data": "No More Data",
|
||||
"no_pay_way": "There is no suitable payment channel in the system",
|
||||
"no_select_data": "No Data Available",
|
||||
"no_selected_apps": "No choice of applications yet",
|
||||
"not_model_config": "No related model configured",
|
||||
"not_open": "Not Open",
|
||||
"not_permission": "The current subscription package does not support team operation logs",
|
||||
@ -996,6 +1016,7 @@
|
||||
"read_quote": "View citations",
|
||||
"redo_tip": "Redo ctrl shift z",
|
||||
"redo_tip_mac": "Redo ⌘ shift z",
|
||||
"reorder_failed": "Sorting failed",
|
||||
"request_end": "All Loaded",
|
||||
"request_error": "request_error",
|
||||
"request_more": "Click to Load More",
|
||||
@ -1004,11 +1025,13 @@
|
||||
"resume_failed": "Resume Failed",
|
||||
"root_folder": "Root Folder",
|
||||
"save_failed": "save_failed",
|
||||
"save_success": "Saved Successfully",
|
||||
"save_success": "Save successfully",
|
||||
"scan_code": "Scan the QR code to pay",
|
||||
"select_file_failed": "File Selection Failed",
|
||||
"select_reference_variable": "Select Reference Variable",
|
||||
"select_tag": "Filter tags",
|
||||
"select_template": "Select Template",
|
||||
"selected": "Selected",
|
||||
"set_avatar": "Click to set_avatar",
|
||||
"share_link": "Share Link",
|
||||
"speech_error_tip": "Speech to Text Failed",
|
||||
@ -1199,7 +1222,9 @@
|
||||
"system.Help Document": "Help Document",
|
||||
"system_help_chatbot": "Help Chatbot",
|
||||
"tag_list": "Tag List",
|
||||
"tag_manage": "Tag management",
|
||||
"team_tag": "Team Tag",
|
||||
"team_tags_set": "Team Tags",
|
||||
"templateTags.Image_generation": "Image generation",
|
||||
"templateTags.Office_services": "Office Services",
|
||||
"templateTags.Roleplay": "role play",
|
||||
@ -1207,8 +1232,7 @@
|
||||
"templateTags.Writing": "Writing",
|
||||
"template_market": "Template Market",
|
||||
"textarea_variable_picker_tip": "Enter \"/\" to select a variable",
|
||||
"ui.textarea.Magnifying": "Magnifying",
|
||||
"un_used": "Unused",
|
||||
"ui.textarea.Magnifying": "enlarge",
|
||||
"unauth_token": "The certificate has expired, please log in again",
|
||||
"undo_tip": "Undo ctrl z",
|
||||
"undo_tip_mac": "Undo ⌘ z ",
|
||||
|
||||
@ -4,6 +4,9 @@
|
||||
"api_key": "API 密钥",
|
||||
"bills_and_invoices": "账单与发票",
|
||||
"channel": "模型渠道",
|
||||
"config_app": "精选应用",
|
||||
"config_copyright": "版权信息",
|
||||
"config_home": "门户配置",
|
||||
"config_model": "模型配置",
|
||||
"confirm_logout": "确认退出登录?",
|
||||
"create_channel": "新增渠道",
|
||||
@ -11,7 +14,12 @@
|
||||
"custom_model": "自定义模型",
|
||||
"default_model": "预设模型",
|
||||
"default_model_config": "默认模型配置",
|
||||
"gateway.cname_tip": "请到您的域名服务商处,比如添加该域名的、CNAME 解析到 Ixjgiwggswmb.sealoshzh.site,解析生效后即可绑定自定义域名。",
|
||||
"gateway.save_config": "保存",
|
||||
"gateway.share": "分享",
|
||||
"gateways": "门户管理",
|
||||
"logout": "登出",
|
||||
"logs": "首页日志",
|
||||
"model.active": "启用",
|
||||
"model.alias": "别名",
|
||||
"model.alias_tip": "模型在系统中展示的名字,方便用户理解",
|
||||
|
||||
32
packages/web/i18n/zh-CN/account_gate.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"Gate": "门户",
|
||||
"Gate List": "门户列表",
|
||||
"Gate app avatar updated": "门户应用图标更新",
|
||||
"Gate app created successfully": "门户应用创建成功",
|
||||
"No Gates Available": "暂无可用门户",
|
||||
"Operation failed": "操作失败",
|
||||
"available_tools": "可用工具",
|
||||
"confirm_delete_gate": "确认删除门户",
|
||||
"deep_thinking": "深度思考",
|
||||
"delete_gate": "删除门户",
|
||||
"dialog_prompt_text": "对话框提示文字",
|
||||
"disabled": "关闭",
|
||||
"enabled": "启用",
|
||||
"example": "示意图",
|
||||
"file_upload": "文件上传",
|
||||
"gate_list": "门户列表",
|
||||
"gate_logo": "LOGO预览",
|
||||
"gate_name": "门户名称",
|
||||
"image_upload": "图片上传",
|
||||
"no_gate_available": "没有可用门户",
|
||||
"no_gate_to_delete": "没有可以删除的门户了",
|
||||
"quick_app": "快捷应用",
|
||||
"slogan": "标语",
|
||||
"status": "状态",
|
||||
"suggestion_ratio_1_1": "建议比例 1:1",
|
||||
"suggestion_ratio_4_1": "建议比例 4:1",
|
||||
"team_name": "团队名",
|
||||
"upload": "上传",
|
||||
"voice_input": "语音输入",
|
||||
"web_search": "联网搜索"
|
||||
}
|
||||
@ -48,6 +48,7 @@
|
||||
"create_by_template": "从模板创建",
|
||||
"create_copy_success": "创建副本成功",
|
||||
"create_empty_app": "创建空白应用",
|
||||
"create_empty_gate": "创建空白门户",
|
||||
"create_empty_plugin": "创建空白插件",
|
||||
"create_empty_workflow": "创建空白工作流",
|
||||
"cron.every_day": "每天执行",
|
||||
@ -123,6 +124,8 @@
|
||||
"permission.des.manage": "写权限基础上,可配置发布渠道、查看对话日志、分配该应用权限",
|
||||
"permission.des.read": "可使用该应用进行对话",
|
||||
"permission.des.write": "可查看和编辑应用",
|
||||
"permission.des.log": "可查看对话日志",
|
||||
"permission.name.log": "查看日志",
|
||||
"plugin.Instructions": "使用说明",
|
||||
"plugin_cost_by_token": "依据 token 消耗计费",
|
||||
"plugin_cost_per_times": "{{cost}} 积分/次",
|
||||
@ -148,6 +151,7 @@
|
||||
"team_tags_set": "团队标签",
|
||||
"temperature": "温度",
|
||||
"temperature_tip": "范围 0~10。值越大,代表模型回答越发散;值越小,代表回答越严谨。",
|
||||
"template.gate": "门户",
|
||||
"template.hard_strict": "严格问答模板",
|
||||
"template.hard_strict_des": "在问答模板基础上,对模型的回答做更严格的要求。",
|
||||
"template.qa_template": "问答模板",
|
||||
@ -181,6 +185,8 @@
|
||||
"tts_browser": "浏览器自带(免费)",
|
||||
"tts_close": "关闭",
|
||||
"type.All": "全部",
|
||||
"type.Create gate": "创建门户",
|
||||
"type.Create gate tip": "门户不该在这里被创建",
|
||||
"type.Create http plugin tip": "通过 OpenAPI Schema 批量创建插件,兼容 GPTs 格式",
|
||||
"type.Create mcp tools tip": "通过输入 MCP 地址,自动解析并批量创建可调用的 MCP 工具",
|
||||
"type.Create one plugin tip": "可以自定义输入和输出的工作流,通常用于封装重复使用的工作流",
|
||||
@ -189,6 +195,7 @@
|
||||
"type.Create simple bot tip": "通过填表单形式,创建简单的 AI 应用,适合新手",
|
||||
"type.Create workflow bot": "创建工作流",
|
||||
"type.Create workflow tip": "通过低代码的方式,构建逻辑复杂的多轮对话 AI 应用,推荐高级玩家使用",
|
||||
"type.Gate": "门户",
|
||||
"type.Http plugin": "HTTP 插件",
|
||||
"type.Import from json": "导入 JSON 配置",
|
||||
"type.Import from json tip": "通过 JSON 配置文件,直接创建应用",
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
"chat.quote.No Data": "找不到该文件",
|
||||
"chat.quote.deleted": "该数据已被删除~",
|
||||
"chat.waiting_for_response": "请等待对话完成",
|
||||
"chat_gate_app": "门户首页",
|
||||
"chat_history": "聊天记录",
|
||||
"chat_input_guide_lexicon_is_empty": "还没有配置词库",
|
||||
"chat_test_app": "调试-{{name}}",
|
||||
|
||||
@ -13,8 +13,11 @@
|
||||
"Confirm": "确认",
|
||||
"Continue_Adding": "继续添加",
|
||||
"Copy": "复制",
|
||||
"Create Success": "创建成功",
|
||||
"Creating": "创建中",
|
||||
"Delete": "删除",
|
||||
"Delete Failed": "删除失败",
|
||||
"Delete Success": "删除成功",
|
||||
"Detail": "详情",
|
||||
"Documents": "文档",
|
||||
"Done": "完成",
|
||||
@ -44,12 +47,14 @@
|
||||
"Folder": "文件夹",
|
||||
"FullScreen": "全屏",
|
||||
"FullScreenLight": "全屏预览",
|
||||
"Gate.service.is.unavailable": "门户不可用",
|
||||
"Import": "导入",
|
||||
"Input": "输入",
|
||||
"Instructions": "使用说明",
|
||||
"Intro": "介绍",
|
||||
"Loading": "加载中...",
|
||||
"Login": "登录",
|
||||
"Manage tags": "管理标签",
|
||||
"More": "更多",
|
||||
"Move": "移动",
|
||||
"Name": "名称",
|
||||
@ -74,22 +79,29 @@
|
||||
"Run": "运行",
|
||||
"Running": "运行中",
|
||||
"Save": "保存",
|
||||
"Save Failed": "保存失败",
|
||||
"Save Success": "保存成功",
|
||||
"Save_and_exit": "保存并退出",
|
||||
"Search": "搜索",
|
||||
"Select tags": "选择标签",
|
||||
"Select_all": "全选",
|
||||
"Setting": "设置",
|
||||
"Status": "状态",
|
||||
"Submit": "提交",
|
||||
"Success": "成功",
|
||||
"Tag already added": "标签已经添加过了",
|
||||
"Tags": "标签",
|
||||
"Team": "团队",
|
||||
"UnKnow": "未知",
|
||||
"Unlimited": "无限制",
|
||||
"Update": "更新",
|
||||
"Update Success": "更新成功",
|
||||
"Username": "用户名",
|
||||
"Waiting": "等待中",
|
||||
"Warning": "警告",
|
||||
"Website": "网站",
|
||||
"action_confirm": "操作确认",
|
||||
"add_app": "新增应用",
|
||||
"add_new": "新增",
|
||||
"add_new_param": "新增参数",
|
||||
"add_success": "添加成功",
|
||||
@ -135,6 +147,9 @@
|
||||
"code_error.error_code.503": "服务器暂时过载或正在维护",
|
||||
"code_error.error_code.504": "网关超时",
|
||||
"code_error.error_message.403": "凭证错误",
|
||||
"code_error.error_message.405": "方式不允许",
|
||||
"code_error.error_message.422": "Params非法",
|
||||
"code_error.error_message.500": "系统错误",
|
||||
"code_error.error_message.510": "账户余额不足",
|
||||
"code_error.error_message.511": "没有权限操作此模型",
|
||||
"code_error.error_message.513": "没有权限读取该文件",
|
||||
@ -829,10 +844,12 @@
|
||||
"folder.open_dataset": "打开知识库",
|
||||
"folder_description": "文件夹描述",
|
||||
"free": "免费",
|
||||
"gate.placeholder": "你可以问我任何问题",
|
||||
"get_QR_failed": "获取二维码失败",
|
||||
"get_app_failed": "获取应用失败",
|
||||
"get_laf_failed": "获取Laf函数列表失败",
|
||||
"has_verification": "已验证,点击取消绑定",
|
||||
"have_a_try": "试一试",
|
||||
"have_done": "已完成",
|
||||
"import_failed": "导入失败",
|
||||
"import_success": "导入成功",
|
||||
@ -911,11 +928,14 @@
|
||||
"next_step": "下一步",
|
||||
"no": "否",
|
||||
"no_child_folder": "没有子目录了,就放这里吧",
|
||||
"no_data_available": "无有效数据",
|
||||
"no_intro": "暂无介绍",
|
||||
"no_laf_env": "系统未配置Laf环境",
|
||||
"no_matching_apps_found": "没有找到匹配的应用",
|
||||
"no_more_data": "没有更多了~",
|
||||
"no_pay_way": "系统无合适的支付渠道",
|
||||
"no_select_data": "没有可选值",
|
||||
"no_selected_apps": "暂无选择的应用",
|
||||
"not_model_config": "未配置相关模型",
|
||||
"not_open": "未开启",
|
||||
"not_permission": "当前订阅套餐不支持团队操作日志",
|
||||
@ -996,6 +1016,7 @@
|
||||
"read_quote": "查看引用",
|
||||
"redo_tip": "恢复 ctrl shift z",
|
||||
"redo_tip_mac": "恢复 ⌘ shift z",
|
||||
"reorder_failed": "排序失败",
|
||||
"request_end": "已加载全部",
|
||||
"request_error": "请求异常",
|
||||
"request_more": "点击加载更多",
|
||||
@ -1008,7 +1029,9 @@
|
||||
"scan_code": "扫码支付",
|
||||
"select_file_failed": "选择文件异常",
|
||||
"select_reference_variable": "选择引用变量",
|
||||
"select_tag": "筛选标签",
|
||||
"select_template": "选择模板",
|
||||
"selected": "已选择",
|
||||
"set_avatar": "点击设置头像",
|
||||
"share_link": "分享链接",
|
||||
"speech_error_tip": "语音转文字失败",
|
||||
@ -1199,7 +1222,9 @@
|
||||
"system.Help Document": "帮助文档",
|
||||
"system_help_chatbot": "机器人助手",
|
||||
"tag_list": "标签列表",
|
||||
"tag_manage": "标签管理",
|
||||
"team_tag": "团队标签",
|
||||
"team_tags_set": "团队标签",
|
||||
"templateTags.Image_generation": "图片生成",
|
||||
"templateTags.Office_services": "办公服务",
|
||||
"templateTags.Roleplay": "角色扮演",
|
||||
@ -1207,6 +1232,7 @@
|
||||
"templateTags.Writing": "文本创作",
|
||||
"template_market": "模板市场",
|
||||
"textarea_variable_picker_tip": "输入\"/\"可选择变量",
|
||||
"tool_select": "工具选择",
|
||||
"ui.textarea.Magnifying": "放大",
|
||||
"un_used": "未使用",
|
||||
"unauth_token": "凭证已过期,请重新登录",
|
||||
|
||||
@ -4,6 +4,9 @@
|
||||
"api_key": "API 金鑰",
|
||||
"bills_and_invoices": "帳單與發票",
|
||||
"channel": "模型管道",
|
||||
"config_app": "精選應用",
|
||||
"config_copyright": "應用配置",
|
||||
"config_home": "首頁配置",
|
||||
"config_model": "模型設定",
|
||||
"confirm_logout": "確認登出登入?",
|
||||
"create_channel": "新增頻道",
|
||||
@ -11,7 +14,12 @@
|
||||
"custom_model": "自訂模型",
|
||||
"default_model": "預設模型",
|
||||
"default_model_config": "預設模型設定",
|
||||
"gateway.cname_tip": "請到您的域名服務商處,比如添加該域名的、CNAME 解析到 Ixjgiwggswmb.sealoshzh.site,解析生效後即可綁定自定義域名。",
|
||||
"gateway.save_config": "保存",
|
||||
"gateway.share": "分享",
|
||||
"gateways": "門戶管理",
|
||||
"logout": "登出",
|
||||
"logs": "首頁日誌",
|
||||
"model.active": "啟用",
|
||||
"model.alias": "別名",
|
||||
"model.alias_tip": "模型在系統中展示的名字,方便使用者理解",
|
||||
|
||||
30
packages/web/i18n/zh-Hant/account_gate.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"Gate": "門戶",
|
||||
"Gate List": "門戶列表",
|
||||
"Gate app avatar updated": "門戶應用圖標更新",
|
||||
"Gate app created successfully": "門戶應用創建成功",
|
||||
"No Gates Available": "暫無可用門戶",
|
||||
"Operation failed": "操作失敗",
|
||||
"available_tools": "可用工具",
|
||||
"confirm_delete_gate": "確認刪除門戶",
|
||||
"deep_thinking": "深度思考",
|
||||
"delete_gate": "刪除門戶",
|
||||
"dialog_prompt_text": "對話框提示文字",
|
||||
"disabled": "關閉",
|
||||
"enabled": "啟用",
|
||||
"example": "示意圖",
|
||||
"file_upload": "文件上傳",
|
||||
"gate_list": "門戶列表",
|
||||
"gate_logo": "LOGO預覽",
|
||||
"image_upload": "圖片上傳",
|
||||
"no_gate_available": "沒有可用門戶",
|
||||
"no_gate_to_delete": "沒有可以刪除的門戶了",
|
||||
"quick_app": "快捷應用",
|
||||
"slogan": "標語",
|
||||
"status": "狀態",
|
||||
"suggestion_ratio_1_1": "建議比例 1:1",
|
||||
"suggestion_ratio_4_1": "建議比例 4:1",
|
||||
"team_name": "團隊名",
|
||||
"voice_input": "語音輸入",
|
||||
"web_search": "聯網搜索"
|
||||
}
|
||||
@ -48,6 +48,7 @@
|
||||
"create_by_template": "從範本建立",
|
||||
"create_copy_success": "建立副本成功",
|
||||
"create_empty_app": "建立空白應用程式",
|
||||
"create_empty_gate": "創建空白門戶",
|
||||
"create_empty_plugin": "建立空白外掛",
|
||||
"create_empty_workflow": "建立空白工作流程",
|
||||
"cron.every_day": "每天執行",
|
||||
@ -123,6 +124,8 @@
|
||||
"permission.des.manage": "在寫入權限基礎上,可以設定發布通道、檢視對話紀錄、分配這個應用程式的權限",
|
||||
"permission.des.read": "可以使用這個應用程式進行對話",
|
||||
"permission.des.write": "可以檢視和編輯應用程式",
|
||||
"permission.des.log": "可查看對話日誌",
|
||||
"permission.name.log": "查看日誌",
|
||||
"plugin.Instructions": "使用說明",
|
||||
"plugin_cost_by_token": "根據 token 消耗計費",
|
||||
"plugin_cost_per_times": "{{cost}} 積分/次",
|
||||
@ -148,6 +151,7 @@
|
||||
"team_tags_set": "團隊標籤",
|
||||
"temperature": "溫度",
|
||||
"temperature_tip": "範圍 0~10。\n值越大,代表模型回答越發散;值越小,代表回答越嚴謹。",
|
||||
"template.gate": "門戶",
|
||||
"template.hard_strict": "嚴格問答範本",
|
||||
"template.hard_strict_des": "在問答範本基礎上,對模型的回答做出更嚴格的要求。",
|
||||
"template.qa_template": "問答範本",
|
||||
@ -181,6 +185,8 @@
|
||||
"tts_browser": "瀏覽器自帶 (免費)",
|
||||
"tts_close": "關閉",
|
||||
"type.All": "全部",
|
||||
"type.Create gate": "創建門戶",
|
||||
"type.Create gate tip": "門戶不該在這裡被創建",
|
||||
"type.Create http plugin tip": "透過 OpenAPI Schema 批次建立外掛,相容 GPTs 格式",
|
||||
"type.Create mcp tools tip": "通過輸入 MCP 地址,自動解析並批量創建可調用的 MCP 工具",
|
||||
"type.Create one plugin tip": "可以自訂輸入和輸出的工作流程,通常用於封裝重複使用的工作流程",
|
||||
@ -189,6 +195,7 @@
|
||||
"type.Create simple bot tip": "透過填寫表單的方式,建立簡單的 AI 應用程式,適合新手",
|
||||
"type.Create workflow bot": "建立工作流程",
|
||||
"type.Create workflow tip": "透過低程式碼的方式,建立邏輯複雜的多輪對話 AI 應用程式,建議進階使用者使用",
|
||||
"type.Gate": "門戶",
|
||||
"type.Http plugin": "HTTP 外掛",
|
||||
"type.Import from json": "匯入 JSON 設定",
|
||||
"type.Import from json tip": "透過 JSON 設定文件,直接建立應用",
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
"chat.quote.No Data": "找不到該文件",
|
||||
"chat.quote.deleted": "該資料已被刪除~",
|
||||
"chat.waiting_for_response": "請等待對話完成",
|
||||
"chat_gate_app": "門戶首頁",
|
||||
"chat_history": "對話紀錄",
|
||||
"chat_input_guide_lexicon_is_empty": "尚未設定詞彙庫",
|
||||
"chat_test_app": "除錯-{{name}}",
|
||||
|
||||
@ -13,8 +13,10 @@
|
||||
"Confirm": "確認",
|
||||
"Continue_Adding": "繼續新增",
|
||||
"Copy": "複製",
|
||||
"Create Success": "創建成功",
|
||||
"Creating": "建立中",
|
||||
"Delete": "刪除",
|
||||
"Delete Success": "刪除成功",
|
||||
"Detail": "詳細資料",
|
||||
"Documents": "文件",
|
||||
"Done": "完成",
|
||||
@ -44,12 +46,14 @@
|
||||
"Folder": "資料夾",
|
||||
"FullScreen": "全屏",
|
||||
"FullScreenLight": "全屏預覽",
|
||||
"Gate.service.is.unavailable": "門戶不可用",
|
||||
"Import": "匯入",
|
||||
"Input": "輸入",
|
||||
"Instructions": "使用說明",
|
||||
"Intro": "介紹",
|
||||
"Loading": "載入中...",
|
||||
"Login": "登入",
|
||||
"Manage tags": "管理標籤",
|
||||
"More": "更多",
|
||||
"Move": "移動",
|
||||
"Name": "名稱",
|
||||
@ -74,22 +78,28 @@
|
||||
"Run": "執行",
|
||||
"Running": "執行中",
|
||||
"Save": "儲存",
|
||||
"Save Failed": "保存失敗",
|
||||
"Save Success": "保存成功",
|
||||
"Save_and_exit": "儲存並離開",
|
||||
"Search": "搜尋",
|
||||
"Select tags": "選擇標籤",
|
||||
"Select_all": "全選",
|
||||
"Setting": "設定",
|
||||
"Status": "狀態",
|
||||
"Submit": "送出",
|
||||
"Success": "成功",
|
||||
"Tag already added": "標籤已經添加過了",
|
||||
"Team": "團隊",
|
||||
"UnKnow": "未知",
|
||||
"Unlimited": "無限制",
|
||||
"Update": "更新",
|
||||
"Update Success": "更新成功",
|
||||
"Username": "使用者名稱",
|
||||
"Waiting": "等待中",
|
||||
"Warning": "警告",
|
||||
"Website": "網站",
|
||||
"action_confirm": "確認",
|
||||
"add_app": "新增應用",
|
||||
"add_new": "新增",
|
||||
"add_new_param": "新增參數",
|
||||
"add_success": "新增成功",
|
||||
@ -139,6 +149,9 @@
|
||||
"code_error.error_message.511": "無權操作此模型",
|
||||
"code_error.error_message.513": "無權讀取此檔案",
|
||||
"code_error.error_message.514": "API 金鑰無效",
|
||||
"code_error.error_message[405]": "方式不允許",
|
||||
"code_error.error_message[422]": "Params非法",
|
||||
"code_error.error_message[500]": "系統錯誤",
|
||||
"code_error.openapi_error.api_key_not_exist": "API 金鑰不存在",
|
||||
"code_error.openapi_error.exceed_limit": "最多 10 組 API 金鑰",
|
||||
"code_error.openapi_error.un_auth": "無權操作此 API 金鑰",
|
||||
@ -829,10 +842,12 @@
|
||||
"folder.open_dataset": "開啟知識庫",
|
||||
"folder_description": "資料夾描述",
|
||||
"free": "免費",
|
||||
"gate.placeholder": "你可以問我任何問題",
|
||||
"get_QR_failed": "取得 QR Code 失敗",
|
||||
"get_app_failed": "取得應用程式失敗",
|
||||
"get_laf_failed": "取得 LAF 函式清單失敗",
|
||||
"has_verification": "已驗證,點選解除綁定",
|
||||
"have_a_try": "試一試",
|
||||
"have_done": "已完成",
|
||||
"import_failed": "匯入失敗",
|
||||
"import_success": "匯入成功",
|
||||
@ -911,11 +926,14 @@
|
||||
"next_step": "下一步",
|
||||
"no": "否",
|
||||
"no_child_folder": "無子目錄,放置在此",
|
||||
"no_data_available": "無有效數據",
|
||||
"no_intro": "暫無介紹",
|
||||
"no_laf_env": "系統未設定 LAF 環境",
|
||||
"no_matching_apps_found": "沒有找到匹配的應用",
|
||||
"no_more_data": "沒有更多資料了",
|
||||
"no_pay_way": "系統無合適的支付渠道",
|
||||
"no_select_data": "沒有可選擇的資料",
|
||||
"no_selected_apps": "暫無選擇的應用",
|
||||
"not_model_config": "未設定相關模型",
|
||||
"not_open": "未開啟",
|
||||
"not_permission": "當前訂閱套餐不支持團隊操作日誌",
|
||||
@ -1004,11 +1022,13 @@
|
||||
"resume_failed": "恢復失敗",
|
||||
"root_folder": "根目錄",
|
||||
"save_failed": "儲存失敗",
|
||||
"save_success": "儲存成功",
|
||||
"save_success": "保存成功",
|
||||
"scan_code": "掃碼支付",
|
||||
"select_file_failed": "選擇檔案失敗",
|
||||
"select_reference_variable": "選擇引用變數",
|
||||
"select_tag": "篩選標籤",
|
||||
"select_template": "選擇範本",
|
||||
"selected": "已選擇",
|
||||
"set_avatar": "點選設定頭像",
|
||||
"share_link": "分享連結",
|
||||
"speech_error_tip": "語音轉文字失敗",
|
||||
@ -1199,7 +1219,9 @@
|
||||
"system.Help Document": "說明文件",
|
||||
"system_help_chatbot": "機器人助手",
|
||||
"tag_list": "標籤列表",
|
||||
"tag_manage": "標籤管理",
|
||||
"team_tag": "團隊標籤",
|
||||
"team_tags_set": "團隊標籤",
|
||||
"templateTags.Image_generation": "圖片生成",
|
||||
"templateTags.Office_services": "辦公服務",
|
||||
"templateTags.Roleplay": "角色扮演",
|
||||
@ -1208,7 +1230,6 @@
|
||||
"template_market": "模板市場",
|
||||
"textarea_variable_picker_tip": "輸入「/」以選擇變數",
|
||||
"ui.textarea.Magnifying": "放大",
|
||||
"un_used": "未使用",
|
||||
"unauth_token": "憑證已過期,請重新登入",
|
||||
"undo_tip": "復原 ctrl z",
|
||||
"undo_tip_mac": "復原 ⌘ z ",
|
||||
|
||||
2
packages/web/types/i18next.d.ts
vendored
@ -34,6 +34,7 @@ export interface I18nNamespaces {
|
||||
account_info: typeof account_info;
|
||||
account_usage: typeof account_usage;
|
||||
account_bill: typeof account_bill;
|
||||
account_gate: typeof account_gate;
|
||||
account_apikey: typeof account_apikey;
|
||||
account_setting: typeof account_setting;
|
||||
account_inform: typeof account_inform;
|
||||
@ -71,6 +72,7 @@ declare module 'i18next' {
|
||||
'account_info',
|
||||
'account_usage',
|
||||
'account_bill',
|
||||
'account_gate',
|
||||
'account_apikey',
|
||||
'account_setting',
|
||||
'account_inform',
|
||||
|
||||
29
projects/app/src/components/GatePageContainer/index.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { useTheme, type BoxProps } from '@chakra-ui/react';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
|
||||
const GatePageContainer = ({
|
||||
children,
|
||||
isLoading,
|
||||
insertProps = {},
|
||||
...props
|
||||
}: BoxProps & { isLoading?: boolean; insertProps?: BoxProps }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<MyBox h={'100%'} py={[0, '16px']} pr={[0, '16px']} {...props}>
|
||||
<MyBox
|
||||
isLoading={isLoading}
|
||||
h={'100%'}
|
||||
overflow={'overlay'}
|
||||
bg={'myGray.25'}
|
||||
borderRadius={[0, '12px']}
|
||||
overflowX={'visible'}
|
||||
{...insertProps}
|
||||
>
|
||||
{children}
|
||||
</MyBox>
|
||||
</MyBox>
|
||||
);
|
||||
};
|
||||
|
||||
export default GatePageContainer;
|
||||
@ -45,6 +45,9 @@ const pcUnShowLayoutRoute: Record<string, boolean> = {
|
||||
'/login': true,
|
||||
'/login/provider': true,
|
||||
'/login/fastlogin': true,
|
||||
'/chat/gate': true,
|
||||
'/chat/gate/store': true,
|
||||
'/chat/gate/application': true,
|
||||
'/chat/share': true,
|
||||
'/chat/team': true,
|
||||
'/app/edit': true,
|
||||
@ -57,6 +60,9 @@ const phoneUnShowLayoutRoute: Record<string, boolean> = {
|
||||
'/login': true,
|
||||
'/login/provider': true,
|
||||
'/login/fastlogin': true,
|
||||
'/chat/gate': true,
|
||||
'/chat/gate/store': true,
|
||||
'/chat/gate/application': true,
|
||||
'/chat/share': true,
|
||||
'/chat/team': true,
|
||||
'/tools/price': true,
|
||||
|
||||
@ -82,6 +82,7 @@ const Navbar = ({ unread }: { unread: number }) => {
|
||||
'/account/team',
|
||||
'/account/usage',
|
||||
'/account/thirdParty',
|
||||
'/account/gateway',
|
||||
'/account/apikey',
|
||||
'/account/setting',
|
||||
'/account/inform',
|
||||
|
||||
@ -110,7 +110,14 @@ const OneRowSelector = ({ list, onChange, disableTip, ...props }: Props) => {
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
const MultipleRowSelector = ({ list, onChange, disableTip, placeholder, ...props }: Props) => {
|
||||
const MultipleRowSelector = ({
|
||||
list,
|
||||
onChange,
|
||||
disableTip,
|
||||
placeholder,
|
||||
showAvatar = true,
|
||||
...props
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { llmModelList, embeddingModelList, ttsModelList, sttModelList, reRankModelList } =
|
||||
useSystemStore();
|
||||
@ -193,6 +200,7 @@ const MultipleRowSelector = ({ list, onChange, disableTip, placeholder, ...props
|
||||
|
||||
return (
|
||||
<HStack spacing={1}>
|
||||
{showAvatar && (
|
||||
<Avatar
|
||||
borderRadius={'0'}
|
||||
mr={2}
|
||||
@ -200,10 +208,12 @@ const MultipleRowSelector = ({ list, onChange, disableTip, placeholder, ...props
|
||||
fallbackSrc={HUGGING_FACE_ICON}
|
||||
w={avatarSize}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Box>{modelData?.name}</Box>
|
||||
</HStack>
|
||||
);
|
||||
}, [modelList, props.value, t, avatarSize]);
|
||||
}, [modelList, props.value, t, showAvatar, avatarSize]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
|
||||
@ -281,7 +281,7 @@ const ChatInput = ({
|
||||
[
|
||||
File,
|
||||
TextareaDom,
|
||||
fileList,
|
||||
fileList.length,
|
||||
handleSend,
|
||||
hasFileUploading,
|
||||
havInput,
|
||||
@ -296,7 +296,8 @@ const ChatInput = ({
|
||||
setValue,
|
||||
showSelectFile,
|
||||
showSelectImg,
|
||||
t
|
||||
t,
|
||||
whisperConfig?.open
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@ -0,0 +1,427 @@
|
||||
import React, { useRef, useCallback, useMemo, useState, useEffect, useContext } from 'react';
|
||||
import { Box, Flex, Textarea, IconButton, useBreakpointValue } from '@chakra-ui/react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { getWebDefaultLLMModel } from '@/web/common/system/utils';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import type { ChatBoxInputFormType, ChatBoxInputType, SendPromptFnType } from '../type';
|
||||
import { textareaMinH } from '../constants';
|
||||
import type { UseFormReturn } from 'react-hook-form';
|
||||
import { useFieldArray } from 'react-hook-form';
|
||||
import { ChatBoxContext } from '../Provider';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import { documentFileType } from '@fastgpt/global/common/file/constants';
|
||||
import FilePreview from '../../components/FilePreview';
|
||||
import { useFileUpload } from '../hooks/useFileUpload';
|
||||
import ComplianceTip from '@/components/common/ComplianceTip/index';
|
||||
import VoiceInput, { type VoiceInputComponentRef } from './VoiceInput';
|
||||
import { useRouter } from 'next/router';
|
||||
import { appWorkflow2Form } from '@fastgpt/global/core/app/utils';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { AppContext } from '@/pageComponents/app/detail/context';
|
||||
import { AppFormContext } from '@/pages/chat/gate/index';
|
||||
import Icon from '@fastgpt/web/components/common/Icon';
|
||||
import AIModelSelector from '@/components/Select/AIModelSelector';
|
||||
import type { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/node';
|
||||
|
||||
const GateToolSelect = dynamic(
|
||||
() => import('@/pageComponents/app/detail/Gate/components/GateToolSelect'),
|
||||
{
|
||||
ssr: false
|
||||
}
|
||||
);
|
||||
|
||||
const fileTypeFilter = (file: File) => {
|
||||
return (
|
||||
file.type.includes('image') ||
|
||||
documentFileType.split(',').some((type) => file.name.endsWith(type.trim()))
|
||||
);
|
||||
};
|
||||
|
||||
type Props = {
|
||||
onSendMessage: SendPromptFnType;
|
||||
onStop: () => void;
|
||||
TextareaDom: React.MutableRefObject<HTMLTextAreaElement | null>;
|
||||
resetInputVal: (val: ChatBoxInputType) => void;
|
||||
chatForm: UseFormReturn<ChatBoxInputFormType>;
|
||||
placeholder?: string;
|
||||
selectedTools?: FlowNodeTemplateType[];
|
||||
onSelectTools?: (toolIds: FlowNodeTemplateType[]) => void;
|
||||
};
|
||||
|
||||
const GateChatInput = ({
|
||||
onSendMessage,
|
||||
onStop,
|
||||
TextareaDom,
|
||||
resetInputVal,
|
||||
chatForm,
|
||||
placeholder,
|
||||
selectedTools: externalSelectedToolIds,
|
||||
onSelectTools
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { isPc } = useSystem();
|
||||
const router = useRouter();
|
||||
const buttonSize = useBreakpointValue({ base: 'sm', md: 'md' });
|
||||
const VoiceInputRef = useRef<VoiceInputComponentRef>(null);
|
||||
|
||||
// 使用AppFormContext替代本地appForm状态
|
||||
const { appForm, setAppForm } = useContext(AppFormContext);
|
||||
|
||||
const { setValue, watch, control } = chatForm;
|
||||
const inputValue = watch('input');
|
||||
|
||||
const outLinkAuthData = useContextSelector(ChatBoxContext, (v) => v.outLinkAuthData);
|
||||
const appId = useContextSelector(ChatBoxContext, (v) => v.appId);
|
||||
const chatId = useContextSelector(ChatBoxContext, (v) => v.chatId);
|
||||
const isChatting = useContextSelector(ChatBoxContext, (v) => v.isChatting);
|
||||
const fileSelectConfig = useContextSelector(ChatBoxContext, (v) => v.fileSelectConfig);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const selectedTools = externalSelectedToolIds ?? [];
|
||||
const setSelectedToolIds = onSelectTools!;
|
||||
|
||||
const { llmModelList } = useSystemStore();
|
||||
const modelList = useMemo(
|
||||
() => llmModelList.map((item) => ({ label: item.name, value: item.model })),
|
||||
[llmModelList]
|
||||
);
|
||||
const defaultModel = useMemo(() => getWebDefaultLLMModel(llmModelList).model, [llmModelList]);
|
||||
const [selectedModel, setSelectedModel] = useState(defaultModel);
|
||||
|
||||
const showModelSelector = useMemo(() => {
|
||||
return router.pathname === '/chat/gate';
|
||||
}, [router.pathname]);
|
||||
|
||||
// 是否显示工具选择器
|
||||
const showTools = useMemo(() => {
|
||||
return router.pathname === '/chat/gate';
|
||||
}, [router.pathname]);
|
||||
|
||||
// 当模型选择变化时更新appForm
|
||||
useEffect(() => {
|
||||
if (!showTools) return;
|
||||
|
||||
setAppForm((prevAppForm) => ({
|
||||
...prevAppForm,
|
||||
aiSettings: {
|
||||
...prevAppForm.aiSettings,
|
||||
model: selectedModel
|
||||
}
|
||||
}));
|
||||
}, [selectedModel, showTools, setAppForm]);
|
||||
|
||||
const fileCtrl = useFieldArray({
|
||||
control,
|
||||
name: 'files'
|
||||
});
|
||||
|
||||
const {
|
||||
File,
|
||||
onOpenSelectFile,
|
||||
fileList,
|
||||
onSelectFile,
|
||||
uploadFiles,
|
||||
removeFiles,
|
||||
replaceFiles,
|
||||
hasFileUploading
|
||||
} = useFileUpload({
|
||||
fileSelectConfig,
|
||||
fileCtrl,
|
||||
outLinkAuthData,
|
||||
appId,
|
||||
chatId
|
||||
});
|
||||
|
||||
const havInput = !!inputValue || fileList.length > 0;
|
||||
const canSendMessage = havInput && !hasFileUploading;
|
||||
|
||||
// Upload files
|
||||
useRequest2(uploadFiles, {
|
||||
manual: false,
|
||||
errorToast: t('common:upload_file_error'),
|
||||
refreshDeps: [fileList, outLinkAuthData, chatId]
|
||||
});
|
||||
|
||||
const handleSend = useCallback(
|
||||
async (val?: string) => {
|
||||
if (!canSendMessage) return;
|
||||
const textareaValue = val || TextareaDom.current?.value || '';
|
||||
|
||||
onSendMessage({
|
||||
text: textareaValue.trim(),
|
||||
files: fileList,
|
||||
gateModel: showModelSelector ? selectedModel : undefined,
|
||||
selectedTool: selectedTools.length > 0 ? selectedTools.join(',') : null // 将工具ID数组转换为逗号分隔的字符串
|
||||
});
|
||||
replaceFiles([]);
|
||||
},
|
||||
[
|
||||
TextareaDom,
|
||||
canSendMessage,
|
||||
fileList,
|
||||
onSendMessage,
|
||||
replaceFiles,
|
||||
showModelSelector,
|
||||
selectedModel,
|
||||
selectedTools
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
w="full"
|
||||
maxW="100%"
|
||||
minH="132px"
|
||||
background="var(--White, #FFF)"
|
||||
border="0.5px solid rgba(0, 0, 0, 0.13)"
|
||||
boxShadow="0px 5px 16px -4px rgba(19, 51, 107, 0.08)"
|
||||
borderRadius="20px"
|
||||
position="relative"
|
||||
px={4}
|
||||
pb={'62px'}
|
||||
pt={fileList.length > 0 ? 0 : 4}
|
||||
overflow="hidden"
|
||||
transition="all 0.2s ease"
|
||||
_hover={{
|
||||
border: '0.5px solid rgba(0, 0, 0, 0.20)',
|
||||
boxShadow: '0px 5px 20px -4px rgba(19, 51, 107, 0.13)'
|
||||
}}
|
||||
_focus-within={{
|
||||
border: '0.5px solid rgba(0, 0, 0, 0.20)',
|
||||
boxShadow: '0px 5px 20px -4px rgba(19, 51, 107, 0.13)'
|
||||
}}
|
||||
>
|
||||
{/* file preview */}
|
||||
<Box px={[1, 3]}>
|
||||
<FilePreview fileList={fileList} removeFiles={removeFiles} />
|
||||
</Box>
|
||||
|
||||
<Textarea
|
||||
ref={TextareaDom}
|
||||
value={inputValue}
|
||||
onChange={(e) => {
|
||||
const textarea = e.target;
|
||||
textarea.style.height = textareaMinH;
|
||||
textarea.style.height = `${textarea.scrollHeight}px`;
|
||||
setValue('input', textarea.value);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
// enter send.(pc or iframe && enter and unPress shift)
|
||||
const isEnter = e.keyCode === 13;
|
||||
if (isEnter && TextareaDom.current && (e.ctrlKey || e.altKey)) {
|
||||
// Add a new line
|
||||
const index = TextareaDom.current.selectionStart;
|
||||
const val = TextareaDom.current.value;
|
||||
TextareaDom.current.value = `${val.slice(0, index)}\n${val.slice(index)}`;
|
||||
TextareaDom.current.selectionStart = index + 1;
|
||||
TextareaDom.current.selectionEnd = index + 1;
|
||||
|
||||
TextareaDom.current.style.height = textareaMinH;
|
||||
TextareaDom.current.style.height = `${TextareaDom.current.scrollHeight}px`;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// 全选内容
|
||||
// @ts-ignore
|
||||
e.key === 'a' && e.ctrlKey && e.target?.select();
|
||||
|
||||
if ((isPc || window !== parent) && e.keyCode === 13 && !e.shiftKey) {
|
||||
handleSend();
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
onPaste={(e) => {
|
||||
const clipboardData = e.clipboardData;
|
||||
if (clipboardData && (fileSelectConfig.canSelectFile || fileSelectConfig.canSelectImg)) {
|
||||
const items = clipboardData.items;
|
||||
const files = Array.from(items)
|
||||
.map((item) => (item.kind === 'file' ? item.getAsFile() : undefined))
|
||||
.filter((file) => {
|
||||
return file && fileTypeFilter(file);
|
||||
}) as File[];
|
||||
onSelectFile({ files });
|
||||
|
||||
if (files.length > 0) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
variant="unstyled"
|
||||
resize="none"
|
||||
minH="60px"
|
||||
maxH="300px"
|
||||
fontFamily="PingFang SC"
|
||||
fontSize="15px"
|
||||
lineHeight="1.6"
|
||||
letterSpacing="0.5px"
|
||||
overflowY="auto"
|
||||
css={{
|
||||
'&::-webkit-scrollbar': {
|
||||
width: '4px'
|
||||
},
|
||||
'&::-webkit-scrollbar-track': {
|
||||
width: '6px',
|
||||
background: 'transparent'
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
background: '#E2E8F0',
|
||||
borderRadius: '24px'
|
||||
}
|
||||
}}
|
||||
_placeholder={{
|
||||
color: '#A4A4A4',
|
||||
fontSize: '15px'
|
||||
}}
|
||||
/>
|
||||
{/* Bottom Toolbar */}
|
||||
<Flex
|
||||
position="absolute"
|
||||
left="0"
|
||||
right="0"
|
||||
bottom="3"
|
||||
px="4"
|
||||
justify="space-between"
|
||||
align="center"
|
||||
w="100%"
|
||||
maxW="100%"
|
||||
>
|
||||
<Flex align="center" gap={2} overflow="hidden" maxW="65%" flexShrink={1} flexWrap="nowrap">
|
||||
{showModelSelector && (
|
||||
<AIModelSelector
|
||||
list={modelList}
|
||||
value={selectedModel}
|
||||
showAvatar={false}
|
||||
onChange={setSelectedModel}
|
||||
bg={'myGray.50'}
|
||||
borderRadius={'lg'}
|
||||
/>
|
||||
)}
|
||||
{showTools && (
|
||||
<GateToolSelect
|
||||
selectedTools={selectedTools}
|
||||
onToolsChange={setSelectedToolIds}
|
||||
buttonSize={buttonSize}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<Flex align="center" gap="2px" flexShrink={0}>
|
||||
<IconButton
|
||||
aria-label="Upload file"
|
||||
icon={<MyIcon name={'support/gate/chat/paperclip'} w={'20px'} h={'20px'} />}
|
||||
size="auto" // 尝试移除buttonSize变量的影响
|
||||
variant="ghost"
|
||||
display="flex"
|
||||
padding="8px"
|
||||
alignItems="center"
|
||||
minW="36px" // 使用minW而不是w
|
||||
minH="36px" // 使用minH而不是h
|
||||
w="36px"
|
||||
h="36px"
|
||||
boxSize="36px" // 添加boxSize属性更强制性地控制尺寸
|
||||
onClick={() => onOpenSelectFile()}
|
||||
flexShrink={0}
|
||||
_hover={{
|
||||
background: 'var(--light-general-surface-opacity-005, rgba(17, 24, 36, 0.05))',
|
||||
'& svg path': {
|
||||
fill: '#3370FF !important'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
aria-label="Voice input"
|
||||
icon={<Icon name={'support/gate/chat/voiceGray'} w={'20px'} h={'20px'} />}
|
||||
size="auto"
|
||||
variant="ghost"
|
||||
display="flex"
|
||||
padding="8px"
|
||||
w="36px"
|
||||
h="36px"
|
||||
alignItems="center"
|
||||
onClick={() => VoiceInputRef.current?.onSpeak?.()}
|
||||
flexShrink={0}
|
||||
_hover={{
|
||||
background: 'var(--light-general-surface-opacity-005, rgba(17, 24, 36, 0.05))',
|
||||
'& svg path': {
|
||||
fill: '#3370FF !important'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<Box w="2px" h="16px" bg="#F0F1F6" mx={1} flexShrink={0} />
|
||||
|
||||
{isChatting ? (
|
||||
<IconButton
|
||||
aria-label="Stop"
|
||||
icon={
|
||||
<MyIcon
|
||||
animation={'zoomStopIcon 0.4s infinite alternate'}
|
||||
width={['22px', '25px']}
|
||||
height={['22px', '25px']}
|
||||
name={'stop'}
|
||||
color={'gray.500'}
|
||||
/>
|
||||
}
|
||||
size="auto"
|
||||
onClick={onStop}
|
||||
borderRadius="12px"
|
||||
w="36px"
|
||||
h="36px"
|
||||
variant="ghost"
|
||||
flexShrink={0}
|
||||
/>
|
||||
) : (
|
||||
<IconButton
|
||||
aria-label="Send"
|
||||
icon={
|
||||
<MyIcon
|
||||
name={'core/chat/sendFill'}
|
||||
width={['18px', '20px']}
|
||||
height={['18px', '20px']}
|
||||
color={'white'}
|
||||
/>
|
||||
}
|
||||
size="auto"
|
||||
bg={
|
||||
!canSendMessage
|
||||
? 'var(--light-general-surface-opacity-01, rgba(17, 24, 36, 0.10))'
|
||||
: '#3370FF'
|
||||
}
|
||||
_hover={{
|
||||
bg: !canSendMessage
|
||||
? 'var(--light-general-surface-opacity-01, rgba(17, 24, 36, 0.10))'
|
||||
: '#2860E1'
|
||||
}}
|
||||
borderRadius="12px"
|
||||
w="36px"
|
||||
h="36px"
|
||||
onClick={() => handleSend()}
|
||||
flexShrink={0}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<File onSelect={(files) => onSelectFile({ files })} />
|
||||
{/* <ComplianceTip type={'chat'} /> */}
|
||||
|
||||
{/* voice input and loading container */}
|
||||
{!inputValue && (
|
||||
<VoiceInput
|
||||
ref={VoiceInputRef}
|
||||
onSendMessage={onSendMessage}
|
||||
resetInputVal={resetInputVal}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(GateChatInput);
|
||||
@ -6,12 +6,10 @@ import React, {
|
||||
useCallback,
|
||||
useState,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
useMemo
|
||||
useImperativeHandle
|
||||
} from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { ChatBoxContext } from '../Provider';
|
||||
@ -23,7 +21,12 @@ export interface VoiceInputComponentRef {
|
||||
}
|
||||
|
||||
type VoiceInputProps = {
|
||||
onSendMessage: (params: { text: string; files?: any[]; autoTTSResponse?: boolean }) => void;
|
||||
onSendMessage: (params: {
|
||||
text: string;
|
||||
files?: any[];
|
||||
autoTTSResponse?: boolean;
|
||||
gateModel?: string;
|
||||
}) => void;
|
||||
resetInputVal: (val: { text: string }) => void;
|
||||
};
|
||||
|
||||
|
||||
@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import { Box, Flex, Text } from '@chakra-ui/react';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
|
||||
type Props = {
|
||||
teamName?: string;
|
||||
teamAvatar?: string;
|
||||
slogan?: string;
|
||||
};
|
||||
|
||||
const ChatWelcome = ({ teamName = 'FastGPT', teamAvatar, slogan }: Props) => {
|
||||
return (
|
||||
<Flex direction="column" align="center" gap={4} maxW="700px">
|
||||
<Flex align="center" gap={5}>
|
||||
{teamAvatar ? (
|
||||
<Flex
|
||||
w="60px"
|
||||
h="60px"
|
||||
borderRadius="15px"
|
||||
overflow="hidden"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<Avatar w="100%" h="100%" src={teamAvatar} borderRadius="15px" />
|
||||
</Flex>
|
||||
) : (
|
||||
<Box
|
||||
w="60px"
|
||||
h="60px"
|
||||
bg="white"
|
||||
border="1.25px solid #ECECEC"
|
||||
borderRadius="15px"
|
||||
overflow="hidden"
|
||||
>
|
||||
<Avatar w="100%" h="100%" src={teamAvatar} borderRadius="15px" />
|
||||
</Box>
|
||||
)}
|
||||
<Text fontSize="2xl" fontWeight="bold" color="#111824" fontFamily="Inter">
|
||||
{teamName}
|
||||
</Text>
|
||||
</Flex>
|
||||
{slogan && (
|
||||
<Text
|
||||
fontSize="lg"
|
||||
color="#707070"
|
||||
fontFamily="PingFang SC"
|
||||
textAlign="center"
|
||||
maxW="600px"
|
||||
whiteSpace="pre-line"
|
||||
>
|
||||
{slogan}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ChatWelcome);
|
||||
@ -14,7 +14,7 @@ import type {
|
||||
} from '@fastgpt/global/core/chat/type.d';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
import { Box, Checkbox } from '@chakra-ui/react';
|
||||
import { Box, Checkbox, Flex, HStack, Text } from '@chakra-ui/react';
|
||||
import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus';
|
||||
import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt';
|
||||
import { useForm } from 'react-hook-form';
|
||||
@ -67,6 +67,17 @@ import TimeBox from './components/TimeBox';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { VariableInputEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { valueTypeFormat } from '@fastgpt/global/core/workflow/runtime/utils';
|
||||
import GateChatInput from './Input/GateChatInput';
|
||||
import ChatWelcome from './components/ChatWelcome';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { useRouter } from 'next/router';
|
||||
import { getTeamGateConfig, getTeamGateConfigCopyRight } from '@/web/support/user/team/gate/api';
|
||||
import type { getGateConfigCopyRightResponse } from '@fastgpt/global/support/user/team/gate/api';
|
||||
import type { GateSchemaType } from '@fastgpt/global/support/user/team/gate/type';
|
||||
import type { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/node';
|
||||
import type { AppListItemType } from '@fastgpt/global/core/app/type';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import ComplianceTip from '@/components/common/ComplianceTip/index';
|
||||
|
||||
const FeedbackModal = dynamic(() => import('./components/FeedbackModal'));
|
||||
const ReadFeedbackModal = dynamic(() => import('./components/ReadFeedbackModal'));
|
||||
@ -89,6 +100,9 @@ type Props = OutLinkChatAuthProps &
|
||||
showVoiceIcon?: boolean;
|
||||
showEmptyIntro?: boolean;
|
||||
active?: boolean; // can use
|
||||
selectedTools?: FlowNodeTemplateType[];
|
||||
onSelectTools?: (toolIds: FlowNodeTemplateType[]) => void;
|
||||
recommendApps?: AppListItemType[];
|
||||
|
||||
onStartChat?: (e: StartChatFnProps) => Promise<
|
||||
StreamResponseType & {
|
||||
@ -105,7 +119,10 @@ const ChatBox = ({
|
||||
showEmptyIntro = false,
|
||||
active = true,
|
||||
onStartChat,
|
||||
chatType
|
||||
chatType,
|
||||
selectedTools,
|
||||
onSelectTools,
|
||||
recommendApps = []
|
||||
}: Props) => {
|
||||
const ScrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const { t } = useTranslation();
|
||||
@ -127,6 +144,7 @@ const ChatBox = ({
|
||||
const [questionGuides, setQuestionGuide] = useState<string[]>([]);
|
||||
|
||||
const appAvatar = useContextSelector(ChatItemContext, (v) => v.chatBoxData?.app?.avatar);
|
||||
const appIntro = useContextSelector(ChatItemContext, (v) => v.chatBoxData?.app?.intro);
|
||||
const userAvatar = useContextSelector(ChatItemContext, (v) => v.chatBoxData?.userAvatar);
|
||||
const chatBoxData = useContextSelector(ChatItemContext, (v) => v.chatBoxData);
|
||||
const ChatBoxRef = useContextSelector(ChatItemContext, (v) => v.ChatBoxRef);
|
||||
@ -407,6 +425,10 @@ const ChatBox = ({
|
||||
pluginController.current?.abort(signal);
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const inGateRoute = useMemo(() => {
|
||||
return router.pathname === '/chat/gate';
|
||||
}, [router.pathname]);
|
||||
/**
|
||||
* user confirm send prompt
|
||||
*/
|
||||
@ -417,7 +439,8 @@ const ChatBox = ({
|
||||
history = chatRecords,
|
||||
autoTTSResponse = false,
|
||||
isInteractivePrompt = false,
|
||||
hideInUI = false
|
||||
hideInUI = false,
|
||||
gateModel = ''
|
||||
}) => {
|
||||
variablesForm.handleSubmit(
|
||||
async ({ variables = {} }) => {
|
||||
@ -536,6 +559,7 @@ const ChatBox = ({
|
||||
});
|
||||
|
||||
const { responseText } = await onStartChat({
|
||||
gateModel,
|
||||
messages, // 保证最后一条是 Human 的消息
|
||||
responseChatItemId: responseChatId,
|
||||
controller: abortSignal,
|
||||
@ -1085,6 +1109,30 @@ const ChatBox = ({
|
||||
welcomeText
|
||||
]);
|
||||
|
||||
const [gateConfig, setGateConfig] = useState<GateSchemaType | undefined>(undefined);
|
||||
const [copyRightConfig, setCopyRightConfig] = useState<
|
||||
getGateConfigCopyRightResponse | undefined
|
||||
>(undefined);
|
||||
// 加载 gateConfig 和 copyRightConfig
|
||||
useEffect(() => {
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
const gateConfig = await getTeamGateConfig();
|
||||
setGateConfig(gateConfig);
|
||||
const copyRightConfig = await getTeamGateConfigCopyRight();
|
||||
setCopyRightConfig(copyRightConfig);
|
||||
} catch (error) {
|
||||
console.error('Failed to load gate config:', error);
|
||||
}
|
||||
};
|
||||
loadConfig();
|
||||
}, []);
|
||||
const { userInfo } = useUserStore();
|
||||
|
||||
const showWelcome = useMemo(() => {
|
||||
return router.pathname === '/chat/gate' && chatRecords.length === 0;
|
||||
}, [router.pathname, chatRecords.length]);
|
||||
|
||||
return (
|
||||
<MyBox
|
||||
isLoading={isLoading}
|
||||
@ -1094,10 +1142,108 @@ const ChatBox = ({
|
||||
position={'relative'}
|
||||
>
|
||||
<Script src={getWebReqUrl('/js/html2pdf.bundle.min.js')} strategy="lazyOnload"></Script>
|
||||
{/* chat box container */}
|
||||
{RenderRecords}
|
||||
|
||||
{chatRecords.length === 0 && showWelcome ? (
|
||||
<Flex
|
||||
flex={1}
|
||||
direction="column"
|
||||
align="center"
|
||||
justify="space-between"
|
||||
h="100%"
|
||||
w="100%"
|
||||
position="relative"
|
||||
maxW="1360px"
|
||||
mx="auto"
|
||||
pt={{ base: '100px', sm: '120px', md: '158px' }}
|
||||
px={{ base: '20px', sm: '30px', md: '40px' }}
|
||||
pb={{ base: '12px', sm: '12px' }}
|
||||
gap={{ base: 4, md: 6 }}
|
||||
>
|
||||
<Flex direction="column" align="center" justify="center" w="100%" gap="44px">
|
||||
<Box>
|
||||
<ChatWelcome
|
||||
teamName={copyRightConfig?.name || chatBoxData?.app?.name}
|
||||
teamAvatar={copyRightConfig?.logo}
|
||||
slogan={appIntro}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* message input */}
|
||||
{onStartChat && chatStarted && active && !isInteractive && (
|
||||
<Box w={{ base: 'calc(100% - 48px)', md: '700px' }} maxH="132px" h="100%" px={0}>
|
||||
<HStack mb={3}>
|
||||
{recommendApps.map((item) => (
|
||||
<HStack
|
||||
gap={1}
|
||||
key={item._id}
|
||||
border={'base'}
|
||||
px={2}
|
||||
py={1}
|
||||
borderRadius={'sm'}
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
bg: 'primary.50',
|
||||
borderColor: 'primary.400'
|
||||
}}
|
||||
onClick={() => {
|
||||
router.push(`/chat/gate/application?appId=${item._id}`);
|
||||
}}
|
||||
>
|
||||
<Avatar src={item.avatar} w="1rem" h="1rem" borderRadius={'sm'} />
|
||||
<Box fontSize={'sm'}>{item.name}</Box>
|
||||
</HStack>
|
||||
))}
|
||||
</HStack>
|
||||
<GateChatInput
|
||||
onSendMessage={sendPrompt}
|
||||
onStop={() => chatController.current?.abort('stop')}
|
||||
TextareaDom={TextareaDom}
|
||||
resetInputVal={resetInputVal}
|
||||
chatForm={chatForm}
|
||||
placeholder={gateConfig?.placeholderText || '你可以问我任何问题'}
|
||||
selectedTools={selectedTools}
|
||||
onSelectTools={onSelectTools}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{/* 移动端下的版权信息容器 */}
|
||||
<Box w="100%" mt="auto">
|
||||
{/* 在inGateRoute状态下显示底部语句 */}
|
||||
{inGateRoute && <ComplianceTip type={'chat'} />}
|
||||
</Box>
|
||||
</Flex>
|
||||
) : (
|
||||
<>
|
||||
{RenderRecords}
|
||||
|
||||
{/* 移动端下的输入框和版权信息容器 */}
|
||||
<Flex direction="column" w="100%" mb={{ base: '12px', sm: 0 }}>
|
||||
{/* message input */}
|
||||
{onStartChat && chatStarted && active && !isInteractive && (
|
||||
<Box
|
||||
m={['0 auto', '10px auto']}
|
||||
w={'100%'}
|
||||
maxW={['auto', 'min(800px, 100%)']}
|
||||
px={['16px', 5]}
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
{inGateRoute && (
|
||||
<GateChatInput
|
||||
onSendMessage={sendPrompt}
|
||||
onStop={() => chatController.current?.abort('stop')}
|
||||
TextareaDom={TextareaDom}
|
||||
resetInputVal={resetInputVal}
|
||||
chatForm={chatForm}
|
||||
placeholder={gateConfig?.placeholderText || t('common:gate.placeholder')}
|
||||
selectedTools={selectedTools}
|
||||
onSelectTools={onSelectTools}
|
||||
/>
|
||||
)}
|
||||
{!inGateRoute && (
|
||||
<ChatInput
|
||||
onSendMessage={sendPrompt}
|
||||
onStop={() => chatController.current?.abort('stop')}
|
||||
@ -1106,6 +1252,15 @@ const ChatBox = ({
|
||||
chatForm={chatForm}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 在inGateRoute状态下显示底部语句 */}
|
||||
{/* {inGateRoute && <ComplianceTip type={'chat'} />} */}
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* user feedback modal */}
|
||||
{!!feedbackId && chatId && (
|
||||
<FeedbackModal
|
||||
|
||||
@ -27,6 +27,8 @@ export type ChatBoxInputType = {
|
||||
files?: UserInputFileItemType[];
|
||||
isInteractivePrompt?: boolean;
|
||||
hideInUI?: boolean;
|
||||
gateModel?: string;
|
||||
selectedTool?: string | null;
|
||||
};
|
||||
|
||||
export type SendPromptFnType = (
|
||||
|
||||
@ -26,6 +26,8 @@ export type StartChatFnProps = {
|
||||
controller: AbortController;
|
||||
variables: Record<string, any>;
|
||||
generatingMessage: (e: generatingMessageProps) => void;
|
||||
gateModel?: string;
|
||||
selectedTool?: string | null;
|
||||
};
|
||||
|
||||
export type onStartChatType = (e: StartChatFnProps) => Promise<
|
||||
|
||||
@ -46,16 +46,22 @@ function MemberModal({
|
||||
const collaboratorList = useContextSelector(CollaboratorContext, (v) => v.collaboratorList);
|
||||
const [filterClass, setFilterClass] = useState<'member' | 'org' | 'group'>();
|
||||
const {
|
||||
paths,
|
||||
paths: orgPaths,
|
||||
onClickOrg,
|
||||
members: orgMembers,
|
||||
MemberScrollData: OrgMemberScrollData,
|
||||
onPathClick,
|
||||
onPathClick: onOrgPathClick,
|
||||
orgs,
|
||||
searchKey,
|
||||
setSearchKey
|
||||
} = useOrg({ withPermission: false });
|
||||
|
||||
const onExpandOrg = (org: OrgListItemType) => {
|
||||
setFilterClass('org');
|
||||
setSearchKey('');
|
||||
onClickOrg(org);
|
||||
};
|
||||
|
||||
const {
|
||||
data: members,
|
||||
ScrollData: TeamMemberScrollData,
|
||||
@ -104,8 +110,8 @@ function MemberModal({
|
||||
permissionList?.read?.value
|
||||
);
|
||||
const perLabel = useMemo(() => {
|
||||
if (selectedPermission === undefined) return '';
|
||||
return getPerLabelList(selectedPermission!).join('、');
|
||||
if (selectedPermission === undefined) return [];
|
||||
return getPerLabelList(selectedPermission!);
|
||||
}, [getPerLabelList, selectedPermission]);
|
||||
|
||||
const onUpdateCollaborators = useContextSelector(
|
||||
@ -194,6 +200,7 @@ function MemberModal({
|
||||
placeholder={t('user:search_group_org_user')}
|
||||
bgColor="myGray.50"
|
||||
onChange={(e) => setSearchKey(e.target.value)}
|
||||
value={searchKey}
|
||||
/>
|
||||
|
||||
<Flex flexDirection="column" mt="3" overflow={'auto'} flex={'1 0 0'} h={0}>
|
||||
@ -238,21 +245,21 @@ function MemberModal({
|
||||
? t('user:team.org.org')
|
||||
: t('user:team.group.group')
|
||||
},
|
||||
...paths
|
||||
...orgPaths
|
||||
]}
|
||||
onClick={(parentId) => {
|
||||
if (parentId === '') {
|
||||
setFilterClass(undefined);
|
||||
onPathClick('');
|
||||
onOrgPathClick('');
|
||||
} else if (
|
||||
parentId === 'member' ||
|
||||
parentId === 'org' ||
|
||||
parentId === 'group'
|
||||
) {
|
||||
setFilterClass(parentId);
|
||||
onPathClick('');
|
||||
onOrgPathClick('');
|
||||
} else {
|
||||
onPathClick(parentId);
|
||||
onOrgPathClick(parentId);
|
||||
}
|
||||
}}
|
||||
rootName={t('common:Team')}
|
||||
@ -329,8 +336,8 @@ function MemberModal({
|
||||
bgColor: 'myGray.200'
|
||||
}}
|
||||
onClick={(e) => {
|
||||
onClickOrg(org);
|
||||
// setPath(getOrgChildrenPath(org));
|
||||
// onClickOrg(org);
|
||||
onExpandOrg(org);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
@ -438,7 +445,7 @@ function MemberModal({
|
||||
borderRadius={'md'}
|
||||
h={'32px'}
|
||||
>
|
||||
{t(perLabel as any)}
|
||||
{perLabel.map((item) => t(item as any)).join('、')}
|
||||
<ChevronDownIcon fontSize={'md'} />
|
||||
</Flex>
|
||||
}
|
||||
|
||||
@ -7,7 +7,8 @@ import {
|
||||
Radio,
|
||||
useOutsideClick,
|
||||
HStack,
|
||||
MenuButton
|
||||
MenuButton,
|
||||
Checkbox
|
||||
} from '@chakra-ui/react';
|
||||
import React, { useMemo, useRef, useState } from 'react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
@ -82,27 +83,28 @@ function PermissionSelect({
|
||||
const selectedSingleValue = useMemo(() => {
|
||||
if (!permissionList) return undefined;
|
||||
|
||||
const per = new Permission({ per: value });
|
||||
const per = new Permission({ per: value, permissionList });
|
||||
|
||||
if (per.hasManagePer) return permissionList['manage'].value;
|
||||
if (per.hasWritePer) return permissionList['write'].value;
|
||||
|
||||
return permissionList['read'].value;
|
||||
}, [permissionList, value]);
|
||||
// const selectedMultipleValues = useMemo(() => {
|
||||
// const per = new Permission({ per: value });
|
||||
//
|
||||
// return permissionSelectList.multipleCheckBoxList
|
||||
// .filter((item) => {
|
||||
// return per.checkPer(item.value);
|
||||
// })
|
||||
// .map((item) => item.value);
|
||||
// }, [permissionSelectList.multipleCheckBoxList, value]);
|
||||
const selectedMultipleValues = useMemo(() => {
|
||||
const per = new Permission({ per: value, permissionList });
|
||||
|
||||
const onSelectPer = (per: PermissionValueType) => {
|
||||
if (per === value) return;
|
||||
onChange(per);
|
||||
setIsOpen(false);
|
||||
return permissionSelectList.multipleCheckBoxList
|
||||
.filter((item) => {
|
||||
return per.checkPer(item.value);
|
||||
})
|
||||
.map((item) => item.value);
|
||||
}, [permissionList, permissionSelectList.multipleCheckBoxList, value]);
|
||||
|
||||
const onSelectPer = (perValue: PermissionValueType) => {
|
||||
if (perValue === value) return;
|
||||
const per = new Permission({ per: perValue, permissionList });
|
||||
per.addPer(...selectedMultipleValues);
|
||||
onChange(per.value);
|
||||
};
|
||||
|
||||
useOutsideClick({
|
||||
@ -155,7 +157,7 @@ function PermissionSelect({
|
||||
{/* The list of single select permissions */}
|
||||
{permissionSelectList.singleCheckBoxList.map((item) => {
|
||||
const change = () => {
|
||||
const per = new Permission({ per: value });
|
||||
const per = new Permission({ per: value, permissionList });
|
||||
per.removePer(selectedSingleValue);
|
||||
per.addPer(item.value);
|
||||
onSelectPer(per.value);
|
||||
@ -184,50 +186,65 @@ function PermissionSelect({
|
||||
);
|
||||
})}
|
||||
|
||||
{/* <MyDivider my={3} />
|
||||
|
||||
{multipleValues.length > 0 && <Box m="4">其他权限(多选)</Box>} */}
|
||||
|
||||
{/* The list of multiple select permissions */}
|
||||
{/* {list
|
||||
.filter((item) => item.type === 'multiple')
|
||||
.map((item) => {
|
||||
{permissionSelectList.multipleCheckBoxList.length > 0 && (
|
||||
<>
|
||||
<MyDivider my={2} />
|
||||
{permissionSelectList.multipleCheckBoxList.map((item) => {
|
||||
const isChecked = selectedMultipleValues.includes(item.value);
|
||||
const isDisabled = new Permission({
|
||||
per: selectedSingleValue,
|
||||
permissionList
|
||||
}).checkPer(item.value);
|
||||
const change = () => {
|
||||
if (checkPermission(valueState, item.value)) {
|
||||
setValueState(new Permission(valueState).remove(item.value).value);
|
||||
if (isDisabled) return;
|
||||
const per = new Permission({ per: value, permissionList });
|
||||
if (isChecked) {
|
||||
per.removePer(item.value);
|
||||
} else {
|
||||
setValueState(new Permission(valueState).add(item.value).value);
|
||||
per.addPer(item.value);
|
||||
}
|
||||
onChange(per.value);
|
||||
};
|
||||
return (
|
||||
<Flex
|
||||
key={item.value}
|
||||
{...(checkPermission(valueState, item.value)
|
||||
{...(isChecked
|
||||
? {
|
||||
color: 'primary.500',
|
||||
bg: 'myWhite.300'
|
||||
color: 'primary.600'
|
||||
}
|
||||
: {})}
|
||||
whiteSpace="pre-wrap"
|
||||
flexDirection="row"
|
||||
justifyContent="start"
|
||||
p="2"
|
||||
_hover={{
|
||||
bg: 'myGray.50'
|
||||
}}
|
||||
{...MenuStyle}
|
||||
maxW={['70vw', '260px']}
|
||||
{...(isDisabled
|
||||
? {
|
||||
cursor: 'not-allowed'
|
||||
}
|
||||
: {
|
||||
cursor: 'pointer'
|
||||
})}
|
||||
onClick={change}
|
||||
>
|
||||
<Checkbox
|
||||
size="lg"
|
||||
isChecked={checkPermission(valueState, item.value)}
|
||||
onChange={change}
|
||||
isChecked={isChecked}
|
||||
// onChange={(e) => {
|
||||
// e.stopPropagation();
|
||||
// e.preventDefault();
|
||||
// }}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
<Flex px="4" flexDirection="column" onClick={change}>
|
||||
<Box fontWeight="500">{item.name}</Box>
|
||||
<Box fontWeight="400">{item.description}</Box>
|
||||
</Flex>
|
||||
<Box ml={4}>
|
||||
<Box>{t(item.name as any)}</Box>
|
||||
<Box color={'myGray.500'} fontSize={'mini'}>
|
||||
{t(item.description as any)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
})}*/}
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
|
||||
{onDelete && (
|
||||
<>
|
||||
<MyDivider my={2} h={'2px'} borderColor={'myGray.200'} />
|
||||
|
||||
@ -22,7 +22,8 @@ export enum TabEnum {
|
||||
'apikey' = 'apikey',
|
||||
'loginout' = 'loginout',
|
||||
'team' = 'team',
|
||||
'model' = 'model'
|
||||
'model' = 'model',
|
||||
gateway = 'gateway'
|
||||
}
|
||||
|
||||
const AccountContainer = ({
|
||||
@ -60,8 +61,17 @@ const AccountContainer = ({
|
||||
icon: 'support/usage/usageRecordLight',
|
||||
label: t('account:usage_records'),
|
||||
value: TabEnum.usage
|
||||
},
|
||||
...(userInfo?.team?.permission.hasManagePer
|
||||
? [
|
||||
{
|
||||
icon: 'support/gate/gateLight',
|
||||
label: t('account:gateways'),
|
||||
value: TabEnum.gateway
|
||||
}
|
||||
]
|
||||
: [])
|
||||
]
|
||||
: []),
|
||||
...(feConfigs?.show_pay && userInfo?.team?.permission.hasManagePer
|
||||
? [
|
||||
|
||||
@ -0,0 +1,273 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { ModalBody, ModalFooter, Button, Text, Flex, Box, IconButton } from '@chakra-ui/react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import type { SelectAppItemType } from '@fastgpt/global/core/workflow/template/system/abandoned/runApp/type';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import SelectMultipleResource from './SelectMultipleResource';
|
||||
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import DndDrag, { Draggable } from '@fastgpt/web/components/common/DndDrag';
|
||||
import {
|
||||
type GetResourceFolderListProps,
|
||||
type GetResourceListItemResponse
|
||||
} from '@fastgpt/global/common/parentFolder/type';
|
||||
import { getMyApps } from '@/web/core/app/api';
|
||||
import { listFeatureApps, batchUpdateFeaturedApps } from '@/web/support/user/team/gate/featureApp';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
|
||||
// 扩展的应用类型,包含显示所需的属性
|
||||
type ExtendedSelectAppItemType = SelectAppItemType & {
|
||||
name: string;
|
||||
avatar?: string;
|
||||
};
|
||||
|
||||
const AddFeatureAppModal = ({
|
||||
isOpen = true,
|
||||
value,
|
||||
filterAppIds = [],
|
||||
onClose,
|
||||
onSuccess
|
||||
}: {
|
||||
isOpen?: boolean;
|
||||
value?: ExtendedSelectAppItemType[];
|
||||
filterAppIds?: string[];
|
||||
onClose: () => void;
|
||||
onSuccess: (e: ExtendedSelectAppItemType[]) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [selectedApps, setSelectedApps] = useState<ExtendedSelectAppItemType[]>([]);
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
|
||||
// 使用 listFeatureApps 初始化已选择的应用数组
|
||||
const { data: featureApps = [], loading: loadingFeatureApps } = useRequest2(
|
||||
() => listFeatureApps(),
|
||||
{
|
||||
manual: false,
|
||||
onSuccess: (data) => {
|
||||
const initialSelectedApps = data.map((app) => ({
|
||||
id: app._id,
|
||||
name: app.name,
|
||||
avatar: app.avatar
|
||||
}));
|
||||
setSelectedApps(initialSelectedApps);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const getAppList = useCallback(
|
||||
async ({ parentId }: GetResourceFolderListProps) => {
|
||||
return getMyApps({
|
||||
parentId,
|
||||
searchKey,
|
||||
type: [AppTypeEnum.folder, AppTypeEnum.simple, AppTypeEnum.workflow]
|
||||
}).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, searchKey]
|
||||
);
|
||||
|
||||
const handleAppSelect = useCallback((appId: string, appData: GetResourceListItemResponse) => {
|
||||
setSelectedApps((prev) => {
|
||||
const exists = prev.find((app) => app.id === appId);
|
||||
if (exists) {
|
||||
// 如果已存在,则移除
|
||||
return prev.filter((app) => app.id !== appId);
|
||||
} else {
|
||||
// 如果不存在,则添加到末尾
|
||||
return [...prev, { id: appId, name: appData.name, avatar: appData.avatar }];
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleAppUnselect = useCallback((appId: string) => {
|
||||
setSelectedApps((prev) => prev.filter((app) => app.id !== appId));
|
||||
}, []);
|
||||
|
||||
// 处理拖拽排序
|
||||
const handleDragEnd = useCallback((reorderedList: ExtendedSelectAppItemType[]) => {
|
||||
setSelectedApps(reorderedList);
|
||||
}, []);
|
||||
|
||||
// 批量更新特色应用
|
||||
const { runAsync: updateFeaturedApps, loading: isUpdating } = useRequest2(
|
||||
() => {
|
||||
const updates = [{ featuredApps: selectedApps.map((app) => app.id) }];
|
||||
return batchUpdateFeaturedApps(updates);
|
||||
},
|
||||
{
|
||||
manual: true,
|
||||
onSuccess: () => {
|
||||
onSuccess(selectedApps);
|
||||
onClose();
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('更新特色应用失败:', error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen={isOpen}
|
||||
title={t('common:core.module.Select app')}
|
||||
iconSrc="/imgs/workflow/ai.svg"
|
||||
onClose={onClose}
|
||||
position={'relative'}
|
||||
w={'900px'}
|
||||
maxW={'90vw'}
|
||||
>
|
||||
<ModalBody flex={'1 0 0'} overflow={'hidden'} minH={'500px'} position={'relative'}>
|
||||
<Flex h="100%" gap={4}>
|
||||
{/* 左侧应用选择区域 */}
|
||||
<Flex direction="column" flex={1} h="100%">
|
||||
{/* 搜索框 */}
|
||||
<Box mb={4}>
|
||||
<SearchInput
|
||||
value={searchKey}
|
||||
onChange={(e) => setSearchKey(e.target.value)}
|
||||
placeholder={t('app:search_app')}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* 应用选择区域 */}
|
||||
<Box flex={1} overflow="auto">
|
||||
<SelectMultipleResource
|
||||
selectedIds={selectedApps.map((app) => app.id)}
|
||||
onSelect={handleAppSelect}
|
||||
server={getAppList}
|
||||
searchKey={searchKey}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
{/* 右侧已选择应用排序区域 */}
|
||||
<Box w="300px" h="100%" borderLeft="1px solid" borderColor="gray.200" pl={4}>
|
||||
<Flex direction="column" h="100%">
|
||||
<Text fontSize="sm" fontWeight="medium" mb={3}>
|
||||
{t('common:selected')} {selectedApps.length}
|
||||
</Text>
|
||||
|
||||
{selectedApps.length > 0 ? (
|
||||
<Box flex={1} overflow="auto">
|
||||
<DndDrag<ExtendedSelectAppItemType>
|
||||
onDragEndCb={handleDragEnd}
|
||||
dataList={selectedApps}
|
||||
>
|
||||
{({ provided }) => (
|
||||
<Flex
|
||||
flexDirection={'column'}
|
||||
gap={2}
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
{selectedApps.map((app, index) => (
|
||||
<Draggable key={app.id} draggableId={String(app.id)} index={index}>
|
||||
{(provided, snapshot) => (
|
||||
<Flex
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
style={{
|
||||
...provided.draggableProps.style,
|
||||
opacity: snapshot.isDragging ? 0.8 : 1
|
||||
}}
|
||||
alignItems="center"
|
||||
gap={2}
|
||||
p={2}
|
||||
bg="white"
|
||||
borderRadius="md"
|
||||
border="1px solid"
|
||||
borderColor="gray.200"
|
||||
fontSize="sm"
|
||||
_hover={{
|
||||
bg: 'gray.50',
|
||||
borderColor: 'gray.300'
|
||||
}}
|
||||
>
|
||||
{/* 拖拽图标 */}
|
||||
<Flex
|
||||
{...provided.dragHandleProps}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
w="16px"
|
||||
h="16px"
|
||||
cursor="grab"
|
||||
_active={{ cursor: 'grabbing' }}
|
||||
>
|
||||
<MyIcon name="drag" w={'10px'} h={'12px'} color={'gray.500'} />
|
||||
</Flex>
|
||||
|
||||
{/* 应用图标 */}
|
||||
<Avatar src={app.avatar} w="20px" h="20px" borderRadius="4px" />
|
||||
|
||||
{/* 应用名称 */}
|
||||
<Text flex={1} fontSize="12px" fontWeight="500" noOfLines={1}>
|
||||
{app.name}
|
||||
</Text>
|
||||
|
||||
{/* 删除按钮 */}
|
||||
<IconButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
icon={<MyIcon name="delete" w="12px" />}
|
||||
aria-label="remove"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleAppUnselect(app.id);
|
||||
}}
|
||||
_hover={{ bg: 'red.50', color: 'red.500' }}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</Flex>
|
||||
)}
|
||||
</DndDrag>
|
||||
</Box>
|
||||
) : (
|
||||
<Flex
|
||||
flex={1}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
color="gray.500"
|
||||
fontSize="sm"
|
||||
>
|
||||
<Text>{t('common:no_selected_apps')}</Text>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant={'whiteBase'} onClick={onClose}>
|
||||
{t('common:Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
ml={2}
|
||||
isDisabled={selectedApps.length === 0 || loadingFeatureApps || isUpdating}
|
||||
isLoading={isUpdating}
|
||||
onClick={() => {
|
||||
if (selectedApps.length === 0) return;
|
||||
updateFeaturedApps();
|
||||
}}
|
||||
>
|
||||
{t('common:Confirm')} ({selectedApps.length})
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(AddFeatureAppModal);
|
||||
@ -0,0 +1,270 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { ModalBody, ModalFooter, Button, Text, Flex, Box, IconButton } from '@chakra-ui/react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import type { SelectAppItemType } from '@fastgpt/global/core/workflow/template/system/abandoned/runApp/type';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import SelectMultipleResource from './SelectMultipleResource';
|
||||
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import DndDrag, { Draggable } from '@fastgpt/web/components/common/DndDrag';
|
||||
import {
|
||||
type GetResourceFolderListProps,
|
||||
type GetResourceListItemResponse
|
||||
} from '@fastgpt/global/common/parentFolder/type';
|
||||
import { getMyApps } from '@/web/core/app/api';
|
||||
import { listQuickApps, batchUpdateQuickApps } from '@/web/support/user/team/gate/quickApp';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
|
||||
// 扩展的应用类型,包含显示所需的属性
|
||||
type ExtendedSelectAppItemType = SelectAppItemType & {
|
||||
name: string;
|
||||
avatar?: string;
|
||||
};
|
||||
|
||||
const AddQuickAppModal = ({
|
||||
isOpen = true,
|
||||
value,
|
||||
filterAppIds = [],
|
||||
onClose,
|
||||
onSuccess
|
||||
}: {
|
||||
isOpen?: boolean;
|
||||
value?: ExtendedSelectAppItemType[];
|
||||
filterAppIds?: string[];
|
||||
onClose: () => void;
|
||||
onSuccess: (e: ExtendedSelectAppItemType[]) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [selectedApps, setSelectedApps] = useState<ExtendedSelectAppItemType[]>([]);
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
|
||||
// 使用 listQuickApps 初始化已选择的应用数组
|
||||
const { data: quickApps = [], loading: loadingQuickApps } = useRequest2(() => listQuickApps(), {
|
||||
manual: false,
|
||||
onSuccess: (data) => {
|
||||
const initialSelectedApps = data.map((app) => ({
|
||||
id: app._id,
|
||||
name: app.name,
|
||||
avatar: app.avatar
|
||||
}));
|
||||
setSelectedApps(initialSelectedApps);
|
||||
}
|
||||
});
|
||||
|
||||
const getAppList = useCallback(
|
||||
async ({ parentId }: GetResourceFolderListProps) => {
|
||||
return getMyApps({
|
||||
parentId,
|
||||
searchKey,
|
||||
type: [AppTypeEnum.folder, AppTypeEnum.simple, AppTypeEnum.workflow]
|
||||
}).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, searchKey]
|
||||
);
|
||||
|
||||
const handleAppSelect = useCallback((appId: string, appData: GetResourceListItemResponse) => {
|
||||
setSelectedApps((prev) => {
|
||||
const exists = prev.find((app) => app.id === appId);
|
||||
if (exists) {
|
||||
// 如果已存在,则移除
|
||||
return prev.filter((app) => app.id !== appId);
|
||||
} else {
|
||||
// 如果不存在,则添加到末尾
|
||||
return [...prev, { id: appId, name: appData.name, avatar: appData.avatar }];
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleAppUnselect = useCallback((appId: string) => {
|
||||
setSelectedApps((prev) => prev.filter((app) => app.id !== appId));
|
||||
}, []);
|
||||
|
||||
// 处理拖拽排序
|
||||
const handleDragEnd = useCallback((reorderedList: ExtendedSelectAppItemType[]) => {
|
||||
setSelectedApps(reorderedList);
|
||||
}, []);
|
||||
|
||||
// 批量更新快速应用
|
||||
const { runAsync: updateQuickApps, loading: isUpdating } = useRequest2(
|
||||
() => {
|
||||
const updates = [{ quickApps: selectedApps.map((app) => app.id) }];
|
||||
return batchUpdateQuickApps(updates);
|
||||
},
|
||||
{
|
||||
manual: true,
|
||||
onSuccess: () => {
|
||||
onSuccess(selectedApps);
|
||||
onClose();
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('更新快速应用失败:', error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen={isOpen}
|
||||
title={t('common:core.module.Select app')}
|
||||
iconSrc="/imgs/workflow/ai.svg"
|
||||
onClose={onClose}
|
||||
position={'relative'}
|
||||
w={'900px'}
|
||||
maxW={'90vw'}
|
||||
>
|
||||
<ModalBody flex={'1 0 0'} overflow={'hidden'} minH={'500px'} position={'relative'}>
|
||||
<Flex h="100%" gap={4}>
|
||||
{/* 左侧应用选择区域 */}
|
||||
<Flex direction="column" flex={1} h="100%">
|
||||
{/* 搜索框 */}
|
||||
<Box mb={4}>
|
||||
<SearchInput
|
||||
value={searchKey}
|
||||
onChange={(e) => setSearchKey(e.target.value)}
|
||||
placeholder={t('app:search_app')}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* 应用选择区域 */}
|
||||
<Box flex={1} overflow="auto">
|
||||
<SelectMultipleResource
|
||||
selectedIds={selectedApps.map((app) => app.id)}
|
||||
onSelect={handleAppSelect}
|
||||
server={getAppList}
|
||||
searchKey={searchKey}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
{/* 右侧已选择应用排序区域 */}
|
||||
<Box w="300px" h="100%" borderLeft="1px solid" borderColor="gray.200" pl={4}>
|
||||
<Flex direction="column" h="100%">
|
||||
<Text fontSize="sm" fontWeight="medium" mb={3}>
|
||||
{t('common:selected')} {selectedApps.length}
|
||||
</Text>
|
||||
|
||||
{selectedApps.length > 0 ? (
|
||||
<Box flex={1} overflow="auto">
|
||||
<DndDrag<ExtendedSelectAppItemType>
|
||||
onDragEndCb={handleDragEnd}
|
||||
dataList={selectedApps}
|
||||
>
|
||||
{({ provided }) => (
|
||||
<Flex
|
||||
flexDirection={'column'}
|
||||
gap={2}
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
{selectedApps.map((app, index) => (
|
||||
<Draggable key={app.id} draggableId={String(app.id)} index={index}>
|
||||
{(provided, snapshot) => (
|
||||
<Flex
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
style={{
|
||||
...provided.draggableProps.style,
|
||||
opacity: snapshot.isDragging ? 0.8 : 1
|
||||
}}
|
||||
alignItems="center"
|
||||
gap={2}
|
||||
p={2}
|
||||
bg="white"
|
||||
borderRadius="md"
|
||||
border="1px solid"
|
||||
borderColor="gray.200"
|
||||
fontSize="sm"
|
||||
_hover={{
|
||||
bg: 'gray.50',
|
||||
borderColor: 'gray.300'
|
||||
}}
|
||||
>
|
||||
{/* 拖拽图标 */}
|
||||
<Flex
|
||||
{...provided.dragHandleProps}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
w="16px"
|
||||
h="16px"
|
||||
cursor="grab"
|
||||
_active={{ cursor: 'grabbing' }}
|
||||
>
|
||||
<MyIcon name="drag" w={'10px'} h={'12px'} color={'gray.500'} />
|
||||
</Flex>
|
||||
|
||||
{/* 应用图标 */}
|
||||
<Avatar src={app.avatar} w="20px" h="20px" borderRadius="4px" />
|
||||
|
||||
{/* 应用名称 */}
|
||||
<Text flex={1} fontSize="12px" fontWeight="500" noOfLines={1}>
|
||||
{app.name}
|
||||
</Text>
|
||||
|
||||
{/* 删除按钮 */}
|
||||
<IconButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
icon={<MyIcon name="delete" w="12px" />}
|
||||
aria-label="remove"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleAppUnselect(app.id);
|
||||
}}
|
||||
_hover={{ bg: 'red.50', color: 'red.500' }}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</Flex>
|
||||
)}
|
||||
</DndDrag>
|
||||
</Box>
|
||||
) : (
|
||||
<Flex
|
||||
flex={1}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
color="gray.500"
|
||||
fontSize="sm"
|
||||
>
|
||||
<Text>{t('common:no_selected_apps')}</Text>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant={'whiteBase'} onClick={onClose}>
|
||||
{t('common:Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
ml={2}
|
||||
isDisabled={selectedApps.length === 0 || loadingQuickApps || isUpdating}
|
||||
isLoading={isUpdating}
|
||||
onClick={() => {
|
||||
if (selectedApps.length === 0) return;
|
||||
updateQuickApps();
|
||||
}}
|
||||
>
|
||||
{t('common:Confirm')} ({selectedApps.length})
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(AddQuickAppModal);
|
||||
688
projects/app/src/pageComponents/account/gateway/AppTable.tsx
Normal file
@ -0,0 +1,688 @@
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
HStack,
|
||||
Text,
|
||||
IconButton,
|
||||
Button,
|
||||
useDisclosure,
|
||||
Tooltip,
|
||||
Wrap,
|
||||
WrapItem,
|
||||
Checkbox,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuList,
|
||||
MenuItem
|
||||
} from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import React, { useMemo, useState, useEffect, useCallback } from 'react';
|
||||
import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||
import MultipleSelect, {
|
||||
useMultipleSelect
|
||||
} from '@fastgpt/web/components/common/MySelect/MultipleSelect';
|
||||
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { delAppById } from '@/web/core/app/api';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import type { AppListItemType } from '@fastgpt/global/core/app/type.d';
|
||||
import { getTeamTags } from '@/web/core/app/api/tags';
|
||||
import type { TagSchemaType } from '@fastgpt/global/core/app/tags';
|
||||
import GateAppInfoModal from './GateAppInfoModal';
|
||||
import TagManageModal from './TagManageModal';
|
||||
import DndDrag, { Draggable } from '@fastgpt/web/components/common/DndDrag';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import { listFeatureApps, reorderFeatureApps } from '@/web/support/user/team/gate/featureApp';
|
||||
import AddFeatureAppModal from './AddFeatureAppModal';
|
||||
|
||||
// 设置最大可见标签数
|
||||
const MAX_VISIBLE_TAGS = 2;
|
||||
|
||||
// 自定义 hook:应用选择逻辑
|
||||
const useAppSelection = (filteredApps: AppListItemType[]) => {
|
||||
const [selectedAppIds, setSelectedAppIds] = useState<string[]>([]);
|
||||
|
||||
const handleAppSelect = useCallback((appId: string, isSelected: boolean) => {
|
||||
setSelectedAppIds((prev) =>
|
||||
isSelected ? [...prev.filter((id) => id !== appId), appId] : prev.filter((id) => id !== appId)
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleSelectAll = useCallback(
|
||||
(isSelected: boolean) => {
|
||||
setSelectedAppIds(isSelected ? filteredApps.map((app) => app._id) : []);
|
||||
},
|
||||
[filteredApps]
|
||||
);
|
||||
|
||||
const isAllSelected = useMemo(
|
||||
() => filteredApps.length > 0 && filteredApps.every((app) => selectedAppIds.includes(app._id)),
|
||||
[filteredApps, selectedAppIds]
|
||||
);
|
||||
|
||||
const isIndeterminate = useMemo(() => {
|
||||
const selectedCount = filteredApps.filter((app) => selectedAppIds.includes(app._id)).length;
|
||||
return selectedCount > 0 && selectedCount < filteredApps.length;
|
||||
}, [filteredApps, selectedAppIds]);
|
||||
|
||||
return {
|
||||
selectedAppIds,
|
||||
handleAppSelect,
|
||||
handleSelectAll,
|
||||
isAllSelected,
|
||||
isIndeterminate
|
||||
};
|
||||
};
|
||||
|
||||
// 标签组件
|
||||
const AppTags = ({ tags, tagMap }: { tags?: string[]; tagMap: Map<string, TagSchemaType> }) => {
|
||||
if (!tags?.length) return null;
|
||||
|
||||
const validTags = tags.filter((tagId) => tagMap.get(tagId));
|
||||
const visibleTags = validTags.slice(0, MAX_VISIBLE_TAGS);
|
||||
const remainingCount = Math.max(0, validTags.length - MAX_VISIBLE_TAGS);
|
||||
|
||||
const TagItem = ({ tagId }: { tagId: string }) => {
|
||||
const tag = tagMap.get(tagId);
|
||||
if (!tag) return null;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
padding="10px 8px"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
height="22px"
|
||||
minWidth="32px"
|
||||
borderRadius="6px"
|
||||
backgroundColor="#F4F4F5"
|
||||
>
|
||||
<Text
|
||||
fontSize="12px"
|
||||
fontWeight="500"
|
||||
lineHeight="16px"
|
||||
color="#525252"
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
{tag.name}
|
||||
</Text>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<HStack spacing={2} wrap="wrap">
|
||||
{visibleTags.map((tagId) => (
|
||||
<TagItem key={tagId} tagId={tagId} />
|
||||
))}
|
||||
{remainingCount > 0 && (
|
||||
<Tooltip
|
||||
label={
|
||||
<Wrap spacing={2} maxW="300px" p={2}>
|
||||
{validTags.slice(MAX_VISIBLE_TAGS).map((tagId) => (
|
||||
<WrapItem key={tagId}>
|
||||
<TagItem tagId={tagId} />
|
||||
</WrapItem>
|
||||
))}
|
||||
</Wrap>
|
||||
}
|
||||
hasArrow
|
||||
placement="top"
|
||||
bg="white"
|
||||
boxShadow="lg"
|
||||
>
|
||||
<Flex
|
||||
padding="10px 8px"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
height="22px"
|
||||
width="31px"
|
||||
borderRadius="6px"
|
||||
backgroundColor="#F4F4F5"
|
||||
>
|
||||
<Text fontSize="12px" fontWeight="500" color="#525252">
|
||||
+{remainingCount}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
)}
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
// 应用行组件
|
||||
const AppRow = ({
|
||||
app,
|
||||
index,
|
||||
tagMap,
|
||||
selectedAppIds,
|
||||
onAppSelect,
|
||||
onEdit,
|
||||
onDelete
|
||||
}: {
|
||||
app: AppListItemType;
|
||||
index: number;
|
||||
tagMap: Map<string, TagSchemaType>;
|
||||
selectedAppIds: string[];
|
||||
onAppSelect: (appId: string, isSelected: boolean) => void;
|
||||
onEdit: (app: AppListItemType) => void;
|
||||
onDelete: (appId: string) => void;
|
||||
}) => {
|
||||
return (
|
||||
<Draggable key={app._id} draggableId={String(app._id)} index={index}>
|
||||
{(provided, snapshot) => (
|
||||
<MyBox
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
style={{
|
||||
...provided.draggableProps.style,
|
||||
opacity: snapshot.isDragging ? 0.8 : 1
|
||||
}}
|
||||
display="flex"
|
||||
pl={2}
|
||||
bg="white"
|
||||
h={12}
|
||||
w="full"
|
||||
borderBottom="1px solid var(--Gray-Modern-150, #F0F1F6)"
|
||||
_hover={{
|
||||
bg: 'white',
|
||||
border: '1px solid var(--Gray-Modern-200, #E8EBF0)',
|
||||
boxShadow:
|
||||
'0px 4px 4px 0px rgba(19, 51, 107, 0.05), 0px 0px 1px 0px rgba(19, 51, 107, 0.08)',
|
||||
borderRadius: '6px',
|
||||
zIndex: 2
|
||||
}}
|
||||
fontSize="mini"
|
||||
alignItems="center"
|
||||
>
|
||||
{/* 名称列 */}
|
||||
<Box display="flex" w="20%">
|
||||
<Flex alignItems="center" gap="10px" width="100%" pl="24px">
|
||||
<Checkbox
|
||||
isChecked={selectedAppIds.includes(app._id)}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
onAppSelect(app._id, e.target.checked);
|
||||
}}
|
||||
size="sm"
|
||||
/>
|
||||
|
||||
<Flex {...provided.dragHandleProps} cursor="grab">
|
||||
<MyIcon name="drag" w="10.5px" h="14px" color="#667085" />
|
||||
</Flex>
|
||||
|
||||
<Flex gap="6px" alignItems="center">
|
||||
<Flex
|
||||
w="20px"
|
||||
h="20px"
|
||||
borderRadius="4px"
|
||||
overflow="hidden"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
bg={
|
||||
app.avatar
|
||||
? 'transparent'
|
||||
: 'linear-gradient(200.75deg, #61D2C4 13.74%, #40CAA1 89.76%)'
|
||||
}
|
||||
boxShadow="sm"
|
||||
>
|
||||
{app.avatar ? (
|
||||
<Avatar src={app.avatar} alt={app.name} w="100%" h="100%" />
|
||||
) : (
|
||||
<Text color="white" fontSize="16px" fontWeight="bold">
|
||||
{app.name.charAt(0).toUpperCase()}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<Text
|
||||
fontSize="12px"
|
||||
fontWeight="500"
|
||||
color="#111824"
|
||||
maxWidth="60px"
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
{app.name}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
{/* 介绍列 */}
|
||||
<Box w="40%" pl={4}>
|
||||
<Text color="myGray.500" noOfLines={1}>
|
||||
{app.intro}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* 标签列 */}
|
||||
<Box w="30%" pl={4}>
|
||||
<AppTags tags={app.tags} tagMap={tagMap} />
|
||||
</Box>
|
||||
|
||||
{/* 操作列 */}
|
||||
<Flex w="10%" justifyContent="center">
|
||||
<HStack spacing={2}>
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
icon={<MyIcon name="edit" w="14px" />}
|
||||
aria-label="edit"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEdit(app);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
colorScheme="red"
|
||||
icon={<MyIcon name="delete" w="14px" />}
|
||||
aria-label="delete"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete(app._id);
|
||||
}}
|
||||
/>
|
||||
</HStack>
|
||||
</Flex>
|
||||
</MyBox>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
};
|
||||
|
||||
const AppTable = () => {
|
||||
const { t } = useTranslation();
|
||||
const [editingApp, setEditingApp] = useState<AppListItemType | null>(null);
|
||||
const [search, setSearch] = useState('');
|
||||
const [localAppList, setLocalAppList] = useState<AppListItemType[]>([]);
|
||||
|
||||
// 使用多选标签的 hook
|
||||
const {
|
||||
value: selectedTags,
|
||||
setValue: setSelectedTags,
|
||||
isSelectAll,
|
||||
setIsSelectAll
|
||||
} = useMultipleSelect<string>([], false);
|
||||
|
||||
// 模态框状态
|
||||
const tagModal = useDisclosure();
|
||||
const addAppModal = useDisclosure();
|
||||
|
||||
// API 请求
|
||||
const {
|
||||
data: appList = [],
|
||||
loading: loadingApps,
|
||||
refresh: refreshApps
|
||||
} = useRequest2(() => listFeatureApps(), { manual: false });
|
||||
|
||||
const {
|
||||
data: tagList = [],
|
||||
loading: loadingTags,
|
||||
refresh: refreshTags
|
||||
} = useRequest2(() => getTeamTags(), { manual: false });
|
||||
|
||||
const { runAsync: onReorderApps } = useRequest2(
|
||||
({ appId, toIndex }: { appId: string; toIndex: number }) => reorderFeatureApps(appId, toIndex),
|
||||
{
|
||||
onSuccess: refreshApps,
|
||||
errorToast: t('common:reorder_failed')
|
||||
}
|
||||
);
|
||||
|
||||
const { openConfirm: openConfirmDel, ConfirmModal: DelConfirmModal } = useConfirm({
|
||||
type: 'delete',
|
||||
title: '确认删除该应用?'
|
||||
});
|
||||
|
||||
const { runAsync: onDeleteApp } = useRequest2(delAppById, {
|
||||
onSuccess: refreshApps,
|
||||
successToast: t('common:delete_success'),
|
||||
errorToast: t('common:delete_failed')
|
||||
});
|
||||
|
||||
// 计算属性
|
||||
const loading = loadingApps || loadingTags;
|
||||
|
||||
const tagMap = useMemo(() => {
|
||||
const map = new Map<string, TagSchemaType>();
|
||||
(tagList as TagSchemaType[]).forEach((tag) => map.set(tag._id, tag));
|
||||
return map;
|
||||
}, [tagList]);
|
||||
|
||||
const filteredApps = useMemo(() => {
|
||||
return localAppList.filter((app) => {
|
||||
const searchMatch =
|
||||
!search ||
|
||||
app.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
app.intro?.toLowerCase().includes(search.toLowerCase());
|
||||
|
||||
// 多选标签筛选逻辑:如果选择了全部或没有选择任何标签,显示所有应用
|
||||
// 如果选择了特定标签,应用必须包含至少一个选中的标签
|
||||
const tagMatch =
|
||||
isSelectAll ||
|
||||
selectedTags.length === 0 ||
|
||||
(app.tags && app.tags.some((tag) => selectedTags.includes(tag)));
|
||||
|
||||
return searchMatch && tagMatch;
|
||||
});
|
||||
}, [localAppList, search, selectedTags, isSelectAll]);
|
||||
|
||||
const allTags = useMemo(
|
||||
() =>
|
||||
Array.from(new Set(appList.flatMap((app) => app.tags || []))).map((tag) => ({
|
||||
label: tagMap.get(tag)?.name || tag,
|
||||
value: tag
|
||||
})),
|
||||
[appList, tagMap]
|
||||
);
|
||||
|
||||
// 自定义 hooks
|
||||
const selection = useAppSelection(filteredApps);
|
||||
|
||||
// 副作用
|
||||
useEffect(() => {
|
||||
setLocalAppList(appList);
|
||||
}, [appList]);
|
||||
|
||||
// 事件处理
|
||||
const handleDragEnd = async (list: AppListItemType[]) => {
|
||||
// 先更新本地状态以提供即时反馈
|
||||
setLocalAppList(list);
|
||||
|
||||
// 找到被移动的应用 - 需要找到移动距离最大的那个应用
|
||||
let movedApp: AppListItemType | null = null;
|
||||
let originalIndex = -1;
|
||||
let newIndex = -1;
|
||||
let maxDistance = 0;
|
||||
|
||||
// 找到位置发生变化的应用中移动距离最大的(这个就是被拖拽的应用)
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const currentApp = list[i];
|
||||
const origIndex = filteredApps.findIndex((app) => app._id === currentApp._id);
|
||||
|
||||
if (origIndex !== -1 && origIndex !== i) {
|
||||
const distance = Math.abs(origIndex - i);
|
||||
if (distance > maxDistance) {
|
||||
maxDistance = distance;
|
||||
movedApp = currentApp;
|
||||
originalIndex = origIndex;
|
||||
newIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (movedApp && originalIndex !== -1 && newIndex !== -1) {
|
||||
try {
|
||||
// 计算在完整应用列表中的目标位置
|
||||
let targetIndex: number;
|
||||
|
||||
if (newIndex === 0) {
|
||||
// 移动到第一位
|
||||
targetIndex = 0;
|
||||
} else if (newIndex === list.length - 1) {
|
||||
// 移动到最后一位,找到最后一个应用在完整列表中的位置
|
||||
const lastApp = list[newIndex - 1];
|
||||
const lastAppIndexInFullList = appList.findIndex((app) => app._id === lastApp._id);
|
||||
targetIndex = lastAppIndexInFullList + 1;
|
||||
} else {
|
||||
// 移动到中间位置
|
||||
if (originalIndex < newIndex) {
|
||||
// 向下拖拽:目标位置是新位置后面那个应用在完整列表中的位置
|
||||
const nextApp = list[newIndex + 1];
|
||||
const nextAppIndexInFullList = appList.findIndex((app) => app._id === nextApp._id);
|
||||
targetIndex = nextAppIndexInFullList - 1;
|
||||
} else {
|
||||
// 向上拖拽:目标位置是新位置前面那个应用在完整列表中的位置+1
|
||||
const prevApp = list[newIndex - 1];
|
||||
const prevAppIndexInFullList = appList.findIndex((app) => app._id === prevApp._id);
|
||||
targetIndex = prevAppIndexInFullList + 1;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('拖拽信息:', {
|
||||
appName: movedApp.name,
|
||||
originalIndex,
|
||||
newIndex,
|
||||
targetIndex,
|
||||
direction: originalIndex < newIndex ? '向下' : '向上'
|
||||
});
|
||||
|
||||
await onReorderApps({
|
||||
appId: movedApp._id,
|
||||
toIndex: targetIndex
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('重新排序失败:', error);
|
||||
// 如果失败,恢复原始状态
|
||||
setLocalAppList(appList);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleTagModalClose = () => {
|
||||
tagModal.onClose();
|
||||
refreshTags();
|
||||
refreshApps();
|
||||
};
|
||||
|
||||
const handleAddAppSuccess = (selectedApps: any) => {
|
||||
console.log('Selected apps:', selectedApps);
|
||||
refreshApps();
|
||||
};
|
||||
|
||||
return (
|
||||
<MyBox flex="1 0 0" isLoading={loading}>
|
||||
<Flex flexDirection="column" h="100%">
|
||||
{/* 筛选控件 */}
|
||||
<Flex
|
||||
gap={4}
|
||||
mb={4}
|
||||
flexDirection={{ base: 'column', md: 'row' }}
|
||||
alignItems={{ base: 'stretch', md: 'center' }}
|
||||
>
|
||||
<Flex gap={4}>
|
||||
<SearchInput
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder={t('app:search_app')}
|
||||
/>
|
||||
<Box w="200px">
|
||||
<Menu closeOnSelect={false}>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
rightIcon={<MyIcon name={'core/chat/chevronDown'} w={4} color={'myGray.500'} />}
|
||||
variant={'outline'}
|
||||
size={'sm'}
|
||||
fontSize={'sm'}
|
||||
textAlign={'left'}
|
||||
w="100%"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
{isSelectAll
|
||||
? t('common:All')
|
||||
: selectedTags.length === 0
|
||||
? t('common:select_tag')
|
||||
: `已选择: ${selectedTags.length}`}
|
||||
</MenuButton>
|
||||
<MenuList maxH="300px" overflowY="auto">
|
||||
<MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (isSelectAll) {
|
||||
setSelectedTags([]);
|
||||
setIsSelectAll(false);
|
||||
} else {
|
||||
setSelectedTags(allTags.map((tag) => tag.value));
|
||||
setIsSelectAll(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
isChecked={isSelectAll}
|
||||
mr={2}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
if (isSelectAll) {
|
||||
setSelectedTags([]);
|
||||
setIsSelectAll(false);
|
||||
} else {
|
||||
setSelectedTags(allTags.map((tag) => tag.value));
|
||||
setIsSelectAll(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{t('common:All')}
|
||||
</MenuItem>
|
||||
{allTags.map((tag) => (
|
||||
<MenuItem
|
||||
key={tag.value}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (isSelectAll) {
|
||||
// 如果当前是全选状态,取消全选并只选择当前项
|
||||
setSelectedTags([tag.value]);
|
||||
setIsSelectAll(false);
|
||||
} else {
|
||||
// 正常的多选逻辑
|
||||
if (selectedTags.includes(tag.value)) {
|
||||
setSelectedTags(selectedTags.filter((t) => t !== tag.value));
|
||||
} else {
|
||||
setSelectedTags([...selectedTags, tag.value]);
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
isChecked={isSelectAll || selectedTags.includes(tag.value)}
|
||||
mr={2}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
if (isSelectAll) {
|
||||
// 如果当前是全选状态,取消全选并只选择当前项
|
||||
setSelectedTags([tag.value]);
|
||||
setIsSelectAll(false);
|
||||
} else {
|
||||
// 正常的多选逻辑
|
||||
if (selectedTags.includes(tag.value)) {
|
||||
setSelectedTags(selectedTags.filter((t) => t !== tag.value));
|
||||
} else {
|
||||
setSelectedTags([...selectedTags, tag.value]);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{tag.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex gap={3}>
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
leftIcon={<MyIcon name="common/add2" w="14px" />}
|
||||
onClick={addAppModal.onOpen}
|
||||
minW="120px"
|
||||
>
|
||||
{t('common:add_app')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
leftIcon={<MyIcon name="common/settingLight" w="14px" />}
|
||||
onClick={tagModal.onOpen}
|
||||
minW="120px"
|
||||
>
|
||||
{t('common:tag_manage')}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
{/* 表头 */}
|
||||
<Flex
|
||||
bg="white"
|
||||
h={8}
|
||||
mt={5}
|
||||
pl={8}
|
||||
rounded="md"
|
||||
alignItems="center"
|
||||
fontSize="mini"
|
||||
fontWeight="medium"
|
||||
>
|
||||
<Box w="20%">
|
||||
<Flex alignItems="center" gap={2}>
|
||||
<Checkbox
|
||||
isChecked={selection.isAllSelected}
|
||||
isIndeterminate={selection.isIndeterminate}
|
||||
onChange={(e) => selection.handleSelectAll(e.target.checked)}
|
||||
size="sm"
|
||||
/>
|
||||
{t('common:Name')}
|
||||
</Flex>
|
||||
</Box>
|
||||
<Box w="40%">{t('common:Intro')}</Box>
|
||||
<Box w="30%">{t('common:Tags')}</Box>
|
||||
<Box w="10%">{t('common:Action')}</Box>
|
||||
</Flex>
|
||||
|
||||
{/* 应用列表 */}
|
||||
<Box overflow="auto" mt={4} maxH="calc(100vh - 200px)">
|
||||
{filteredApps.length > 0 ? (
|
||||
<DndDrag<AppListItemType> onDragEndCb={handleDragEnd} dataList={filteredApps}>
|
||||
{({ provided }) => (
|
||||
<Flex flexDirection="column" {...provided.droppableProps} ref={provided.innerRef}>
|
||||
{filteredApps.map((app, index) => (
|
||||
<AppRow
|
||||
key={app._id}
|
||||
app={app}
|
||||
index={index}
|
||||
tagMap={tagMap}
|
||||
selectedAppIds={selection.selectedAppIds}
|
||||
onAppSelect={selection.handleAppSelect}
|
||||
onEdit={setEditingApp}
|
||||
onDelete={(appId) => openConfirmDel(() => onDeleteApp(appId))()}
|
||||
/>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</Flex>
|
||||
)}
|
||||
</DndDrag>
|
||||
) : (
|
||||
<EmptyTip
|
||||
text={loading ? t('common:Loading') : t('common:no_matching_apps_found')}
|
||||
py={2}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* 模态框 */}
|
||||
{editingApp && (
|
||||
<GateAppInfoModal
|
||||
app={editingApp}
|
||||
onClose={() => setEditingApp(null)}
|
||||
onUpdateSuccess={refreshApps}
|
||||
/>
|
||||
)}
|
||||
{tagModal.isOpen && <TagManageModal onClose={handleTagModalClose} />}
|
||||
{addAppModal.isOpen && (
|
||||
<AddFeatureAppModal
|
||||
isOpen={addAppModal.isOpen}
|
||||
onClose={addAppModal.onClose}
|
||||
onSuccess={handleAddAppSuccess}
|
||||
/>
|
||||
)}
|
||||
<DelConfirmModal />
|
||||
</Flex>
|
||||
</MyBox>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppTable;
|
||||
@ -0,0 +1,204 @@
|
||||
import React from 'react';
|
||||
import { Button, Flex, useDisclosure } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import ShareGateModal from './ShareModol';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { getMyAppsGate, postCreateApp, putAppById } from '@/web/core/app/api';
|
||||
import { emptyTemplates } from '@/web/core/app/templates';
|
||||
import { saveGateConfig } from './HomeTable';
|
||||
import type { GateSchemaType } from '@fastgpt/global/support/user/team/gate/type';
|
||||
import type { putUpdateGateConfigCopyRightData } from '@fastgpt/global/support/user/team/gate/api';
|
||||
import { saveCopyRightConfig } from './CopyrightTable';
|
||||
import type { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
|
||||
import { form2AppWorkflow } from '@/web/core/app/utils';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
|
||||
type Props = {
|
||||
tab: 'home' | 'copyright' | 'app' | 'logs';
|
||||
appForm?: AppSimpleEditFormType;
|
||||
gateConfig?: GateSchemaType;
|
||||
copyRightConfig?: putUpdateGateConfigCopyRightData;
|
||||
};
|
||||
|
||||
const ConfigButtons = ({ tab, appForm, gateConfig, copyRightConfig }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
// 保存配置
|
||||
const { runAsync: saveHomeConfig, loading: savingHome } = useRequest2(
|
||||
async () => {
|
||||
if (!!gateConfig) {
|
||||
await saveGateConfig({
|
||||
...gateConfig,
|
||||
status: true
|
||||
});
|
||||
toast({
|
||||
title: t('common:save_success'),
|
||||
status: 'success'
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
manual: true,
|
||||
onError: (err) => {
|
||||
toast({
|
||||
title: t('common:save_failed'),
|
||||
status: 'error',
|
||||
description: err?.message
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
console.log('buttons appForm', appForm);
|
||||
const { nodes, edges } = appForm
|
||||
? form2AppWorkflow(appForm, t)
|
||||
: {
|
||||
nodes: emptyTemplates[AppTypeEnum.gate].nodes,
|
||||
edges: emptyTemplates[AppTypeEnum.gate].edges
|
||||
};
|
||||
|
||||
// 保存版权配置
|
||||
const { runAsync: saveCopyrightConfig, loading: savingCopyright } = useRequest2(
|
||||
async () => {
|
||||
// 保存其他版权配置
|
||||
if (!!copyRightConfig) {
|
||||
await saveCopyRightConfig(copyRightConfig);
|
||||
toast({
|
||||
title: t('common:save_success'),
|
||||
status: 'success'
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
manual: true,
|
||||
onError: (err) => {
|
||||
toast({
|
||||
title: t('common:save_failed'),
|
||||
status: 'error',
|
||||
description: err?.message
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const { ttsModelList, sttModelList } = useSystemStore();
|
||||
const checkAndCreateGateApp = async () => {
|
||||
try {
|
||||
// 获取应用列表
|
||||
const apps = await getMyAppsGate();
|
||||
const gateApp = apps.find((app) => app.type === AppTypeEnum.gate);
|
||||
const currentTeamAvatar = copyRightConfig?.logo;
|
||||
const currentSlogan = gateConfig?.slogan;
|
||||
if (gateApp) {
|
||||
if (
|
||||
gateApp.avatar !== currentTeamAvatar ||
|
||||
gateApp.intro !== currentSlogan ||
|
||||
nodes !== emptyTemplates[AppTypeEnum.gate].nodes ||
|
||||
edges !== emptyTemplates[AppTypeEnum.gate].edges
|
||||
) {
|
||||
await putAppById(gateApp._id, {
|
||||
avatar: currentTeamAvatar,
|
||||
intro: currentSlogan,
|
||||
name: gateConfig?.name,
|
||||
nodes,
|
||||
edges,
|
||||
chatConfig: {
|
||||
ttsConfig:
|
||||
ttsModelList.length > 0
|
||||
? {
|
||||
type: 'model',
|
||||
model: ttsModelList[0].model
|
||||
}
|
||||
: undefined,
|
||||
whisperConfig: {
|
||||
open: sttModelList.length > 0,
|
||||
autoSend: false,
|
||||
autoTTSResponse: false
|
||||
},
|
||||
fileSelectConfig: {
|
||||
canSelectFile: true,
|
||||
customPdfParse: false,
|
||||
canSelectImg: true,
|
||||
maxFiles: 10
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await postCreateApp({
|
||||
avatar: gateConfig?.logo,
|
||||
name: 'App',
|
||||
intro: gateConfig?.slogan,
|
||||
type: AppTypeEnum.gate,
|
||||
modules: emptyTemplates[AppTypeEnum.gate].nodes,
|
||||
edges: emptyTemplates[AppTypeEnum.gate].edges,
|
||||
chatConfig: {
|
||||
ttsConfig:
|
||||
ttsModelList.length > 0
|
||||
? {
|
||||
type: 'model',
|
||||
model: ttsModelList[0].model
|
||||
}
|
||||
: undefined,
|
||||
whisperConfig: {
|
||||
open: sttModelList.length > 0,
|
||||
autoSend: false,
|
||||
autoTTSResponse: false
|
||||
},
|
||||
fileSelectConfig: {
|
||||
canSelectFile: true,
|
||||
customPdfParse: false,
|
||||
canSelectImg: true,
|
||||
maxFiles: 10
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: t('common:error.Create failed'),
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
};
|
||||
const handleSave = async () => {
|
||||
if (tab === 'home') {
|
||||
await saveHomeConfig();
|
||||
await checkAndCreateGateApp();
|
||||
} else if (tab === 'copyright') {
|
||||
await saveCopyrightConfig();
|
||||
await checkAndCreateGateApp();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex>
|
||||
<Button
|
||||
variant="primaryOutline"
|
||||
mr={2}
|
||||
leftIcon={<MyIcon name="support/gate/home/savePrimary" />}
|
||||
onClick={handleSave}
|
||||
isLoading={tab === 'home' ? savingHome : savingCopyright}
|
||||
>
|
||||
{t('account:gateway.save_config')}
|
||||
</Button>
|
||||
<Button
|
||||
variant={'primary'}
|
||||
mr={2}
|
||||
leftIcon={<MyIcon name="support/gate/home/shareLight" />}
|
||||
onClick={onOpen}
|
||||
>
|
||||
{t('account:gateway.share')}
|
||||
</Button>
|
||||
|
||||
{/* 分享门户弹窗 */}
|
||||
<ShareGateModal gateConfig={gateConfig} isOpen={isOpen} onClose={onClose} />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfigButtons;
|
||||
@ -0,0 +1,393 @@
|
||||
import React from 'react';
|
||||
import { Box, Flex, Text, Input, useBreakpointValue, Image } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import type { putUpdateGateConfigCopyRightData } from '@fastgpt/global/support/user/team/gate/api';
|
||||
import { updateTeamGateConfigCopyRight } from '@/web/support/user/team/gate/api';
|
||||
|
||||
type Props = {
|
||||
gateName: string;
|
||||
gateLogo: string;
|
||||
gateBanner: string;
|
||||
onNameChange?: (name: string) => void;
|
||||
onLogoChange?: (logo: string) => void;
|
||||
onBannerChange?: (banner: string) => void;
|
||||
};
|
||||
|
||||
export const saveCopyRightConfig = async (data: putUpdateGateConfigCopyRightData) => {
|
||||
try {
|
||||
await updateTeamGateConfigCopyRight(data);
|
||||
} catch (e) {
|
||||
console.error('Error saving copyright config:', e);
|
||||
}
|
||||
};
|
||||
|
||||
// 斜线背景样式
|
||||
const stripedBackgroundStyle = {
|
||||
backgroundImage:
|
||||
'linear-gradient(135deg, #f0f0f0 25%, transparent 25%, transparent 50%, #f0f0f0 50%, #f0f0f0 75%, transparent 75%, transparent)',
|
||||
backgroundSize: '5px 5px',
|
||||
padding: '12px',
|
||||
borderRadius: '16px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.05)',
|
||||
border: '1px dashed #e0e0e0'
|
||||
};
|
||||
|
||||
// 添加悬浮遮罩样式
|
||||
const uploadOverlayStyle = {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
border: '1px dashed var(--Royal-Blue-200, #C5D7FF)',
|
||||
background: 'rgba(255, 255, 255, 0.5)',
|
||||
backdropFilter: 'blur(2px)',
|
||||
zIndex: 10,
|
||||
opacity: 0,
|
||||
transition: 'opacity 0.3s ease',
|
||||
_groupHover: {
|
||||
opacity: 1
|
||||
}
|
||||
};
|
||||
|
||||
const CopyrightTable = ({
|
||||
gateName,
|
||||
gateLogo,
|
||||
gateBanner,
|
||||
onNameChange,
|
||||
onLogoChange,
|
||||
onBannerChange
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// 使用useForm管理表单数据
|
||||
const { setValue, watch } = useForm({
|
||||
defaultValues: {
|
||||
name: gateName,
|
||||
logo: gateLogo,
|
||||
banner: gateBanner
|
||||
}
|
||||
});
|
||||
|
||||
// 从表单中获取logo和banner值
|
||||
const logo = watch('logo');
|
||||
const banner = watch('banner');
|
||||
|
||||
const handleGateNameChange = (name: string) => {
|
||||
setValue('name', name);
|
||||
onNameChange?.(name);
|
||||
};
|
||||
const handleGateLogoChange = (logo: string) => {
|
||||
setValue('logo', logo);
|
||||
onLogoChange?.(logo);
|
||||
};
|
||||
const handleGateBannerChange = (banner: string) => {
|
||||
setValue('banner', banner);
|
||||
onBannerChange?.(banner);
|
||||
};
|
||||
|
||||
// 添加文件选择器 - 分别为左右两侧Logo创建选择器
|
||||
const {
|
||||
File: LogoFile,
|
||||
onOpen: onOpenLogoFile,
|
||||
onSelectImage: onSelectLogoImage
|
||||
} = useSelectFile({
|
||||
fileType: '.jpg,.png,.svg',
|
||||
multiple: false
|
||||
});
|
||||
|
||||
const {
|
||||
File: BannerFile,
|
||||
onOpen: onOpenBannerFile,
|
||||
onSelectImage: onSelectBannerImage
|
||||
} = useSelectFile({
|
||||
fileType: '.jpg,.png,.svg',
|
||||
multiple: false
|
||||
});
|
||||
|
||||
// 响应式尺寸 - 根据设计比例调整
|
||||
const logoBoxSize = useBreakpointValue({ md: '60px' });
|
||||
const logoBorderRadius = useBreakpointValue({ base: '5.8px', md: '15px' });
|
||||
const titleFontSize = useBreakpointValue({ base: '18px', md: '28px' });
|
||||
const dividerHeight = useBreakpointValue({ base: '70px', md: '84px' });
|
||||
|
||||
// 左侧带文本的Logo稍大一些
|
||||
const logoBoxSizeWithText = useBreakpointValue({ base: '28px', md: '60px' });
|
||||
|
||||
return (
|
||||
<Box flex={'1 0 0'} overflow={'hidden'} display="flex" justifyContent="center">
|
||||
<Box w="100%" maxW={{ base: '100%', md: '640px' }} py={{ base: 4, md: 6 }}>
|
||||
<Flex flexDirection={'column'} gap={{ base: 4, md: 6 }}>
|
||||
{/* 基础设置区域 */}
|
||||
<Flex flexDirection="column" gap={{ base: 3, md: 4 }}>
|
||||
<Flex alignItems="center" gap={3}>
|
||||
<Box w="4px" h="16px" bg="#3370FF" borderRadius="6px" />
|
||||
<Text fontSize={{ base: '14px', md: '16px' }} fontWeight={500}>
|
||||
{t('common:base_config')}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Flex direction="column" gap={2}>
|
||||
<Text fontSize="14px" color="#485264" fontWeight={500}>
|
||||
{t('account_gate:gate_name')}
|
||||
</Text>
|
||||
<Input
|
||||
value={watch('name')}
|
||||
onChange={(e) => handleGateNameChange(e.target.value)}
|
||||
bg="#FBFBFC"
|
||||
border="1px solid #E8EBF0"
|
||||
borderRadius="8px"
|
||||
height={{ base: '36px', md: '40px' }}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
{/* Logo 设置区域 */}
|
||||
<Flex flexDirection="column" gap={{ base: 3, md: 4 }}>
|
||||
<Text fontSize="14px" color="#485264" fontWeight={500}>
|
||||
{t('account_gate:gate_logo')}
|
||||
</Text>
|
||||
|
||||
<Flex gap={{ base: 4, md: 8 }} alignItems="center" justifyContent="flex-start">
|
||||
{/* 左侧 Banner 显示 - 带文字 */}
|
||||
<Flex direction="column" gap={2} alignItems="center">
|
||||
<Box
|
||||
sx={stripedBackgroundStyle}
|
||||
onClick={onOpenBannerFile}
|
||||
cursor="pointer"
|
||||
role="group"
|
||||
position="relative"
|
||||
width="100%"
|
||||
padding="20px"
|
||||
>
|
||||
<Flex gap={{ base: 3, md: 5 }} alignItems="center" width="100%">
|
||||
{banner ? (
|
||||
<Box
|
||||
width="100%"
|
||||
height={logoBoxSizeWithText}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
position="relative"
|
||||
overflow="hidden"
|
||||
boxSizing="border-box"
|
||||
transition="all 0.3s ease"
|
||||
display="flex"
|
||||
>
|
||||
<Image
|
||||
src={banner}
|
||||
alt="Team Banner"
|
||||
width="100%"
|
||||
height="100%"
|
||||
objectFit="contain"
|
||||
objectPosition="center"
|
||||
style={
|
||||
{
|
||||
imageRendering: 'crisp-edges'
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<Box
|
||||
width="100%"
|
||||
height={logoBoxSizeWithText}
|
||||
bg="white"
|
||||
border="0.483px solid #ECECEC"
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
position="relative"
|
||||
overflow="hidden"
|
||||
boxSizing="border-box"
|
||||
transition="all 0.3s ease"
|
||||
>
|
||||
<Flex
|
||||
width="40px"
|
||||
height="40px"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
flexShrink="0"
|
||||
aspectRatio="1/1"
|
||||
>
|
||||
<Image
|
||||
src={banner}
|
||||
alt="Team Banner"
|
||||
width="100%"
|
||||
height="100%"
|
||||
objectFit="contain"
|
||||
fallbackSrc="/icon/logo.svg"
|
||||
style={
|
||||
{
|
||||
imageRendering: 'crisp-edges'
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{/* 悬浮遮罩 - 4:1 */}
|
||||
<Box
|
||||
sx={{
|
||||
...uploadOverlayStyle,
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
}}
|
||||
borderRadius="16px"
|
||||
>
|
||||
<Flex direction="column" alignItems="center" justifyContent="center">
|
||||
<MyIcon
|
||||
name="support/gate/home/upload"
|
||||
width="24px"
|
||||
height="24px"
|
||||
color="blue.500"
|
||||
/>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Box>
|
||||
<Text fontSize="12px" color="#667085" alignSelf="flex-start">
|
||||
{t('account_gate:suggestion_ratio_4_1')}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Box display={{ base: 'none', md: 'block' }} w="1px" h={dividerHeight} bg="#F0F1F6" />
|
||||
|
||||
{/* 右侧 Logo 显示 - 仅Logo */}
|
||||
<Flex direction="column" gap={2} alignItems="center">
|
||||
<Box
|
||||
sx={stripedBackgroundStyle}
|
||||
onClick={onOpenLogoFile}
|
||||
cursor="pointer"
|
||||
role="group"
|
||||
position="relative"
|
||||
>
|
||||
{logo ? (
|
||||
<Flex
|
||||
width={logoBoxSize}
|
||||
height={logoBoxSize}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
position="relative"
|
||||
overflow="hidden"
|
||||
borderRadius={logoBorderRadius}
|
||||
boxSizing="border-box"
|
||||
transition="all 0.3s ease"
|
||||
>
|
||||
<Image
|
||||
src={logo}
|
||||
alt="Team Logo"
|
||||
width="100%"
|
||||
height="100%"
|
||||
objectFit="contain"
|
||||
style={
|
||||
{
|
||||
imageRendering: 'crisp-edges'
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
) : (
|
||||
<Box
|
||||
width={logoBoxSize}
|
||||
height={logoBoxSize}
|
||||
bg="white"
|
||||
border="0.483px solid #ECECEC"
|
||||
borderRadius={logoBorderRadius}
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
position="relative"
|
||||
overflow="hidden"
|
||||
boxSizing="border-box"
|
||||
transition="all 0.3s ease"
|
||||
>
|
||||
<Flex
|
||||
width="40px"
|
||||
height="40px"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
flexShrink="0"
|
||||
aspectRatio="1/1"
|
||||
>
|
||||
<Image
|
||||
src={logo}
|
||||
alt="Team Logo"
|
||||
width="100%"
|
||||
height="100%"
|
||||
objectFit="contain"
|
||||
fallbackSrc="/icon/logo.svg"
|
||||
/>
|
||||
</Flex>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 悬浮遮罩 - 1:1 */}
|
||||
<Box
|
||||
sx={{
|
||||
...uploadOverlayStyle,
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
}}
|
||||
borderRadius="16px"
|
||||
>
|
||||
<Flex direction="column" alignItems="center" justifyContent="center">
|
||||
<MyIcon
|
||||
name="support/gate/home/upload"
|
||||
width="24px"
|
||||
height="24px"
|
||||
color="blue.500"
|
||||
/>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Box>
|
||||
<Text fontSize="12px" color="#667085">
|
||||
{t('account_gate:suggestion_ratio_1_1')}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
{/* 文件选择器组件 */}
|
||||
<LogoFile
|
||||
onSelect={(e: File[]) =>
|
||||
onSelectLogoImage(e, {
|
||||
maxH: 3000,
|
||||
maxW: 3000,
|
||||
callback: (e: string) => {
|
||||
setValue('logo', e);
|
||||
handleGateLogoChange(e);
|
||||
}
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
<BannerFile
|
||||
onSelect={(e: File[]) =>
|
||||
onSelectBannerImage(e, {
|
||||
maxH: 3000,
|
||||
maxW: 3000,
|
||||
callback: (e: string) => {
|
||||
setValue('banner', e);
|
||||
handleGateBannerChange(e);
|
||||
}
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default CopyrightTable;
|
||||
@ -0,0 +1,359 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
FormControl,
|
||||
Input,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
Textarea,
|
||||
HStack,
|
||||
Text,
|
||||
Tag as ChakraTag,
|
||||
TagCloseButton,
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
PopoverBody,
|
||||
useDisclosure
|
||||
} from '@chakra-ui/react';
|
||||
import type { AppListItemType } from '@fastgpt/global/core/app/type.d';
|
||||
import type { TagSchemaType } from '@fastgpt/global/core/app/tags';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import React, { useCallback, useState, useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
|
||||
import { putAppById } from '@/web/core/app/api';
|
||||
import {
|
||||
getTeamTags,
|
||||
addTagToApp,
|
||||
removeTagFromApp,
|
||||
batchAddTagsToApp,
|
||||
batchRemoveTagsFromApp
|
||||
} from '@/web/core/app/api/tags';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
|
||||
interface AppInfoModalProps {
|
||||
app: AppListItemType;
|
||||
onClose: () => void;
|
||||
onUpdateSuccess?: () => void;
|
||||
}
|
||||
|
||||
const AppInfoModal = ({ app, onClose, onUpdateSuccess }: AppInfoModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const { isOpen, onOpen, onClose: onClosePopover } = useDisclosure();
|
||||
const [appTags, setAppTags] = useState<string[]>(app.tags || []);
|
||||
const [availableTags, setAvailableTags] = useState<TagSchemaType[]>([]);
|
||||
const [initialTags, setInitialTags] = useState<string[]>(app.tags || []);
|
||||
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
||||
|
||||
const { ttsModelList, sttModelList } = useSystemStore();
|
||||
|
||||
const {
|
||||
File,
|
||||
onOpen: onOpenSelectFile,
|
||||
onSelectImage
|
||||
} = useSelectFile({
|
||||
fileType: '.jpg,.png',
|
||||
multiple: false
|
||||
});
|
||||
|
||||
const {
|
||||
register,
|
||||
setValue,
|
||||
watch,
|
||||
formState: { errors },
|
||||
handleSubmit
|
||||
} = useForm({
|
||||
defaultValues: {
|
||||
name: app.name,
|
||||
avatar: app.avatar,
|
||||
intro: app.intro
|
||||
}
|
||||
});
|
||||
const avatar = watch('avatar');
|
||||
|
||||
// 获取所有标签
|
||||
const { data: tags = [], loading: loadingTags } = useRequest2(
|
||||
async () => {
|
||||
const result = await getTeamTags();
|
||||
return result as TagSchemaType[];
|
||||
},
|
||||
{
|
||||
manual: false,
|
||||
refreshDeps: [refreshTrigger],
|
||||
onSuccess: (data) => {
|
||||
setAvailableTags(data);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// 如果应用的标签有变化,通知父组件刷新
|
||||
if (app.tags && initialTags && JSON.stringify(app.tags) !== JSON.stringify(initialTags)) {
|
||||
setInitialTags([...app.tags]);
|
||||
if (onUpdateSuccess) onUpdateSuccess();
|
||||
}
|
||||
}, [app.tags, initialTags, onUpdateSuccess]);
|
||||
|
||||
// 添加标签到应用
|
||||
const { runAsync: addTag, loading: addTagLoading } = useRequest2(
|
||||
async (tagId: string) => {
|
||||
if (!appTags.includes(tagId)) {
|
||||
setAppTags([...appTags, tagId]);
|
||||
}
|
||||
return tagId;
|
||||
},
|
||||
{
|
||||
onSuccess: (tagId) => {
|
||||
onClosePopover();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 从应用移除标签
|
||||
const { runAsync: removeTag, loading: removeTagLoading } = useRequest2(async (tagId: string) => {
|
||||
setAppTags(appTags.filter((id) => id !== tagId));
|
||||
return tagId;
|
||||
});
|
||||
|
||||
// 保存所有标签更改
|
||||
const saveTagChanges = useCallback(async () => {
|
||||
const tagsToAdd = appTags.filter((tagId) => !initialTags.includes(tagId));
|
||||
const tagsToRemove = initialTags.filter((tagId) => !appTags.includes(tagId));
|
||||
|
||||
let hasChanges = false;
|
||||
|
||||
if (tagsToAdd.length > 0) {
|
||||
await batchAddTagsToApp(app._id, tagsToAdd);
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
if (tagsToRemove.length > 0) {
|
||||
await batchRemoveTagsFromApp(app._id, tagsToRemove);
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
setInitialTags([...appTags]);
|
||||
|
||||
return hasChanges;
|
||||
}, [appTags, initialTags, app._id]);
|
||||
|
||||
// submit config
|
||||
const { runAsync: saveSubmitSuccess, loading: btnLoading } = useRequest2(
|
||||
async (data: { name: string; avatar: string; intro: string }) => {
|
||||
// 使用正确的 API 函数 putAppById
|
||||
await putAppById(app._id, {
|
||||
name: data.name,
|
||||
avatar: data.avatar,
|
||||
intro: data.intro
|
||||
});
|
||||
|
||||
// 保存标签变更
|
||||
const tagsChanged = await saveTagChanges();
|
||||
|
||||
return tagsChanged; // 返回标签是否有变更
|
||||
},
|
||||
{
|
||||
onSuccess(tagsChanged) {
|
||||
toast({
|
||||
title: t('common:update_success'),
|
||||
status: 'success'
|
||||
});
|
||||
if (onUpdateSuccess) onUpdateSuccess();
|
||||
onClose();
|
||||
},
|
||||
errorToast: t('common:update_failed')
|
||||
}
|
||||
);
|
||||
|
||||
const saveSubmitError = useCallback(() => {
|
||||
const deepSearch = (obj: any): string => {
|
||||
if (!obj) return t('common:submit_failed');
|
||||
if (!!obj.message) {
|
||||
return obj.message;
|
||||
}
|
||||
return deepSearch(Object.values(obj)[0]);
|
||||
};
|
||||
toast({
|
||||
title: deepSearch(errors),
|
||||
status: 'error',
|
||||
duration: 4000,
|
||||
isClosable: true
|
||||
});
|
||||
}, [errors, t, toast]);
|
||||
|
||||
const saveUpdateModel = useCallback(
|
||||
() => handleSubmit((data) => saveSubmitSuccess(data), saveSubmitError)(),
|
||||
[handleSubmit, saveSubmitError, saveSubmitSuccess]
|
||||
);
|
||||
|
||||
// 获取标签样式
|
||||
const getTagStyle = (color: string) => {
|
||||
// 处理自定义颜色 (#XXXXXX)
|
||||
if (color.startsWith('#')) {
|
||||
return {
|
||||
bg: `${color}15`, // 15 表示透明度
|
||||
color: color
|
||||
};
|
||||
}
|
||||
// 预设颜色
|
||||
const colorMap: Record<string, { bg: string; color: string }> = {
|
||||
blue: { bg: 'blue.50', color: 'blue.600' },
|
||||
green: { bg: 'green.50', color: 'green.600' },
|
||||
red: { bg: 'red.50', color: 'red.600' },
|
||||
yellow: { bg: 'yellow.50', color: 'yellow.600' },
|
||||
purple: { bg: 'purple.50', color: 'purple.600' },
|
||||
teal: { bg: 'teal.50', color: 'teal.600' }
|
||||
};
|
||||
return colorMap[color] || colorMap.blue;
|
||||
};
|
||||
|
||||
// 获取当前选中的标签
|
||||
const getSelectedTags = useCallback(() => {
|
||||
return tags.filter((tag) => appTags.includes(tag._id));
|
||||
}, [tags, appTags]);
|
||||
|
||||
// 获取未选中的标签
|
||||
const getUnselectedTags = useCallback(() => {
|
||||
return tags.filter((tag) => !appTags.includes(tag._id));
|
||||
}, [tags, appTags]);
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen={true}
|
||||
onClose={onClose}
|
||||
iconSrc="/imgs/workflow/ai.svg"
|
||||
title={t('common:core.app.setting')}
|
||||
>
|
||||
<ModalBody>
|
||||
<Box fontSize={'sm'}>{t('common:core.app.Name and avatar')}</Box>
|
||||
<Flex mt={2} alignItems={'center'}>
|
||||
<Avatar
|
||||
src={avatar}
|
||||
w={['26px', '34px']}
|
||||
h={['26px', '34px']}
|
||||
cursor={'pointer'}
|
||||
borderRadius={'md'}
|
||||
mr={4}
|
||||
title={t('common:set_avatar')}
|
||||
onClick={() => onOpenSelectFile()}
|
||||
/>
|
||||
<FormControl>
|
||||
<Input
|
||||
bg={'myWhite.600'}
|
||||
placeholder={t('common:core.app.Set a name for your app')}
|
||||
{...register('name', {
|
||||
required: true
|
||||
})}
|
||||
></Input>
|
||||
</FormControl>
|
||||
</Flex>
|
||||
<Box mt={4} mb={1} fontSize={'sm'}>
|
||||
{t('common:core.app.App intro')}
|
||||
</Box>
|
||||
<Textarea
|
||||
rows={4}
|
||||
maxLength={500}
|
||||
placeholder={t('common:core.app.Make a brief introduction of your app')}
|
||||
bg={'myWhite.600'}
|
||||
{...register('intro')}
|
||||
/>
|
||||
|
||||
{/* 标签管理部分 */}
|
||||
<Box mt={4} mb={2} fontSize={'sm'}>
|
||||
标签
|
||||
</Box>
|
||||
<Flex direction="column" gap={2}>
|
||||
<Flex wrap="wrap" gap={2} mb={2} minH="30px">
|
||||
{getSelectedTags().map((tag) => (
|
||||
<ChakraTag
|
||||
key={tag._id}
|
||||
size="md"
|
||||
variant="subtle"
|
||||
{...getTagStyle(tag.color)}
|
||||
px={3}
|
||||
py={1}
|
||||
borderRadius="full"
|
||||
>
|
||||
{tag.name}
|
||||
<TagCloseButton onClick={() => removeTag(tag._id)} isDisabled={removeTagLoading} />
|
||||
</ChakraTag>
|
||||
))}
|
||||
|
||||
<Popover isOpen={isOpen} onClose={onClosePopover} placement="bottom-start">
|
||||
<PopoverTrigger>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
leftIcon={<MyIcon name="common/addLight" w="12px" />}
|
||||
onClick={onOpen}
|
||||
isLoading={loadingTags}
|
||||
fontWeight="normal"
|
||||
h="30px"
|
||||
>
|
||||
添加标签
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent w="200px">
|
||||
<PopoverBody p={2}>
|
||||
{getUnselectedTags().length === 0 ? (
|
||||
<Text fontSize="sm" color="gray.500" textAlign="center" p={2}>
|
||||
没有可添加的标签
|
||||
</Text>
|
||||
) : (
|
||||
<Flex direction="column" gap={1}>
|
||||
{getUnselectedTags().map((tag) => (
|
||||
<ChakraTag
|
||||
key={tag._id}
|
||||
size="md"
|
||||
variant="subtle"
|
||||
{...getTagStyle(tag.color)}
|
||||
px={3}
|
||||
py={1.5}
|
||||
borderRadius="full"
|
||||
cursor="pointer"
|
||||
onClick={() => addTag(tag._id)}
|
||||
_hover={{ opacity: 0.8 }}
|
||||
>
|
||||
{tag.name}
|
||||
</ChakraTag>
|
||||
))}
|
||||
</Flex>
|
||||
)}
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button variant={'whiteBase'} mr={3} onClick={onClose}>
|
||||
{t('common:Close')}
|
||||
</Button>
|
||||
<Button isLoading={btnLoading} onClick={saveUpdateModel}>
|
||||
{t('common:Save')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
|
||||
<File
|
||||
onSelect={(e) =>
|
||||
onSelectImage(e, {
|
||||
maxH: 300,
|
||||
maxW: 300,
|
||||
callback: (e) => setValue('avatar', e)
|
||||
})
|
||||
}
|
||||
/>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(AppInfoModal);
|
||||
@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import { Box, Flex, Text, Avatar, Heading, Button } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import type { AppListItemType } from '@fastgpt/global/core/app/type';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
type Props = {
|
||||
gateApps: AppListItemType[];
|
||||
};
|
||||
|
||||
const GateAppsList = ({ gateApps }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
|
||||
const handleGateClick = (appId: string) => {
|
||||
router.push(`/app/detail?appId=${appId}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box w="220px" h="100%" bg="#FBFBFC" borderRight="1px solid #E8EBF0" p={5} overflowY="auto">
|
||||
<Flex justifyContent="space-between" alignItems="center" mb={4}>
|
||||
<Heading size="sm">{t('account_gate:gate_list')}</Heading>
|
||||
</Flex>
|
||||
|
||||
{gateApps.length === 0 ? (
|
||||
<Flex direction="column" justify="center" align="center" h="180px" gap={4}>
|
||||
<Text color="gray.500" fontSize="sm" textAlign="center">
|
||||
{t('account_gate:no_gate_available')}
|
||||
</Text>
|
||||
</Flex>
|
||||
) : (
|
||||
<Flex direction="column" gap={3}>
|
||||
{gateApps.map((gate) => (
|
||||
<Flex
|
||||
key={gate._id}
|
||||
align="center"
|
||||
p={3}
|
||||
borderRadius="md"
|
||||
cursor="pointer"
|
||||
transition="all 0.2s ease"
|
||||
bg="white"
|
||||
border="1px solid"
|
||||
borderColor="gray.100"
|
||||
boxShadow="0 2px 8px rgba(0,0,0,0.06)"
|
||||
_hover={{
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.08)',
|
||||
borderColor: 'primary.300'
|
||||
}}
|
||||
onClick={() => handleGateClick(gate._id)}
|
||||
>
|
||||
<Avatar src={gate.avatar} size="sm" mr={3} borderRadius="md" />
|
||||
<Box>
|
||||
<Text fontSize="sm" fontWeight="medium" className="textEllipsis">
|
||||
{gate.name}
|
||||
</Text>
|
||||
{gate.intro && (
|
||||
<Text fontSize="xs" color="gray.500" className="textEllipsis">
|
||||
{gate.intro}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default GateAppsList;
|
||||
529
projects/app/src/pageComponents/account/gateway/HomeTable.tsx
Normal file
@ -0,0 +1,529 @@
|
||||
import React, { useRef, useEffect, useCallback, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Text,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Stack,
|
||||
Input,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Link
|
||||
} from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import ToolSelect from './ToolSelect';
|
||||
import type { putUpdateGateConfigData } from '@fastgpt/global/support/user/team/gate/api';
|
||||
import { updateTeamGateConfig } from '@/web/support/user/team/gate/api';
|
||||
import { appWorkflow2Form, getDefaultAppForm } from '@fastgpt/global/core/app/utils';
|
||||
import type { SimpleAppSnapshotType } from '@/pageComponents/app/detail/SimpleApp/useSnapshots';
|
||||
import { getAppConfigByDiff } from '@/web/core/app/diff';
|
||||
import { v1Workflow2V2 } from '@/web/core/workflow/adapt';
|
||||
import { useMount } from 'ahooks';
|
||||
import type { AppDetailType, AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
|
||||
import { useSimpleAppSnapshots } from '@/pageComponents/app/detail/Gate/useSnapshots';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import AddQuickAppModal from './AddQuickAppModal';
|
||||
import { listQuickApps } from '@/web/support/user/team/gate/quickApp';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import type { AppListItemType } from '@fastgpt/global/core/app/type';
|
||||
|
||||
export const saveGateConfig = async (data: putUpdateGateConfigData) => {
|
||||
try {
|
||||
await updateTeamGateConfig(data);
|
||||
} catch (error) {
|
||||
console.error('Failed to save gate config:', error);
|
||||
}
|
||||
};
|
||||
|
||||
type Props = {
|
||||
appDetail: AppDetailType;
|
||||
tools: string[];
|
||||
slogan: string;
|
||||
placeholderText: string;
|
||||
onSloganChange?: (slogan: string) => void;
|
||||
onPlaceholderChange?: (text: string) => void;
|
||||
onToolsChange?: (tools: string[]) => void;
|
||||
onAppFormChange?: (appForm: AppSimpleEditFormType) => void;
|
||||
};
|
||||
|
||||
const HomeTable = ({
|
||||
appDetail,
|
||||
slogan,
|
||||
placeholderText,
|
||||
onSloganChange,
|
||||
onPlaceholderChange,
|
||||
onAppFormChange
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
// 批量获取插件信息
|
||||
const [appForm, setAppForm] = useState(getDefaultAppForm());
|
||||
// 快捷应用modal状态
|
||||
const [isQuickAppModalOpen, setIsQuickAppModalOpen] = useState(false);
|
||||
|
||||
// 获取快捷应用数据
|
||||
const {
|
||||
data: quickApps = [],
|
||||
loading: loadingQuickApps,
|
||||
refresh: refreshQuickApps
|
||||
} = useRequest2(() => listQuickApps(), {
|
||||
manual: false
|
||||
});
|
||||
|
||||
const { forbiddenSaveSnapshot, past, setPast, saveSnapshot } = useSimpleAppSnapshots(
|
||||
appDetail._id
|
||||
);
|
||||
useMount(() => {
|
||||
if (appDetail.version !== 'v2') {
|
||||
const form = appWorkflow2Form({
|
||||
nodes: v1Workflow2V2((appDetail.modules || []) as any)?.nodes,
|
||||
chatConfig: appDetail.chatConfig
|
||||
});
|
||||
return updateAppForm(form);
|
||||
}
|
||||
|
||||
// 读取旧的存储记录
|
||||
const pastSnapshot = (() => {
|
||||
try {
|
||||
const pastSnapshot = localStorage.getItem(`${appDetail._id}-past`);
|
||||
return pastSnapshot ? (JSON.parse(pastSnapshot) as SimpleAppSnapshotType[]) : [];
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
})();
|
||||
const defaultState = pastSnapshot?.[pastSnapshot.length - 1]?.state;
|
||||
if (pastSnapshot?.[0]?.diff && defaultState) {
|
||||
setPast(
|
||||
pastSnapshot
|
||||
.map((item) => {
|
||||
if (!item.state && !item.diff) return;
|
||||
if (!item.diff) {
|
||||
return {
|
||||
title: t('app:initial_form'),
|
||||
isSaved: true,
|
||||
appForm: defaultState
|
||||
};
|
||||
}
|
||||
|
||||
const currentState = getAppConfigByDiff(defaultState, item.diff);
|
||||
return {
|
||||
title: item.title,
|
||||
isSaved: item.isSaved,
|
||||
appForm: currentState
|
||||
};
|
||||
})
|
||||
.filter(Boolean) as SimpleAppSnapshotType[]
|
||||
);
|
||||
|
||||
const pastState = getAppConfigByDiff(defaultState, pastSnapshot[0].diff);
|
||||
localStorage.removeItem(`${appDetail._id}-past`);
|
||||
return updateAppForm(pastState);
|
||||
}
|
||||
|
||||
// 无旧的记录,正常初始化
|
||||
if (past.length === 0) {
|
||||
const appForm = appWorkflow2Form({
|
||||
nodes: appDetail.modules,
|
||||
chatConfig: appDetail.chatConfig
|
||||
});
|
||||
saveSnapshot({
|
||||
appForm,
|
||||
title: t('app:initial_form'),
|
||||
isSaved: true
|
||||
});
|
||||
updateAppForm(appForm);
|
||||
} else {
|
||||
updateAppForm(past[0].appForm);
|
||||
}
|
||||
});
|
||||
|
||||
// 通用样式变量
|
||||
const spacing = {
|
||||
xs: '4px',
|
||||
sm: '8px',
|
||||
md: '12px',
|
||||
lg: '16px',
|
||||
xl: '20px'
|
||||
};
|
||||
|
||||
const formStyles = {
|
||||
fontSize: '14px',
|
||||
lineHeight: '20px',
|
||||
fontWeight: '500',
|
||||
letterSpacing: '0.1px'
|
||||
};
|
||||
|
||||
const inputStyles = {
|
||||
padding: '10px 12px',
|
||||
height: '40px',
|
||||
fontSize: '14px',
|
||||
lineHeight: '20px',
|
||||
letterSpacing: '0.25px'
|
||||
};
|
||||
|
||||
const handleSloganChange = (val: string) => {
|
||||
onSloganChange?.(val);
|
||||
};
|
||||
|
||||
const handlePlaceholderChange = (val: string) => {
|
||||
onPlaceholderChange?.(val);
|
||||
};
|
||||
|
||||
// 快捷应用相关处理函数
|
||||
const handleOpenQuickAppModal = () => {
|
||||
setIsQuickAppModalOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseQuickAppModal = () => {
|
||||
setIsQuickAppModalOpen(false);
|
||||
};
|
||||
|
||||
const handleQuickAppSuccess = (selectedApps: any[]) => {
|
||||
// 刷新快捷应用列表
|
||||
refreshQuickApps();
|
||||
console.log('快捷应用更新成功:', selectedApps);
|
||||
setIsQuickAppModalOpen(false);
|
||||
};
|
||||
|
||||
// 修改 setAppForm,使其同时调用父组件的回调
|
||||
const updateAppForm = useCallback(
|
||||
(newAppForm: AppSimpleEditFormType) => {
|
||||
setAppForm(newAppForm);
|
||||
onAppFormChange?.(newAppForm);
|
||||
},
|
||||
[onAppFormChange]
|
||||
);
|
||||
|
||||
// 渲染快捷应用项
|
||||
const renderQuickAppItem = (app: AppListItemType, index: number) => {
|
||||
const gradients = [
|
||||
'linear-gradient(200.75deg, #67BFFF 13.74%, #5BA6FF 89.76%)', // 蓝色渐变
|
||||
'linear-gradient(200.75deg, #7895FE 13.74%, #7177FF 89.76%)', // 紫色渐变
|
||||
'linear-gradient(200.75deg, #67BFFF 13.74%, #5BA6FF 89.76%)', // 蓝色渐变
|
||||
'linear-gradient(200.75deg, #67BFFF 13.74%, #5BA6FF 89.76%)' // 蓝色渐变
|
||||
];
|
||||
|
||||
const gradient = gradients[index % gradients.length];
|
||||
|
||||
return (
|
||||
<React.Fragment key={app._id}>
|
||||
{/* 应用项 */}
|
||||
<Flex
|
||||
flexDirection="column"
|
||||
alignItems="flex-start"
|
||||
padding="4px 0px"
|
||||
gap="10px"
|
||||
w="80px"
|
||||
h="28px"
|
||||
borderRadius="6px"
|
||||
>
|
||||
<Flex alignItems="center" gap="4px" w="80px" h="20px">
|
||||
<Box
|
||||
w="20px"
|
||||
h="20px"
|
||||
background={gradient}
|
||||
borderRadius="6px"
|
||||
position="relative"
|
||||
overflow="hidden"
|
||||
>
|
||||
{app.avatar ? (
|
||||
<Avatar src={app.avatar} alt={app.name} w="100%" h="100%" borderRadius="6px" />
|
||||
) : (
|
||||
<MyIcon
|
||||
name="core/app/type/simple"
|
||||
position="absolute"
|
||||
left="15%"
|
||||
right="15%"
|
||||
top="15%"
|
||||
bottom="15%"
|
||||
color="white"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<Text
|
||||
w="56px"
|
||||
h="16px"
|
||||
fontFamily="PingFang SC"
|
||||
fontWeight={400}
|
||||
fontSize="12px"
|
||||
lineHeight="16px"
|
||||
letterSpacing="0.004em"
|
||||
color="#111824"
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
{app.name}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
{/* 分隔线 - 除了最后一个应用之外都显示 */}
|
||||
{index < Math.min(quickApps.length - 1, 3) && (
|
||||
<Box w="11.46px" h="0px" border="1px solid #DFE2EA" transform="rotate(90deg)" />
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box flex="1 0 0" overflow="auto" px={spacing.sm}>
|
||||
<Flex
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
gap={spacing.xl}
|
||||
maxW="640px"
|
||||
mx="auto"
|
||||
pb={6}
|
||||
pt={{ base: 4, md: 6 }}
|
||||
>
|
||||
{/* 快捷应用 */}
|
||||
<FormControl
|
||||
display={'flex'}
|
||||
flexDirection={'column'}
|
||||
justifyContent={'center'}
|
||||
alignItems={'flex-start'}
|
||||
gap={'8px'}
|
||||
>
|
||||
{/* 标题行 */}
|
||||
<Flex alignItems={'center'} gap={'1'}>
|
||||
<Text
|
||||
color={'var(--Gray-Modern-600, #485264)'}
|
||||
fontFamily={'PingFang SC'}
|
||||
fontSize={'14px'}
|
||||
fontWeight={500}
|
||||
lineHeight={'20px'}
|
||||
letterSpacing={'0.1px'}
|
||||
>
|
||||
{t('account_gate:quick_app')}
|
||||
</Text>
|
||||
<MyIcon name="common/help" w="16px" h="16px" color="#667085" />
|
||||
</Flex>
|
||||
|
||||
{/* 下拉框区域 */}
|
||||
<Flex alignItems="center" gap="8px" w="640px" h="40px">
|
||||
{/* 应用容器 */}
|
||||
<Box
|
||||
position="relative"
|
||||
w="600px"
|
||||
h="40px"
|
||||
bg="#FBFBFC"
|
||||
border="1px solid #E8EBF0"
|
||||
borderRadius="8px"
|
||||
>
|
||||
{/* 应用列表 */}
|
||||
<Flex
|
||||
position="absolute"
|
||||
alignItems="center"
|
||||
gap="8px"
|
||||
w="560px"
|
||||
h="28px"
|
||||
left="12px"
|
||||
top="calc(50% - 14px)"
|
||||
>
|
||||
{quickApps.length > 0 ? (
|
||||
quickApps.slice(0, 4).map((app, index) => renderQuickAppItem(app, index))
|
||||
) : (
|
||||
<Text fontSize="12px" color="#667085" fontFamily="PingFang SC">
|
||||
{loadingQuickApps ? '加载中...' : '暂无快捷应用'}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
{/* 设置按钮 */}
|
||||
<Flex
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
padding="7px"
|
||||
gap="6px"
|
||||
w="32px"
|
||||
h="32px"
|
||||
borderRadius="6px"
|
||||
cursor="pointer"
|
||||
onClick={handleOpenQuickAppModal}
|
||||
_hover={{
|
||||
bg: 'myGray.100'
|
||||
}}
|
||||
>
|
||||
<MyIcon name="common/settingLight" w="18px" h="18px" color="#667085" />
|
||||
</Flex>
|
||||
</Flex>
|
||||
</FormControl>
|
||||
|
||||
{/* 可用工具选择 */}
|
||||
<FormControl display="flex" flexDirection="column" gap={spacing.sm} w="full">
|
||||
<ToolSelect
|
||||
appForm={appForm}
|
||||
setAppForm={updateAppForm} // 使用 updateAppForm 替代 setAppForm
|
||||
/>
|
||||
</FormControl>
|
||||
{/* slogan设置 */}
|
||||
<FormControl display="flex" flexDirection="column" gap={spacing.sm} w="full">
|
||||
<Flex alignItems="center" gap={spacing.xs}>
|
||||
<Text
|
||||
fontWeight={formStyles.fontWeight}
|
||||
fontSize={formStyles.fontSize}
|
||||
lineHeight={formStyles.lineHeight}
|
||||
letterSpacing={formStyles.letterSpacing}
|
||||
color="myGray.700"
|
||||
>
|
||||
{t('account_gate:slogan')}
|
||||
</Text>
|
||||
<Link
|
||||
color="primary.500"
|
||||
fontSize={formStyles.fontSize}
|
||||
fontWeight={formStyles.fontWeight}
|
||||
textDecoration="underline"
|
||||
>
|
||||
{t('account_gate:example')}
|
||||
</Link>
|
||||
</Flex>
|
||||
<Input
|
||||
value={slogan}
|
||||
onChange={(e) => handleSloganChange(e.target.value)}
|
||||
bg="myGray.50"
|
||||
borderWidth="1px"
|
||||
borderColor="myGray.200"
|
||||
borderRadius="8px"
|
||||
p={inputStyles.padding}
|
||||
h={inputStyles.height}
|
||||
fontSize={inputStyles.fontSize}
|
||||
lineHeight={inputStyles.lineHeight}
|
||||
letterSpacing={inputStyles.letterSpacing}
|
||||
color="gray.900"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
{/* 对话提示文字 */}
|
||||
<FormControl display="flex" flexDirection="column" gap={spacing.sm} w="full">
|
||||
<Flex alignItems="center" gap={spacing.xs}>
|
||||
<Text
|
||||
fontWeight={formStyles.fontWeight}
|
||||
fontSize={formStyles.fontSize}
|
||||
lineHeight={formStyles.lineHeight}
|
||||
letterSpacing={formStyles.letterSpacing}
|
||||
color="myGray.700"
|
||||
>
|
||||
{t('account_gate:dialog_prompt_text')}
|
||||
</Text>
|
||||
<Link
|
||||
color="primary.500"
|
||||
fontSize={formStyles.fontSize}
|
||||
fontWeight={formStyles.fontWeight}
|
||||
textDecoration="underline"
|
||||
>
|
||||
{t('account_gate:example')}
|
||||
</Link>
|
||||
</Flex>
|
||||
<Input
|
||||
value={placeholderText}
|
||||
onChange={(e) => handlePlaceholderChange(e.target.value)}
|
||||
bg="myGray.50"
|
||||
borderWidth="1px"
|
||||
borderColor="myGray.200"
|
||||
borderRadius="8px"
|
||||
p={inputStyles.padding}
|
||||
h={inputStyles.height}
|
||||
fontSize={inputStyles.fontSize}
|
||||
lineHeight={inputStyles.lineHeight}
|
||||
letterSpacing={inputStyles.letterSpacing}
|
||||
color="gray.900"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
{/* 可用工具 */}
|
||||
{/* <FormControl display="flex" flexDirection="column" gap={spacing.sm} w="full">
|
||||
<Flex gap={spacing.xs}>
|
||||
<FormLabel
|
||||
fontWeight={formStyles.fontWeight}
|
||||
fontSize={formStyles.fontSize}
|
||||
lineHeight={formStyles.lineHeight}
|
||||
letterSpacing={formStyles.letterSpacing}
|
||||
color="myGray.700"
|
||||
mb="0"
|
||||
>
|
||||
{t('account_gate:available_tools')}
|
||||
</FormLabel>
|
||||
<QuestionTip />
|
||||
</Flex>
|
||||
<CheckboxGroup colorScheme="blue" value={tools} onChange={handleToolsChange}>
|
||||
<Wrap spacing={toolsSpacing}>
|
||||
{[
|
||||
{ value: 'webSearch', label: t('account_gate:web_search') },
|
||||
{ value: 'deepThinking', label: t('account_gate:deep_thinking') },
|
||||
{ value: 'fileUpload', label: t('account_gate:file_upload') },
|
||||
{ value: 'imageUpload', label: t('account_gate:image_upload') },
|
||||
{ value: 'voiceInput', label: t('account_gate:voice_input') }
|
||||
].map((item) => (
|
||||
<WrapItem key={item.value}>
|
||||
<Flex
|
||||
p={`${spacing.sm} ${spacing.lg} ${spacing.sm} ${spacing.md}`}
|
||||
borderWidth="1px"
|
||||
borderColor={
|
||||
tools.includes(item.value as GateTool) ? 'primary.500' : 'myGray.200'
|
||||
}
|
||||
borderRadius="7px"
|
||||
bg={tools.includes(item.value as GateTool) ? 'blue.50' : 'white'}
|
||||
transition="all 0.2s ease-in-out"
|
||||
_hover={{
|
||||
bg: tools.includes(item.value as GateTool) ? 'blue.100' : 'myGray.50',
|
||||
borderColor: tools.includes(item.value as GateTool)
|
||||
? 'primary.600'
|
||||
: 'myGray.300',
|
||||
boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.05)',
|
||||
transform: 'translateY(-1px)'
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
value={item.value}
|
||||
colorScheme="blue"
|
||||
isChecked={tools.includes(item.value as GateTool)}
|
||||
>
|
||||
<Text
|
||||
fontSize={formStyles.fontSize}
|
||||
lineHeight={formStyles.lineHeight}
|
||||
fontWeight={formStyles.fontWeight}
|
||||
letterSpacing={formStyles.letterSpacing}
|
||||
>
|
||||
{item.label}
|
||||
</Text>
|
||||
</Checkbox>
|
||||
</Flex>
|
||||
</WrapItem>
|
||||
))}
|
||||
</Wrap>
|
||||
</CheckboxGroup>
|
||||
</FormControl> */}
|
||||
</Flex>
|
||||
|
||||
{/* 快捷应用配置Modal */}
|
||||
{isQuickAppModalOpen && (
|
||||
<AddQuickAppModal
|
||||
isOpen={isQuickAppModalOpen}
|
||||
onClose={handleCloseQuickAppModal}
|
||||
onSuccess={handleQuickAppSuccess}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomeTable;
|
||||
|
||||
// 导出常量供其他组件使用
|
||||
export const spacing = {
|
||||
xs: '4px',
|
||||
sm: '8px',
|
||||
md: '12px',
|
||||
lg: '16px',
|
||||
xl: '20px'
|
||||
};
|
||||
|
||||
export const formStyles = {
|
||||
fontSize: '14px',
|
||||
lineHeight: '20px',
|
||||
fontWeight: '500',
|
||||
letterSpacing: '0.1px'
|
||||
};
|
||||
@ -0,0 +1,186 @@
|
||||
import React, { useMemo, useState, useEffect } from 'react';
|
||||
import { Box, type BoxProps, Flex, Checkbox } from '@chakra-ui/react';
|
||||
import {
|
||||
type GetResourceFolderListProps,
|
||||
type GetResourceListItemResponse,
|
||||
type ParentIdType
|
||||
} from '@fastgpt/global/common/parentFolder/type';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import Loading from '@fastgpt/web/components/common/MyLoading';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { FolderImgUrl } from '@fastgpt/global/common/file/image/constants';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
type ResourceItemType = GetResourceListItemResponse & {
|
||||
open: boolean;
|
||||
children?: ResourceItemType[];
|
||||
};
|
||||
|
||||
const rootId = 'root';
|
||||
|
||||
const SelectMultipleResource = ({
|
||||
server,
|
||||
selectedIds = [],
|
||||
onSelect,
|
||||
maxH = ['80vh', '600px'],
|
||||
searchKey = ''
|
||||
}: {
|
||||
server: (e: GetResourceFolderListProps) => Promise<GetResourceListItemResponse[]>;
|
||||
selectedIds?: string[];
|
||||
onSelect: (id: string, appData: GetResourceListItemResponse) => any;
|
||||
maxH?: BoxProps['maxH'];
|
||||
searchKey?: string;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [dataList, setDataList] = useState<ResourceItemType[]>([]);
|
||||
const [requestingIdList, setRequestingIdList] = useState<ParentIdType[]>([]);
|
||||
|
||||
const concatRoot = useMemo(() => {
|
||||
const root: ResourceItemType = {
|
||||
id: rootId,
|
||||
open: true,
|
||||
avatar: FolderImgUrl,
|
||||
name: t('common:root_folder'),
|
||||
isFolder: true,
|
||||
children: dataList
|
||||
};
|
||||
return [root];
|
||||
}, [dataList, t]);
|
||||
|
||||
const { runAsync: requestServer } = useRequest2((e: GetResourceFolderListProps) => {
|
||||
if (requestingIdList.includes(e.parentId)) return Promise.reject(null);
|
||||
|
||||
setRequestingIdList((state) => [...state, e.parentId]);
|
||||
return server(e).finally(() =>
|
||||
setRequestingIdList((state) => state.filter((id) => id !== e.parentId))
|
||||
);
|
||||
}, {});
|
||||
|
||||
const { loading, refresh } = useRequest2(() => requestServer({ parentId: null }), {
|
||||
manual: false,
|
||||
onSuccess: (data) => {
|
||||
setDataList(
|
||||
data.map((item) => ({
|
||||
...item,
|
||||
open: false
|
||||
}))
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// 当搜索关键词变化时,重新加载数据
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, [searchKey, refresh]);
|
||||
|
||||
const Render = useMemoizedFn(
|
||||
({ list, index = 0 }: { list: ResourceItemType[]; index?: number }) => {
|
||||
return (
|
||||
<>
|
||||
{list.map((item) => (
|
||||
<Box key={item.id} _notLast={{ mb: 0.5 }} userSelect={'none'}>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
cursor={'pointer'}
|
||||
py={1}
|
||||
pl={index === 0 ? '0.5rem' : `${1.75 * (index - 1) + 0.5}rem`}
|
||||
pr={2}
|
||||
borderRadius={'md'}
|
||||
_hover={{
|
||||
bg: 'myGray.100'
|
||||
}}
|
||||
onClick={async () => {
|
||||
if (item.id === rootId) return;
|
||||
// folder => open(request children) or close
|
||||
if (item.isFolder) {
|
||||
if (!item.children) {
|
||||
const data = await requestServer({ parentId: item.id });
|
||||
item.children = data.map((childItem) => ({
|
||||
...childItem,
|
||||
open: false
|
||||
}));
|
||||
}
|
||||
|
||||
item.open = !item.open;
|
||||
setDataList([...dataList]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Checkbox for non-folder items */}
|
||||
{!item.isFolder && item.id !== rootId && (
|
||||
<Checkbox
|
||||
isChecked={selectedIds.includes(item.id)}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
onSelect(item.id, item);
|
||||
}}
|
||||
mr={2}
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
|
||||
{index !== 0 && (
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
visibility={item.isFolder ? 'visible' : 'hidden'}
|
||||
w={'1.25rem'}
|
||||
h={'1.25rem'}
|
||||
cursor={'pointer'}
|
||||
borderRadius={'xs'}
|
||||
_hover={{
|
||||
bg: 'rgba(31, 35, 41, 0.08)'
|
||||
}}
|
||||
>
|
||||
<MyIcon
|
||||
name={
|
||||
requestingIdList.includes(item.id)
|
||||
? 'common/loading'
|
||||
: 'common/rightArrowFill'
|
||||
}
|
||||
w={'14px'}
|
||||
color={'myGray.500'}
|
||||
transform={item.open ? 'rotate(90deg)' : 'none'}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
<Avatar
|
||||
ml={index !== 0 ? '0.5rem' : 0}
|
||||
src={item.avatar}
|
||||
w={'1.25rem'}
|
||||
borderRadius={'sm'}
|
||||
/>
|
||||
<Box
|
||||
fontSize={['md', 'sm']}
|
||||
ml={2}
|
||||
className="textEllipsis"
|
||||
color={selectedIds.includes(item.id) ? 'primary.600' : 'inherit'}
|
||||
fontWeight={selectedIds.includes(item.id) ? 'medium' : 'normal'}
|
||||
>
|
||||
{item.name}
|
||||
</Box>
|
||||
</Flex>
|
||||
{item.children && item.open && (
|
||||
<Box mt={0.5}>
|
||||
<Render list={item.children} index={index + 1} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
return loading ? (
|
||||
<Loading fixed={false} />
|
||||
) : (
|
||||
<Box maxH={maxH} h={'100%'} overflow={'auto'}>
|
||||
<Render list={concatRoot} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectMultipleResource;
|
||||
@ -0,0 +1,169 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { Box, type BoxProps, Flex } from '@chakra-ui/react';
|
||||
import {
|
||||
type GetResourceFolderListProps,
|
||||
type GetResourceListItemResponse,
|
||||
type ParentIdType
|
||||
} from '@fastgpt/global/common/parentFolder/type';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import Loading from '@fastgpt/web/components/common/MyLoading';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { FolderImgUrl } from '@fastgpt/global/common/file/image/constants';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
type ResourceItemType = GetResourceListItemResponse & {
|
||||
open: boolean;
|
||||
children?: ResourceItemType[];
|
||||
};
|
||||
|
||||
const rootId = 'root';
|
||||
|
||||
const SelectOneResource = ({
|
||||
server,
|
||||
value,
|
||||
onSelect,
|
||||
maxH = ['80vh', '600px']
|
||||
}: {
|
||||
server: (e: GetResourceFolderListProps) => Promise<GetResourceListItemResponse[]>;
|
||||
value?: ParentIdType;
|
||||
onSelect: (e?: string) => any;
|
||||
maxH?: BoxProps['maxH'];
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [dataList, setDataList] = useState<ResourceItemType[]>([]);
|
||||
const [requestingIdList, setRequestingIdList] = useState<ParentIdType[]>([]);
|
||||
|
||||
const concatRoot = useMemo(() => {
|
||||
const root: ResourceItemType = {
|
||||
id: rootId,
|
||||
open: true,
|
||||
avatar: FolderImgUrl,
|
||||
name: t('common:root_folder'),
|
||||
isFolder: true,
|
||||
children: dataList
|
||||
};
|
||||
return [root];
|
||||
}, [dataList, t]);
|
||||
|
||||
const { runAsync: requestServer } = useRequest2((e: GetResourceFolderListProps) => {
|
||||
if (requestingIdList.includes(e.parentId)) return Promise.reject(null);
|
||||
|
||||
setRequestingIdList((state) => [...state, e.parentId]);
|
||||
return server(e).finally(() =>
|
||||
setRequestingIdList((state) => state.filter((id) => id !== e.parentId))
|
||||
);
|
||||
}, {});
|
||||
|
||||
const { loading } = useRequest2(() => requestServer({ parentId: null }), {
|
||||
manual: false,
|
||||
onSuccess: (data) => {
|
||||
setDataList(
|
||||
data.map((item) => ({
|
||||
...item,
|
||||
open: false
|
||||
}))
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const Render = useMemoizedFn(
|
||||
({ list, index = 0 }: { list: ResourceItemType[]; index?: number }) => {
|
||||
return (
|
||||
<>
|
||||
{list.map((item) => (
|
||||
<Box key={item.id} _notLast={{ mb: 0.5 }} userSelect={'none'}>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
cursor={'pointer'}
|
||||
py={1}
|
||||
pl={index === 0 ? '0.5rem' : `${1.75 * (index - 1) + 0.5}rem`}
|
||||
pr={2}
|
||||
borderRadius={'md'}
|
||||
_hover={{
|
||||
bg: 'myGray.100'
|
||||
}}
|
||||
{...(item.id === value
|
||||
? {
|
||||
bg: 'primary.50 !important',
|
||||
onClick: () => onSelect(undefined)
|
||||
}
|
||||
: {
|
||||
onClick: async () => {
|
||||
if (item.id === rootId) return;
|
||||
// folder => open(request children) or close
|
||||
if (item.isFolder) {
|
||||
if (!item.children) {
|
||||
const data = await requestServer({ parentId: item.id });
|
||||
item.children = data.map((item) => ({
|
||||
...item,
|
||||
open: false
|
||||
}));
|
||||
}
|
||||
|
||||
item.open = !item.open;
|
||||
setDataList([...dataList]);
|
||||
} else {
|
||||
onSelect(item.id);
|
||||
}
|
||||
}
|
||||
})}
|
||||
>
|
||||
{index !== 0 && (
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
visibility={item.isFolder ? 'visible' : 'hidden'}
|
||||
w={'1.25rem'}
|
||||
h={'1.25rem'}
|
||||
cursor={'pointer'}
|
||||
borderRadius={'xs'}
|
||||
_hover={{
|
||||
bg: 'rgba(31, 35, 41, 0.08)'
|
||||
}}
|
||||
>
|
||||
<MyIcon
|
||||
name={
|
||||
requestingIdList.includes(item.id)
|
||||
? 'common/loading'
|
||||
: 'common/rightArrowFill'
|
||||
}
|
||||
w={'14px'}
|
||||
color={'myGray.500'}
|
||||
transform={item.open ? 'rotate(90deg)' : 'none'}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
<Avatar
|
||||
ml={index !== 0 ? '0.5rem' : 0}
|
||||
src={item.avatar}
|
||||
w={'1.25rem'}
|
||||
borderRadius={'sm'}
|
||||
/>
|
||||
<Box fontSize={['md', 'sm']} ml={2} className="textEllipsis">
|
||||
{item.name}
|
||||
</Box>
|
||||
</Flex>
|
||||
{item.children && item.open && (
|
||||
<Box mt={0.5}>
|
||||
<Render list={item.children} index={index + 1} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
return loading ? (
|
||||
<Loading fixed={false} />
|
||||
) : (
|
||||
<Box maxH={maxH} h={'100%'} overflow={'auto'}>
|
||||
<Render list={concatRoot} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectOneResource;
|
||||
196
projects/app/src/pageComponents/account/gateway/ShareModol.tsx
Normal file
@ -0,0 +1,196 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Box, Flex, Text, IconButton, Input } from '@chakra-ui/react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useCopyData } from '@fastgpt/web/hooks/useCopyData';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { CopyIcon } from '@chakra-ui/icons';
|
||||
import type { GateSchemaType } from '@fastgpt/global/support/user/team/gate/type';
|
||||
|
||||
// 分享门户组件
|
||||
const ShareGateModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
gateConfig
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
gateConfig: GateSchemaType | undefined;
|
||||
}) => {
|
||||
const { copyData } = useCopyData();
|
||||
|
||||
// 门户链接和自定义域名
|
||||
const [defaultGateUrl] = useState(`${window.location.origin}/chat/gate`);
|
||||
|
||||
// 复制链接
|
||||
const handleCopyLink = (link: string) => {
|
||||
copyData(link, '链接已复制');
|
||||
};
|
||||
|
||||
// 保存配置
|
||||
const handleSave = () => {
|
||||
// 保存自定义域名的逻辑
|
||||
onClose();
|
||||
};
|
||||
|
||||
// 获取门户状态
|
||||
const isGateEnabled = gateConfig?.status || false;
|
||||
|
||||
return (
|
||||
<MyModal isOpen={isOpen} onClose={onClose} maxW="500px">
|
||||
<Box
|
||||
position="relative"
|
||||
width="500px"
|
||||
maxHeight="80vh"
|
||||
gap={'20px'}
|
||||
bg="#FFFFFF"
|
||||
boxShadow="0px 32px 64px -12px rgba(19, 51, 107, 0.2), 0px 0px 1px rgba(19, 51, 107, 0.2)"
|
||||
borderRadius="10px"
|
||||
overflowY="auto"
|
||||
>
|
||||
{/* 弹窗头部 */}
|
||||
<Flex
|
||||
boxSizing="border-box"
|
||||
w="500px"
|
||||
h="48px"
|
||||
bg="#FBFBFC"
|
||||
borderBottom="1px solid #F4F4F7"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
px="20px"
|
||||
borderTopLeftRadius="10px"
|
||||
borderTopRightRadius="10px"
|
||||
overflow="hidden"
|
||||
>
|
||||
<Flex alignItems="center" gap="10px">
|
||||
<MyIcon name="support/gate/home/sharePrimary" color="#3370FF" />
|
||||
<Text
|
||||
fontFamily="PingFang SC"
|
||||
fontWeight="500"
|
||||
fontSize="16px"
|
||||
lineHeight="24px"
|
||||
letterSpacing="0.15px"
|
||||
color="#24282C"
|
||||
>
|
||||
分享门户
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
{/* 弹窗内容 */}
|
||||
<Flex
|
||||
direction="column"
|
||||
alignItems="flex-start"
|
||||
padding="24px 36px"
|
||||
gap="24px"
|
||||
w="100%"
|
||||
h="100%"
|
||||
>
|
||||
{/* 上部内容区 */}
|
||||
<Flex direction="column" gap="20px" w="428px">
|
||||
{/* 提示信息 */}
|
||||
<Flex
|
||||
bg="#F0F4FF"
|
||||
borderRadius="6px"
|
||||
p="6px 12px"
|
||||
alignItems="center"
|
||||
w="100%"
|
||||
h="44px"
|
||||
>
|
||||
<Text
|
||||
fontFamily="PingFang SC"
|
||||
fontWeight="500"
|
||||
fontSize="12px"
|
||||
lineHeight="16px"
|
||||
letterSpacing="0.5px"
|
||||
color="#3370FF"
|
||||
>
|
||||
通过门户进入的用户仍需登录账号及应用鉴权。
|
||||
门户仅支持与已配置的应用对话,对话记录与站内聊天记录互通。
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
{/* 门户状态 */}
|
||||
<Flex alignItems="center" gap="12px">
|
||||
<Text
|
||||
fontFamily="PingFang SC"
|
||||
fontWeight="500"
|
||||
fontSize="14px"
|
||||
lineHeight="20px"
|
||||
letterSpacing="0.1px"
|
||||
color="#111824"
|
||||
>
|
||||
门户状态:
|
||||
</Text>
|
||||
<Flex
|
||||
bg={isGateEnabled ? '#EDFBF3' : '#FFF0F0'}
|
||||
borderRadius="6px"
|
||||
p="4px 8px"
|
||||
alignItems="center"
|
||||
gap="4px"
|
||||
>
|
||||
<Box
|
||||
w="6px"
|
||||
h="6px"
|
||||
borderRadius="50%"
|
||||
bg={isGateEnabled ? '#039855' : '#D92D20'}
|
||||
></Box>
|
||||
<Text
|
||||
fontFamily="PingFang SC"
|
||||
fontWeight="500"
|
||||
fontSize="12px"
|
||||
lineHeight="16px"
|
||||
letterSpacing="0.5px"
|
||||
color={isGateEnabled ? '#039855' : '#D92D20'}
|
||||
>
|
||||
{isGateEnabled ? '已启用' : '已禁用'}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
{/* 默认地址 */}
|
||||
<Flex direction="column" alignItems="flex-start" gap="8px" w="100%">
|
||||
<Text
|
||||
fontFamily="PingFang SC"
|
||||
fontWeight="500"
|
||||
fontSize="14px"
|
||||
lineHeight="20px"
|
||||
letterSpacing="0.1px"
|
||||
color="#24282C"
|
||||
>
|
||||
默认地址
|
||||
</Text>
|
||||
<Flex w="100%" alignItems="center" gap="8px">
|
||||
<Input
|
||||
value={defaultGateUrl}
|
||||
readOnly
|
||||
h="32px"
|
||||
bg="#FFFFFF"
|
||||
border="1px solid #3370FF"
|
||||
boxShadow="0px 0px 0px 2.4px rgba(51, 112, 255, 0.15)"
|
||||
borderRadius="6px"
|
||||
fontSize="12px"
|
||||
color="#111824"
|
||||
pl="12px"
|
||||
flex="1"
|
||||
/>
|
||||
<IconButton
|
||||
aria-label="复制链接"
|
||||
icon={<CopyIcon />}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
colorScheme="gray"
|
||||
onClick={() => handleCopyLink(defaultGateUrl)}
|
||||
h="32px"
|
||||
w="32px"
|
||||
minW="32px"
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Box>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShareGateModal;
|
||||
@ -0,0 +1,779 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
Input,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
useToast,
|
||||
HStack,
|
||||
IconButton,
|
||||
Container,
|
||||
Divider,
|
||||
Text,
|
||||
Checkbox
|
||||
} from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import {
|
||||
getTeamTags,
|
||||
createTag,
|
||||
updateTag,
|
||||
deleteTag,
|
||||
batchAddTagsToApp,
|
||||
batchRemoveTagsFromApp,
|
||||
batchAddAppsToTag
|
||||
} from '@/web/core/app/api/tags';
|
||||
import type { TagWithCountType } from '@fastgpt/global/core/app/tags';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
|
||||
import SelectMultipleResource from './SelectMultipleResource';
|
||||
import {
|
||||
type GetResourceFolderListProps,
|
||||
type GetResourceListItemResponse
|
||||
} from '@fastgpt/global/common/parentFolder/type';
|
||||
import { getMyApps } from '@/web/core/app/api';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import type { AppListItemType } from '@fastgpt/global/core/app/type.d';
|
||||
|
||||
interface TagManageModalProps {
|
||||
onClose: () => void;
|
||||
onTagsUpdated?: () => void;
|
||||
}
|
||||
|
||||
type ViewMode = 'tagList' | 'appSelection';
|
||||
|
||||
const TagManageModal = ({ onClose, onTagsUpdated }: TagManageModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
||||
const [editingTag, setEditingTag] = useState<{
|
||||
_id?: string;
|
||||
name: string;
|
||||
}>({ name: '' });
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('tagList');
|
||||
const [selectedTagForAddApps, setSelectedTagForAddApps] = useState<TagWithCountType | null>(null);
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
const [selectedAppIds, setSelectedAppIds] = useState<string[]>([]);
|
||||
const [allApps, setAllApps] = useState<AppListItemType[]>([]);
|
||||
const [initialAppsWithTag, setInitialAppsWithTag] = useState<string[]>([]);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// 获取标签列表
|
||||
const { data: tags = [], loading: loadingTags } = useRequest2(
|
||||
async () => {
|
||||
const result = await getTeamTags(true);
|
||||
return result as TagWithCountType[];
|
||||
},
|
||||
{
|
||||
manual: false,
|
||||
refreshDeps: [refreshTrigger],
|
||||
onSuccess: (data) => {
|
||||
console.log('getTeamTags success', data);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 创建标签
|
||||
const { runAsync: createTagMutate, loading: createLoading } = useRequest2(
|
||||
(data: { name: string }) => createTag(data),
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: '标签创建成功',
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
isClosable: true
|
||||
});
|
||||
setRefreshTrigger((prev) => prev + 1);
|
||||
setIsCreating(false);
|
||||
setEditingTag({ name: '' });
|
||||
onTagsUpdated?.();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 更新标签
|
||||
const { runAsync: updateTagMutate, loading: updateLoading } = useRequest2(
|
||||
(data: { tagId: string; name: string }) => updateTag(data),
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: '标签更新成功',
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
isClosable: true
|
||||
});
|
||||
setRefreshTrigger((prev) => prev + 1);
|
||||
setIsEditing(false);
|
||||
setEditingTag({ name: '' });
|
||||
onTagsUpdated?.();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 删除标签
|
||||
const { runAsync: deleteTagMutate, loading: deleteLoading } = useRequest2(
|
||||
(tagId: string) => deleteTag(tagId),
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: '标签删除成功',
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
isClosable: true
|
||||
});
|
||||
setRefreshTrigger((prev) => prev + 1);
|
||||
onTagsUpdated?.();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 当创建或编辑模式激活时,聚焦输入框
|
||||
useEffect(() => {
|
||||
if ((isCreating || isEditing) && inputRef.current) {
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
}, 100);
|
||||
}
|
||||
}, [isCreating, isEditing]);
|
||||
|
||||
// 处理创建标签
|
||||
const handleCreateTag = () => {
|
||||
if (!editingTag.name.trim()) {
|
||||
toast({
|
||||
title: '标签名称不能为空',
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
isClosable: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
createTagMutate({
|
||||
name: editingTag.name
|
||||
});
|
||||
};
|
||||
|
||||
// 处理更新标签
|
||||
const handleUpdateTag = () => {
|
||||
if (!editingTag.name.trim()) {
|
||||
toast({
|
||||
title: '标签名称不能为空',
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
isClosable: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!editingTag._id) return;
|
||||
|
||||
updateTagMutate({
|
||||
tagId: editingTag._id,
|
||||
name: editingTag.name
|
||||
});
|
||||
};
|
||||
|
||||
// 处理删除标签
|
||||
const handleDeleteTag = (tagId: string) => {
|
||||
deleteTagMutate(tagId);
|
||||
};
|
||||
|
||||
// 开始编辑标签
|
||||
const startEditTag = (tag: TagWithCountType) => {
|
||||
if (isEditing && editingTag._id === tag._id) {
|
||||
cancelEdit();
|
||||
return;
|
||||
}
|
||||
|
||||
setEditingTag({
|
||||
_id: tag._id,
|
||||
name: tag.name
|
||||
});
|
||||
setIsEditing(true);
|
||||
setIsCreating(false);
|
||||
};
|
||||
|
||||
// 开始创建新标签
|
||||
const startCreateTag = () => {
|
||||
setEditingTag({ name: '' });
|
||||
setIsCreating(true);
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
// 取消编辑或创建
|
||||
const cancelEdit = () => {
|
||||
setIsEditing(false);
|
||||
setIsCreating(false);
|
||||
setEditingTag({ name: '' });
|
||||
};
|
||||
|
||||
// 获取应用列表的函数
|
||||
const getAppList = useCallback(
|
||||
async ({ parentId }: GetResourceFolderListProps) => {
|
||||
const apps = await getMyApps({
|
||||
parentId,
|
||||
searchKey,
|
||||
type: [AppTypeEnum.folder, AppTypeEnum.simple, AppTypeEnum.workflow, AppTypeEnum.plugin]
|
||||
});
|
||||
|
||||
// 保存所有应用数据,用于后续判断哪些应用已经有当前标签
|
||||
setAllApps(apps);
|
||||
|
||||
// 如果是第一次加载(parentId 为 null)且有选中的标签,保存初始有标签的应用
|
||||
if (parentId === null && selectedTagForAddApps && initialAppsWithTag.length === 0) {
|
||||
const appsWithCurrentTag = apps
|
||||
.filter((app) => app.tags?.includes(selectedTagForAddApps._id))
|
||||
.map((app) => app._id);
|
||||
setInitialAppsWithTag(appsWithCurrentTag);
|
||||
}
|
||||
|
||||
return apps.map<GetResourceListItemResponse>((item) => ({
|
||||
id: item._id,
|
||||
name: item.name,
|
||||
avatar: item.avatar,
|
||||
isFolder: item.type === AppTypeEnum.folder
|
||||
}));
|
||||
},
|
||||
[searchKey, selectedTagForAddApps, initialAppsWithTag.length]
|
||||
);
|
||||
|
||||
// 处理应用选择
|
||||
const handleAppSelect = useCallback(
|
||||
(appId: string, appData: GetResourceListItemResponse) => {
|
||||
if (!selectedTagForAddApps) return;
|
||||
|
||||
// 获取当前目录中有标签的应用
|
||||
const currentAppsWithTag = allApps
|
||||
.filter((app) => app.tags?.includes(selectedTagForAddApps._id))
|
||||
.map((app) => app._id);
|
||||
|
||||
// 判断这个应用是否初始就被选中(包括初始有标签的 + 当前目录中有标签的)
|
||||
const allInitialSelected = [...new Set([...initialAppsWithTag, ...currentAppsWithTag])];
|
||||
const isInitiallySelected = allInitialSelected.includes(appId);
|
||||
const isCurrentlyInSelectedIds = selectedAppIds.includes(appId);
|
||||
|
||||
setSelectedAppIds((prev) => {
|
||||
if (isInitiallySelected) {
|
||||
// 如果是初始就选中的应用
|
||||
if (isCurrentlyInSelectedIds) {
|
||||
// 当前在 selectedAppIds 中,移除它(表示取消选择)
|
||||
return prev.filter((id) => id !== appId);
|
||||
} else {
|
||||
// 当前不在 selectedAppIds 中,添加它(表示取消选择)
|
||||
return [...prev, appId];
|
||||
}
|
||||
} else {
|
||||
// 如果是初始没有选中的应用
|
||||
if (isCurrentlyInSelectedIds) {
|
||||
// 当前已选中,取消选择
|
||||
return prev.filter((id) => id !== appId);
|
||||
} else {
|
||||
// 当前未选中,添加选择
|
||||
return [...prev, appId];
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
[selectedTagForAddApps, initialAppsWithTag, allApps, selectedAppIds]
|
||||
);
|
||||
|
||||
// 获取当前选中的应用ID列表(包括已有标签的应用)
|
||||
const getSelectedIds = useCallback(() => {
|
||||
if (!selectedTagForAddApps) return [];
|
||||
|
||||
// 获取当前目录中有标签的应用
|
||||
const currentAppsWithTag = allApps
|
||||
.filter((app) => app.tags?.includes(selectedTagForAddApps._id))
|
||||
.map((app) => app._id);
|
||||
|
||||
// 合并:初始有标签的应用 + 当前目录中有标签的应用 + 用户手动选中的应用
|
||||
// 然后减去用户手动取消选择的应用
|
||||
const allInitialSelected = [...new Set([...initialAppsWithTag, ...currentAppsWithTag])];
|
||||
|
||||
// 计算最终选中的应用:
|
||||
// 1. 从所有初始选中的应用开始
|
||||
// 2. 加上用户新选中的应用
|
||||
// 3. 减去用户取消选择的应用
|
||||
const finalSelected = new Set(allInitialSelected);
|
||||
|
||||
// 处理用户的选择变更
|
||||
selectedAppIds.forEach((appId) => {
|
||||
if (allInitialSelected.includes(appId)) {
|
||||
// 如果这个应用初始是选中的,现在在 selectedAppIds 中表示用户取消了选择
|
||||
finalSelected.delete(appId);
|
||||
} else {
|
||||
// 如果这个应用初始不是选中的,现在在 selectedAppIds 中表示用户新选择了它
|
||||
finalSelected.add(appId);
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(finalSelected);
|
||||
}, [selectedTagForAddApps, initialAppsWithTag, allApps, selectedAppIds]);
|
||||
|
||||
// 批量更新应用标签
|
||||
const { runAsync: updateAppTags, loading: isUpdating } = useRequest2(
|
||||
async () => {
|
||||
if (!selectedTagForAddApps) return;
|
||||
|
||||
// 直接使用 getSelectedIds 获取最终应该拥有该标签的应用列表
|
||||
const finalSelectedIds = getSelectedIds();
|
||||
|
||||
// 使用新的批量添加应用到标签的 API 进行全量更新
|
||||
// 传入最终选中的所有应用 ID
|
||||
await batchAddAppsToTag(selectedTagForAddApps._id, finalSelectedIds);
|
||||
},
|
||||
{
|
||||
manual: true,
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: '标签应用更新成功',
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
isClosable: true
|
||||
});
|
||||
setRefreshTrigger((prev) => prev + 1);
|
||||
onTagsUpdated?.();
|
||||
// 返回标签列表视图
|
||||
setViewMode('tagList');
|
||||
setSelectedTagForAddApps(null);
|
||||
setSelectedAppIds([]);
|
||||
setSearchKey('');
|
||||
setInitialAppsWithTag([]); // 清理初始应用列表
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('更新标签应用失败:', error);
|
||||
toast({
|
||||
title: '更新标签应用失败',
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
isClosable: true
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 开始添加应用到标签
|
||||
const startAddAppsToTag = (tag: TagWithCountType) => {
|
||||
setSelectedTagForAddApps(tag);
|
||||
setViewMode('appSelection');
|
||||
setSelectedAppIds([]);
|
||||
setSearchKey('');
|
||||
setInitialAppsWithTag([]); // 重置初始应用列表,将在 getAppList 中重新设置
|
||||
};
|
||||
|
||||
// 返回标签列表
|
||||
const backToTagList = () => {
|
||||
setViewMode('tagList');
|
||||
setSelectedTagForAddApps(null);
|
||||
setSelectedAppIds([]);
|
||||
setSearchKey('');
|
||||
setInitialAppsWithTag([]); // 清理初始应用列表
|
||||
};
|
||||
|
||||
const isLoading = loadingTags || createLoading || updateLoading || deleteLoading;
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
iconSrc="/imgs/modal/tag.svg"
|
||||
title={viewMode === 'tagList' ? '分类管理' : `为标签"${selectedTagForAddApps?.name}"添加应用`}
|
||||
w="580px"
|
||||
maxW="100%"
|
||||
isLoading={isLoading || isUpdating}
|
||||
>
|
||||
<ModalBody px={9} py={6}>
|
||||
<Container maxW="100%" p={0}>
|
||||
{viewMode === 'tagList' ? (
|
||||
<>
|
||||
{/* 标签列表视图 */}
|
||||
{/* 头部区域 */}
|
||||
<Flex direction="column" gap={4} w="100%" h="40px" mb={4}>
|
||||
<Flex justifyContent="space-between" alignItems="center" w="100%" h="32px">
|
||||
<Flex alignItems="center" gap={2}>
|
||||
<MyIcon name="common/list" w="20px" h="20px" color="#111824" />
|
||||
<Box
|
||||
fontSize="16px"
|
||||
fontWeight="500"
|
||||
lineHeight="24px"
|
||||
letterSpacing="0.15px"
|
||||
color="#111824"
|
||||
>
|
||||
共 {tags.length} 个分类
|
||||
</Box>
|
||||
</Flex>
|
||||
<Button
|
||||
leftIcon={<MyIcon name="common/addLight" w="16px" h="16px" color="#485264" />}
|
||||
onClick={startCreateTag}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
bg="white"
|
||||
border="1px solid #DFE2EA"
|
||||
boxShadow="0px 1px 2px rgba(19, 51, 107, 0.05), 0px 0px 1px rgba(19, 51, 107, 0.08)"
|
||||
borderRadius="6px"
|
||||
h="32px"
|
||||
px="14px"
|
||||
fontSize="12px"
|
||||
fontWeight="500"
|
||||
lineHeight="16px"
|
||||
letterSpacing="0.5px"
|
||||
color="#485264"
|
||||
isDisabled={isCreating}
|
||||
_hover={{
|
||||
bg: 'gray.50'
|
||||
}}
|
||||
>
|
||||
新建
|
||||
</Button>
|
||||
</Flex>
|
||||
<Divider borderColor="#E8EBF0" />
|
||||
</Flex>
|
||||
|
||||
{/* 标签列表区域 */}
|
||||
<Flex direction="column" gap={2} w="100%" maxH="304px" overflowY="auto">
|
||||
{/* 创建新标签表单 */}
|
||||
{isCreating && (
|
||||
<Flex
|
||||
alignItems="center"
|
||||
p="4px 8px"
|
||||
gap={2}
|
||||
w="100%"
|
||||
h="36px"
|
||||
borderRadius="4px"
|
||||
bg="transparent"
|
||||
>
|
||||
<Flex alignItems="center" gap={2} w="195px" h="28px">
|
||||
<Box
|
||||
position="relative"
|
||||
w="168px"
|
||||
h="28px"
|
||||
bg="white"
|
||||
border="1px solid #3370FF"
|
||||
boxShadow="0px 0px 0px 2.4px rgba(51, 112, 255, 0.15)"
|
||||
borderRadius="4px"
|
||||
>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
value={editingTag.name}
|
||||
onChange={(e) => setEditingTag({ ...editingTag, name: e.target.value })}
|
||||
placeholder="新建分类"
|
||||
maxLength={20}
|
||||
bg="transparent"
|
||||
border="none"
|
||||
h="100%"
|
||||
w="100%"
|
||||
px="8px"
|
||||
fontSize="12px"
|
||||
fontWeight="400"
|
||||
lineHeight="16px"
|
||||
letterSpacing="0.004em"
|
||||
color="#111824"
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
_placeholder={{ color: '#667085' }}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
fontSize="14px"
|
||||
fontWeight="400"
|
||||
lineHeight="20px"
|
||||
letterSpacing="0.25px"
|
||||
color="#667085"
|
||||
>
|
||||
(0)
|
||||
</Box>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{/* 标签列表 */}
|
||||
{(tags as TagWithCountType[]).map((tag, index) => (
|
||||
<React.Fragment key={tag._id}>
|
||||
{isEditing && editingTag._id === tag._id ? (
|
||||
// 编辑模式
|
||||
<Flex
|
||||
alignItems="center"
|
||||
p="4px 8px"
|
||||
gap={2}
|
||||
w="100%"
|
||||
h="36px"
|
||||
borderRadius="4px"
|
||||
bg="transparent"
|
||||
>
|
||||
<Flex alignItems="center" gap={2} w="195px" h="28px">
|
||||
<Box
|
||||
position="relative"
|
||||
w="168px"
|
||||
h="28px"
|
||||
bg="white"
|
||||
border="1px solid #3370FF"
|
||||
boxShadow="0px 0px 0px 2.4px rgba(51, 112, 255, 0.15)"
|
||||
borderRadius="4px"
|
||||
>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
value={editingTag.name}
|
||||
onChange={(e) =>
|
||||
setEditingTag({ ...editingTag, name: e.target.value })
|
||||
}
|
||||
maxLength={20}
|
||||
bg="transparent"
|
||||
border="none"
|
||||
h="100%"
|
||||
w="100%"
|
||||
px="8px"
|
||||
fontSize="12px"
|
||||
fontWeight="400"
|
||||
lineHeight="16px"
|
||||
letterSpacing="0.004em"
|
||||
color="#111824"
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
_placeholder={{ color: '#667085' }}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
fontSize="14px"
|
||||
fontWeight="400"
|
||||
lineHeight="20px"
|
||||
letterSpacing="0.25px"
|
||||
color="#667085"
|
||||
>
|
||||
({tag.count || 0})
|
||||
</Box>
|
||||
</Flex>
|
||||
</Flex>
|
||||
) : (
|
||||
// 普通显示模式
|
||||
<Flex
|
||||
alignItems="center"
|
||||
p="4px 8px"
|
||||
gap={2}
|
||||
w="100%"
|
||||
h="36px"
|
||||
borderRadius="4px"
|
||||
bg="transparent"
|
||||
_hover={{ bg: '#F9F9F9' }}
|
||||
>
|
||||
<Flex alignItems="center" gap={2} flex={1}>
|
||||
<Flex
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
p="10px 8px"
|
||||
h="28px"
|
||||
bg="#F4F4F5"
|
||||
borderRadius="6px"
|
||||
minW="fit-content"
|
||||
>
|
||||
<Box
|
||||
fontSize="12px"
|
||||
fontWeight="500"
|
||||
lineHeight="16px"
|
||||
color="#525252"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
{tag.name}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box fontSize="14px" color="#667085">
|
||||
({tag.count || 0})
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
<Flex alignItems="center" gap={2}>
|
||||
<IconButton
|
||||
aria-label="添加"
|
||||
icon={
|
||||
<MyIcon name="common/addLight" w="16px" h="16px" color="#485264" />
|
||||
}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
w="24px"
|
||||
h="24px"
|
||||
borderRadius="6px"
|
||||
onClick={() => startAddAppsToTag(tag)}
|
||||
isDisabled={isCreating}
|
||||
/>
|
||||
<IconButton
|
||||
aria-label="编辑"
|
||||
icon={<MyIcon name="edit" w="16px" h="16px" color="#485264" />}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
w="24px"
|
||||
h="24px"
|
||||
borderRadius="6px"
|
||||
onClick={() => startEditTag(tag)}
|
||||
isDisabled={isCreating}
|
||||
/>
|
||||
<IconButton
|
||||
aria-label="删除"
|
||||
icon={<MyIcon name="delete" w="16px" h="16px" color="#485264" />}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
w="24px"
|
||||
h="24px"
|
||||
borderRadius="6px"
|
||||
onClick={() => handleDeleteTag(tag._id)}
|
||||
isDisabled={isCreating}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
{index < tags.length - 1 && <Divider borderColor="#E8EBF0" />}
|
||||
</React.Fragment>
|
||||
))}
|
||||
|
||||
{tags.length === 0 && !loadingTags && (
|
||||
<Flex
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
h="100px"
|
||||
color="gray.500"
|
||||
fontSize="14px"
|
||||
>
|
||||
暂无标签
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* 应用选择视图 */}
|
||||
<Flex direction="column" h="500px" gap={4}>
|
||||
{/* 头部区域 */}
|
||||
<Flex direction="column" gap={4} w="100%" h="46px">
|
||||
<Flex justifyContent="space-between" alignItems="center" w="100%" h="38px">
|
||||
<Flex alignItems="center" gap={3}>
|
||||
<IconButton
|
||||
aria-label="返回"
|
||||
icon={
|
||||
<MyIcon name="common/leftArrowLight" w="18px" h="18px" color="#485264" />
|
||||
}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
w="32px"
|
||||
h="32px"
|
||||
borderRadius="6px"
|
||||
onClick={backToTagList}
|
||||
_hover={{
|
||||
bg: 'rgba(31, 35, 41, 0.08)'
|
||||
}}
|
||||
/>
|
||||
<Flex alignItems="center" gap={2}>
|
||||
<Flex
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
px="8px"
|
||||
py="6px"
|
||||
h="28px"
|
||||
bg="#F4F4F5"
|
||||
borderRadius="6px"
|
||||
minW="fit-content"
|
||||
>
|
||||
<Box
|
||||
fontSize="12px"
|
||||
fontWeight="500"
|
||||
lineHeight="16px"
|
||||
color="#525252"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
{selectedTagForAddApps?.name}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box
|
||||
fontSize="14px"
|
||||
fontWeight="400"
|
||||
lineHeight="20px"
|
||||
letterSpacing="0.25px"
|
||||
color="#667085"
|
||||
>
|
||||
({selectedTagForAddApps?.count || 0})
|
||||
</Box>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex alignItems="center" gap={2}>
|
||||
<Box w="200px" h="32px">
|
||||
<SearchInput
|
||||
value={searchKey}
|
||||
onChange={(e) => setSearchKey(e.target.value)}
|
||||
placeholder="搜索"
|
||||
bg="#F7F8FA"
|
||||
border="1px solid #E8EBF0"
|
||||
borderRadius="6px"
|
||||
h="32px"
|
||||
fontSize="12px"
|
||||
/>
|
||||
</Box>
|
||||
<Button
|
||||
leftIcon={<MyIcon name="save" w="16px" h="16px" color="#FFFFFF" />}
|
||||
onClick={() => updateAppTags()}
|
||||
size="sm"
|
||||
bg="#3370FF"
|
||||
color="white"
|
||||
boxShadow="0px 1px 2px rgba(19, 51, 107, 0.05), 0px 0px 1px rgba(19, 51, 107, 0.08)"
|
||||
borderRadius="6px"
|
||||
h="32px"
|
||||
px="14px"
|
||||
fontSize="12px"
|
||||
fontWeight="500"
|
||||
lineHeight="16px"
|
||||
letterSpacing="0.5px"
|
||||
isLoading={isUpdating}
|
||||
_hover={{
|
||||
bg: '#2C5CE6'
|
||||
}}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Divider borderColor="#E8EBF0" />
|
||||
</Flex>
|
||||
|
||||
{/* 应用选择区域 */}
|
||||
<Box flex={1} overflow="auto" w="100%" maxH="400px">
|
||||
<SelectMultipleResource
|
||||
selectedIds={getSelectedIds()}
|
||||
onSelect={handleAppSelect}
|
||||
server={getAppList}
|
||||
searchKey={searchKey}
|
||||
maxH="400px"
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
</Container>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter borderTopWidth="1px" py={4}>
|
||||
<Flex gap={3}>
|
||||
{(isCreating || isEditing) && viewMode === 'tagList' && (
|
||||
<>
|
||||
<Button variant="outline" size="sm" onClick={cancelEdit}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
size="sm"
|
||||
onClick={isCreating ? handleCreateTag : handleUpdateTag}
|
||||
>
|
||||
{isCreating ? '创建' : '保存'}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{!isCreating && !isEditing && viewMode === 'tagList' && (
|
||||
<Button onClick={onClose}>关闭</Button>
|
||||
)}
|
||||
{viewMode === 'appSelection' && <Button onClick={backToTagList}>关闭</Button>}
|
||||
</Flex>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagManageModal;
|
||||
326
projects/app/src/pageComponents/account/gateway/ToolSelect.tsx
Normal file
@ -0,0 +1,326 @@
|
||||
import { Box, Button, Flex, Grid, useDisclosure, Text } from '@chakra-ui/react';
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { SmallAddIcon } from '@chakra-ui/icons';
|
||||
import type { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import {
|
||||
FlowNodeInputTypeEnum,
|
||||
FlowNodeTypeEnum
|
||||
} from '@fastgpt/global/core/workflow/node/constant';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import { keyframes } from '@emotion/react';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
|
||||
import { getWebLLMModel } from '@/web/common/system/utils';
|
||||
import ToolSelectModal, {
|
||||
childAppSystemKey
|
||||
} from '@/pageComponents/app/detail/Gate/components/ToolSelectModal';
|
||||
import ConfigToolModal from '@/pageComponents/app/detail/Gate/components/ConfigToolModal';
|
||||
|
||||
// 定义粉碎动画关键帧
|
||||
const shatterKeyframes = keyframes`
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
filter: blur(0);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
transform: scale(0.7) rotate(5deg) translateY(10px);
|
||||
filter: blur(2px);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: scale(0.2) rotate(-5deg) translateY(15px);
|
||||
filter: blur(4px);
|
||||
}
|
||||
`;
|
||||
|
||||
// 定义淡入动画关键帧
|
||||
const fadeInKeyframes = keyframes`
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
// 样式常量
|
||||
const spacing = {
|
||||
xs: 2
|
||||
};
|
||||
|
||||
const formStyles = {
|
||||
fontWeight: 500,
|
||||
fontSize: '14px',
|
||||
lineHeight: '20px',
|
||||
letterSpacing: '0.1px'
|
||||
};
|
||||
|
||||
const ToolSelect = ({
|
||||
appForm,
|
||||
setAppForm
|
||||
}: {
|
||||
appForm: AppSimpleEditFormType;
|
||||
setAppForm: (newAppForm: AppSimpleEditFormType) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [configTool, setConfigTool] = useState<
|
||||
AppSimpleEditFormType['selectedTools'][number] | null
|
||||
>(null);
|
||||
|
||||
// 添加删除状态管理
|
||||
const [deletingToolIds, setDeletingToolIds] = useState<Set<string>>(new Set());
|
||||
|
||||
const {
|
||||
isOpen: isOpenToolsSelect,
|
||||
onOpen: onOpenToolsSelect,
|
||||
onClose: onCloseToolsSelect
|
||||
} = useDisclosure();
|
||||
const selectedModel = getWebLLMModel(appForm.aiSettings.model);
|
||||
|
||||
// 使用 useCallback 缓存删除函数
|
||||
const handleDeleteTool = useCallback(
|
||||
(toolId: string) => {
|
||||
// 先设置删除标记,触发动画
|
||||
setDeletingToolIds((prev) => new Set([...prev, toolId]));
|
||||
|
||||
// 设置延时,等待动画完成后再从数组中移除
|
||||
setTimeout(() => {
|
||||
const newAppForm = {
|
||||
...appForm,
|
||||
selectedTools: appForm.selectedTools.filter((tool) => tool.id !== toolId)
|
||||
};
|
||||
setAppForm(newAppForm);
|
||||
|
||||
// 清除删除标记
|
||||
setDeletingToolIds((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(toolId);
|
||||
return newSet;
|
||||
});
|
||||
}, 150); // 动画持续时间缩短到150ms
|
||||
},
|
||||
[appForm, setAppForm]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 标题区域 */}
|
||||
<Flex alignItems="center" justifyContent="space-between" width="100%">
|
||||
<Flex alignItems="center" gap={1}>
|
||||
<Text
|
||||
fontWeight={formStyles.fontWeight}
|
||||
fontSize={formStyles.fontSize}
|
||||
lineHeight={formStyles.lineHeight}
|
||||
letterSpacing={formStyles.letterSpacing}
|
||||
color="myGray.700"
|
||||
>
|
||||
{t('common:core.app.Tool call')}
|
||||
</Text>
|
||||
<QuestionTip label={t('app:plugin_dispatch_tip')} />
|
||||
</Flex>
|
||||
|
||||
{/* 已有工具时显示新增按钮 */}
|
||||
{appForm.selectedTools.length > 0 && (
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="primary"
|
||||
variant="outline"
|
||||
leftIcon={<SmallAddIcon />}
|
||||
onClick={onOpenToolsSelect}
|
||||
_hover={{ bg: 'blue.50' }}
|
||||
>
|
||||
{t('common:Add')}
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{/* 工具容器 */}
|
||||
{appForm.selectedTools.length > 0 ? (
|
||||
<Box mt={2}>
|
||||
<Grid gridTemplateColumns={'repeat(3, minmax(0, 1fr))'} gridGap={[2, 4]}>
|
||||
{appForm.selectedTools.map((item) => {
|
||||
const isDeleting = deletingToolIds.has(item.id);
|
||||
|
||||
return (
|
||||
<MyTooltip key={item.id} label={item.intro}>
|
||||
<Flex
|
||||
overflow={'hidden'}
|
||||
display={'flex'}
|
||||
height={'40px'}
|
||||
padding={'8px 12px'}
|
||||
flexDirection={'row'}
|
||||
justifyContent={'flex-start'}
|
||||
alignItems={'center'}
|
||||
flex={'1 0 0'}
|
||||
borderRadius={'6px'}
|
||||
border={'0.5px solid var(--Gray-Modern-200, #E8EBF0)'}
|
||||
background={'#FFF'}
|
||||
boxShadow={
|
||||
'0px 4px 4px 0px rgba(19, 51, 107, 0.05), 0px 0px 1px 0px rgba(19, 51, 107, 0.08)'
|
||||
}
|
||||
_hover={{
|
||||
transform: 'translateY(-2px)',
|
||||
borderRadius: '6px',
|
||||
border: '0.5px solid var(--Gray-Modern-200, #E8EBF0)',
|
||||
background: '#FFF',
|
||||
boxShadow:
|
||||
'0px 4px 4px 0px rgba(19, 51, 107, 0.05), 0px 0px 1px 0px rgba(19, 51, 107, 0.08)'
|
||||
}}
|
||||
cursor={'pointer'}
|
||||
transition="all 0.2s ease"
|
||||
position="relative"
|
||||
role="group"
|
||||
animation={isDeleting ? `${shatterKeyframes} 0.15s ease forwards` : undefined}
|
||||
onClick={() => {
|
||||
if (
|
||||
item.inputs
|
||||
.filter((input) => !childAppSystemKey.includes(input.key))
|
||||
.every(
|
||||
(input) =>
|
||||
input.toolDescription ||
|
||||
input.renderTypeList.includes(FlowNodeInputTypeEnum.selectLLMModel) ||
|
||||
input.renderTypeList.includes(FlowNodeInputTypeEnum.fileSelect)
|
||||
) ||
|
||||
item.flowNodeType === FlowNodeTypeEnum.tool ||
|
||||
item.flowNodeType === FlowNodeTypeEnum.toolSet
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setConfigTool(item);
|
||||
}}
|
||||
>
|
||||
<Flex alignItems="center" width="100%">
|
||||
<Avatar src={item.avatar} borderRadius={'6px'} w={'20px'} h={'20px'} />
|
||||
<Box
|
||||
ml={'6px'}
|
||||
className={'textEllipsis'}
|
||||
fontSize={'sm'}
|
||||
fontWeight="medium"
|
||||
color={'myGray.900'}
|
||||
flex="1"
|
||||
>
|
||||
{item.name}
|
||||
</Box>
|
||||
<Flex
|
||||
className="delete"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
ml={2}
|
||||
w="22px"
|
||||
h="22px"
|
||||
borderRadius="sm"
|
||||
cursor="pointer"
|
||||
transition="all 0.2s"
|
||||
_hover={{
|
||||
background: 'rgba(17, 24, 36, 0.05)',
|
||||
color: 'red.600'
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteTool(item.id);
|
||||
}}
|
||||
opacity="0"
|
||||
_groupHover={{
|
||||
opacity: 1,
|
||||
animation: `${fadeInKeyframes} 0.2s ease`
|
||||
}}
|
||||
>
|
||||
<MyIcon
|
||||
className="delete"
|
||||
name={'delete' as any}
|
||||
w={'16px'}
|
||||
h={'16px'}
|
||||
color={'inherit'}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</MyTooltip>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
</Box>
|
||||
) : (
|
||||
<Box
|
||||
mt={2}
|
||||
display="flex"
|
||||
width="100%"
|
||||
height="80px"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
borderRadius="4px"
|
||||
border="1px dashed var(--Gray-Modern-250, #DFE2EA)"
|
||||
cursor="pointer"
|
||||
onClick={onOpenToolsSelect}
|
||||
_hover={{
|
||||
borderColor: 'primary.300',
|
||||
bg: 'gray.100',
|
||||
'.hoverContent': { color: 'primary.500' }
|
||||
}}
|
||||
transition="all 0.2s"
|
||||
position="relative"
|
||||
>
|
||||
<Flex
|
||||
className="hoverContent"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
flexDirection="row"
|
||||
gap={'6px'}
|
||||
color="gray.500"
|
||||
>
|
||||
<SmallAddIcon boxSize={5} />
|
||||
<Box fontSize="sm" fontWeight="medium">
|
||||
{t('common:Choose')}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{isOpenToolsSelect && (
|
||||
<ToolSelectModal
|
||||
selectedTools={appForm.selectedTools}
|
||||
chatConfig={appForm.chatConfig}
|
||||
selectedModel={selectedModel}
|
||||
onAddTool={(e) => {
|
||||
const newAppForm = {
|
||||
...appForm,
|
||||
selectedTools: [...appForm.selectedTools, e]
|
||||
};
|
||||
setAppForm(newAppForm);
|
||||
}}
|
||||
onRemoveTool={(e) => {
|
||||
const newAppForm = {
|
||||
...appForm,
|
||||
selectedTools: appForm.selectedTools.filter((item) => item.pluginId !== e.id)
|
||||
};
|
||||
setAppForm(newAppForm);
|
||||
}}
|
||||
onClose={onCloseToolsSelect}
|
||||
/>
|
||||
)}
|
||||
{configTool && (
|
||||
<ConfigToolModal
|
||||
configTool={configTool}
|
||||
onCloseConfigTool={() => setConfigTool(null)}
|
||||
onAddTool={(e) => {
|
||||
const newAppForm = {
|
||||
...appForm,
|
||||
selectedTools: appForm.selectedTools.map((item) =>
|
||||
item.pluginId === configTool.pluginId ? e : item
|
||||
)
|
||||
};
|
||||
setAppForm(newAppForm);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ToolSelect);
|
||||
@ -0,0 +1,535 @@
|
||||
import React, { useCallback, useMemo, useState, useEffect } from 'react';
|
||||
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Box,
|
||||
Button,
|
||||
css,
|
||||
Flex,
|
||||
Grid
|
||||
} from '@chakra-ui/react';
|
||||
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import type {
|
||||
NodeTemplateListItemType,
|
||||
NodeTemplateListType
|
||||
} from '@fastgpt/global/core/workflow/type/node.d';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import {
|
||||
getPluginGroups,
|
||||
getPreviewPluginNode,
|
||||
getSystemPlugTemplates,
|
||||
getSystemPluginPaths
|
||||
} from '@/web/core/app/api/plugin';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { getTeamPlugTemplates } from '@/web/core/app/api/plugin';
|
||||
import type { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
|
||||
import { getAppFolderPath } from '@/web/core/app/api/app';
|
||||
import FolderPath from '@/components/common/folder/Path';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import CostTooltip from '@/components/core/app/plugin/CostTooltip';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppContext } from '@/pageComponents/app/detail/context';
|
||||
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import MyAvatar from '@fastgpt/web/components/common/Avatar';
|
||||
import type { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
|
||||
type Props = {
|
||||
selectedPluginIds: string[];
|
||||
onSelectPlugins: (plugins: NodeTemplateListItemType[]) => void;
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
enum TemplateTypeEnum {
|
||||
'systemPlugin' = 'systemPlugin',
|
||||
'teamPlugin' = 'teamPlugin'
|
||||
}
|
||||
|
||||
const ToolSelectModal = ({ selectedPluginIds, onSelectPlugins, onCancel }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||
|
||||
const [tempSelectedIds, setTempSelectedIds] = useState<string[]>([...selectedPluginIds]);
|
||||
const [templateType, setTemplateType] = useState(TemplateTypeEnum.systemPlugin);
|
||||
const [parentId, setParentId] = useState<ParentIdType>('');
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
|
||||
// 监听 ESC 键关闭弹窗
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
onCancel();
|
||||
}
|
||||
};
|
||||
|
||||
// 添加事件监听
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
// 组件卸载时清除事件监听
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [onCancel]);
|
||||
|
||||
const {
|
||||
data: templates = [],
|
||||
runAsync: loadTemplates,
|
||||
loading: isLoading
|
||||
} = useRequest2(
|
||||
async ({
|
||||
type = templateType,
|
||||
parentId = '',
|
||||
searchVal = searchKey
|
||||
}: {
|
||||
type?: TemplateTypeEnum;
|
||||
parentId?: ParentIdType;
|
||||
searchVal?: string;
|
||||
}) => {
|
||||
if (type === TemplateTypeEnum.systemPlugin) {
|
||||
return getSystemPlugTemplates({ parentId, searchKey: searchVal });
|
||||
} else if (type === TemplateTypeEnum.teamPlugin) {
|
||||
return getTeamPlugTemplates({
|
||||
parentId,
|
||||
searchKey: searchVal
|
||||
}).then((res) => res.filter((app) => app.id !== appDetail._id));
|
||||
}
|
||||
},
|
||||
{
|
||||
onSuccess(_, [{ type = templateType, parentId = '' }]) {
|
||||
setTemplateType(type);
|
||||
setParentId(parentId);
|
||||
},
|
||||
refreshDeps: [templateType, searchKey, parentId],
|
||||
errorToast: t('common:core.module.templates.Load plugin error')
|
||||
}
|
||||
);
|
||||
|
||||
const { data: paths = [] } = useRequest2(
|
||||
() => {
|
||||
if (templateType === TemplateTypeEnum.teamPlugin)
|
||||
return getAppFolderPath({ sourceId: parentId, type: 'current' });
|
||||
return getSystemPluginPaths({ sourceId: parentId, type: 'current' });
|
||||
},
|
||||
{
|
||||
manual: false,
|
||||
refreshDeps: [parentId]
|
||||
}
|
||||
);
|
||||
|
||||
const onUpdateParentId = useCallback(
|
||||
(parentId: ParentIdType) => {
|
||||
loadTemplates({
|
||||
parentId
|
||||
});
|
||||
},
|
||||
[loadTemplates]
|
||||
);
|
||||
|
||||
useRequest2(() => loadTemplates({ searchVal: searchKey }), {
|
||||
manual: false,
|
||||
throttleWait: 300,
|
||||
refreshDeps: [searchKey]
|
||||
});
|
||||
|
||||
// 处理确认选择,获取完整的插件信息
|
||||
const handleConfirm = async () => {
|
||||
console.log('即将保存的插件选择:', tempSelectedIds);
|
||||
|
||||
try {
|
||||
// 从当前已加载的模板中直接获取信息,避免额外的 API 调用
|
||||
const selectedPlugins = templates
|
||||
.filter((template) => tempSelectedIds.includes(template.id))
|
||||
.map((template) => ({
|
||||
id: template.id,
|
||||
name: template.name,
|
||||
avatar: template.avatar,
|
||||
intro: template.intro || '',
|
||||
isFolder: template.isFolder || false,
|
||||
flowNodeType: template.flowNodeType,
|
||||
templateType: template.templateType
|
||||
}));
|
||||
|
||||
// 对于不在当前模板中的插件(可能来自其他路径或之前选择),需要获取信息
|
||||
const missingIds = tempSelectedIds.filter(
|
||||
(id) => !selectedPlugins.some((plugin) => plugin.id === id)
|
||||
);
|
||||
|
||||
if (missingIds.length > 0) {
|
||||
// 批量获取缺失的插件信息,而不是一个个调用 API
|
||||
const promises = missingIds.map((pluginId) =>
|
||||
getPreviewPluginNode({ appId: pluginId })
|
||||
.then((template) => ({
|
||||
id: pluginId,
|
||||
name: template.name,
|
||||
avatar: template.avatar,
|
||||
intro: template.intro || '',
|
||||
isFolder: false,
|
||||
flowNodeType: template.flowNodeType,
|
||||
templateType: template.templateType
|
||||
}))
|
||||
.catch((error) => {
|
||||
console.error('获取插件信息失败:', pluginId, error);
|
||||
return null;
|
||||
})
|
||||
);
|
||||
|
||||
// 明确指定类型,排除 null 值
|
||||
const additionalPlugins = (await Promise.all(promises)).filter(
|
||||
(
|
||||
item
|
||||
): item is {
|
||||
id: string;
|
||||
name: string;
|
||||
avatar: string | undefined;
|
||||
intro: string;
|
||||
isFolder: boolean;
|
||||
flowNodeType: FlowNodeTypeEnum;
|
||||
templateType: string;
|
||||
} => Boolean(item)
|
||||
);
|
||||
|
||||
selectedPlugins.push(...additionalPlugins);
|
||||
}
|
||||
|
||||
console.log('处理后的插件列表:', selectedPlugins);
|
||||
onSelectPlugins(selectedPlugins);
|
||||
} catch (error) {
|
||||
console.error('处理插件选择时出错:', error);
|
||||
} finally {
|
||||
onCancel();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen
|
||||
title={t('common:core.app.Tool call')}
|
||||
iconSrc="core/app/toolCall"
|
||||
onClose={onCancel}
|
||||
maxW={['90vw', '700px']}
|
||||
w={'700px'}
|
||||
h={['90vh', '80vh']}
|
||||
>
|
||||
{/* Header: row and search */}
|
||||
<Box px={[3, 6]} pt={4} display={'flex'} justifyContent={'space-between'} w={'full'}>
|
||||
<FillRowTabs
|
||||
list={[
|
||||
{
|
||||
icon: 'phoneTabbar/tool',
|
||||
label: t('common:navbar.Toolkit'),
|
||||
value: TemplateTypeEnum.systemPlugin
|
||||
},
|
||||
{
|
||||
icon: 'core/modules/teamPlugin',
|
||||
label: t('common:core.module.template.Team app'),
|
||||
value: TemplateTypeEnum.teamPlugin
|
||||
}
|
||||
]}
|
||||
py={'5px'}
|
||||
px={'15px'}
|
||||
value={templateType}
|
||||
onChange={(e) =>
|
||||
loadTemplates({
|
||||
type: e as TemplateTypeEnum,
|
||||
parentId: null
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Box w={300}>
|
||||
<SearchInput
|
||||
value={searchKey}
|
||||
onChange={(e) => setSearchKey(e.target.value)}
|
||||
placeholder={
|
||||
templateType === TemplateTypeEnum.systemPlugin
|
||||
? t('common:plugin.Search plugin')
|
||||
: t('app:search_app')
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
{/* route components */}
|
||||
{!searchKey && parentId && (
|
||||
<Flex mt={2} px={[3, 6]}>
|
||||
<FolderPath paths={paths} FirstPathDom={null} onClick={onUpdateParentId} />
|
||||
</Flex>
|
||||
)}
|
||||
<MyBox isLoading={isLoading} mt={2} px={[3, 6]} pb={3} flex={'1 0 0'} overflowY={'auto'}>
|
||||
<RenderList
|
||||
templates={templates}
|
||||
type={templateType}
|
||||
setParentId={onUpdateParentId}
|
||||
selectedIds={tempSelectedIds}
|
||||
toggleSelection={(id) => {
|
||||
setTempSelectedIds((prev) => {
|
||||
return prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id];
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</MyBox>
|
||||
|
||||
{/* Footer buttons - 改用内部内容替代 footer 属性 */}
|
||||
<Flex
|
||||
px={[3, 6]}
|
||||
py={4}
|
||||
justify="flex-end"
|
||||
w="full"
|
||||
gap={3}
|
||||
borderTop="1px solid"
|
||||
borderColor="gray.100"
|
||||
>
|
||||
<Button variant="outline" onClick={onCancel}>
|
||||
{t('common:Cancel')}
|
||||
</Button>
|
||||
<Button colorScheme="blue" onClick={handleConfirm}>
|
||||
{t('common:Confirm')}
|
||||
</Button>
|
||||
</Flex>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ToolSelectModal);
|
||||
|
||||
const RenderList = React.memo(function RenderList({
|
||||
templates,
|
||||
type,
|
||||
setParentId,
|
||||
selectedIds,
|
||||
toggleSelection
|
||||
}: {
|
||||
templates: NodeTemplateListItemType[];
|
||||
type: TemplateTypeEnum;
|
||||
setParentId: (parentId: ParentIdType) => any;
|
||||
selectedIds: string[];
|
||||
toggleSelection: (id: string) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data: pluginGroups = [] } = useRequest2(getPluginGroups, {
|
||||
manual: false
|
||||
});
|
||||
|
||||
const formatTemplatesArray = useMemo(() => {
|
||||
const data = (() => {
|
||||
if (type === TemplateTypeEnum.systemPlugin) {
|
||||
return pluginGroups.map((group) => {
|
||||
const copy: NodeTemplateListType = group.groupTypes.map((type) => ({
|
||||
list: [],
|
||||
type: type.typeId,
|
||||
label: type.typeName
|
||||
}));
|
||||
templates.forEach((item) => {
|
||||
const index = copy.findIndex((template) => template.type === item.templateType);
|
||||
if (index === -1) return;
|
||||
copy[index].list.push(item);
|
||||
});
|
||||
return {
|
||||
label: group.groupName,
|
||||
list: copy.filter((item) => item.list.length > 0)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
list: [
|
||||
{
|
||||
list: templates,
|
||||
type: '',
|
||||
label: ''
|
||||
}
|
||||
],
|
||||
label: ''
|
||||
}
|
||||
];
|
||||
})();
|
||||
|
||||
return data.filter(({ list }) => list.length > 0);
|
||||
}, [pluginGroups, templates, type]);
|
||||
|
||||
const gridStyle = useMemo(() => {
|
||||
if (type === TemplateTypeEnum.teamPlugin) {
|
||||
return {
|
||||
gridTemplateColumns: ['1fr', '1fr'],
|
||||
py: 2,
|
||||
avatarSize: '2rem'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
gridTemplateColumns: ['1fr', '1fr 1fr'],
|
||||
py: 3,
|
||||
avatarSize: '1.75rem'
|
||||
};
|
||||
}, [type]);
|
||||
|
||||
const PluginListRender = useMemoizedFn(({ list = [] }: { list: NodeTemplateListType }) => {
|
||||
return (
|
||||
<>
|
||||
{list.map((item, i) => {
|
||||
return (
|
||||
<Box
|
||||
key={item.type}
|
||||
css={css({
|
||||
span: {
|
||||
display: 'block'
|
||||
}
|
||||
})}
|
||||
>
|
||||
<Flex>
|
||||
<Box fontSize={'sm'} my={2} fontWeight={'500'} flex={1} color={'myGray.900'}>
|
||||
{t(item.label as any)}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Grid gridTemplateColumns={gridStyle.gridTemplateColumns} rowGap={2} columnGap={3}>
|
||||
{item.list.map((template) => {
|
||||
const selected = selectedIds.includes(template.id);
|
||||
|
||||
// 判断是否是嵌套插件
|
||||
const isNestedPlugin = template.isFolder;
|
||||
|
||||
return (
|
||||
<MyTooltip
|
||||
key={template.id}
|
||||
placement={'right'}
|
||||
label={
|
||||
<Box py={2}>
|
||||
<Flex alignItems={'center'}>
|
||||
<MyAvatar
|
||||
src={template.avatar}
|
||||
w={'1.75rem'}
|
||||
objectFit={'contain'}
|
||||
borderRadius={'sm'}
|
||||
/>
|
||||
<Box fontWeight={'bold'} ml={3} color={'myGray.900'}>
|
||||
{t(template.name as any)}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box mt={2} color={'myGray.500'} maxH={'100px'} overflow={'hidden'}>
|
||||
{t(template.intro as any) || t('common:core.workflow.Not intro')}
|
||||
</Box>
|
||||
{type === TemplateTypeEnum.systemPlugin && (
|
||||
<CostTooltip
|
||||
cost={template.currentCost}
|
||||
hasTokenFee={template.hasTokenFee}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
py={gridStyle.py}
|
||||
px={3}
|
||||
_hover={{ bg: 'myWhite.600' }}
|
||||
borderRadius={'sm'}
|
||||
whiteSpace={'nowrap'}
|
||||
overflow={'hidden'}
|
||||
textOverflow={'ellipsis'}
|
||||
>
|
||||
<MyAvatar
|
||||
src={template.avatar}
|
||||
w={gridStyle.avatarSize}
|
||||
objectFit={'contain'}
|
||||
borderRadius={'sm'}
|
||||
flexShrink={0}
|
||||
/>
|
||||
<Box
|
||||
color={'myGray.900'}
|
||||
fontWeight={'500'}
|
||||
fontSize={'sm'}
|
||||
flex={'1 0 0'}
|
||||
ml={3}
|
||||
className="textEllipsis"
|
||||
>
|
||||
{t(template.name as any)}
|
||||
</Box>
|
||||
|
||||
{selected ? (
|
||||
<Button
|
||||
size={'sm'}
|
||||
variant={'grayDanger'}
|
||||
leftIcon={<MyIcon name={'delete'} w={'16px'} mr={-1} />}
|
||||
onClick={() => toggleSelection(template.id)}
|
||||
px={2}
|
||||
fontSize={'mini'}
|
||||
>
|
||||
{t('common:Remove')}
|
||||
</Button>
|
||||
) : isNestedPlugin ? (
|
||||
<Button
|
||||
size={'sm'}
|
||||
variant={'whiteBase'}
|
||||
leftIcon={<MyIcon name={'common/arrowRight'} w={'16px'} mr={-1.5} />}
|
||||
onClick={() => setParentId(template.id)}
|
||||
px={2}
|
||||
fontSize={'mini'}
|
||||
>
|
||||
{t('common:Open')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size={'sm'}
|
||||
variant={'primaryOutline'}
|
||||
leftIcon={<MyIcon name={'common/addLight'} w={'16px'} mr={-1.5} />}
|
||||
onClick={() => toggleSelection(template.id)}
|
||||
px={2}
|
||||
fontSize={'mini'}
|
||||
>
|
||||
{t('common:Add')}
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
</MyTooltip>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
return templates.length === 0 ? (
|
||||
<EmptyTip text={t('app:module.No Modules')} />
|
||||
) : (
|
||||
<Box flex={'1 0 0'} overflow={'overlay'}>
|
||||
<Accordion defaultIndex={[0]} allowMultiple reduceMotion>
|
||||
{formatTemplatesArray.length > 1 ? (
|
||||
<>
|
||||
{formatTemplatesArray.map(({ list, label }, index) => (
|
||||
<AccordionItem key={index} border={'none'}>
|
||||
<AccordionButton
|
||||
fontSize={'sm'}
|
||||
fontWeight={'500'}
|
||||
color={'myGray.900'}
|
||||
justifyContent={'space-between'}
|
||||
alignItems={'center'}
|
||||
borderRadius={'md'}
|
||||
px={3}
|
||||
>
|
||||
{t(label as any)}
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel py={0}>
|
||||
<PluginListRender list={list} />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<PluginListRender list={formatTemplatesArray?.[0]?.list} />
|
||||
)}
|
||||
</Accordion>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
295
projects/app/src/pageComponents/account/gateway/logs.tsx
Normal file
@ -0,0 +1,295 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import {
|
||||
Flex,
|
||||
Box,
|
||||
TableContainer,
|
||||
Table,
|
||||
Thead,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
Tbody,
|
||||
HStack,
|
||||
Button
|
||||
} from '@chakra-ui/react';
|
||||
import UserBox from '@fastgpt/web/components/common/UserBox';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { getAppChatLogs } from '@/web/core/app/api';
|
||||
import dayjs from 'dayjs';
|
||||
import { ChatSourceEnum, ChatSourceMap } from '@fastgpt/global/core/chat/constants';
|
||||
import { addDays } from 'date-fns';
|
||||
import { usePagination } from '@fastgpt/web/hooks/usePagination';
|
||||
import DateRangePicker, {
|
||||
type DateRangeType
|
||||
} from '@fastgpt/web/components/common/DateRangePicker';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import { cardStyles } from '@/pageComponents/app/detail/constants';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import MultipleSelect, {
|
||||
useMultipleSelect
|
||||
} from '@fastgpt/web/components/common/MySelect/MultipleSelect';
|
||||
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
|
||||
import PopoverConfirm from '@fastgpt/web/components/common/MyPopover/PopoverConfirm';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { downloadFetch } from '@/web/common/system/utils';
|
||||
|
||||
const DetailLogsModal = dynamic(() => import('@/pageComponents/app/detail/Logs/DetailLogsModal'));
|
||||
|
||||
// 修改:将组件改为接收gateAppId作为属性
|
||||
type LogsProps = {
|
||||
gateAppId: string;
|
||||
};
|
||||
|
||||
const Logs = ({ gateAppId }: LogsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [dateRange, setDateRange] = useState<DateRangeType>({
|
||||
from: addDays(new Date(), -7),
|
||||
to: new Date()
|
||||
});
|
||||
console.log('gateAppId', gateAppId);
|
||||
|
||||
const [detailLogsId, setDetailLogsId] = useState<string>();
|
||||
const [logTitle, setLogTitle] = useState<string>();
|
||||
|
||||
// 不再需要获取gateAppId的useEffect
|
||||
|
||||
const {
|
||||
value: chatSources,
|
||||
setValue: setChatSources,
|
||||
isSelectAll: isSelectAllSource,
|
||||
setIsSelectAll: setIsSelectAllSource
|
||||
} = useMultipleSelect<ChatSourceEnum>(Object.values(ChatSourceEnum), true);
|
||||
|
||||
const sourceList = useMemo(
|
||||
() =>
|
||||
Object.entries(ChatSourceMap).map(([key, value]) => ({
|
||||
label: t(value.name as any),
|
||||
value: key as ChatSourceEnum
|
||||
})),
|
||||
[t]
|
||||
);
|
||||
|
||||
const {
|
||||
data: logs,
|
||||
isLoading,
|
||||
Pagination,
|
||||
getData,
|
||||
pageNum,
|
||||
total
|
||||
} = usePagination(getAppChatLogs, {
|
||||
pageSize: 20,
|
||||
params: {
|
||||
appId: gateAppId, // 现在gateAppId始终是字符串类型
|
||||
dateStart: dateRange.from || new Date(),
|
||||
dateEnd: addDays(dateRange.to || new Date(), 1),
|
||||
sources: isSelectAllSource ? undefined : chatSources,
|
||||
logTitle
|
||||
},
|
||||
refreshDeps: [gateAppId, chatSources, logTitle]
|
||||
});
|
||||
|
||||
const { runAsync: exportLogs } = useRequest2(
|
||||
async () => {
|
||||
if (!gateAppId) return; // 即使gateAppId是空字符串,此检查仍然有效
|
||||
|
||||
await downloadFetch({
|
||||
url: '/api/core/app/exportChatLogs',
|
||||
filename: 'chat_logs.csv',
|
||||
body: {
|
||||
// 修复:使用gateAppId替代未定义的appId
|
||||
appId: gateAppId,
|
||||
dateStart: dateRange.from || new Date(),
|
||||
dateEnd: addDays(dateRange.to || new Date(), 1),
|
||||
sources: isSelectAllSource ? undefined : chatSources,
|
||||
logTitle,
|
||||
|
||||
title: t('app:logs_export_title'),
|
||||
sourcesMap: Object.fromEntries(
|
||||
Object.entries(ChatSourceMap).map(([key, config]) => [
|
||||
key,
|
||||
{
|
||||
label: t(config.name as any)
|
||||
}
|
||||
])
|
||||
)
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
refreshDeps: [gateAppId, chatSources, logTitle]
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex flexDirection={'column'} h={'100%'} flex={'1 0 0'}>
|
||||
<Flex flexDir={['column', 'row']} alignItems={['flex-start', 'center']} gap={3}>
|
||||
<Flex alignItems={'center'} gap={2}>
|
||||
<Box fontSize={'mini'} fontWeight={'medium'} color={'myGray.900'}>
|
||||
{t('app:logs_source')}
|
||||
</Box>
|
||||
<Box>
|
||||
<MultipleSelect<ChatSourceEnum>
|
||||
list={sourceList}
|
||||
value={chatSources}
|
||||
onSelect={setChatSources}
|
||||
isSelectAll={isSelectAllSource}
|
||||
setIsSelectAll={setIsSelectAllSource}
|
||||
itemWrap={false}
|
||||
height={'32px'}
|
||||
bg={'myGray.50'}
|
||||
w={'160px'}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} gap={2}>
|
||||
<Box fontSize={'mini'} fontWeight={'medium'} color={'myGray.900'}>
|
||||
{t('common:user.Time')}
|
||||
</Box>
|
||||
<DateRangePicker
|
||||
defaultDate={dateRange}
|
||||
position="bottom"
|
||||
onChange={setDateRange}
|
||||
onSuccess={() => getData(1)}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} gap={2}>
|
||||
<Box fontSize={'mini'} fontWeight={'medium'} color={'myGray.900'} whiteSpace={'nowrap'}>
|
||||
{t('app:logs_title')}
|
||||
</Box>
|
||||
<SearchInput
|
||||
placeholder={t('app:logs_title')}
|
||||
w={'240px'}
|
||||
value={logTitle}
|
||||
onChange={(e) => setLogTitle(e.target.value)}
|
||||
/>
|
||||
</Flex>
|
||||
<Box flex={'1'} />
|
||||
<PopoverConfirm
|
||||
Trigger={<Button size={'md'}>{t('common:Export')}</Button>}
|
||||
showCancel
|
||||
content={t('app:logs_export_confirm_tip', { total })}
|
||||
onConfirm={exportLogs}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<TableContainer mt={[2, 4]} flex={'1 0 0'} h={0} overflowY={'auto'}>
|
||||
<Table variant={'simple'} fontSize={'sm'}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>{t('common:core.app.logs.Source And Time')}</Th>
|
||||
<Th>{t('app:logs_chat_user')}</Th>
|
||||
<Th>{t('app:logs_title')}</Th>
|
||||
<Th>{t('app:logs_message_total')}</Th>
|
||||
<Th>{t('app:feedback_count')}</Th>
|
||||
<Th>{t('common:core.app.feedback.Custom feedback')}</Th>
|
||||
<Th>
|
||||
<Flex gap={1} alignItems={'center'}>
|
||||
{t('app:mark_count')}
|
||||
<QuestionTip label={t('common:core.chat.Mark Description')} />
|
||||
</Flex>
|
||||
</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody fontSize={'xs'}>
|
||||
{logs.map((item) => (
|
||||
<Tr
|
||||
key={item._id}
|
||||
_hover={{ bg: 'myWhite.600' }}
|
||||
cursor={'pointer'}
|
||||
title={t('common:core.view_chat_detail')}
|
||||
onClick={() => setDetailLogsId(item.id)}
|
||||
>
|
||||
<Td>
|
||||
{/* @ts-ignore */}
|
||||
<Box>{item.sourceName || t(ChatSourceMap[item.source]?.name) || item.source}</Box>
|
||||
<Box color={'myGray.500'}>{dayjs(item.time).format('YYYY/MM/DD HH:mm')}</Box>
|
||||
</Td>
|
||||
<Td>
|
||||
<Box>
|
||||
{!!item.outLinkUid ? (
|
||||
item.outLinkUid
|
||||
) : (
|
||||
<UserBox sourceMember={item.sourceMember} />
|
||||
)}
|
||||
</Box>
|
||||
</Td>
|
||||
<Td className="textEllipsis" maxW={'250px'}>
|
||||
{item.customTitle || 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={t('app:logs_empty')}></EmptyTip>}
|
||||
</TableContainer>
|
||||
|
||||
<HStack w={'100%'} mt={3} justifyContent={'center'}>
|
||||
<Pagination />
|
||||
</HStack>
|
||||
|
||||
{!!detailLogsId && (
|
||||
<DetailLogsModal
|
||||
appId={gateAppId} // 现在已经是字符串类型
|
||||
chatId={detailLogsId}
|
||||
onClose={() => {
|
||||
setDetailLogsId(undefined);
|
||||
getData(pageNum);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Logs);
|
||||
@ -19,5 +19,11 @@ export const appTypeMap = {
|
||||
avatar: 'core/app/type/pluginFill',
|
||||
title: i18nT('app:type.Create plugin bot'),
|
||||
emptyCreateText: i18nT('app:create_empty_plugin')
|
||||
},
|
||||
[AppTypeEnum.gate]: {
|
||||
icon: 'support/gate/gateLight',
|
||||
avatar: 'support/gate/gateLight',
|
||||
title: i18nT('app:type.Create gate'),
|
||||
emptyCreateText: i18nT('app:create_empty_gate')
|
||||
}
|
||||
};
|
||||
|
||||
213
projects/app/src/pageComponents/app/detail/Gate/AppCard.tsx
Normal file
@ -0,0 +1,213 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Button,
|
||||
IconButton,
|
||||
HStack,
|
||||
ModalBody,
|
||||
Checkbox,
|
||||
ModalFooter
|
||||
} from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import type { AppSchema, AppSimpleEditFormType } from '@fastgpt/global/core/app/type.d';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import TagsEditModal from '../TagsEditModal';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { AppContext } from '@/pageComponents/app/detail/context';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import MyMenu from '@fastgpt/web/components/common/MyMenu';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { postTransition2Workflow } from '@/web/core/app/api/app';
|
||||
import { form2AppWorkflow } from '@/web/core/app/utils';
|
||||
import type { SimpleAppSnapshotType } from './useSnapshots';
|
||||
import ExportConfigPopover from '@/pageComponents/app/detail/ExportConfigPopover';
|
||||
|
||||
const AppCard = ({
|
||||
appForm,
|
||||
setPast
|
||||
}: {
|
||||
appForm: AppSimpleEditFormType;
|
||||
setPast: (value: React.SetStateAction<SimpleAppSnapshotType[]>) => void;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const onSaveApp = useContextSelector(AppContext, (v) => v.onSaveApp);
|
||||
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
|
||||
const onOpenInfoEdit = useContextSelector(AppContext, (v) => v.onOpenInfoEdit);
|
||||
const onDelApp = useContextSelector(AppContext, (v) => v.onDelApp);
|
||||
|
||||
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(
|
||||
async () => {
|
||||
const { nodes, edges } = form2AppWorkflow(appForm, t);
|
||||
await onSaveApp({
|
||||
nodes,
|
||||
edges,
|
||||
chatConfig: appForm.chatConfig,
|
||||
isPublish: false,
|
||||
versionName: t('app:transition_to_workflow')
|
||||
});
|
||||
|
||||
return postTransition2Workflow({ appId, createNew: transitionCreateNew });
|
||||
},
|
||||
{
|
||||
onSuccess: ({ id }) => {
|
||||
if (id) {
|
||||
router.replace({
|
||||
query: {
|
||||
appId: id
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setPast([]);
|
||||
router.reload();
|
||||
}
|
||||
},
|
||||
successToast: t('common:Success')
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* basic info */}
|
||||
<Box px={[4, 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'} color={'myGray.900'}>
|
||||
{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('common:core.app.tip.Add a intro to app')}
|
||||
</Box>
|
||||
<HStack alignItems={'center'}>
|
||||
<Button
|
||||
size={['sm', 'md']}
|
||||
variant={'whitePrimary'}
|
||||
leftIcon={<MyIcon name={'core/chat/chatLight'} w={'16px'} />}
|
||||
onClick={() => router.push(`/chat?appId=${appId}`)}
|
||||
>
|
||||
{t('common:core.Chat')}
|
||||
</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
|
||||
size={'xs'}
|
||||
Button={
|
||||
<IconButton
|
||||
variant={'whitePrimary'}
|
||||
size={['smSquare', 'mdSquare']}
|
||||
icon={<MyIcon name={'more'} w={'1rem'} />}
|
||||
aria-label={''}
|
||||
/>
|
||||
}
|
||||
menuList={[
|
||||
{
|
||||
children: [
|
||||
{
|
||||
label: (
|
||||
<Flex>
|
||||
<ExportConfigPopover
|
||||
appName={appDetail.name}
|
||||
appForm={appForm}
|
||||
chatConfig={appDetail.chatConfig}
|
||||
/>
|
||||
</Flex>
|
||||
)
|
||||
},
|
||||
{
|
||||
icon: 'core/app/type/workflow',
|
||||
label: t('app:transition_to_workflow'),
|
||||
onClick: () => setTransitionCreateNew(true)
|
||||
},
|
||||
...(appDetail.permission.hasWritePer && feConfigs?.show_team_chat
|
||||
? [
|
||||
{
|
||||
icon: 'core/chat/fileSelect',
|
||||
label: t('common:team_tags_set'),
|
||||
onClick: () => setTeamTagsSet(appDetail)
|
||||
}
|
||||
]
|
||||
: [])
|
||||
]
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
icon: 'delete',
|
||||
type: 'danger',
|
||||
label: t('common:Delete'),
|
||||
onClick: onDelApp
|
||||
}
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
<Box flex={1} />
|
||||
{/* {isPc && ( */}
|
||||
{/* <MyTag */}
|
||||
{/* type="borderFill" */}
|
||||
{/* colorSchema="gray" */}
|
||||
{/* onClick={() => (appDetail.permission.hasManagePer ? onOpenInfoEdit() : undefined)} */}
|
||||
{/* > */}
|
||||
{/* <PermissionIconText defaultPermission={appDetail.defaultPermission} /> */}
|
||||
{/* </MyTag> */}
|
||||
{/* )} */}
|
||||
</HStack>
|
||||
</Box>
|
||||
{TeamTagsSet && <TagsEditModal onClose={() => setTeamTagsSet(undefined)} />}
|
||||
{transitionCreateNew !== undefined && (
|
||||
<MyModal isOpen title={t('app:transition_to_workflow')} iconSrc="core/app/type/workflow">
|
||||
<ModalBody>
|
||||
<Box mb={3}>{t('app:transition_to_workflow_create_new_tip')}</Box>
|
||||
<HStack cursor={'pointer'} onClick={() => setTransitionCreateNew((state) => !state)}>
|
||||
<Checkbox
|
||||
isChecked={transitionCreateNew}
|
||||
icon={<MyIcon name={'common/check'} w={'12px'} />}
|
||||
/>
|
||||
<Box>{t('app: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);
|
||||
89
projects/app/src/pageComponents/app/detail/Gate/ChatGate.tsx
Normal file
@ -0,0 +1,89 @@
|
||||
import { Box, Flex, HStack } from '@chakra-ui/react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import type { AppDetailType, AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { useChatGate } from '../useChatGate';
|
||||
import ChatItemContextProvider, { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
|
||||
import ChatRecordContextProvider from '@/web/core/chat/context/chatRecordContext';
|
||||
import { useChatStore } from '@/web/core/chat/context/useChatStore';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { cardStyles } from '../constants';
|
||||
import ChatQuoteList from '@/pageComponents/chat/ChatQuoteList';
|
||||
|
||||
type Props = {
|
||||
appForm: AppSimpleEditFormType;
|
||||
setRenderEdit: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
appDetail: AppDetailType; // 添加 appDetail prop
|
||||
};
|
||||
const ChatGate = ({ appForm, setRenderEdit, appDetail }: Props) => {
|
||||
const datasetCiteData = useContextSelector(ChatItemContext, (v) => v.datasetCiteData);
|
||||
const setCiteModalData = useContextSelector(ChatItemContext, (v) => v.setCiteModalData);
|
||||
|
||||
useEffect(() => {
|
||||
setRenderEdit(!datasetCiteData);
|
||||
}, [datasetCiteData, setRenderEdit]);
|
||||
|
||||
const { ChatContainer } = useChatGate({
|
||||
appForm,
|
||||
isReady: true,
|
||||
appDetail
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex h={'full'} gap={2}>
|
||||
<MyBox
|
||||
flex={'1 0 0'}
|
||||
w={0}
|
||||
display={'flex'}
|
||||
position={'relative'}
|
||||
flexDirection={'column'}
|
||||
h={'full'}
|
||||
bg={'white'}
|
||||
boxShadow={'3'}
|
||||
>
|
||||
<Box flex={1}>
|
||||
<ChatContainer />
|
||||
</Box>
|
||||
</MyBox>
|
||||
{datasetCiteData && (
|
||||
<Box flex={'1 0 0'} w={0} maxW={'560px'} {...cardStyles} boxShadow={'3'}>
|
||||
<ChatQuoteList
|
||||
rawSearch={datasetCiteData.rawSearch}
|
||||
metadata={datasetCiteData.metadata}
|
||||
onClose={() => setCiteModalData(undefined)}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
const Render = ({ appForm, setRenderEdit, appDetail }: Props) => {
|
||||
const { chatId } = useChatStore();
|
||||
|
||||
const chatRecordProviderParams = useMemo(
|
||||
() => ({
|
||||
chatId: chatId,
|
||||
appId: appDetail._id
|
||||
}),
|
||||
[appDetail._id, chatId]
|
||||
);
|
||||
|
||||
return (
|
||||
<ChatItemContextProvider
|
||||
showRouteToAppDetail={true}
|
||||
showRouteToDatasetDetail={true}
|
||||
isShowReadRawSource={true}
|
||||
isResponseDetail={true}
|
||||
// isShowFullText={true}
|
||||
showNodeStatus
|
||||
>
|
||||
<ChatRecordContextProvider params={chatRecordProviderParams}>
|
||||
<ChatGate appForm={appForm} setRenderEdit={setRenderEdit} appDetail={appDetail} />
|
||||
</ChatRecordContextProvider>
|
||||
</ChatItemContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Render);
|
||||
133
projects/app/src/pageComponents/app/detail/Gate/ChatTest.tsx
Normal file
@ -0,0 +1,133 @@
|
||||
import { Box, Flex, IconButton } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
|
||||
import { useSafeState } from 'ahooks';
|
||||
import type { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
|
||||
import { form2AppWorkflow } from '@/web/core/app/utils';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppContext } from '../context';
|
||||
import { useChatTest } from '../useChatTest';
|
||||
import ChatItemContextProvider, { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
|
||||
import ChatRecordContextProvider from '@/web/core/chat/context/chatRecordContext';
|
||||
import { useChatStore } from '@/web/core/chat/context/useChatStore';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { cardStyles } from '../constants';
|
||||
import ChatQuoteList from '@/pageComponents/chat/ChatQuoteList';
|
||||
import VariablePopover from '@/components/core/chat/ChatContainer/ChatBox/components/VariablePopover';
|
||||
|
||||
type Props = {
|
||||
appForm: AppSimpleEditFormType;
|
||||
setRenderEdit: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
};
|
||||
const ChatTest = ({ appForm, setRenderEdit }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||
const datasetCiteData = useContextSelector(ChatItemContext, (v) => v.datasetCiteData);
|
||||
const setCiteModalData = useContextSelector(ChatItemContext, (v) => v.setCiteModalData);
|
||||
// form2AppWorkflow dependent allDatasets
|
||||
const isVariableVisible = useContextSelector(ChatItemContext, (v) => v.isVariableVisible);
|
||||
|
||||
const [workflowData, setWorkflowData] = useSafeState({
|
||||
nodes: appDetail.modules || [],
|
||||
edges: appDetail.edges || []
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const { nodes, edges } = form2AppWorkflow(appForm, t);
|
||||
setWorkflowData({ nodes, edges });
|
||||
}, [appForm, setWorkflowData, t]);
|
||||
|
||||
useEffect(() => {
|
||||
setRenderEdit(!datasetCiteData);
|
||||
}, [datasetCiteData, setRenderEdit]);
|
||||
|
||||
const { ChatContainer, restartChat, loading } = useChatTest({
|
||||
...workflowData,
|
||||
chatConfig: appForm.chatConfig,
|
||||
isReady: true
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex h={'full'} gap={2}>
|
||||
<MyBox
|
||||
flex={'1 0 0'}
|
||||
w={0}
|
||||
display={'flex'}
|
||||
position={'relative'}
|
||||
flexDirection={'column'}
|
||||
h={'full'}
|
||||
py={4}
|
||||
{...cardStyles}
|
||||
boxShadow={'3'}
|
||||
>
|
||||
<Flex px={[2, 5]} pb={2}>
|
||||
<Box fontSize={['md', 'lg']} fontWeight={'bold'} color={'myGray.900'} mr={3}>
|
||||
{t('app:chat_debug')}
|
||||
</Box>
|
||||
{!isVariableVisible && <VariablePopover showExternalVariables />}
|
||||
<Box flex={1} />
|
||||
<MyTooltip label={t('common: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();
|
||||
restartChat();
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
<Box flex={1}>
|
||||
<ChatContainer />
|
||||
</Box>
|
||||
</MyBox>
|
||||
{datasetCiteData && (
|
||||
<Box flex={'1 0 0'} w={0} maxW={'560px'} {...cardStyles} boxShadow={'3'}>
|
||||
<ChatQuoteList
|
||||
rawSearch={datasetCiteData.rawSearch}
|
||||
metadata={datasetCiteData.metadata}
|
||||
onClose={() => setCiteModalData(undefined)}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
const Render = ({ appForm, setRenderEdit }: Props) => {
|
||||
const { chatId } = useChatStore();
|
||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||
|
||||
const chatRecordProviderParams = useMemo(
|
||||
() => ({
|
||||
chatId: chatId,
|
||||
appId: appDetail._id
|
||||
}),
|
||||
[appDetail._id, chatId]
|
||||
);
|
||||
|
||||
return (
|
||||
<ChatItemContextProvider
|
||||
showRouteToAppDetail={true}
|
||||
showRouteToDatasetDetail={true}
|
||||
isShowReadRawSource={true}
|
||||
isResponseDetail={true}
|
||||
// isShowFullText={true}
|
||||
showNodeStatus
|
||||
>
|
||||
<ChatRecordContextProvider params={chatRecordProviderParams}>
|
||||
<ChatTest appForm={appForm} setRenderEdit={setRenderEdit} />
|
||||
</ChatRecordContextProvider>
|
||||
</ChatItemContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Render);
|
||||
62
projects/app/src/pageComponents/app/detail/Gate/Edit.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
|
||||
import ChatTest from './ChatTest';
|
||||
import AppCard from './AppCard';
|
||||
import EditForm from './EditForm';
|
||||
import type { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
|
||||
import { cardStyles } from '../constants';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import type { SimpleAppSnapshotType } from './useSnapshots';
|
||||
|
||||
const Edit = ({
|
||||
appForm,
|
||||
setAppForm,
|
||||
setPast
|
||||
}: {
|
||||
appForm: AppSimpleEditFormType;
|
||||
setAppForm: React.Dispatch<React.SetStateAction<AppSimpleEditFormType>>;
|
||||
setPast: (value: React.SetStateAction<SimpleAppSnapshotType[]>) => void;
|
||||
}) => {
|
||||
const { isPc } = useSystem();
|
||||
const [renderEdit, setRenderEdit] = useState(true);
|
||||
|
||||
return (
|
||||
<Box
|
||||
display={['block', 'flex']}
|
||||
flex={'1 0 0'}
|
||||
h={0}
|
||||
mt={[4, 0]}
|
||||
gap={1}
|
||||
borderRadius={'lg'}
|
||||
overflowY={['auto', 'unset']}
|
||||
>
|
||||
{renderEdit && (
|
||||
<Box
|
||||
className={styles.EditAppBox}
|
||||
pr={[0, 1]}
|
||||
overflowY={'auto'}
|
||||
minW={['auto', '580px']}
|
||||
flex={'1'}
|
||||
>
|
||||
<Box {...cardStyles} boxShadow={'2'}>
|
||||
<AppCard appForm={appForm} setPast={setPast} />
|
||||
</Box>
|
||||
|
||||
<Box mt={4} {...cardStyles} boxShadow={'3.5'}>
|
||||
<EditForm appForm={appForm} setAppForm={setAppForm} />
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{isPc && (
|
||||
<Box flex={'2 0 0'} w={0} mb={3}>
|
||||
<ChatTest appForm={appForm} setRenderEdit={setRenderEdit} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Edit);
|
||||
404
projects/app/src/pageComponents/app/detail/Gate/EditForm.tsx
Normal file
@ -0,0 +1,404 @@
|
||||
import React, { useEffect, useMemo, useTransition } from 'react';
|
||||
import type { BoxProps } from '@chakra-ui/react';
|
||||
import { Box, Flex, Grid, useTheme, useDisclosure, Button, HStack } from '@chakra-ui/react';
|
||||
import type { AppSimpleEditFormType } from '@fastgpt/global/core/app/type.d';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import Avatar from '@fastgpt/web/components/common/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 { TTSTypeEnum } from '@/web/core/app/constants';
|
||||
import { workflowSystemVariables } from '@/web/core/app/utils';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppContext } from '@/pageComponents/app/detail/context';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
import VariableTip from '@/components/common/Textarea/MyTextarea/VariableTip';
|
||||
import { getWebLLMModel } from '@/web/common/system/utils';
|
||||
import ToolSelect from './components/ToolSelect';
|
||||
|
||||
const DatasetSelectModal = dynamic(() => import('@/components/core/app/DatasetSelectModal'));
|
||||
const DatasetParamsModal = dynamic(() => import('@/components/core/app/DatasetParamsModal'));
|
||||
const TTSSelect = dynamic(() => import('@/components/core/app/TTSSelect'));
|
||||
const QGConfig = dynamic(() => import('@/components/core/app/QGConfig'));
|
||||
const WhisperConfig = dynamic(() => import('@/components/core/app/WhisperConfig'));
|
||||
const InputGuideConfig = dynamic(() => import('@/components/core/app/InputGuideConfig'));
|
||||
const WelcomeTextConfig = dynamic(() => import('@/components/core/app/WelcomeTextConfig'));
|
||||
const FileSelectConfig = dynamic(() => import('@/components/core/app/FileSelect'));
|
||||
|
||||
const BoxStyles: BoxProps = {
|
||||
px: [4, 6],
|
||||
py: '16px',
|
||||
borderBottomWidth: '1px',
|
||||
borderBottomColor: 'borderColor.low'
|
||||
};
|
||||
const LabelStyles: BoxProps = {
|
||||
w: ['60px', '100px'],
|
||||
whiteSpace: 'nowrap',
|
||||
flexShrink: 0,
|
||||
fontSize: 'sm',
|
||||
color: 'myGray.900'
|
||||
};
|
||||
|
||||
const EditForm = ({
|
||||
appForm,
|
||||
setAppForm
|
||||
}: {
|
||||
appForm: AppSimpleEditFormType;
|
||||
setAppForm: React.Dispatch<React.SetStateAction<AppSimpleEditFormType>>;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||
const selectDatasets = useMemo(() => appForm?.dataset?.datasets, [appForm]);
|
||||
const [, startTst] = useTransition();
|
||||
|
||||
const {
|
||||
isOpen: isOpenDatasetSelect,
|
||||
onOpen: onOpenKbSelect,
|
||||
onClose: onCloseKbSelect
|
||||
} = useDisclosure();
|
||||
const {
|
||||
isOpen: isOpenDatasetParams,
|
||||
onOpen: onOpenDatasetParams,
|
||||
onClose: onCloseDatasetParams
|
||||
} = useDisclosure();
|
||||
|
||||
const formatVariables = useMemo(
|
||||
() =>
|
||||
formatEditorVariablePickerIcon([
|
||||
...workflowSystemVariables.filter(
|
||||
(variable) =>
|
||||
!['appId', 'chatId', 'responseChatItemId', 'histories'].includes(variable.key)
|
||||
),
|
||||
...(appForm.chatConfig.variables || [])
|
||||
]).map((item) => ({
|
||||
...item,
|
||||
label: t(item.label as any),
|
||||
parent: {
|
||||
id: 'VARIABLE_NODE_ID',
|
||||
label: t('common:core.module.Variable'),
|
||||
avatar: 'core/workflow/template/variable'
|
||||
}
|
||||
})),
|
||||
[appForm.chatConfig.variables, t]
|
||||
);
|
||||
|
||||
const selectedModel = getWebLLMModel(appForm.aiSettings.model);
|
||||
const tokenLimit = useMemo(() => {
|
||||
return selectedModel?.quoteMaxToken || 3000;
|
||||
}, [selectedModel?.quoteMaxToken]);
|
||||
|
||||
// Force close image select when model not support vision
|
||||
useEffect(() => {
|
||||
if (!selectedModel.vision) {
|
||||
setAppForm((state) => ({
|
||||
...state,
|
||||
chatConfig: {
|
||||
...state.chatConfig,
|
||||
...(state.chatConfig.fileSelectConfig
|
||||
? {
|
||||
fileSelectConfig: {
|
||||
...state.chatConfig.fileSelectConfig,
|
||||
canSelectImg: false
|
||||
}
|
||||
}
|
||||
: {})
|
||||
}
|
||||
}));
|
||||
}
|
||||
}, [selectedModel, setAppForm]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box>
|
||||
{/* ai */}
|
||||
<Box {...BoxStyles}>
|
||||
<Flex alignItems={'center'}>
|
||||
<MyIcon name={'core/app/simpleMode/ai'} w={'20px'} />
|
||||
<FormLabel ml={2} flex={1}>
|
||||
{t('app:ai_settings')}
|
||||
</FormLabel>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} mt={5}>
|
||||
<Box {...LabelStyles}>{t('common:core.ai.Model')}</Box>
|
||||
<Box flex={'1 0 0'}>
|
||||
<SettingLLMModel
|
||||
bg="myGray.50"
|
||||
llmModelType={'all'}
|
||||
defaultData={{
|
||||
model: appForm.aiSettings.model,
|
||||
temperature: appForm.aiSettings.temperature,
|
||||
maxToken: appForm.aiSettings.maxToken,
|
||||
maxHistories: appForm.aiSettings.maxHistories,
|
||||
aiChatReasoning: appForm.aiSettings.aiChatReasoning ?? true,
|
||||
aiChatTopP: appForm.aiSettings.aiChatTopP,
|
||||
aiChatStopSign: appForm.aiSettings.aiChatStopSign,
|
||||
aiChatResponseFormat: appForm.aiSettings.aiChatResponseFormat,
|
||||
aiChatJsonSchema: appForm.aiSettings.aiChatJsonSchema
|
||||
}}
|
||||
onChange={({ maxHistories = 6, ...data }) => {
|
||||
setAppForm((state) => ({
|
||||
...state,
|
||||
aiSettings: {
|
||||
...state.aiSettings,
|
||||
...data,
|
||||
maxHistories
|
||||
}
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
<Box mt={4}>
|
||||
<HStack {...LabelStyles} w={'100%'}>
|
||||
<Box>{t('common:core.ai.Prompt')}</Box>
|
||||
<QuestionTip label={t('common:core.app.tip.systemPromptTip')} />
|
||||
|
||||
<Box flex={1} />
|
||||
<VariableTip color={'myGray.500'} />
|
||||
</HStack>
|
||||
<Box mt={1}>
|
||||
<PromptEditor
|
||||
minH={150}
|
||||
value={appForm.aiSettings.systemPrompt}
|
||||
bg={'myGray.50'}
|
||||
onChange={(text) => {
|
||||
startTst(() => {
|
||||
setAppForm((state) => ({
|
||||
...state,
|
||||
aiSettings: {
|
||||
...state.aiSettings,
|
||||
systemPrompt: text
|
||||
}
|
||||
}));
|
||||
});
|
||||
}}
|
||||
variableLabels={formatVariables}
|
||||
variables={formatVariables}
|
||||
placeholder={t('common:core.app.tip.systemPromptTip')}
|
||||
title={t('common: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('common: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}
|
||||
datasetSearchUsingExtensionQuery={appForm.dataset.datasetSearchUsingExtensionQuery}
|
||||
queryExtensionModel={appForm.dataset.datasetSearchExtensionModel}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<Grid gridTemplateColumns={'repeat(2, minmax(0, 1fr))'} gridGap={[2, 4]}>
|
||||
{selectDatasets.map((item) => (
|
||||
<MyTooltip key={item.datasetId} label={t('common: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.datasetId
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
<Avatar src={item.avatar} w={'1.5rem'} borderRadius={'sm'} />
|
||||
<Box
|
||||
ml={2}
|
||||
flex={'1 0 0'}
|
||||
w={0}
|
||||
className={'textEllipsis'}
|
||||
fontSize={'sm'}
|
||||
color={'myGray.900'}
|
||||
>
|
||||
{item.name}
|
||||
</Box>
|
||||
</Flex>
|
||||
</MyTooltip>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
{/* tool choice */}
|
||||
<Box {...BoxStyles}>
|
||||
<ToolSelect appForm={appForm} setAppForm={setAppForm} />
|
||||
</Box>
|
||||
|
||||
{/* File select */}
|
||||
<Box {...BoxStyles}>
|
||||
<FileSelectConfig
|
||||
forbidVision={!selectedModel?.vision}
|
||||
value={appForm.chatConfig.fileSelectConfig}
|
||||
onChange={(e) => {
|
||||
setAppForm((state) => ({
|
||||
...state,
|
||||
chatConfig: {
|
||||
...state.chatConfig,
|
||||
fileSelectConfig: e
|
||||
}
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</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}>
|
||||
<QGConfig
|
||||
value={appForm.chatConfig.questionGuide}
|
||||
onChange={(e) => {
|
||||
setAppForm((state) => ({
|
||||
...state,
|
||||
chatConfig: {
|
||||
...state.chatConfig,
|
||||
questionGuide: e
|
||||
}
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* question tips */}
|
||||
<Box {...BoxStyles}>
|
||||
<InputGuideConfig
|
||||
appId={appDetail._id}
|
||||
value={appForm.chatConfig.chatInputGuide}
|
||||
onChange={(e) => {
|
||||
setAppForm((state) => ({
|
||||
...state,
|
||||
chatConfig: {
|
||||
...state.chatConfig,
|
||||
chatInputGuide: e
|
||||
}
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{isOpenDatasetSelect && (
|
||||
<DatasetSelectModal
|
||||
isOpen={isOpenDatasetSelect}
|
||||
defaultSelectedDatasets={selectDatasets.map((item) => ({
|
||||
datasetId: item.datasetId,
|
||||
vectorModel: item.vectorModel,
|
||||
name: item.name,
|
||||
avatar: item.avatar
|
||||
}))}
|
||||
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
|
||||
}
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(EditForm);
|
||||
281
projects/app/src/pageComponents/app/detail/Gate/Header.tsx
Normal file
@ -0,0 +1,281 @@
|
||||
import React, { useCallback, useEffect, 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, Flex, IconButton } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import RouteTab from '../RouteTab';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import type { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
|
||||
import { form2AppWorkflow } from '@/web/core/app/utils';
|
||||
import { TabEnum } from '../context';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import MyTag from '@fastgpt/web/components/common/Tag/index';
|
||||
import { publishStatusStyle } from '../constants';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import SaveButton from '../Workflow/components/SaveButton';
|
||||
import { useBoolean, useDebounceEffect, useLockFn } from 'ahooks';
|
||||
import { appWorkflow2Form } from '@fastgpt/global/core/app/utils';
|
||||
import type { onSaveSnapshotFnType, SimpleAppSnapshotType } from './useSnapshots';
|
||||
import { compareSimpleAppSnapshot } from './useSnapshots';
|
||||
import PublishHistories from '../PublishHistoriesSlider';
|
||||
import type { AppVersionSchemaType } from '@fastgpt/global/core/app/version';
|
||||
import { useBeforeunload } from '@fastgpt/web/hooks/useBeforeunload';
|
||||
import { isProduction } from '@fastgpt/global/common/system/constants';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import {
|
||||
checkWorkflowNodeAndConnection,
|
||||
storeEdge2RenderEdge,
|
||||
storeNode2FlowNode
|
||||
} from '@/web/core/workflow/utils';
|
||||
|
||||
const Header = ({
|
||||
forbiddenSaveSnapshot,
|
||||
appForm,
|
||||
setAppForm,
|
||||
past,
|
||||
setPast,
|
||||
saveSnapshot
|
||||
}: {
|
||||
forbiddenSaveSnapshot: React.MutableRefObject<boolean>;
|
||||
appForm: AppSimpleEditFormType;
|
||||
setAppForm: (form: AppSimpleEditFormType) => void;
|
||||
past: SimpleAppSnapshotType[];
|
||||
setPast: (value: React.SetStateAction<SimpleAppSnapshotType[]>) => void;
|
||||
saveSnapshot: onSaveSnapshotFnType;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { isPc } = useSystem();
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const appId = useContextSelector(AppContext, (v) => v.appId);
|
||||
const onSaveApp = useContextSelector(AppContext, (v) => v.onSaveApp);
|
||||
const currentTab = useContextSelector(AppContext, (v) => v.currentTab);
|
||||
|
||||
const { lastAppListRouteType } = useSystemStore();
|
||||
|
||||
const { data: paths = [] } = useRequest2(
|
||||
() => getAppFolderPath({ sourceId: appId, type: 'parent' }),
|
||||
{
|
||||
manual: false,
|
||||
refreshDeps: [appId]
|
||||
}
|
||||
);
|
||||
const onClickRoute = useCallback(
|
||||
(parentId: string) => {
|
||||
router.push({
|
||||
pathname: '/dashboard/apps',
|
||||
query: {
|
||||
parentId,
|
||||
type: lastAppListRouteType
|
||||
}
|
||||
});
|
||||
},
|
||||
[router, lastAppListRouteType]
|
||||
);
|
||||
|
||||
const { runAsync: onClickSave, loading } = useRequest2(
|
||||
async ({
|
||||
isPublish,
|
||||
versionName = formatTime2YMDHMS(new Date()),
|
||||
autoSave
|
||||
}: {
|
||||
isPublish?: boolean;
|
||||
versionName?: string;
|
||||
autoSave?: boolean;
|
||||
}) => {
|
||||
const { nodes, edges } = form2AppWorkflow(appForm, t);
|
||||
await onSaveApp({
|
||||
nodes,
|
||||
edges,
|
||||
chatConfig: appForm.chatConfig,
|
||||
isPublish,
|
||||
versionName,
|
||||
autoSave
|
||||
});
|
||||
setPast((prevPast) =>
|
||||
prevPast.map((item, index) =>
|
||||
index === 0
|
||||
? {
|
||||
...item,
|
||||
isSaved: true
|
||||
}
|
||||
: item
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const [isShowHistories, { setTrue: setIsShowHistories, setFalse: closeHistories }] =
|
||||
useBoolean(false);
|
||||
|
||||
const onSwitchTmpVersion = useCallback(
|
||||
(data: SimpleAppSnapshotType, customTitle: string) => {
|
||||
setAppForm(data.appForm);
|
||||
|
||||
// Remove multiple "copy-"
|
||||
const copyText = t('app:version_copy');
|
||||
const regex = new RegExp(`(${copyText}-)\\1+`, 'g');
|
||||
const title = customTitle.replace(regex, `$1`);
|
||||
|
||||
return saveSnapshot({
|
||||
appForm: data.appForm,
|
||||
title
|
||||
});
|
||||
},
|
||||
[saveSnapshot, setAppForm, t]
|
||||
);
|
||||
const onSwitchCloudVersion = useCallback(
|
||||
(appVersion: AppVersionSchemaType) => {
|
||||
const appForm = appWorkflow2Form({
|
||||
nodes: appVersion.nodes,
|
||||
chatConfig: appVersion.chatConfig
|
||||
});
|
||||
|
||||
const res = saveSnapshot({
|
||||
appForm,
|
||||
title: `${t('app:version_copy')}-${appVersion.versionName}`
|
||||
});
|
||||
forbiddenSaveSnapshot.current = true;
|
||||
|
||||
setAppForm(appForm);
|
||||
|
||||
return res;
|
||||
},
|
||||
[forbiddenSaveSnapshot, saveSnapshot, setAppForm, t]
|
||||
);
|
||||
|
||||
// Check if the workflow is published
|
||||
const [isSaved, setIsSaved] = useState(false);
|
||||
useDebounceEffect(
|
||||
() => {
|
||||
const savedSnapshot = past.find((snapshot) => snapshot.isSaved);
|
||||
const val = compareSimpleAppSnapshot(savedSnapshot?.appForm, appForm);
|
||||
setIsSaved(val);
|
||||
},
|
||||
[past],
|
||||
{ wait: 500 }
|
||||
);
|
||||
|
||||
const onLeaveAutoSave = useLockFn(async () => {
|
||||
if (isSaved) return;
|
||||
try {
|
||||
console.log('Leave auto save');
|
||||
return onClickSave({ isPublish: false, autoSave: true });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (isProduction) {
|
||||
onLeaveAutoSave();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
useBeforeunload({
|
||||
tip: t('common:core.tip.leave page'),
|
||||
callback: onLeaveAutoSave
|
||||
});
|
||||
|
||||
return (
|
||||
<Box h={14}>
|
||||
{!isPc && (
|
||||
<Flex justifyContent={'center'}>
|
||||
<RouteTab />
|
||||
</Flex>
|
||||
)}
|
||||
<Flex w={'full'} alignItems={'center'} position={'relative'} h={'full'}>
|
||||
<Box flex={'1'}>
|
||||
<FolderPath
|
||||
rootName={t('app:all_apps')}
|
||||
paths={paths}
|
||||
hoverStyle={{ color: 'primary.600' }}
|
||||
onClick={onClickRoute}
|
||||
fontSize={'14px'}
|
||||
/>
|
||||
</Box>
|
||||
{isPc && (
|
||||
<Box position={'absolute'} left={'50%'} transform={'translateX(-50%)'}>
|
||||
<RouteTab />
|
||||
</Box>
|
||||
)}
|
||||
{currentTab === TabEnum.appEdit && (
|
||||
<Flex alignItems={'center'}>
|
||||
{!isShowHistories && (
|
||||
<>
|
||||
{isPc && (
|
||||
<MyTag
|
||||
mr={3}
|
||||
type={'borderFill'}
|
||||
showDot
|
||||
colorSchema={
|
||||
isSaved
|
||||
? publishStatusStyle.published.colorSchema
|
||||
: publishStatusStyle.unPublish.colorSchema
|
||||
}
|
||||
>
|
||||
{t(
|
||||
isSaved
|
||||
? 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={setIsShowHistories}
|
||||
/>
|
||||
<SaveButton
|
||||
isLoading={loading}
|
||||
onClickSave={onClickSave}
|
||||
checkData={() => {
|
||||
const { nodes: storeNodes, edges: storeEdges } = form2AppWorkflow(appForm, t);
|
||||
|
||||
const nodes = storeNodes.map((item) => storeNode2FlowNode({ item, t }));
|
||||
const edges = storeEdges.map((item) => storeEdge2RenderEdge({ edge: item }));
|
||||
|
||||
const checkResults = checkWorkflowNodeAndConnection({ nodes, edges });
|
||||
|
||||
if (checkResults) {
|
||||
toast({
|
||||
title: t('app:app.error.publish_unExist_app'),
|
||||
status: 'warning'
|
||||
});
|
||||
}
|
||||
return !checkResults;
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{isShowHistories && currentTab === TabEnum.appEdit && (
|
||||
<PublishHistories<SimpleAppSnapshotType>
|
||||
onClose={closeHistories}
|
||||
past={past}
|
||||
onSwitchTmpVersion={onSwitchTmpVersion}
|
||||
onSwitchCloudVersion={onSwitchCloudVersion}
|
||||
positionStyles={{
|
||||
top: 14,
|
||||
bottom: 3
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
@ -0,0 +1,130 @@
|
||||
import { Button, HStack, ModalBody, ModalFooter } from '@chakra-ui/react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import type { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
|
||||
import { childAppSystemKey } from './ToolSelectModal';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import RenderPluginInput from '@/components/core/chat/ChatContainer/PluginRunBox/components/renderPluginInput';
|
||||
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import UseGuideModal from '@/components/common/Modal/UseGuideModal';
|
||||
|
||||
const ConfigToolModal = ({
|
||||
configTool,
|
||||
onCloseConfigTool,
|
||||
onAddTool
|
||||
}: {
|
||||
configTool: AppSimpleEditFormType['selectedTools'][number];
|
||||
onCloseConfigTool: () => void;
|
||||
onAddTool: (tool: AppSimpleEditFormType['selectedTools'][number]) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { errors }
|
||||
} = useForm({
|
||||
defaultValues: configTool
|
||||
? configTool.inputs.reduce(
|
||||
(acc, input) => {
|
||||
acc[input.key] = input.value || input.defaultValue;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, any>
|
||||
)
|
||||
: {}
|
||||
});
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen
|
||||
isCentered
|
||||
title={t('common:core.app.ToolCall.Parameter setting')}
|
||||
iconSrc="core/app/toolCall"
|
||||
overflow={'auto'}
|
||||
>
|
||||
<ModalBody>
|
||||
<HStack mb={4} spacing={1} fontSize={'sm'}>
|
||||
<MyIcon name={'common/info'} w={'1.25rem'} />
|
||||
<Box flex={1}>{t('app:tool_input_param_tip')}</Box>
|
||||
{!!(configTool?.courseUrl || configTool?.userGuide) && (
|
||||
<UseGuideModal
|
||||
title={configTool?.name}
|
||||
iconSrc={configTool?.avatar}
|
||||
text={configTool?.userGuide}
|
||||
link={configTool?.courseUrl}
|
||||
>
|
||||
{({ onClick }) => (
|
||||
<Box cursor={'pointer'} color={'primary.500'} onClick={onClick}>
|
||||
{t('app:workflow.Input guide')}
|
||||
</Box>
|
||||
)}
|
||||
</UseGuideModal>
|
||||
)}
|
||||
</HStack>
|
||||
{configTool.inputs
|
||||
.filter(
|
||||
(input) =>
|
||||
!input.toolDescription &&
|
||||
!childAppSystemKey.includes(input.key) &&
|
||||
!input.renderTypeList.includes(FlowNodeInputTypeEnum.selectLLMModel) &&
|
||||
!input.renderTypeList.includes(FlowNodeInputTypeEnum.fileSelect)
|
||||
)
|
||||
.map((input) => {
|
||||
return (
|
||||
<Controller
|
||||
key={input.key}
|
||||
control={control}
|
||||
name={input.key}
|
||||
rules={{
|
||||
validate: (value) => {
|
||||
if (input.valueType === WorkflowIOValueTypeEnum.boolean) {
|
||||
return value !== undefined;
|
||||
}
|
||||
return !!value;
|
||||
}
|
||||
}}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<RenderPluginInput
|
||||
value={value}
|
||||
isInvalid={errors && Object.keys(errors).includes(input.key)}
|
||||
onChange={onChange}
|
||||
input={input}
|
||||
setUploading={() => {}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ModalBody>
|
||||
<ModalFooter gap={6}>
|
||||
<Button onClick={onCloseConfigTool} variant={'whiteBase'}>
|
||||
{t('common:Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant={'primary'}
|
||||
onClick={handleSubmit((data) => {
|
||||
onAddTool({
|
||||
...configTool,
|
||||
inputs: configTool.inputs.map((input) => ({
|
||||
...input,
|
||||
value: data[input.key] ?? input.value
|
||||
}))
|
||||
});
|
||||
onCloseConfigTool();
|
||||
})}
|
||||
>
|
||||
{t('common:Confirm')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ConfigToolModal);
|
||||
@ -0,0 +1,262 @@
|
||||
import React, { useState, useMemo, useCallback } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
Text,
|
||||
Checkbox,
|
||||
VStack,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
useDisclosure,
|
||||
HStack
|
||||
} from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { getTeamGateConfig } from '@/web/support/user/team/gate/api';
|
||||
import {
|
||||
getPreviewPluginNode,
|
||||
getSystemPlugTemplates,
|
||||
getTeamPlugTemplates
|
||||
} from '@/web/core/app/api/plugin';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import type { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/node';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
|
||||
type GateToolSelectProps = {
|
||||
selectedTools: FlowNodeTemplateType[];
|
||||
onToolsChange: (tools: FlowNodeTemplateType[]) => void;
|
||||
buttonSize?: string;
|
||||
};
|
||||
|
||||
const GateToolSelect = ({
|
||||
selectedTools,
|
||||
onToolsChange,
|
||||
buttonSize = 'md'
|
||||
}: GateToolSelectProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
// 获取门户配置中的工具列表
|
||||
const { data: gateConfig, loading: loadingGateConfig } = useRequest2(() => getTeamGateConfig(), {
|
||||
manual: false
|
||||
});
|
||||
|
||||
// 获取系统插件模板
|
||||
const { data: systemPlugins = [], loading: loadingSystemPlugins } = useRequest2(
|
||||
() => getSystemPlugTemplates({ parentId: '', searchKey: '' }),
|
||||
{
|
||||
manual: false
|
||||
}
|
||||
);
|
||||
|
||||
// 获取团队插件模板
|
||||
const { data: teamPlugins = [], loading: loadingTeamPlugins } = useRequest2(
|
||||
() => getTeamPlugTemplates({ parentId: '', searchKey: '' }),
|
||||
{
|
||||
manual: false
|
||||
}
|
||||
);
|
||||
|
||||
// 合并所有可用工具
|
||||
const allAvailableTools = useMemo(() => {
|
||||
return [...systemPlugins, ...teamPlugins];
|
||||
}, [systemPlugins, teamPlugins]);
|
||||
|
||||
// 筛选出gate配置中指定的工具,如果没有指定则显示所有工具
|
||||
const availableTools = useMemo(() => {
|
||||
if (!allAvailableTools.length) return [];
|
||||
|
||||
// 如果gate配置中有指定工具,只显示这些工具;否则显示所有工具
|
||||
if (gateConfig?.tools?.length) {
|
||||
return allAvailableTools.filter((tool) => gateConfig.tools.includes(tool.id));
|
||||
}
|
||||
|
||||
return allAvailableTools;
|
||||
}, [gateConfig?.tools, allAvailableTools]);
|
||||
|
||||
// 处理单个工具的选择/取消选择
|
||||
const { runAsync: handleToolSelect, loading: loadingToolSelect } = useRequest2(
|
||||
async (toolId: string, checked: boolean) => {
|
||||
const tool = allAvailableTools.find((item) => item.id === toolId);
|
||||
if (!tool) return;
|
||||
|
||||
if (checked) {
|
||||
const res = await getPreviewPluginNode({ appId: tool.id });
|
||||
onToolsChange([...selectedTools, res]);
|
||||
} else {
|
||||
onToolsChange(selectedTools.filter((item) => item.pluginId !== toolId));
|
||||
}
|
||||
},
|
||||
{
|
||||
refreshDeps: [allAvailableTools, selectedTools, onToolsChange]
|
||||
}
|
||||
);
|
||||
|
||||
const selectedCount = selectedTools.length;
|
||||
const loading = loadingGateConfig || loadingSystemPlugins || loadingTeamPlugins;
|
||||
|
||||
return availableTools.length > 0 ? (
|
||||
<>
|
||||
{availableTools.length > 1 ? (
|
||||
<Button
|
||||
leftIcon={
|
||||
<MyIcon name={'support/gate/chat/toolkitLine'} w={'18px'} h={'18px'} color="blue.500" />
|
||||
}
|
||||
size={buttonSize}
|
||||
display="flex"
|
||||
padding="8px 12px"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
gap="4px"
|
||||
iconSpacing="4px"
|
||||
borderRadius="9999px"
|
||||
border="0.5px solid var(--Royal-Blue-200, #C5D7FF)"
|
||||
background="var(--light-fastgpt-primary-container-low, #F0F4FF)"
|
||||
color="blue.500"
|
||||
fontWeight="500"
|
||||
onClick={onOpen}
|
||||
flexShrink={0}
|
||||
_hover={{
|
||||
background: 'var(--light-fastgpt-primary-container-low, #E6EDFF)'
|
||||
}}
|
||||
>
|
||||
<Box display={{ base: 'none', md: 'block' }}>{t('common:tool_select')}: </Box>
|
||||
{selectedCount}
|
||||
</Button>
|
||||
) : (
|
||||
<MyBox
|
||||
isLoading={loadingToolSelect}
|
||||
display={'flex'}
|
||||
alignItems={'center'}
|
||||
gap={2}
|
||||
h={'40px'}
|
||||
borderRadius={'lg'}
|
||||
border={'base'}
|
||||
px={3}
|
||||
cursor={'pointer'}
|
||||
userSelect={'none'}
|
||||
_hover={{
|
||||
borderColor: 'primary.300'
|
||||
}}
|
||||
{...(selectedTools.length > 0 && {
|
||||
borderColor: 'primary.400 !important',
|
||||
bg: 'primary.50'
|
||||
})}
|
||||
onClick={() => {
|
||||
handleToolSelect(availableTools[0].id, selectedTools.length === 0);
|
||||
}}
|
||||
>
|
||||
<Avatar src={availableTools[0].avatar} w="1.2rem" h="1.2rem" borderRadius={'sm'} />
|
||||
<Text display={['none', 'none', 'block']} fontSize="md" fontWeight="medium">
|
||||
{availableTools[0].name}
|
||||
</Text>
|
||||
</MyBox>
|
||||
)}
|
||||
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="md">
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
<Flex align="center" gap={2}>
|
||||
<MyIcon
|
||||
name={'support/gate/chat/toolkitLine'}
|
||||
w={'20px'}
|
||||
h={'20px'}
|
||||
color="blue.500"
|
||||
/>
|
||||
<Text>工具选择</Text>
|
||||
<Text fontSize="sm" color="myGray.600">
|
||||
({availableTools.length} 个可用)
|
||||
</Text>
|
||||
</Flex>
|
||||
</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
|
||||
<ModalBody pb={6}>
|
||||
<MyBox isLoading={loadingToolSelect}>
|
||||
{loading ? (
|
||||
<Flex justify="center" py={8}>
|
||||
<Text fontSize="sm" color="myGray.500">
|
||||
{t('common:Loading')}
|
||||
</Text>
|
||||
</Flex>
|
||||
) : availableTools.length === 0 ? (
|
||||
<Box py={8} textAlign="center">
|
||||
<EmptyTip text="暂无可用工具" />
|
||||
<Text fontSize="sm" color="myGray.500" mt={3}>
|
||||
请先在门户管理中配置工具,或检查是否有可用的插件
|
||||
</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<VStack align="stretch" spacing={2}>
|
||||
{availableTools.map((tool) => (
|
||||
<Box
|
||||
key={tool.id}
|
||||
p={4}
|
||||
borderRadius="md"
|
||||
cursor="pointer"
|
||||
border="1px solid"
|
||||
borderColor="gray.200"
|
||||
transition="all 0.2s"
|
||||
_hover={{
|
||||
bg: 'blue.50',
|
||||
borderColor: 'blue.300'
|
||||
}}
|
||||
onClick={() =>
|
||||
handleToolSelect(
|
||||
tool.id,
|
||||
!selectedTools.some((item) => item.pluginId === tool.id)
|
||||
)
|
||||
}
|
||||
>
|
||||
<Flex align="center">
|
||||
<Checkbox
|
||||
size="md"
|
||||
isChecked={selectedTools.some((item) => item.pluginId === tool.id)}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
handleToolSelect(tool.id, e.target.checked);
|
||||
}}
|
||||
mr={4}
|
||||
colorScheme="blue"
|
||||
/>
|
||||
<Avatar src={tool.avatar} w="32px" h="32px" mr={3} />
|
||||
<Box flex={1}>
|
||||
<Text fontSize="md" fontWeight="medium" color="myGray.900">
|
||||
{tool.name}
|
||||
</Text>
|
||||
{tool.intro && (
|
||||
<Text fontSize="sm" color="myGray.600" mt={1} noOfLines={2}>
|
||||
{tool.intro}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
))}
|
||||
</VStack>
|
||||
)}
|
||||
|
||||
{selectedTools.length > 0 && (
|
||||
<Box mt={4} p={3} bg="blue.50" borderRadius="md">
|
||||
<Text fontSize="sm" color="blue.700">
|
||||
已选择 {selectedTools.length} 个工具
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</MyBox>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default React.memo(GateToolSelect);
|
||||
@ -0,0 +1,184 @@
|
||||
import { Box, Button, Flex, Grid, useDisclosure } from '@chakra-ui/react';
|
||||
import React, { useState } from 'react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import { SmallAddIcon } from '@chakra-ui/icons';
|
||||
import { type AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { theme } from '@fastgpt/web/styles/theme';
|
||||
import DeleteIcon, { hoverDeleteStyles } from '@fastgpt/web/components/common/Icon/delete';
|
||||
import ToolSelectModal, { childAppSystemKey } from './ToolSelectModal';
|
||||
import {
|
||||
FlowNodeInputTypeEnum,
|
||||
FlowNodeTypeEnum
|
||||
} from '@fastgpt/global/core/workflow/node/constant';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import ConfigToolModal from './ConfigToolModal';
|
||||
import { getWebLLMModel } from '@/web/common/system/utils';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
import { formatToolError } from '@fastgpt/global/core/app/utils';
|
||||
|
||||
const ToolSelect = ({
|
||||
appForm,
|
||||
setAppForm
|
||||
}: {
|
||||
appForm: AppSimpleEditFormType;
|
||||
setAppForm: React.Dispatch<React.SetStateAction<AppSimpleEditFormType>>;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [configTool, setConfigTool] = useState<
|
||||
AppSimpleEditFormType['selectedTools'][number] | null
|
||||
>(null);
|
||||
|
||||
const {
|
||||
isOpen: isOpenToolsSelect,
|
||||
onOpen: onOpenToolsSelect,
|
||||
onClose: onCloseToolsSelect
|
||||
} = useDisclosure();
|
||||
const selectedModel = getWebLLMModel(appForm.aiSettings.model);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex alignItems={'center'}>
|
||||
<Flex alignItems={'center'} flex={1}>
|
||||
<MyIcon name={'core/app/toolCall'} w={'20px'} />
|
||||
<FormLabel ml={2}>{t('common:core.app.Tool call')}</FormLabel>
|
||||
<QuestionTip ml={1} label={t('app:plugin_dispatch_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) => {
|
||||
const toolError = formatToolError(item.pluginData?.error);
|
||||
|
||||
return (
|
||||
<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}
|
||||
borderColor={toolError ? 'red.600' : ''}
|
||||
_hover={{
|
||||
...hoverDeleteStyles,
|
||||
borderColor: toolError ? 'red.600' : 'primary.300'
|
||||
}}
|
||||
cursor={'pointer'}
|
||||
onClick={() => {
|
||||
if (
|
||||
item.inputs
|
||||
.filter((input) => !childAppSystemKey.includes(input.key))
|
||||
.every(
|
||||
(input) =>
|
||||
input.toolDescription ||
|
||||
input.renderTypeList.includes(FlowNodeInputTypeEnum.selectLLMModel) ||
|
||||
input.renderTypeList.includes(FlowNodeInputTypeEnum.fileSelect)
|
||||
) ||
|
||||
toolError ||
|
||||
item.flowNodeType === FlowNodeTypeEnum.tool ||
|
||||
item.flowNodeType === FlowNodeTypeEnum.toolSet
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setConfigTool(item);
|
||||
}}
|
||||
>
|
||||
<Avatar src={item.avatar} w={'1.5rem'} h={'1.5rem'} borderRadius={'sm'} />
|
||||
<Box
|
||||
flex={'1 0 0'}
|
||||
ml={2}
|
||||
gap={2}
|
||||
className={'textEllipsis'}
|
||||
fontSize={'sm'}
|
||||
color={'myGray.900'}
|
||||
>
|
||||
{item.name}
|
||||
</Box>
|
||||
{toolError && (
|
||||
<Flex
|
||||
bg={'red.50'}
|
||||
alignItems={'center'}
|
||||
h={6}
|
||||
px={2}
|
||||
rounded={'6px'}
|
||||
fontSize={'xs'}
|
||||
fontWeight={'medium'}
|
||||
>
|
||||
<MyIcon name={'common/errorFill'} w={'14px'} mr={1} />
|
||||
<Box color={'red.600'}>{t(toolError as any)}</Box>
|
||||
</Flex>
|
||||
)}
|
||||
<DeleteIcon
|
||||
ml={2}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setAppForm((state: AppSimpleEditFormType) => ({
|
||||
...state,
|
||||
selectedTools: state.selectedTools.filter((tool) => tool.id !== item.id)
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</MyTooltip>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
|
||||
{isOpenToolsSelect && (
|
||||
<ToolSelectModal
|
||||
selectedTools={appForm.selectedTools}
|
||||
chatConfig={appForm.chatConfig}
|
||||
selectedModel={selectedModel}
|
||||
onAddTool={(e) => {
|
||||
setAppForm((state) => ({
|
||||
...state,
|
||||
selectedTools: [...state.selectedTools, e]
|
||||
}));
|
||||
}}
|
||||
onRemoveTool={(e) => {
|
||||
setAppForm((state) => ({
|
||||
...state,
|
||||
selectedTools: state.selectedTools.filter((item) => item.pluginId !== e.id)
|
||||
}));
|
||||
}}
|
||||
onClose={onCloseToolsSelect}
|
||||
/>
|
||||
)}
|
||||
{configTool && (
|
||||
<ConfigToolModal
|
||||
configTool={configTool}
|
||||
onCloseConfigTool={() => setConfigTool(null)}
|
||||
onAddTool={(e) => {
|
||||
setAppForm((state) => ({
|
||||
...state,
|
||||
selectedTools: state.selectedTools.map((item) =>
|
||||
item.pluginId === configTool.pluginId ? e : item
|
||||
)
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ToolSelect);
|
||||
@ -0,0 +1,576 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Box,
|
||||
Button,
|
||||
css,
|
||||
Flex,
|
||||
Grid
|
||||
} from '@chakra-ui/react';
|
||||
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import {
|
||||
type FlowNodeTemplateType,
|
||||
type NodeTemplateListItemType,
|
||||
type NodeTemplateListType
|
||||
} from '@fastgpt/global/core/workflow/type/node.d';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import {
|
||||
getPluginGroups,
|
||||
getPreviewPluginNode,
|
||||
getSystemPlugTemplates,
|
||||
getSystemPluginPaths
|
||||
} from '@/web/core/app/api/plugin';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { getTeamPlugTemplates } from '@/web/core/app/api/plugin';
|
||||
import { type ParentIdType } from '@fastgpt/global/common/parentFolder/type';
|
||||
import { getAppFolderPath } from '@/web/core/app/api/app';
|
||||
import FolderPath from '@/components/common/folder/Path';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import CostTooltip from '@/components/core/app/plugin/CostTooltip';
|
||||
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppContext } from '../../context';
|
||||
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import MyAvatar from '@fastgpt/web/components/common/Avatar';
|
||||
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { type AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import type { LLMModelItemType } from '@fastgpt/global/core/ai/model.d';
|
||||
import { workflowStartNodeId } from '@/web/core/app/constants';
|
||||
import ConfigToolModal from './ConfigToolModal';
|
||||
|
||||
type Props = {
|
||||
selectedTools: FlowNodeTemplateType[];
|
||||
chatConfig: AppSimpleEditFormType['chatConfig'];
|
||||
selectedModel: LLMModelItemType;
|
||||
onAddTool: (tool: FlowNodeTemplateType) => void;
|
||||
onRemoveTool: (tool: NodeTemplateListItemType) => void;
|
||||
};
|
||||
|
||||
export const childAppSystemKey: string[] = [
|
||||
NodeInputKeyEnum.forbidStream,
|
||||
NodeInputKeyEnum.history,
|
||||
NodeInputKeyEnum.historyMaxAmount,
|
||||
NodeInputKeyEnum.userChatInput
|
||||
];
|
||||
|
||||
enum TemplateTypeEnum {
|
||||
'systemPlugin' = 'systemPlugin',
|
||||
'teamPlugin' = 'teamPlugin'
|
||||
}
|
||||
|
||||
const ToolSelectModal = ({ onClose, ...props }: Props & { onClose: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||
|
||||
const [templateType, setTemplateType] = useState(TemplateTypeEnum.systemPlugin);
|
||||
const [parentId, setParentId] = useState<ParentIdType>('');
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
|
||||
const {
|
||||
data: templates = [],
|
||||
runAsync: loadTemplates,
|
||||
loading: isLoading
|
||||
} = useRequest2(
|
||||
async ({
|
||||
type = templateType,
|
||||
parentId = '',
|
||||
searchVal = searchKey
|
||||
}: {
|
||||
type?: TemplateTypeEnum;
|
||||
parentId?: ParentIdType;
|
||||
searchVal?: string;
|
||||
}) => {
|
||||
if (type === TemplateTypeEnum.systemPlugin) {
|
||||
return getSystemPlugTemplates({ parentId, searchKey: searchVal });
|
||||
} else if (type === TemplateTypeEnum.teamPlugin) {
|
||||
return getTeamPlugTemplates({
|
||||
parentId,
|
||||
searchKey: searchVal
|
||||
}).then((res) => res.filter((app) => app.id !== appDetail._id));
|
||||
}
|
||||
},
|
||||
{
|
||||
onSuccess(_, [{ type = templateType, parentId = '' }]) {
|
||||
setTemplateType(type);
|
||||
setParentId(parentId);
|
||||
},
|
||||
refreshDeps: [templateType, searchKey, parentId],
|
||||
errorToast: t('common:core.module.templates.Load plugin error')
|
||||
}
|
||||
);
|
||||
|
||||
const { data: paths = [] } = useRequest2(
|
||||
() => {
|
||||
if (templateType === TemplateTypeEnum.teamPlugin)
|
||||
return getAppFolderPath({ sourceId: parentId, type: 'current' });
|
||||
return getSystemPluginPaths({ sourceId: parentId, type: 'current' });
|
||||
},
|
||||
{
|
||||
manual: false,
|
||||
refreshDeps: [parentId]
|
||||
}
|
||||
);
|
||||
|
||||
const onUpdateParentId = useCallback(
|
||||
(parentId: ParentIdType) => {
|
||||
loadTemplates({
|
||||
parentId
|
||||
});
|
||||
},
|
||||
[loadTemplates]
|
||||
);
|
||||
|
||||
useRequest2(() => loadTemplates({ searchVal: searchKey }), {
|
||||
manual: false,
|
||||
throttleWait: 300,
|
||||
refreshDeps: [searchKey]
|
||||
});
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen
|
||||
title={t('common:core.app.Tool call')}
|
||||
iconSrc="core/app/toolCall"
|
||||
onClose={onClose}
|
||||
maxW={['90vw', '700px']}
|
||||
w={'700px'}
|
||||
h={['90vh', '80vh']}
|
||||
>
|
||||
{/* Header: row and search */}
|
||||
<Box px={[3, 6]} pt={4} display={'flex'} justifyContent={'space-between'} w={'full'}>
|
||||
<FillRowTabs
|
||||
list={[
|
||||
{
|
||||
icon: 'phoneTabbar/tool',
|
||||
label: t('common:navbar.Toolkit'),
|
||||
value: TemplateTypeEnum.systemPlugin
|
||||
},
|
||||
{
|
||||
icon: 'core/modules/teamPlugin',
|
||||
label: t('common:core.module.template.Team app'),
|
||||
value: TemplateTypeEnum.teamPlugin
|
||||
}
|
||||
]}
|
||||
py={'5px'}
|
||||
px={'15px'}
|
||||
value={templateType}
|
||||
onChange={(e) =>
|
||||
loadTemplates({
|
||||
type: e as TemplateTypeEnum,
|
||||
parentId: null
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Box w={300}>
|
||||
<SearchInput
|
||||
value={searchKey}
|
||||
onChange={(e) => setSearchKey(e.target.value)}
|
||||
placeholder={
|
||||
templateType === TemplateTypeEnum.systemPlugin
|
||||
? t('common:plugin.Search plugin')
|
||||
: t('app:search_app')
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
{/* route components */}
|
||||
{!searchKey && parentId && (
|
||||
<Flex mt={2} px={[3, 6]}>
|
||||
<FolderPath paths={paths} FirstPathDom={null} onClick={onUpdateParentId} />
|
||||
</Flex>
|
||||
)}
|
||||
<MyBox isLoading={isLoading} mt={2} px={[3, 6]} pb={3} flex={'1 0 0'} overflowY={'auto'}>
|
||||
<RenderList
|
||||
templates={templates}
|
||||
type={templateType}
|
||||
setParentId={onUpdateParentId}
|
||||
{...props}
|
||||
/>
|
||||
</MyBox>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ToolSelectModal);
|
||||
|
||||
const RenderList = React.memo(function RenderList({
|
||||
templates,
|
||||
type,
|
||||
onAddTool,
|
||||
onRemoveTool,
|
||||
setParentId,
|
||||
selectedTools,
|
||||
chatConfig,
|
||||
selectedModel
|
||||
}: Props & {
|
||||
templates: NodeTemplateListItemType[];
|
||||
type: TemplateTypeEnum;
|
||||
setParentId: (parentId: ParentIdType) => any;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [configTool, setConfigTool] = useState<FlowNodeTemplateType>();
|
||||
const onCloseConfigTool = useCallback(() => setConfigTool(undefined), []);
|
||||
const { toast } = useToast();
|
||||
|
||||
const { runAsync: onClickAdd, loading: isLoading } = useRequest2(
|
||||
async (template: NodeTemplateListItemType) => {
|
||||
const res = await getPreviewPluginNode({ appId: template.id });
|
||||
|
||||
/* Invalid plugin check
|
||||
1. Reference type. but not tool description;
|
||||
2. Has dataset select
|
||||
3. Has dynamic external data
|
||||
*/
|
||||
const oneFileInput =
|
||||
res.inputs.filter((input) =>
|
||||
input.renderTypeList.includes(FlowNodeInputTypeEnum.fileSelect)
|
||||
).length === 1;
|
||||
const canUploadFile =
|
||||
chatConfig?.fileSelectConfig?.canSelectFile || chatConfig?.fileSelectConfig?.canSelectImg;
|
||||
const invalidFileInput = oneFileInput && !!canUploadFile;
|
||||
if (
|
||||
res.inputs.some(
|
||||
(input) =>
|
||||
(input.renderTypeList.length === 1 &&
|
||||
input.renderTypeList[0] === FlowNodeInputTypeEnum.reference &&
|
||||
!input.toolDescription) ||
|
||||
input.renderTypeList.includes(FlowNodeInputTypeEnum.selectDataset) ||
|
||||
input.renderTypeList.includes(FlowNodeInputTypeEnum.addInputParam) ||
|
||||
(input.renderTypeList.includes(FlowNodeInputTypeEnum.fileSelect) && !invalidFileInput)
|
||||
)
|
||||
) {
|
||||
return toast({
|
||||
title: t('app:simple_tool_tips'),
|
||||
status: 'warning'
|
||||
});
|
||||
}
|
||||
|
||||
// 判断是否可以直接添加工具,满足以下任一条件:
|
||||
// 1. 有工具描述
|
||||
// 2. 是模型选择类型
|
||||
// 3. 是文件上传类型且:已开启文件上传、非必填、只有一个文件上传输入
|
||||
const hasInputForm =
|
||||
res.inputs.length > 0 &&
|
||||
res.inputs.some((input) => {
|
||||
if (input.toolDescription) {
|
||||
return false;
|
||||
}
|
||||
if (input.key === NodeInputKeyEnum.forbidStream) {
|
||||
return false;
|
||||
}
|
||||
if (input.renderTypeList.includes(FlowNodeInputTypeEnum.input)) {
|
||||
return true;
|
||||
}
|
||||
if (input.renderTypeList.includes(FlowNodeInputTypeEnum.textarea)) {
|
||||
return true;
|
||||
}
|
||||
if (input.renderTypeList.includes(FlowNodeInputTypeEnum.numberInput)) {
|
||||
return true;
|
||||
}
|
||||
if (input.renderTypeList.includes(FlowNodeInputTypeEnum.switch)) {
|
||||
return true;
|
||||
}
|
||||
if (input.renderTypeList.includes(FlowNodeInputTypeEnum.select)) {
|
||||
return true;
|
||||
}
|
||||
if (input.renderTypeList.includes(FlowNodeInputTypeEnum.JSONEditor)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// 构建默认表单数据
|
||||
const defaultForm = {
|
||||
...res,
|
||||
inputs: res.inputs.map((input) => {
|
||||
// 如果是模型选择类型,使用当前选中的模型
|
||||
// if (input.renderTypeList.includes(FlowNodeInputTypeEnum.selectLLMModel)) {
|
||||
// return {
|
||||
// ...input,
|
||||
// value: selectedModel.model
|
||||
// };
|
||||
// }
|
||||
// 如果是文件上传类型,设置为从工作流开始节点获取用户文件
|
||||
if (input.renderTypeList.includes(FlowNodeInputTypeEnum.fileSelect)) {
|
||||
return {
|
||||
...input,
|
||||
value: [[workflowStartNodeId, NodeOutputKeyEnum.userFiles]]
|
||||
};
|
||||
}
|
||||
return input;
|
||||
})
|
||||
};
|
||||
|
||||
if (hasInputForm) {
|
||||
setConfigTool(defaultForm);
|
||||
} else {
|
||||
onAddTool(defaultForm);
|
||||
}
|
||||
},
|
||||
{
|
||||
errorToast: t('common:core.module.templates.Load plugin error')
|
||||
}
|
||||
);
|
||||
|
||||
const { data: pluginGroups = [] } = useRequest2(getPluginGroups, {
|
||||
manual: false
|
||||
});
|
||||
|
||||
const formatTemplatesArray = useMemo(() => {
|
||||
const data = (() => {
|
||||
if (type === TemplateTypeEnum.systemPlugin) {
|
||||
return pluginGroups.map((group) => {
|
||||
const copy: NodeTemplateListType = group.groupTypes.map((type) => ({
|
||||
list: [],
|
||||
type: type.typeId,
|
||||
label: type.typeName
|
||||
}));
|
||||
templates.forEach((item) => {
|
||||
const index = copy.findIndex((template) => template.type === item.templateType);
|
||||
if (index === -1) return;
|
||||
copy[index].list.push(item);
|
||||
});
|
||||
return {
|
||||
label: group.groupName,
|
||||
list: copy.filter((item) => item.list.length > 0)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
list: [
|
||||
{
|
||||
list: templates,
|
||||
type: '',
|
||||
label: ''
|
||||
}
|
||||
],
|
||||
label: ''
|
||||
}
|
||||
];
|
||||
})();
|
||||
|
||||
return data.filter(({ list }) => list.length > 0);
|
||||
}, [pluginGroups, templates, type]);
|
||||
|
||||
const gridStyle = useMemo(() => {
|
||||
if (type === TemplateTypeEnum.teamPlugin) {
|
||||
return {
|
||||
gridTemplateColumns: ['1fr', '1fr'],
|
||||
py: 2,
|
||||
avatarSize: '2rem'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
gridTemplateColumns: ['1fr', '1fr 1fr'],
|
||||
py: 3,
|
||||
avatarSize: '1.75rem'
|
||||
};
|
||||
}, [type]);
|
||||
|
||||
const PluginListRender = useMemoizedFn(({ list = [] }: { list: NodeTemplateListType }) => {
|
||||
return (
|
||||
<>
|
||||
{list.map((item, i) => {
|
||||
return (
|
||||
<Box
|
||||
key={item.type}
|
||||
css={css({
|
||||
span: {
|
||||
display: 'block'
|
||||
}
|
||||
})}
|
||||
>
|
||||
<Flex>
|
||||
<Box fontSize={'sm'} my={2} fontWeight={'500'} flex={1} color={'myGray.900'}>
|
||||
{t(item.label as any)}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Grid gridTemplateColumns={gridStyle.gridTemplateColumns} rowGap={2} columnGap={3}>
|
||||
{item.list.map((template) => {
|
||||
const selected = selectedTools.some((tool) => tool.pluginId === template.id);
|
||||
|
||||
return (
|
||||
<MyTooltip
|
||||
key={template.id}
|
||||
placement={'right'}
|
||||
label={
|
||||
<Box py={2}>
|
||||
<Flex alignItems={'center'}>
|
||||
<MyAvatar
|
||||
src={template.avatar}
|
||||
w={'1.75rem'}
|
||||
objectFit={'contain'}
|
||||
borderRadius={'sm'}
|
||||
/>
|
||||
<Box fontWeight={'bold'} ml={3} color={'myGray.900'}>
|
||||
{t(template.name as any)}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box mt={2} color={'myGray.500'} maxH={'100px'} overflow={'hidden'}>
|
||||
{t(template.intro as any) || t('common:core.workflow.Not intro')}
|
||||
</Box>
|
||||
{type === TemplateTypeEnum.systemPlugin && (
|
||||
<CostTooltip
|
||||
cost={template.currentCost}
|
||||
hasTokenFee={template.hasTokenFee}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
py={gridStyle.py}
|
||||
px={3}
|
||||
_hover={{ bg: 'myWhite.600' }}
|
||||
borderRadius={'sm'}
|
||||
whiteSpace={'nowrap'}
|
||||
overflow={'hidden'}
|
||||
textOverflow={'ellipsis'}
|
||||
>
|
||||
<MyAvatar
|
||||
src={template.avatar}
|
||||
w={gridStyle.avatarSize}
|
||||
objectFit={'contain'}
|
||||
borderRadius={'sm'}
|
||||
flexShrink={0}
|
||||
/>
|
||||
<Box
|
||||
color={'myGray.900'}
|
||||
fontWeight={'500'}
|
||||
fontSize={'sm'}
|
||||
flex={'1 0 0'}
|
||||
ml={3}
|
||||
className="textEllipsis"
|
||||
>
|
||||
{t(template.name as any)}
|
||||
</Box>
|
||||
|
||||
{selected ? (
|
||||
<Button
|
||||
size={'sm'}
|
||||
variant={'grayDanger'}
|
||||
leftIcon={<MyIcon name={'delete'} w={'16px'} mr={-1} />}
|
||||
onClick={() => onRemoveTool(template)}
|
||||
px={2}
|
||||
fontSize={'mini'}
|
||||
>
|
||||
{t('common:Remove')}
|
||||
</Button>
|
||||
) : template.flowNodeType === 'toolSet' ? (
|
||||
<Flex gap={2}>
|
||||
<Button
|
||||
size={'sm'}
|
||||
variant={'whiteBase'}
|
||||
isLoading={isLoading}
|
||||
leftIcon={<MyIcon name={'common/arrowRight'} w={'16px'} mr={-1.5} />}
|
||||
onClick={() => setParentId(template.id)}
|
||||
px={2}
|
||||
fontSize={'mini'}
|
||||
>
|
||||
{t('common:Open')}
|
||||
</Button>
|
||||
<Button
|
||||
size={'sm'}
|
||||
variant={'primaryOutline'}
|
||||
leftIcon={<MyIcon name={'common/addLight'} w={'16px'} mr={-1.5} />}
|
||||
isLoading={isLoading}
|
||||
onClick={() => onClickAdd(template)}
|
||||
px={2}
|
||||
fontSize={'mini'}
|
||||
>
|
||||
{t('common:Add')}
|
||||
</Button>
|
||||
</Flex>
|
||||
) : template.isFolder ? (
|
||||
<Button
|
||||
size={'sm'}
|
||||
variant={'whiteBase'}
|
||||
leftIcon={<MyIcon name={'common/arrowRight'} w={'16px'} mr={-1.5} />}
|
||||
onClick={() => setParentId(template.id)}
|
||||
px={2}
|
||||
fontSize={'mini'}
|
||||
>
|
||||
{t('common:Open')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size={'sm'}
|
||||
variant={'primaryOutline'}
|
||||
leftIcon={<MyIcon name={'common/addLight'} w={'16px'} mr={-1.5} />}
|
||||
isLoading={isLoading}
|
||||
onClick={() => onClickAdd(template)}
|
||||
px={2}
|
||||
fontSize={'mini'}
|
||||
>
|
||||
{t('common:Add')}
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
</MyTooltip>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
return templates.length === 0 ? (
|
||||
<EmptyTip text={t('app:module.No Modules')} />
|
||||
) : (
|
||||
<Box flex={'1 0 0'} overflow={'overlay'}>
|
||||
<Accordion defaultIndex={[0]} allowMultiple reduceMotion>
|
||||
{formatTemplatesArray.length > 1 ? (
|
||||
<>
|
||||
{formatTemplatesArray.map(({ list, label }, index) => (
|
||||
<AccordionItem key={index} border={'none'}>
|
||||
<AccordionButton
|
||||
fontSize={'sm'}
|
||||
fontWeight={'500'}
|
||||
color={'myGray.900'}
|
||||
justifyContent={'space-between'}
|
||||
alignItems={'center'}
|
||||
borderRadius={'md'}
|
||||
px={3}
|
||||
>
|
||||
{t(label as any)}
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel py={0}>
|
||||
<PluginListRender list={list} />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<PluginListRender list={formatTemplatesArray?.[0]?.list} />
|
||||
)}
|
||||
</Accordion>
|
||||
|
||||
{!!configTool && (
|
||||
<ConfigToolModal
|
||||
configTool={configTool}
|
||||
onCloseConfigTool={onCloseConfigTool}
|
||||
onAddTool={onAddTool}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
});
|
||||