diff --git a/.vscode/settings.json b/.vscode/settings.json index 82794cc44..723cfe006 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -27,5 +27,8 @@ }, "markdown.copyFiles.destination": { "/docSite/content/**/*": "${documentWorkspaceFolder}/docSite/assets/imgs/" + }, + "[svg]": { + "editor.defaultFormatter": "jock.svg" } } \ No newline at end of file diff --git a/packages/global/common/error/code/common.ts b/packages/global/common/error/code/common.ts index e1800fe78..5d0ce5e0b 100644 --- a/packages/global/common/error/code/common.ts +++ b/packages/global/common/error/code/common.ts @@ -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) => { diff --git a/packages/global/core/app/constants.ts b/packages/global/core/app/constants.ts index 7a0cf126b..4ea840039 100644 --- a/packages/global/core/app/constants.ts +++ b/packages/global/core/app/constants.ts @@ -7,6 +7,7 @@ import { } from './type'; export enum AppTypeEnum { + gate = 'gate', folder = 'folder', simple = 'simple', workflow = 'advanced', diff --git a/packages/global/core/app/tags.d.ts b/packages/global/core/app/tags.d.ts new file mode 100644 index 000000000..9e85b10a5 --- /dev/null +++ b/packages/global/core/app/tags.d.ts @@ -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; +}; diff --git a/packages/global/core/app/type.d.ts b/packages/global/core/app/type.d.ts index f49e5d75f..c375ab65b 100644 --- a/packages/global/core/app/type.d.ts +++ b/packages/global/core/app/type.d.ts @@ -65,6 +65,7 @@ export type AppListItemType = { inheritPermission?: boolean; private?: boolean; sourceMember: SourceMemberType; + tags?: string[]; }; export type AppDetailType = AppSchema & { diff --git a/packages/global/support/user/team/gate/api.d.ts b/packages/global/support/user/team/gate/api.d.ts new file mode 100644 index 000000000..17f4a9306 --- /dev/null +++ b/packages/global/support/user/team/gate/api.d.ts @@ -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; +}; diff --git a/packages/global/support/user/team/gate/type.d.ts b/packages/global/support/user/team/gate/type.d.ts new file mode 100644 index 000000000..6133bab4b --- /dev/null +++ b/packages/global/support/user/team/gate/type.d.ts @@ -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; +}; diff --git a/packages/service/core/app/schema.ts b/packages/service/core/app/schema.ts index 3844a07f2..784caa64f 100644 --- a/packages/service/core/app/schema.ts +++ b/packages/service/core/app/schema.ts @@ -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] diff --git a/packages/service/core/app/tags/controller.ts b/packages/service/core/app/tags/controller.ts new file mode 100644 index 000000000..3b170e7f5 --- /dev/null +++ b/packages/service/core/app/tags/controller.ts @@ -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 = {}; + 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; +}; diff --git a/packages/service/core/app/tags/schema.ts b/packages/service/core/app/tags/schema.ts new file mode 100644 index 000000000..58511c23d --- /dev/null +++ b/packages/service/core/app/tags/schema.ts @@ -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(TagCollectionName, TagSchema); diff --git a/packages/service/support/user/team/gate/controller.ts b/packages/service/support/user/team/gate/controller.ts new file mode 100644 index 000000000..478a18b98 --- /dev/null +++ b/packages/service/support/user/team/gate/controller.ts @@ -0,0 +1,435 @@ +import { MongoTeamGate, gateCollectionName } from './schema'; +import { Types } from '../../../../common/mongo'; + +/** + * 创建团队门户配置 + */ +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 = {}; + 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 }: { teamId: string; appId: string }) => { + await MongoTeamGate.updateOne({ teamId }, { $pull: { featuredApps: new Types.ObjectId(appId) } }); + 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; + } + + await MongoTeamGate.bulkWrite(operations); + 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 +}: { + teamId: string; + appIds: string[]; +}) => { + if (!appIds || appIds.length === 0) { + return false; + } + + await MongoTeamGate.updateOne( + { teamId }, + { $pull: { featuredApps: { $in: appIds.map((id) => new Types.ObjectId(id)) } } } + ); + 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 ( + updates: { + teamId: string; + quickApps: string[]; + }[] +) => { + const operations = updates.map((update) => { + const { teamId, quickApps } = update; + // 将字符串数组转换为 ObjectId 数组 + const objectIdArray = quickApps.map((id) => new Types.ObjectId(id)); + return { + updateOne: { + filter: { teamId }, + update: { $set: { quickApps: objectIdArray } }, + upsert: true + } + }; + }); + + if (operations.length === 0) { + return true; + } + + await MongoTeamGate.bulkWrite(operations); + 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; +}; diff --git a/packages/service/support/user/team/gate/schema.ts b/packages/service/support/user/team/gate/schema.ts new file mode 100644 index 000000000..9c86404dd --- /dev/null +++ b/packages/service/support/user/team/gate/schema.ts @@ -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(gateCollectionName, GateConfigSchema); diff --git a/packages/service/support/user/team/teamSchema.ts b/packages/service/support/user/team/teamSchema.ts index c12d89a8c..41c48555c 100644 --- a/packages/service/support/user/team/teamSchema.ts +++ b/packages/service/support/user/team/teamSchema.ts @@ -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() diff --git a/packages/web/components/common/Icon/constants.ts b/packages/web/components/common/Icon/constants.ts index 586420fe6..e61082650 100644 --- a/packages/web/components/common/Icon/constants.ts +++ b/packages/web/components/common/Icon/constants.ts @@ -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'), diff --git a/packages/web/components/common/Icon/icons/support/gate/chat/fileGray.svg b/packages/web/components/common/Icon/icons/support/gate/chat/fileGray.svg new file mode 100644 index 000000000..d76e8687c --- /dev/null +++ b/packages/web/components/common/Icon/icons/support/gate/chat/fileGray.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/web/components/common/Icon/icons/support/gate/chat/historySlider/chevron-left2.svg b/packages/web/components/common/Icon/icons/support/gate/chat/historySlider/chevron-left2.svg new file mode 100644 index 000000000..e97167347 --- /dev/null +++ b/packages/web/components/common/Icon/icons/support/gate/chat/historySlider/chevron-left2.svg @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/support/gate/chat/historySlider/chevron-right2.svg b/packages/web/components/common/Icon/icons/support/gate/chat/historySlider/chevron-right2.svg new file mode 100644 index 000000000..280bbfe87 --- /dev/null +++ b/packages/web/components/common/Icon/icons/support/gate/chat/historySlider/chevron-right2.svg @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/support/gate/chat/historySlider/clear-all.svg b/packages/web/components/common/Icon/icons/support/gate/chat/historySlider/clear-all.svg new file mode 100644 index 000000000..4d01289c3 --- /dev/null +++ b/packages/web/components/common/Icon/icons/support/gate/chat/historySlider/clear-all.svg @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/support/gate/chat/historySlider/new_chat.svg b/packages/web/components/common/Icon/icons/support/gate/chat/historySlider/new_chat.svg new file mode 100644 index 000000000..6a8312deb --- /dev/null +++ b/packages/web/components/common/Icon/icons/support/gate/chat/historySlider/new_chat.svg @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/support/gate/chat/imageGray.svg b/packages/web/components/common/Icon/icons/support/gate/chat/imageGray.svg new file mode 100644 index 000000000..ef95ca837 --- /dev/null +++ b/packages/web/components/common/Icon/icons/support/gate/chat/imageGray.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/web/components/common/Icon/icons/support/gate/chat/paperclip.svg b/packages/web/components/common/Icon/icons/support/gate/chat/paperclip.svg new file mode 100644 index 000000000..e13e3c103 --- /dev/null +++ b/packages/web/components/common/Icon/icons/support/gate/chat/paperclip.svg @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/support/gate/chat/sidebar/CollapseButton.svg b/packages/web/components/common/Icon/icons/support/gate/chat/sidebar/CollapseButton.svg new file mode 100644 index 000000000..cfc2a6b30 --- /dev/null +++ b/packages/web/components/common/Icon/icons/support/gate/chat/sidebar/CollapseButton.svg @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/support/gate/chat/sidebar/appGray.svg b/packages/web/components/common/Icon/icons/support/gate/chat/sidebar/appGray.svg new file mode 100644 index 000000000..633552488 --- /dev/null +++ b/packages/web/components/common/Icon/icons/support/gate/chat/sidebar/appGray.svg @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/support/gate/chat/sidebar/chatGray.svg b/packages/web/components/common/Icon/icons/support/gate/chat/sidebar/chatGray.svg new file mode 100644 index 000000000..80af80da9 --- /dev/null +++ b/packages/web/components/common/Icon/icons/support/gate/chat/sidebar/chatGray.svg @@ -0,0 +1,18 @@ + + + + + + + + + + \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/support/gate/chat/toolkitLine.svg b/packages/web/components/common/Icon/icons/support/gate/chat/toolkitLine.svg new file mode 100644 index 000000000..d576e8687 --- /dev/null +++ b/packages/web/components/common/Icon/icons/support/gate/chat/toolkitLine.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/support/gate/chat/voiceGray.svg b/packages/web/components/common/Icon/icons/support/gate/chat/voiceGray.svg new file mode 100644 index 000000000..00a65c6d7 --- /dev/null +++ b/packages/web/components/common/Icon/icons/support/gate/chat/voiceGray.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/support/gate/gateLight.svg b/packages/web/components/common/Icon/icons/support/gate/gateLight.svg new file mode 100644 index 000000000..528919bdc --- /dev/null +++ b/packages/web/components/common/Icon/icons/support/gate/gateLight.svg @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/support/gate/home/add.svg b/packages/web/components/common/Icon/icons/support/gate/home/add.svg new file mode 100644 index 000000000..49848f612 --- /dev/null +++ b/packages/web/components/common/Icon/icons/support/gate/home/add.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/web/components/common/Icon/icons/support/gate/home/infoRoundedPrimary.svg b/packages/web/components/common/Icon/icons/support/gate/home/infoRoundedPrimary.svg new file mode 100644 index 000000000..852682f08 --- /dev/null +++ b/packages/web/components/common/Icon/icons/support/gate/home/infoRoundedPrimary.svg @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/support/gate/home/savePrimary.svg b/packages/web/components/common/Icon/icons/support/gate/home/savePrimary.svg new file mode 100644 index 000000000..966537e6e --- /dev/null +++ b/packages/web/components/common/Icon/icons/support/gate/home/savePrimary.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/support/gate/home/shareLight.svg b/packages/web/components/common/Icon/icons/support/gate/home/shareLight.svg new file mode 100644 index 000000000..1d0b4f142 --- /dev/null +++ b/packages/web/components/common/Icon/icons/support/gate/home/shareLight.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/support/gate/home/sharePrimary.svg b/packages/web/components/common/Icon/icons/support/gate/home/sharePrimary.svg new file mode 100644 index 000000000..5d485b6c6 --- /dev/null +++ b/packages/web/components/common/Icon/icons/support/gate/home/sharePrimary.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/support/gate/home/upload.svg b/packages/web/components/common/Icon/icons/support/gate/home/upload.svg new file mode 100644 index 000000000..8199532aa --- /dev/null +++ b/packages/web/components/common/Icon/icons/support/gate/home/upload.svg @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/packages/web/components/common/MySelect/GateSelect.tsx b/packages/web/components/common/MySelect/GateSelect.tsx new file mode 100644 index 000000000..36ddb2159 --- /dev/null +++ b/packages/web/components/common/MySelect/GateSelect.tsx @@ -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 = Omit & { + 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; + ScrollData?: ReturnType['ScrollData']; +}; + +const GateSelect = ( + { + placeholder, + value, + isSearch = false, + width = '100%', + list = [], + onChange, + isLoading = false, + ScrollData, + ...props + }: GateSelectProps, + ref: ForwardedRef<{ + focus: () => void; + }> +) => { + const ButtonRef = useRef(null); + const MenuListRef = useRef(null); + const SelectedItemRef = useRef(null); + const SearchInputRef = useRef(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) => ( + + { + if (value !== item.value) { + onclickChange(item.value); + } + }} + whiteSpace={'pre-wrap'} + fontSize={'sm'} + display={'block'} + mb={0.5} + > + + + {item.icon && ( + + )} + {item.label} + + {item.tagText && ( + + {item.tagText} + + )} + + {item.description && ( + + {item.description} + + )} + + {item.showBorder && } + + ))} + + ); + }, [filterList, value]); + + const isSelecting = loading || isLoading; + + return ( + + + } + 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} + > + + + {isSelecting && } + {isSearch && isOpen ? ( + 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); + }} + /> + ) : ( + + {selectItem?.icon && ( + + )} + {selectItem?.alias || selectItem?.label || placeholder} + + )} + + {selectItem?.tagText && ( + + {selectItem.tagText} + + )} + + + + { + 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 ? {ListRender} : ListRender} + + + + ); +}; + +export default forwardRef(GateSelect) as ( + props: GateSelectProps & { ref?: React.Ref } +) => JSX.Element; diff --git a/packages/web/components/common/Tabs/FillRowTabs.tsx b/packages/web/components/common/Tabs/FillRowTabs.tsx index da1376b11..125abbb77 100644 --- a/packages/web/components/common/Tabs/FillRowTabs.tsx +++ b/packages/web/components/common/Tabs/FillRowTabs.tsx @@ -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(null); + const itemsRef = useRef>(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 ( + {/* 滑动背景元素 */} + + {list.map((item) => ( { + 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', - color: 'primary.600' - } - : { - color: 'myGray.500', - _hover: { - color: 'primary.600' - }, - onClick: () => onChange(item.value) - })} + 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' + }} > {item.icon && } {item.label} diff --git a/packages/web/i18n/en/account.json b/packages/web/i18n/en/account.json index b57baec0a..5fbf0f8e9 100644 --- a/packages/web/i18n/en/account.json +++ b/packages/web/i18n/en/account.json @@ -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.", diff --git a/packages/web/i18n/en/account_gate.json b/packages/web/i18n/en/account_gate.json new file mode 100644 index 000000000..caa6b349c --- /dev/null +++ b/packages/web/i18n/en/account_gate.json @@ -0,0 +1,30 @@ +{ + "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", + "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" +} diff --git a/packages/web/i18n/en/app.json b/packages/web/i18n/en/app.json index 579b9c8a3..0ef320da7 100644 --- a/packages/web/i18n/en/app.json +++ b/packages/web/i18n/en/app.json @@ -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", @@ -150,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", @@ -183,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.", @@ -191,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", diff --git a/packages/web/i18n/en/chat.json b/packages/web/i18n/en/chat.json index ff931d947..27da44655 100644 --- a/packages/web/i18n/en/chat.json +++ b/packages/web/i18n/en/chat.json @@ -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}}", diff --git a/packages/web/i18n/en/common.json b/packages/web/i18n/en/common.json index cc65ed973..ed21d658a 100644 --- a/packages/web/i18n/en/common.json +++ b/packages/web/i18n/en/common.json @@ -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,13 @@ "folder.open_dataset": "Open Dataset", "folder_description": "Folder Description", "free": "Free", + "gate.copyright": "The content is generated by third-party AI and is for reference only. The authenticity, accuracy and legality of the information are the responsibility of the provider.", + "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 +929,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 +1017,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 +1026,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 +1223,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 +1233,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 ", diff --git a/packages/web/i18n/zh-CN/account.json b/packages/web/i18n/zh-CN/account.json index f7ed40859..ebbc14254 100644 --- a/packages/web/i18n/zh-CN/account.json +++ b/packages/web/i18n/zh-CN/account.json @@ -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": "模型在系统中展示的名字,方便用户理解", diff --git a/packages/web/i18n/zh-CN/account_gate.json b/packages/web/i18n/zh-CN/account_gate.json new file mode 100644 index 000000000..65cff3100 --- /dev/null +++ b/packages/web/i18n/zh-CN/account_gate.json @@ -0,0 +1,31 @@ +{ + "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": "没有可以删除的门户了", + "slogan": "标语", + "status": "状态", + "suggestion_ratio_1_1": "建议比例 1:1", + "suggestion_ratio_4_1": "建议比例 4:1", + "team_name": "团队名", + "upload": "上传", + "voice_input": "语音输入", + "web_search": "联网搜索" +} diff --git a/packages/web/i18n/zh-CN/app.json b/packages/web/i18n/zh-CN/app.json index a50e7246f..5e7761ffd 100644 --- a/packages/web/i18n/zh-CN/app.json +++ b/packages/web/i18n/zh-CN/app.json @@ -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": "每天执行", @@ -150,6 +151,7 @@ "team_tags_set": "团队标签", "temperature": "温度", "temperature_tip": "范围 0~10。值越大,代表模型回答越发散;值越小,代表回答越严谨。", + "template.gate": "门户", "template.hard_strict": "严格问答模板", "template.hard_strict_des": "在问答模板基础上,对模型的回答做更严格的要求。", "template.qa_template": "问答模板", @@ -183,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": "可以自定义输入和输出的工作流,通常用于封装重复使用的工作流", @@ -191,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 配置文件,直接创建应用", diff --git a/packages/web/i18n/zh-CN/chat.json b/packages/web/i18n/zh-CN/chat.json index 89d37dcd6..8ae753321 100644 --- a/packages/web/i18n/zh-CN/chat.json +++ b/packages/web/i18n/zh-CN/chat.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}}", diff --git a/packages/web/i18n/zh-CN/common.json b/packages/web/i18n/zh-CN/common.json index f34667686..c94150954 100644 --- a/packages/web/i18n/zh-CN/common.json +++ b/packages/web/i18n/zh-CN/common.json @@ -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,13 @@ "folder.open_dataset": "打开知识库", "folder_description": "文件夹描述", "free": "免费", + "gate.copyright": "内容由第三方 AI 生成,仅供参考,信息真实性、准确性、合法性由提供者负责", + "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 +929,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 +1017,7 @@ "read_quote": "查看引用", "redo_tip": "恢复 ctrl shift z", "redo_tip_mac": "恢复 ⌘ shift z", + "reorder_failed": "排序失败", "request_end": "已加载全部", "request_error": "请求异常", "request_more": "点击加载更多", @@ -1008,7 +1030,9 @@ "scan_code": "扫码支付", "select_file_failed": "选择文件异常", "select_reference_variable": "选择引用变量", + "select_tag": "筛选标签", "select_template": "选择模板", + "selected": "已选择", "set_avatar": "点击设置头像", "share_link": "分享链接", "speech_error_tip": "语音转文字失败", @@ -1199,7 +1223,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 +1233,7 @@ "templateTags.Writing": "文本创作", "template_market": "模板市场", "textarea_variable_picker_tip": "输入\"/\"可选择变量", + "tool_select": "工具选择", "ui.textarea.Magnifying": "放大", "un_used": "未使用", "unauth_token": "凭证已过期,请重新登录", diff --git a/packages/web/i18n/zh-Hant/account.json b/packages/web/i18n/zh-Hant/account.json index e65f2fed1..97df97fb3 100644 --- a/packages/web/i18n/zh-Hant/account.json +++ b/packages/web/i18n/zh-Hant/account.json @@ -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": "模型在系統中展示的名字,方便使用者理解", diff --git a/packages/web/i18n/zh-Hant/account_gate.json b/packages/web/i18n/zh-Hant/account_gate.json new file mode 100644 index 000000000..121f3dc11 --- /dev/null +++ b/packages/web/i18n/zh-Hant/account_gate.json @@ -0,0 +1,29 @@ +{ + "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": "沒有可以刪除的門戶了", + "slogan": "標語", + "status": "狀態", + "suggestion_ratio_1_1": "建議比例 1:1", + "suggestion_ratio_4_1": "建議比例 4:1", + "team_name": "團隊名", + "voice_input": "語音輸入", + "web_search": "聯網搜索" +} diff --git a/packages/web/i18n/zh-Hant/app.json b/packages/web/i18n/zh-Hant/app.json index 8b0cc3ffe..e3fb70d89 100644 --- a/packages/web/i18n/zh-Hant/app.json +++ b/packages/web/i18n/zh-Hant/app.json @@ -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": "每天執行", @@ -150,6 +151,7 @@ "team_tags_set": "團隊標籤", "temperature": "溫度", "temperature_tip": "範圍 0~10。\n值越大,代表模型回答越發散;值越小,代表回答越嚴謹。", + "template.gate": "門戶", "template.hard_strict": "嚴格問答範本", "template.hard_strict_des": "在問答範本基礎上,對模型的回答做出更嚴格的要求。", "template.qa_template": "問答範本", @@ -183,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": "可以自訂輸入和輸出的工作流程,通常用於封裝重複使用的工作流程", @@ -191,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 設定文件,直接建立應用", diff --git a/packages/web/i18n/zh-Hant/chat.json b/packages/web/i18n/zh-Hant/chat.json index 53164268f..a72649b73 100644 --- a/packages/web/i18n/zh-Hant/chat.json +++ b/packages/web/i18n/zh-Hant/chat.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}}", diff --git a/packages/web/i18n/zh-Hant/common.json b/packages/web/i18n/zh-Hant/common.json index d7312e69d..4e72df0cd 100644 --- a/packages/web/i18n/zh-Hant/common.json +++ b/packages/web/i18n/zh-Hant/common.json @@ -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,13 @@ "folder.open_dataset": "開啟知識庫", "folder_description": "資料夾描述", "free": "免費", + "gate.copyright": "內容由第三方 AI 生成,僅供參考,信息真實性、準確性、合法性由提供者負責", + "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 +927,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 +1023,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 +1220,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 +1231,6 @@ "template_market": "模板市場", "textarea_variable_picker_tip": "輸入「/」以選擇變數", "ui.textarea.Magnifying": "放大", - "un_used": "未使用", "unauth_token": "憑證已過期,請重新登入", "undo_tip": "復原 ctrl z", "undo_tip_mac": "復原 ⌘ z ", diff --git a/packages/web/types/i18next.d.ts b/packages/web/types/i18next.d.ts index 5a2903e72..6c8675fd3 100644 --- a/packages/web/types/i18next.d.ts +++ b/packages/web/types/i18next.d.ts @@ -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', diff --git a/projects/app/src/components/GatePageContainer/index.tsx b/projects/app/src/components/GatePageContainer/index.tsx new file mode 100644 index 000000000..49b8e59d8 --- /dev/null +++ b/projects/app/src/components/GatePageContainer/index.tsx @@ -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 ( + + + {children} + + + ); +}; + +export default GatePageContainer; diff --git a/projects/app/src/components/Layout/index.tsx b/projects/app/src/components/Layout/index.tsx index f682e8791..0d6c7fc0e 100644 --- a/projects/app/src/components/Layout/index.tsx +++ b/projects/app/src/components/Layout/index.tsx @@ -45,6 +45,9 @@ const pcUnShowLayoutRoute: Record = { '/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 = { '/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, diff --git a/projects/app/src/components/Layout/navbar.tsx b/projects/app/src/components/Layout/navbar.tsx index a26fbe956..250135ea2 100644 --- a/projects/app/src/components/Layout/navbar.tsx +++ b/projects/app/src/components/Layout/navbar.tsx @@ -82,6 +82,7 @@ const Navbar = ({ unread }: { unread: number }) => { '/account/team', '/account/usage', '/account/thirdParty', + '/account/gateway', '/account/apikey', '/account/setting', '/account/inform', diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/Input/GateChatInput.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/Input/GateChatInput.tsx new file mode 100644 index 000000000..147db6bb1 --- /dev/null +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/Input/GateChatInput.tsx @@ -0,0 +1,466 @@ +import React, { useRef, useCallback, useMemo, useState, useEffect, useContext } from 'react'; +import { Box, Flex, Textarea, IconButton, useBreakpointValue, Button } 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 GateSelect from '@fastgpt/web/components/common/MySelect/GateSelect'; + +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; + resetInputVal: (val: ChatBoxInputType) => void; + chatForm: UseFormReturn; + placeholder?: string; + selectedToolIds?: string[]; + onSelectedToolIdsChange?: (toolIds: string[]) => void; +}; + +const GateChatInput = ({ + onSendMessage, + onStop, + TextareaDom, + resetInputVal, + chatForm, + placeholder, + selectedToolIds: externalSelectedToolIds, + onSelectedToolIdsChange +}: Props) => { + const { t } = useTranslation(); + const { isPc } = useSystem(); + const router = useRouter(); + const buttonSize = useBreakpointValue({ base: 'sm', md: 'md' }); + const VoiceInputRef = useRef(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); + + // 如果有外部传入的工具选择,使用外部的;否则使用内部状态 + const [internalSelectedToolIds, setInternalSelectedToolIds] = useState([]); + const selectedToolIds = externalSelectedToolIds ?? internalSelectedToolIds; + const setSelectedToolIds = onSelectedToolIdsChange ?? setInternalSelectedToolIds; + + const { appDetail } = useContextSelector(AppContext, (v) => v); + 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.startsWith('/chat/gate') && + !router.pathname.includes('/chat/gate/application') + ); + }, [router.pathname]); + + // 是否显示工具选择器 + const showTools = useMemo(() => { + return router.pathname === '/chat/gate'; + }, [router.pathname]); + + // 初始化加载appForm - 从Gate应用获取配置 + useEffect(() => { + if (!appId || !showTools) return; + + const fetchAppForm = async () => { + try { + // 加载Gate应用列表 + // 获取当前应用或第一个可用的Gate应用 + const currentApp = appDetail; + + if (currentApp && currentApp.modules) { + // 将模块转换为appForm格式 + const form = appWorkflow2Form({ + nodes: currentApp.modules, + chatConfig: currentApp.chatConfig || {} + }); + setAppForm(form); + // 如果选择了模型,设置为默认模型 + if (form.aiSettings.model) { + setSelectedModel(form.aiSettings.model); + } + } + } catch (error) { + console.error('加载Gate应用信息失败:', error); + } + }; + + fetchAppForm(); + }, [appId, showTools, appDetail, setAppForm]); + + // 当模型选择变化时更新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: selectedToolIds.length > 0 ? selectedToolIds.join(',') : null // 将工具ID数组转换为逗号分隔的字符串 + }); + replaceFiles([]); + }, + [ + TextareaDom, + canSendMessage, + fileList, + onSendMessage, + replaceFiles, + showModelSelector, + selectedModel, + selectedToolIds + ] + ); + + return ( + + {/* file preview */} + + + + + {/* voice input and loading container */} + {!inputValue && ( + + )} + +