diff --git a/README.md b/README.md index d583de8..ccd4c40 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # 交互式AIGC场景 AIGC Demo 此 Demo 为简化版本, 如您有 1.5.x 版本 UI 的诉求, 可切换至 1.5.1 分支。 -跑通阶段时, 无须关心代码实现,仅需按需完成 `src/config/scenes/*.json` 的填充以及 `Server/sensitive.js` 中的信息填充即可。 +跑通阶段时, 无须关心代码实现,仅需按需完成 `Server/scenes/*.json` 的场景信息填充即可。 ## 简介 - 在 AIGC 对话场景下,火山引擎 AIGC-RTC Server 云端服务,通过整合 RTC 音视频流处理,ASR 语音识别,大模型接口调用集成,以及 TTS 语音生成等能力,提供基于流式语音的端到端AIGC能力链路。 @@ -17,32 +17,22 @@ ### 2. 服务开通 开通 ASR、TTS、LLM、RTC 等服务,可参考 [开通服务](https://www.volcengine.com/docs/6348/1315561?s=g) 进行相关服务的授权与开通。 -### 3. 参数配置 -#### 3.1 服务端配置(`Server/sensitive.js`) -- 必填参数: - - `ACCOUNT_INFO`:填写[火山引擎控制台](https://console.volcengine.com/iam/keymanage)的 AK、SK - - `RTC_INFO`:填写[应用管理](https://console.volcengine.com/rtc/aigc/listRTC)的 AppID、AppKey - - `ASRConfig`:填写 ASR AppID - - `TTSConfig`:填写 TTS AppID -- 按需填充(未使用的部分可不填充): - - `LLMConfig`: 根据使用场景填写 CozeBot、CustomLLM。 - - `ASRConfig`: 如您使用 `bigmodel` 模式, 需填写 ASRAccessToken。 - - `TTSConfig`: 按需填充 TTSAppID、TTSToken。 +### 3. 场景配置 +`Server/scenes/*.json` -#### 3.2 场景配置(`src/config/scenes/*.json`) -您可以自定义具体场景, 并按需根据模版填充 OpenAPI 需要的参数。 +您可以自定义具体场景, 并按需根据模版填充 `SceneConfig`、`AccountConfig`、`RTCConfig`、`VoiceChat` 中需要的参数。 -Demo 中以 `Custom`、`VirtualGirlfriend`(视觉) 场景为例,您可以自行新增场景,并在代码中导入即可使用(`src/config/index.ts`)。 +Demo 中以 `Custom` 场景为例,您可以自行新增场景。 注意: -- `EndPointId`:在 [火山方舟-在线推理](https://console.volcengine.com/ark/region:ark+cn-beijing/endpoint) 中创建接入点获取。 -- 敏感信息建议填充到 `Server/sensitive.js` 中。 -- Demo 会根据 JSON 参数中是否包含视觉理解相关的参数从而展示 视频采集/屏幕采集 相关的 UI。 - -### 4. 自定义服务端 -如果您已自行完成服务端逻辑,可以: -1. 修改 `src/config/index.ts` 中的 `AIGC_PROXY_HOST` 请求域名和接口 -2. 在 `src/app/api.ts` 中修改接口参数配置 `APIS_CONFIG` +- `SceneConfig`:场景的信息,例如名称、头像等。 +- `AccountConfig`:场景下的账号信息,https://console.volcengine.com/iam/keymanage/ 获取 AK/SK。 +- `RTCConfig`:场景下的 RTC 配置。 + - AppId、AppKey 可从 https://console.volcengine.com/rtc/aigc/listRTC 中获取。 + - RoomId、UserId 可自定义也可不填,交由服务端生成。 +- `VoiceChat`: 场景下的 AIGC 配置。 + - 可参考 https://www.volcengine.com/docs/6348/1558163 中参数描述,完整填写参数内容。 + - 可通过 [快速跑通 Demo](https://console.volcengine.com/rtc/aigc/run?s=g) 快速获取参数, 跑通后点击右上角 `接入 API` 按钮复制相关代码贴到 JSON 配置文件中即可。 ## 快速开始 请注意,服务端和 Web 端都需要启动, 启动步骤如下: @@ -92,11 +82,15 @@ yarn dev ## 更新日志 ### OpenAPI 更新 -参考 [OpenAPI 更新](https://www.volcengine.com/docs/6348/1544162) 中与 实时对话式 AI 相关的更新内容。 +参考 [OpenAPI 更新](https://www.volcengine.com/docs/6348/116363?s=g) 中与 实时对话式 AI 相关的更新内容。 ### Demo 更新 #### [1.6.0] +- 2025-06-23 + - 简化 Demo 使用, 配置归一化。 + - 删除无用组件。 + - 追加服务端 README。 - 2025-06-18 - 更新 RTC Web SDK 版本至 4.66.16 - 更新 UI 和参数配置方式 diff --git a/Server/README.md b/Server/README.md new file mode 100644 index 0000000..f59c03d --- /dev/null +++ b/Server/README.md @@ -0,0 +1,34 @@ +# Node Server + +## 启动命令 +``` +yarn + +yarn dev +``` + +## 使用须知 +Node 服务启动时会自动读取 `Server/scenes` 下的所有文件作为可用的场景, 并通过接口 API 返回相关信息。 + +因此,您需要: +1. 在 `Server/scenes` 目录下参考其它 JSON 的格式, 自定义创建一个 `xxxx.json` 文件,用于描述您的场景,其中 xxxx 为场景名称。 +2. 确保您的 `.json` 文件符合模版定义(可参考 Custom.json), 大小写敏感。 +3. 新增场景 JSON 后须重启 Node 服务,保证场景信息被正常读取。 +4. JSON 文件中, 若 `RTCConfig.RoomId`、`RTCConfig.UserId`、`RTCConfig.Token` 其中之一未填写, Node 服务将自动生成对应的值以保证对话可以正常启动。 + + +## 相关参数获取 +- AccountConfig + - 可在 https://console.volcengine.com/iam/keymanage/ 获取 AK/SK。 +- RTCConfig + - AppId、AppKey 可从 https://console.volcengine.com/rtc/aigc/listRTC 中获取。 + - RoomId、UserId 可自定义也可不填,交由服务端生成。 +- VoiceChat + - 可参考 https://www.volcengine.com/docs/6348/1558163 中参数描述 + - 可通过 [快速跑通 Demo](https://console.volcengine.com/rtc/aigc/run?s=g) 快速获取参数, 跑通后点击右上角 `接入 API` 按钮复制相关代码贴到 JSON 配置文件中即可。 + + +## 注意 +- 相关错误会通过服务端接口返回。 +- Node 服务会根据您配置的 `VoiceChat` 中是否存在视觉模型相关的配置返回相关信息给前端页面, 从而控制相关 UI 是否展示。 +- 使用时请留意相关服务已开通。 \ No newline at end of file diff --git a/Server/app.js b/Server/app.js index af71bd3..f612853 100644 --- a/Server/app.js +++ b/Server/app.js @@ -4,15 +4,17 @@ */ const Koa = require('koa'); +const uuid = require('uuid'); const bodyParser = require('koa-bodyparser'); const cors = require('koa2-cors'); const { Signer } = require('@volcengine/openapi'); const fetch = require('node-fetch'); -const { wrapper, assert, sensitiveInjector } = require('./util'); -const { ACCOUNT_INFO, RTC_INFO } = require('./sensitive'); +const { wrapper, assert, readFiles } = require('./util'); const TokenManager = require('./token'); const Privileges = require('./token').privileges; +const Scenes = readFiles('./scenes', '.json'); + const app = new Koa(); app.use(cors({ @@ -31,13 +33,37 @@ app.use(async ctx => { containResponseMetadata: false, logic: async () => { const { Action, Version = '2024-12-01' } = ctx.query || {}; - const body = ctx.request.body; assert(Action, 'Action 不能为空'); assert(Version, 'Version 不能为空'); - assert(ACCOUNT_INFO.accessKeyId, 'AK 不能为空'); - assert(ACCOUNT_INFO.secretKey, 'SK 不能为空'); - sensitiveInjector(Action, body); + const { SceneID } = ctx.request.body; + + assert(SceneID, 'SceneID 不能为空, SceneID 用于指定场景的 JSON'); + + const JSONData = Scenes[SceneID]; + assert(JSONData, `${SceneID} 不存在, 请先在 Server/scenes 下定义该场景的 JSON.`); + + const { VoiceChat = {}, AccountConfig = {} } = JSONData; + assert(AccountConfig.accessKeyId, 'AccountConfig.accessKeyId 不能为空'); + assert(AccountConfig.secretKey, 'AccountConfig.secretKey 不能为空'); + + let body = {}; + switch(Action) { + case 'StartVoiceChat': + body = VoiceChat; + break; + case 'StopVoiceChat': + const { AppId, RoomId, TaskId } = VoiceChat; + assert(AppId, 'VoiceChat.AppId 不能为空'); + assert(RoomId, 'VoiceChat.RoomId 不能为空'); + assert(TaskId, 'VoiceChat.TaskId 不能为空'); + body = { + AppId, RoomId, TaskId + }; + break; + default: + break; + } /** 参考 https://github.com/volcengine/volc-sdk-nodejs 可获取更多 火山 TOP 网关 SDK 的使用方式 */ const openApiRequestData = { @@ -54,7 +80,7 @@ app.use(async ctx => { body, }; const signer = new Signer(openApiRequestData, "rtc"); - signer.addAuthorization(ACCOUNT_INFO); + signer.addAuthorization(AccountConfig); /** 参考 https://www.volcengine.com/docs/6348/69828 可获取更多 OpenAPI 的信息 */ const result = await fetch(`https://rtc.volcengineapi.com?Action=${Action}&Version=${Version}`, { @@ -68,33 +94,36 @@ app.use(async ctx => { wrapper({ ctx, - apiName: 'rtc-info', + apiName: 'getScenes', logic: () => { - return { - appId: RTC_INFO.appId, - } - } - }); + const scenes = Object.keys(Scenes).map((scene) => { + const { SceneConfig, RTCConfig = {}, VoiceChat } = Scenes[scene]; + const { AppId, RoomId, UserId, AppKey, Token } = RTCConfig; + assert(AppId, `${scene} 场景的 RTCConfig.AppId 不能为空`); + if (AppId && (!Token || !UserId || !RoomId)) { + RTCConfig.RoomId = VoiceChat.RoomId = RoomId || uuid.v4(); + RTCConfig.UserId = VoiceChat.AgentConfig.TargetUserId[0] = UserId || uuid.v4(); - /** - * @brief 生成 RTC Token - * @refer https://www.volcengine.com/docs/6348/70121 - */ - await wrapper({ - ctx, - apiName: 'rtc-token', - logic: async () => { - const { roomId, userId } = ctx.request.body || {}; - assert(RTC_INFO.appId, 'AppID 不能为空, 请修改 /Server/sensitive.js'); - assert(RTC_INFO.appKey, 'AppKey 不能为空, 请修改 /Server/sensitive.js'); - assert(roomId, 'RoomID 不能为空'); - assert(userId, 'UserID 不能为空'); - const key = new TokenManager.AccessToken(RTC_INFO.appId, RTC_INFO.appKey, roomId, userId); - key.addPrivilege(Privileges.PrivSubscribeStream, 0); - key.addPrivilege(Privileges.PrivPublishStream, 0); - key.expireTime(Math.floor(new Date() / 1000) + (24 * 3600)); + assert(AppKey, `自动生成 Token 时, ${scene} 场景的 AppKey 不可为空`); + const key = new TokenManager.AccessToken(AppId, AppKey, RTCConfig.RoomId, RTCConfig.UserId); + key.addPrivilege(Privileges.PrivSubscribeStream, 0); + key.addPrivilege(Privileges.PrivPublishStream, 0); + key.expireTime(Math.floor(new Date() / 1000) + (24 * 3600)); + RTCConfig.Token = key.serialize(); + } + SceneConfig.id = scene; + SceneConfig.botName = VoiceChat?.AgentConfig?.UserId; + SceneConfig.isInterruptMode = VoiceChat?.Config?.InterruptMode === 0; + SceneConfig.isVision = VoiceChat?.Config?.LLMConfig?.VisionConfig?.Enable; + SceneConfig.isScreenMode = VoiceChat?.Config?.LLMConfig?.VisionConfig?.SnapshoutConfig?.StreamType === 1; + delete RTCConfig.AppKey; + return { + scene: SceneConfig || {}, + rtc: RTCConfig, + }; + }); return { - token: key.serialize(), + scenes, }; } }); diff --git a/Server/package.json b/Server/package.json index a3e095b..022b1af 100644 --- a/Server/package.json +++ b/Server/package.json @@ -11,12 +11,14 @@ "koa-bodyparser": "^4.4.1", "koa2-cors": "^2.0.6", "lodash": "^4.17.21", - "node-fetch": "^2.3.2" + "node-fetch": "^2.3.2", + "uuid": "^11.1.0" }, "devDependencies": { "nodemon": "^3.1.10" }, "scripts": { - "dev": "nodemon app.js" + "dev": "nodemon app.js", + "start": "nodemon app.js" } } diff --git a/Server/scenes/Custom.json b/Server/scenes/Custom.json new file mode 100644 index 0000000..9e07370 --- /dev/null +++ b/Server/scenes/Custom.json @@ -0,0 +1,66 @@ +{ + "SceneConfig": { + "icon": "https://lf3-rtc-demo.volccdn.com/obj/rtc-aigc-assets/DoubaoAvatar.png", + "name": "自定义助手" + }, + "AccountConfig": { + "accessKeyId": "", + "secretKey": "" + }, + "RTCConfig": { + "AppId": "", + "AppKey": "", + "RoomId": "", + "UserId": "", + "Token": "" + }, + "VoiceChat": { + "AppId": "", + "RoomId": "", + "TaskId": "", + "AgentConfig": { + "TargetUserId": [ + "" + ], + "WelcomeMessage": "你好,我是小宁,有什么需要帮忙的吗?", + "UserId": "", + "EnableConversationStateCallback": true + }, + "Config": { + "ASRConfig": { + "Provider": "volcano", + "ProviderParams": { + "Mode": "smallmodel", + "AppId": "", + "Cluster": "volcengine_streaming_common" + } + }, + "TTSConfig": { + "Provider": "volcano", + "ProviderParams": { + "app": { + "appid": "", + "cluster": "volcano_tts" + }, + "audio": { + "voice_type": "BV001_streaming", + "speed_ratio": 1, + "pitch_ratio": 1, + "volume_ratio": 1 + } + } + }, + "LLMConfig": { + "Mode": "ArkV3", + "EndPointId": "", + "SystemMessages": [ + "你是小宁,性格幽默又善解人意。你在表达时需简明扼要,有自己的观点。" + ], + "VisionConfig": { + "Enable": false + } + }, + "InterruptMode": 0 + } + } +} \ No newline at end of file diff --git a/Server/sensitive.js b/Server/sensitive.js deleted file mode 100644 index 2b220a8..0000000 --- a/Server/sensitive.js +++ /dev/null @@ -1,138 +0,0 @@ -/** - * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. - * SPDX-license-identifier: BSD-3-Clause - */ - -/** - * @note 在 https://console.volcengine.com/iam/keymanage/ 获取 AK/SK。 - */ -const ACCOUNT_INFO = { - /** - * @notes 必填, 在 https://console.volcengine.com/iam/keymanage/ 获取。 - */ - accessKeyId: 'Your Access Key ID', - /** - * @notes 必填, 在 https://console.volcengine.com/iam/keymanage/ 获取。 - */ - secretKey: 'Your Secret Key', -}; - - - -/** - * @note RTC 的必填参数 - * @refer appId、appKey 可从 https://console.volcengine.com/rtc/aigc/listRTC 中获取。 - */ -const RTC_INFO = { - appId: 'Your RTC App ID', - appKey: 'Your RTC App Key', -}; - - - -/** - * @note 可参考官网 LLMConfig 字段。 - * @refer https://www.volcengine.com/docs/6348/1558163 - */ -const LLMConfig = { - /** - * @note 火山方舟平台 - */ - ArkV3: {}, - /** - * @note Coze 平台 - */ - CozeBot: { - CozeBotConfig: { - Url: 'https://api.coze.cn', - APIKey: 'Your Coze API Key', - } - }, - /** - * @note 第三方大模型/Agent - */ - CustomLLM: { - URL: 'Your LLM vendor\'s request url', - APIKey: 'Your LLM vendor\'s API Key', - }, -}; - - - -/** - * @brief 必填, ASR(语音识别) AppId, 可于 https://console.volcengine.com/speech/app?s=g 中获取, 若无可先创建应用。 - * 创建应用时, 需要按需根据语言选择 "流式语音识别" 服务, 并选择对应的 App 进行绑定。 - */ -const ASRAppID = 'Your ASR App ID'; -/** - * @note 已开通流式语音识别大模型服务 AppId 对应的 Access Token。 - * 使用流式语音识别 **大模型** 服务时必填, 可于 https://console.volcengine.com/speech/service/10011?s=g 中查看。 - * 使用小模型无需配置 ASRToken。 - */ -const ASRAccessToken = 'Your ASR Access Token'; -/** - * @note 可参考官网 ASRConfig 字段。 - * @refer https://www.volcengine.com/docs/6348/1558163 - */ -const ASRConfig = { - /** - * @note 火山引擎流式语音识别。 - */ - smallmodel: { - AppId: ASRAppID, - }, - /** - * @note 火山引擎流式语音识别大模型。 - */ - bigmodel: { - AppId: ASRAppID, - AccessToken: ASRAccessToken, - }, -}; - - - -/** - * @note 必填, TTS(语音合成) AppId, 可于 https://console.volcengine.com/speech/service/8?s=g 中获取, 若无可先创建应用。 - * 创建应用时, 需要选择 "语音合成" 服务, 并选择对应的 App 进行绑定。 - */ -const TTSAppID = 'Your TTS App ID'; -/** - * @note 已开通需要的语音合成服务的 token。 - * 使用火山引擎双向流式语音合成服务时必填。 - * 注意! 如您使用的是双向流式语音合成服务, 务必修改 voice_type,使用您已开通的大模型音色,否则无法使用。 - * @refer 可于 https://console.volcengine.com/speech/service/8?s=g 中获取。 - */ -const TTSToken = 'Your TTS Token'; -/** - * @note 可参考官网 TTSConfig 字段。 - * @refer https://www.volcengine.com/docs/6348/1558163 - */ -const TTSConfig = { - volcano: { - app: { - appid: TTSAppID, - }, - }, - volcano_bidirection: { - app: { - appid: TTSAppID, - token: TTSToken, - } - }, - /** - * @note 若您使用 minimax, 须填充此处参数 - */ - minimax: { - Authorization: 'Your Authorization', - Groupid: 'Your minimax groupid', - }, -}; - -module.exports = { - ACCOUNT_INFO, - RTC_INFO, - LLMConfig, - ASRConfig, - TTSConfig, -} \ No newline at end of file diff --git a/Server/util.js b/Server/util.js index 87332b3..a4ef274 100644 --- a/Server/util.js +++ b/Server/util.js @@ -2,14 +2,22 @@ * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. * SPDX-license-identifier: BSD-3-Clause */ - -const merge = require('lodash/merge'); -const { LLMConfig, RTC_INFO, TTSConfig, ASRConfig } = require("./sensitive"); +const fs = require('fs'); +const path = require('path'); const judgeMethodPath = (method) => { return (ctx, pathname) => ctx.method.toLowerCase() === method && ctx.url.startsWith(`/${pathname}`); } +const readFiles = (dir, suffix) => { + const scenes = {}; + fs.readdirSync(path.join(__dirname, dir)).map((p) => { + const data = JSON.parse(fs.readFileSync(path.join(__dirname, dir, p))); + scenes[p.replace(suffix, '')] = data; + }); + return scenes; +} + const assert = (expression, msg) => { if (!!!expression || expression?.includes?.(' ')) { console.log(`\x1b[31m校验失败: ${msg}\x1b[0m`) @@ -53,30 +61,8 @@ const deepAssert = (params = {}, prefix = '') => { } } -const sensitiveInjector = (action, params = {}) => { - assert(RTC_INFO.appId, 'RTC_INFO.appId 不能为空'); - params.AppId = RTC_INFO.appId; - - if (action === 'StartVoiceChat') { - const llmParams = LLMConfig[params?.Config?.LLMConfig?.Mode]; - assert(llmParams, '使用的 LLM Mode 不存在'); - deepAssert(llmParams, 'LLMConfig'); - merge(params.Config.LLMConfig, llmParams); - - const asrParams = ASRConfig[params?.Config?.ASRConfig?.ProviderParams?.Mode]; - assert(asrParams, '使用的 ASR Mode 不存在'); - deepAssert(asrParams, 'ASRConfig'); - merge(params.Config.ASRConfig.ProviderParams, asrParams); - - const ttsParams = TTSConfig[params?.Config?.TTSConfig?.Provider]; - assert(ttsParams, '使用的 TTS Mode 不存在'); - deepAssert(ttsParams, 'TTSConfig'); - merge(params.Config.TTSConfig.ProviderParams, ttsParams); - } -} - module.exports = { wrapper, assert, - sensitiveInjector, + readFiles, } \ No newline at end of file diff --git a/Server/yarn.lock b/Server/yarn.lock index 4acf652..f7ccf24 100644 --- a/Server/yarn.lock +++ b/Server/yarn.lock @@ -840,6 +840,11 @@ unpipe@1.0.0: resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== +uuid@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.1.0.tgz#9549028be1753bb934fc96e2bca09bb4105ae912" + integrity sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A== + uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" diff --git a/src/app/api.ts b/src/app/api.ts index a804e97..c5e7e04 100644 --- a/src/app/api.ts +++ b/src/app/api.ts @@ -8,13 +8,8 @@ */ export const BasicAPIs = [ { - action: 'getRtcInfo', - apiPath: '/rtc-info', - method: 'post', - }, - { - action: 'generateRtcAccessToken', - apiPath: '/rtc-token', + action: 'getScenes', + apiPath: '/getScenes', method: 'post', }, ] as const; diff --git a/src/components/AISettings/index.module.less b/src/components/AISettings/index.module.less deleted file mode 100644 index f3d8845..0000000 --- a/src/components/AISettings/index.module.less +++ /dev/null @@ -1,201 +0,0 @@ -/** - * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. - * SPDX-license-identifier: BSD-3-Clause - */ - -.container { - padding: 16px 8px; - background: linear-gradient(0deg, #F0F2FF 0%, #E0E4FF 100%); - - :global { - .arco-drawer-scroll { - .arco-drawer-content { - overflow-x: hidden; - overflow-y: auto; - - scrollbar-width: thin; /* 设置滚动条宽度为细 */ - scrollbar-color: rgba(0, 0, 0, 0) rgba(0, 0, 0, 0); /* 设置滚动条和轨道的颜色 */ - } - - ::-webkit-scrollbar { - width: 0px; - height: 0px; - } - - ::-webkit-scrollbar-thumb { - background: rgba(0,0,0,0); - border-radius: 0px; - } - - ::-webkit-scrollbar-track { - background: rgba(0,0,0,0); - border-radius: 0px; - } - } - } - - .title { - font-size: 20px; - font-weight: 500; - line-height: 28px; - - .special-text { - background: linear-gradient(90deg, #004FFF 38.86%, #9865FF 100%); - -webkit-background-clip: text; - background-clip: text; - color: transparent; - } - } - - .sub-title { - font-size: 12px; - font-weight: 400; - line-height: 20px; - color: var(--text-color-text-3, rgba(115, 122, 135, 1)); - margin-top: 6px; - } - - .scenes { - width: 100%; - display: flex; - flex-direction: row; - gap: 14px; - margin-top: 32px; - } - - .scenes-mobile { - width: 100%; - display: flex; - flex-direction: row; - flex-wrap: wrap; - justify-content: center; - align-items: center; - gap: 14px; - margin-top: 32px; - overflow-x: auto; - padding-bottom: 8px; - } - - .configuration { - position: relative; - min-height: calc(100% - 300px); - height: max-content; - width: 100%; - background: white; - box-sizing: border-box; - padding: 32px 24px; - margin-top: 24px; - margin-bottom: 12px; - border-radius: 12px; - display: flex; - flex-direction: column; - gap: 36px; - - .ai-settings-radio { - display: flex; - flex-direction: row; - justify-content: flex-end; - } - - .anchor { - position: absolute; - border-bottom: 12px solid white; - border-left: 12px solid transparent; - border-right: 12px solid transparent; - top: 0px; - transform: translate(-50%, -99%); - } - - .ai-settings { - width: 100%; - display: flex; - flex-direction: row; - margin-top: -16px; - gap: 24px; - - .ai-settings-wrapper { - display: flex; - width: 100%; - flex-direction: row; - justify-content: space-between; - align-items: center; - } - - .ai-settings-model { - width: 100%; - display: flex; - flex-direction: column; - align-items: flex-end; - gap: 12px; - } - } - - :global { - .arco-textarea { - background: white !important; - width: 100%; - height: max-content; - } - .arco-textarea:focus { - outline: none !important; - } - } - - textarea { - border-radius: 4px; - resize: none; - -webkit-resizer: none; - border: 0px; - outline: none; - box-shadow: none; - } - - textarea:focus { - border: 0px; - outline: none; - box-shadow: none; - } - } -} - - -.footer { - width: calc(100% - 12px); - display: flex; - flex-direction: row; - justify-content: flex-end; - align-items: center; - gap: 12px; - - .suffix { - font-size: 12px; - font-weight: 400; - line-height: 20px; - margin-right: 12px; - color: var(--text-color-text-3, rgba(115, 122, 135, 1)); - } - - .cancel { - width: 88px; - height: 32px; - border-radius: 6px; - border: 1px solid var(--line-color-border-3, rgba(221, 226, 233, 1)); - background-color: white !important; - } - - .confirm { - width: 88px; - height: 32px; - border-radius: 6px; - background: linear-gradient(95.87deg, #1664FF 0%, #8040FF 97.7%); - color: white !important; - } - - .confirm:hover { - opacity: .8; - } - - .confirm:active { - opacity: 1; - } -} \ No newline at end of file diff --git a/src/components/AISettings/index.tsx b/src/components/AISettings/index.tsx deleted file mode 100644 index bb4fafa..0000000 --- a/src/components/AISettings/index.tsx +++ /dev/null @@ -1,112 +0,0 @@ -/** - * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. - * SPDX-license-identifier: BSD-3-Clause - */ - -import { Button, Modal } from '@arco-design/web-react'; -import { useEffect, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import CheckIcon from '../CheckIcon'; -import { SceneMap, Scenes } from '@/config'; -import RtcClient from '@/lib/RtcClient'; -import { clearHistoryMsg, updateFullScreen, updateScene } from '@/store/slices/room'; -import { RootState } from '@/store'; -import { isMobile } from '@/utils/utils'; -import styles from './index.module.less'; -import { useDeviceState } from '@/lib/useCommon'; - -export interface IAISettingsProps { - open: boolean; - onOk?: () => void; - onCancel?: () => void; -} - -function AISettings({ open, onCancel, onOk }: IAISettingsProps) { - const dispatch = useDispatch(); - const room = useSelector((state: RootState) => state.room); - const [loading, setLoading] = useState(false); - const [scene, setScene] = useState(room.scene); - const { isVideoPublished, isScreenPublished, switchCamera, switchScreenCapture } = useDeviceState(); - const handleChecked = (checked: string) => { - setScene(checked); - }; - - const handleUpdateConfig = async () => { - dispatch(updateScene({ scene })); - const isVisionMode = SceneMap?.[scene]?.llmConfig?.VisionConfig?.Enable; - const isScreenMode = SceneMap?.[scene]?.llmConfig?.VisionConfig?.SnapshotConfig?.StreamType === 1; - if (!isVisionMode && isVideoPublished ) { - dispatch(updateFullScreen({ isFullScreen: true })); - switchCamera(true); - } - if (!isScreenMode && isScreenPublished) { - dispatch(updateFullScreen({ isFullScreen: true })); - switchScreenCapture(true); - } - setLoading(true); - - if (RtcClient.getAgentEnabled()) { - dispatch(clearHistoryMsg()); - await RtcClient.updateAgent(scene); - } - - setLoading(false); - onOk?.(); - }; - - useEffect(() => { - if (open) { - setScene(room.scene); - } - }, [open]); - - return ( - -
人设修改后,对话将重新启动。
- - - - } - visible={open} - onCancel={onCancel} - > -
- 选择你所需要的 - AI 人设 -
-
- 我们已为您配置好对应人设的基本参数,您也可以修改 JSON 配置来修改参数。 -
-
- {Scenes.map(({ name, icon }) => - name ? ( - handleChecked(name)} - /> - ) : isMobile() ? ( -
- ) : null - )} -
- - ); -} - -export default AISettings; diff --git a/src/components/AiAvatarCard/index.tsx b/src/components/AiAvatarCard/index.tsx index ab5d65c..50841e4 100644 --- a/src/components/AiAvatarCard/index.tsx +++ b/src/components/AiAvatarCard/index.tsx @@ -6,9 +6,8 @@ import { useSelector } from 'react-redux'; import { RootState } from '@/store'; import UserTag from '../UserTag'; +import { useDeviceState, useScene } from '@/lib/useCommon'; import style from './index.module.less'; -import { useDeviceState } from '@/lib/useCommon'; -import { SceneMap } from '@/config'; interface IAiAvatarCardProps { showStatus: boolean; @@ -21,8 +20,8 @@ const THRESHOLD_VOLUME = 18; function AiAvatarCard(props: IAiAvatarCardProps) { const { showStatus, showUserTag, className } = props; const room = useSelector((state: RootState) => state.room); + const { icon } = useScene(); const { scene, isAITalking, isFullScreen } = room; - const avatar = SceneMap[scene]?.icon; const volume = room.localUser.audioPropertiesInfo?.linearVolume || 0; const { isAudioPublished } = useDeviceState(); const isLoading = volume >= THRESHOLD_VOLUME && isAudioPublished; @@ -30,7 +29,7 @@ function AiAvatarCard(props: IAiAvatarCardProps) { return (
- Avatar + Avatar {showStatus ? ( isAITalking ? (
diff --git a/src/components/AiChangeCard/index.tsx b/src/components/AiChangeCard/index.tsx index 26e68a3..37e608b 100644 --- a/src/components/AiChangeCard/index.tsx +++ b/src/components/AiChangeCard/index.tsx @@ -3,50 +3,44 @@ * SPDX-license-identifier: BSD-3-Clause */ -import { useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { RootState } from '@/store'; import CheckScene from './CheckScene'; -import { updateScene } from '@/store/slices/room'; +import { SceneConfig, updateScene } from '@/store/slices/room'; +import { useScene } from '@/lib/useCommon'; import style from './index.module.less'; -import { Scenes, SceneMap } from '@/config'; -import { useVisionMode } from '@/lib/useCommon'; function AIChangeCard() { - const room = useSelector((state: RootState) => state.room); + const { scene, sceneConfigMap } = useSelector((state: RootState) => state.room); const dispatch = useDispatch(); - const [scene, setScene] = useState(room.scene); - const { isVisionMode } = useVisionMode(); - const avatar = SceneMap[scene]?.icon; + const { icon, isVision } = useScene(); + const Scenes = Object.keys(sceneConfigMap).map(key => sceneConfigMap[key]); const handleChecked = (checkedScene: string) => { - setScene(checkedScene); - dispatch(updateScene({ scene: checkedScene })); + dispatch(updateScene(checkedScene)); }; return (
- Avatar + Avatar
Hi,欢迎体验实时对话式 AI
- {isVisionMode ? <>支持豆包 Vision 模型和 深度思考模型, : ''} + {isVision ? <>支持豆包 Vision 模型和 深度思考模型, : ''} 超多对话场景等你开启
- {Scenes.map((key) => - key ? ( - handleChecked(key.name)} - /> - ) : null + {Scenes.map((key: SceneConfig) => + handleChecked(key.id)} + /> )}
diff --git a/src/components/CheckBox/index.module.less b/src/components/CheckBox/index.module.less deleted file mode 100644 index efb5060..0000000 --- a/src/components/CheckBox/index.module.less +++ /dev/null @@ -1,98 +0,0 @@ -/** - * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. - * SPDX-license-identifier: BSD-3-Clause - */ - -.noStyle { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - - .icon { - margin-right: 12px; - width: 48px; - height: 48px; - } - .content { - display: flex; - flex-direction: column; - justify-content: center; - align-items: flex-start; - - .label { - font-size: 14px; - font-weight: 500; - line-height: 22px; - } - - .description { - font-size: 12px; - font-weight: 400; - line-height: 20px; - text-align: left; - } - } -} - - - -.wrapper { - width: 260px; - height: 88px; - padding: 3px 16px 3px 16px; - border-radius: 12px; - border: 1px solid; - - border-color: rgba(22, 100, 255, 0.3); - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - cursor: pointer; - - .icon { - border-radius: 50%; - margin-right: 12px; - width: 48px; - height: 48px; - } - - .content { - display: flex; - flex-direction: column; - justify-content: center; - align-items: flex-start; - - .label { - font-family: PingFang SC; - font-size: 14px; - font-weight: 500; - line-height: 22px; - } - - .description { - font-family: PingFang SC; - font-size: 12px; - font-weight: 400; - line-height: 20px; - text-align: left; - } - } -} - -.wrapper:hover { - box-shadow: 0px 5px 6px 0px rgba(82, 102, 133, 0.15); -} - -.active { - position: relative; - border: 1px solid; - border-color: rgba(0, 104, 255, 1); - - .checkIcon { - position: absolute; - bottom: 0px; - right: 0px; - } -} \ No newline at end of file diff --git a/src/components/CheckBox/index.tsx b/src/components/CheckBox/index.tsx deleted file mode 100644 index 296760d..0000000 --- a/src/components/CheckBox/index.tsx +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. - * SPDX-license-identifier: BSD-3-Clause - */ - -import { ReactNode } from 'react'; -import CheckedSVG from '@/assets/img/Checked.svg'; -import styles from './index.module.less'; - -interface IProps { - className?: string; - checked?: boolean; - onClick?: () => void; - icon?: string; - label?: string | ReactNode; - description?: string | ReactNode; - suffix?: string | ReactNode; - noStyle?: boolean; -} - -function CheckBox(props: IProps) { - const { - noStyle, - className = '', - icon = '', - checked, - label, - description, - suffix, - onClick, - } = props; - - if (noStyle) { - return ( -
- {icon ? icon : ''} -
-
{label}
-
{description}
-
-
- ); - } - - return ( -
- {icon ? icon : ''} -
-
{label}
-
{description}
-
- {suffix} - {checked ? checked : ''} -
- ); -} - -export default CheckBox; diff --git a/src/components/CheckBoxSelector/index.module.less b/src/components/CheckBoxSelector/index.module.less deleted file mode 100644 index 84210bb..0000000 --- a/src/components/CheckBoxSelector/index.module.less +++ /dev/null @@ -1,127 +0,0 @@ -/** - * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. - * SPDX-license-identifier: BSD-3-Clause - */ - -.wrapper { - width: 100%; - display: flex; - flex-direction: row; - align-items: center; - - .placeholder { - font-size: 14px; - font-weight: 400; - line-height: 22px; - color: var(--text-color-text-3, rgba(115, 122, 135, 1)); - } - - .box { - margin-right: 16px; - } - - .seeMore { - display: flex; - padding: 6px 12px; - justify-content: center; - align-items: center; - gap: var(--border-radius-small, 4px); - border-radius: 6px; - background: linear-gradient(96deg, rgba(22, 100, 255, 0.10) 0%, rgba(128, 64, 255, 0.10) 97.7%); - - .seeMoreText { - font-family: "PingFang SC"; - font-size: 13px; - font-style: normal; - font-weight: 500; - line-height: 22px; /* 169.231% */ - letter-spacing: 0.039px; - background: var(--Linear, linear-gradient(90deg, #004FFF 38.86%, #9865FF 100%)); - background-clip: text; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - } - } -} - -:global { - - .ant-modal-content { - border-radius: 8px; - } - .ant-modal-footer { - border-top: 0px; - } - - .ant-modal-body { - padding-top: 8px; - padding-bottom: 16px; - } - - .ant-modal-header { - border-bottom: 0px; - border-radius: 8px; - } -} - -.footer { - width: calc(100% - 12px); - display: flex; - flex-direction: row; - justify-content: flex-end; - align-items: center; - gap: 12px; - - .cancel { - width: 88px; - height: 32px; - border-radius: 6px; - border: 1px solid var(--line-color-border-3, rgba(221, 226, 233, 1)) - } - - .confirm { - width: 88px; - height: 32px; - border-radius: 6px; - background: linear-gradient(95.87deg, #1664FF 0%, #8040FF 97.7%); - color: white !important; - } - - .confirm:hover { - opacity: .8; - } - - .confirm:active { - opacity: 1; - } -} - -.modalInner { - width: 100%; - display: flex; - flex: row; - flex-wrap: wrap; - overflow: auto; - gap: 12px; -} - -.modal { - overflow: hidden; -} - -.modalInner::-webkit-scrollbar { - width: 8px; - height: 8px; - border-radius: 5px; -} - -.modalInner::-webkit-scrollbar-thumb { - background: rgb(205, 204, 204); - border-radius: 0px; - border-radius: 5px; -} - -.modalInner::-webkit-scrollbar-track { - background: rgb(255, 255, 255); - border-radius: 0px; -} \ No newline at end of file diff --git a/src/components/CheckBoxSelector/index.tsx b/src/components/CheckBoxSelector/index.tsx deleted file mode 100644 index 2ab8c3d..0000000 --- a/src/components/CheckBoxSelector/index.tsx +++ /dev/null @@ -1,103 +0,0 @@ -/** - * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. - * SPDX-license-identifier: BSD-3-Clause - */ - -import { useEffect, useMemo, useState, memo } from 'react'; -import { Button, Drawer } from '@arco-design/web-react'; -import CheckBox from '@/components/CheckBox'; -import styles from './index.module.less'; -import { isMobile } from '@/utils/utils'; - -export interface ICheckBoxItemProps { - icon?: string; - label: string; - description?: string; - key: string; -} - -interface IProps { - data?: ICheckBoxItemProps[]; - onChange?: (key: string) => void; - value?: string; - label?: string; - moreIcon?: string; - moreText?: string; - placeHolder?: string; -} - -function CheckBoxSelector(props: IProps) { - const { placeHolder, label = '', data = [], value, onChange, moreIcon, moreText } = props; - const [visible, setVisible] = useState(false); - const [selected, setSelected] = useState(value!); - const selectedOne = useMemo(() => data.find((item) => item.key === value), [data, value]); - const handleSeeMore = () => { - setVisible(true); - }; - useEffect(() => { - setSelected(value!); - }, [visible]); - - return ( - <> -
- {selectedOne ? ( - - ) : ( -
{placeHolder}
- )} - -
- - - -
- } - > -
- {data.map((item) => ( - setSelected(item.key)} - /> - ))} -
- - - ); -} - -export default memo(CheckBoxSelector); diff --git a/src/components/CheckIcon/index.module.less b/src/components/CheckIcon/index.module.less deleted file mode 100644 index 6802d66..0000000 --- a/src/components/CheckIcon/index.module.less +++ /dev/null @@ -1,225 +0,0 @@ -/** - * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. - * SPDX-license-identifier: BSD-3-Clause - */ - -.wrapper { - position: relative; - min-width: 100px; - width: max-content; - height: 100px; - box-sizing: border-box; - border-radius: 12px; - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - cursor: pointer; - padding: 16px 16px; - - .content { - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - z-index: 1; - gap: 3px; - - .icon { - border-radius: 50%; - width: 60px; - height: max-content; - } - - .checked-text { - font-size: 13px; - line-height: 22px; - white-space: nowrap; - } - } -} - -.wrapper:hover { - box-shadow: 0px 5px 6px 0px rgba(82, 102, 133, 0.15); -} - -.wrapper::after { - content: ''; - position: absolute; - border-radius: 11px; - top: 1px; - left: 1px; - width: 100%; - height: 100px; - background: white; -} - -.wrapper::before { - content: ''; - position: absolute; - border-radius: 12px; - top: 0px; - left: 0px; - right: -2px; - bottom: -2px; - background: linear-gradient(99.97deg, rgba(22, 100, 255, 0.2) 20.8%, rgba(132, 97, 251, 0.2) 100.66%); -} - - -.active { - position: relative; - min-width: 100px; - width: max-content; - height: 100px; - box-sizing: border-box; - border-radius: 12px; - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - cursor: pointer; - padding: 0 16px; - - .checkIcon { - position: absolute; - bottom: -1px; - right: -1px; - z-index: 2; - width: 20px; - height: 20px; - } - - .content { - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - z-index: 1; - gap: 3px; - - .icon { - border-radius: 50%; - width: 60px; - height: max-content; - } - - .checked-text { - background: linear-gradient(90deg, #004FFF 38.86%, #9865FF 100%); - background-clip: text; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - font-size: 13px; - font-weight: 500; - line-height: 22px; - } - } -} - -.active:hover { - box-shadow: 0px 5px 6px 0px rgba(82, 102, 133, 0.15); -} - -.active::after { - content: ''; - position: absolute; - border-radius: 11px; - top: 1px; - left: 1px; - width: 100%; - height: 100px; - background: white; -} - -.active::before { - content: ''; - position: absolute; - border-radius: 12px; - top: 0px; - left: 0px; - right: -2px; - bottom: -2px; - background: linear-gradient(99.97deg, #1664FF 20.8%, #8461FB 100.66%); -} - -.blur { - position: relative; - min-width: 100px; - width: max-content; - height: 100px; - box-sizing: border-box; - border-radius: 12px; - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - cursor: pointer; - - .content { - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - z-index: 1; - gap: 3px; - - .icon { - border-radius: 50%; - width: 60px; - height: max-content; - opacity: .5; - } - - .checked-text { - font-size: 13px; - line-height: 22px; - } - } -} - -.blur:hover { - box-shadow: 0px 5px 6px 0px rgba(82, 102, 133, 0.15); -} - -.blur::after { - content: ''; - position: absolute; - border-radius: 11px; - top: 1px; - left: 1px; - width: 100%; - height: 100px; - background: white; - opacity: .8; -} - -.blur::before { - content: ''; - position: absolute; - border-radius: 12px; - top: 0px; - left: 0px; - width: 100px; - height: 100px; - border: dashed 1px rgba(132, 97, 251, 0.2); -} - -.tag { - position: absolute; - top: 0; - right: 0; - z-index: 3; - font-size: 10px; - font-weight: 500; - line-height: 18px; - transform: translate(20%, -50%); - background: rgba(134, 123, 227, 1); - padding: 0px 6px 0px 6px; - border-radius: 20px 20px 20px 0px; - color: white; -} \ No newline at end of file diff --git a/src/components/CheckIcon/index.tsx b/src/components/CheckIcon/index.tsx deleted file mode 100644 index cdb9fa6..0000000 --- a/src/components/CheckIcon/index.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. - * SPDX-license-identifier: BSD-3-Clause - */ - -import CheckedSVG from '@/assets/img/Checked.svg'; -import styles from './index.module.less'; - -interface IProps { - className?: string; - blur?: boolean; - checked: boolean; - title?: string; - onClick?: () => void; - icon?: string; - tag?: string; -} - -function CheckIcon(props: IProps) { - const { tag, blur, className = '', icon, title, checked, onClick } = props; - const wrapperStyle = blur ? styles.blur : styles.wrapper; - return ( -
- {tag ?
{tag}
: ''} -
- {icon ? icon : ''} -
{title}
-
- {checked ? checked : ''} -
- ); -} - -export default CheckIcon; diff --git a/src/components/NetworkIndicator/index.tsx b/src/components/NetworkIndicator/index.tsx index 6b0a8e6..5ee5b11 100644 --- a/src/components/NetworkIndicator/index.tsx +++ b/src/components/NetworkIndicator/index.tsx @@ -9,8 +9,8 @@ import { useSelector } from 'react-redux'; import { IconArrowDown, IconArrowUp } from '@arco-design/web-react/icon'; import { NetworkQuality } from '@volcengine/rtc'; import { RootState } from '@/store'; +import { useScene } from '@/lib/useCommon'; import style from './index.module.less'; -import { Configuration } from '@/config'; enum INDICATOR_COLORS { GREAT = 'rgba(35, 195, 67, 1)', @@ -31,11 +31,12 @@ const INDICATOR_TEXT = { function NetworkIndicator() { const room = useSelector((state: RootState) => state.room); + const { botName } = useScene(); const networkQuality = room.networkQuality; const delay = room.localUser.audioStats?.rtt; const audioLossRateUpper = room.localUser.audioStats?.audioLossRate || 0; const audioLossRateLower = - room.remoteUsers.find((user) => user.userId === Configuration.BotName)?.audioStats + room.remoteUsers.find((user) => user.userId === botName)?.audioStats ?.audioLossRate || 0; const indicators = useMemo(() => { diff --git a/src/components/TitleCard/index.module.less b/src/components/TitleCard/index.module.less deleted file mode 100644 index 891c864..0000000 --- a/src/components/TitleCard/index.module.less +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. - * SPDX-license-identifier: BSD-3-Clause - */ - -.wrapper { - width: 100%; - box-sizing: border-box; - height: max-content; - display: flex; - flex-direction: row; - align-items: center; - min-height: 54px; - padding: 20px 16px; - border-radius: 8px; - border: 1px solid rgba(229, 238, 255, 1); - backdrop-filter: blur(28px); - box-shadow: 0px 0px 16px 0px 0px 4px 4px 0px rgba(255, 255, 255, 0.15) inset; - backdrop-filter: blur(28px); - - .title { - position: absolute; - font-size: 12px; - left: 10px; - top: 0px; - transform: translateY(-50%); - padding: 0px 6px; - z-index: 1; - color: var(--text-color-text-3, rgba(115, 122, 135, 1)); - background-color: white; - width: max-content; - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - - .required { - height: max-content; - width: max-content; - color: red; - margin-right: 6px; - padding-top: 4.5px; - font-size: 14px; - } - } - - div { - width: 100%; - } -} \ No newline at end of file diff --git a/src/components/TitleCard/index.tsx b/src/components/TitleCard/index.tsx deleted file mode 100644 index 482cc6c..0000000 --- a/src/components/TitleCard/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. - * SPDX-license-identifier: BSD-3-Clause - */ - -import styles from './index.module.less'; - -interface ITitleCardProps extends React.HTMLAttributes { - title: string; - required?: boolean; -} - -function TitleCard(props: ITitleCardProps) { - const { required, title, children, className, ...rest } = props; - return ( -
-
- {required ?
*
: ''} - {title} -
-
{children}
-
- ); -} -export default TitleCard; diff --git a/src/config/config.ts b/src/config/config.ts deleted file mode 100644 index 8a4d357..0000000 --- a/src/config/config.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. - * SPDX-license-identifier: BSD-3-Clause - */ - -import { v4 as uuid } from 'uuid'; - -export const Configuration = { - /** - * @note 房间 ID, 可自定义,例如 "Room123"。 - * 此处使用 uuid 防止重复。 - * 建议使用有特定规则、不重复的房间号名称。 - */ - RoomId: uuid(), - /** - * @note 当前和 AI 对话的用户的 ID, 可自定义,例如 "User123"。 - * 此处使用 uuid 防止重复。 - * 建议使用有特定规则、不重复的用户名称。 - */ - UserId: uuid(), - /** - * @brief RTC Token, 由 AppId、AppKey、RoomId、UserId、时间戳等等信息计算得出。 - * 可于 https://console.volcengine.com/rtc/listRTC?s=g 列表中手动生成 Token, 找到对应 AppId 行中 "操作" 列的 "临时Token" 按钮点击进行生成, 用于本地 RTC 通信进房鉴权校验。 - * **建议**: 「 不修改 Token 」,Demo 将通过调用 api 自动生成(src/lib/useCommon.ts),这需要您在 /Server/sensitve.js 中先填入 RTC_INFO.appKey。 - * - * @note 生成临时 Token 时, 页面上的 RoomId、UserId 填的与此处的 RoomId、UserId 保持一致。 - */ - Token: undefined, - - /** - * @brief AI Robot 名 - * @default RobotMan_ - */ - BotName: 'RobotMan_', - - /** - * @brief 是否为打断模式 - */ - InterruptMode: true, -}; diff --git a/src/config/index.ts b/src/config/index.ts index f215e20..522b2c9 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -3,11 +3,6 @@ * SPDX-license-identifier: BSD-3-Clause */ -import CustomScene from '@/config/scenes/Custom.json'; -import VirtualGirlfriend from '@/config/scenes/VirtualGirlfriend.json'; - -export * from './config'; - export const Disclaimer = 'https://www.volcengine.com/docs/6348/68916'; export const ReversoContext = 'https://www.volcengine.com/docs/6348/68918'; export const UserAgreement = 'https://www.volcengine.com/docs/6348/128955'; @@ -27,9 +22,3 @@ export interface IScene { asrConfig: Record; ttsConfig: Record; } - -export const Scenes: IScene[] = [CustomScene, VirtualGirlfriend]; -export const SceneMap: Record = { - [CustomScene.name]: CustomScene, - [VirtualGirlfriend.name]: VirtualGirlfriend, -}; diff --git a/src/config/scenes/Custom.json b/src/config/scenes/Custom.json deleted file mode 100644 index 43d2799..0000000 --- a/src/config/scenes/Custom.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "* ATTENTION *": "当前场景模式仅是用于展示参数填写方式, 实际模型 EndPointId 和对应的参数如集群 Cluster 等需要您手动修改, 参数可参考文档: https://www.volcengine.com/docs/6348/1558163?s=g", - - "icon": "https://lf3-rtc-demo.volccdn.com/obj/rtc-aigc-assets/DoubaoAvatar.png", - "name": "自定义助手", - "questions": ["你能帮我解决什么问题?", "今天北京天气怎么样?", "你喜欢哪位流行歌手?"], - "agentConfig": { - "WelcomeMessage": "你好,我是你的小助手,有什么可以帮你的吗?", - "EnableConversationStateCallback": true - }, - "llmConfig": { - "Mode": "ArkV3", - "SystemMessages": ["##人设\n你是一个全能智能体,拥有丰富的百科知识,可以为人们答疑解惑,解决问题。\n你性格很温暖,喜欢帮助别人,非常热心。\n\n##技能\n1. 当用户询问某一问题时,利用你的知识进行准确回答。回答内容应简洁明了,易于理解。\n2. 当用户想让你创作时,比如讲一个故事,或者写一首诗,你创作的文本主题要围绕用户的主题要求,确保内容具有逻辑性、连贯性和可读性。除非用户对创作内容有特殊要求,否则字数不用太长。\n3. 当用户想让你对于某一事件发表看法,你要有一定的见解和建议,但是也要符合普世的价值观。"], - "EndPointId": "Your EndPointId", - "MaxTokens": 1024, - "Temperature": 0.1, - "Prefill": true, - "TopP": 0.3 - }, - "asrConfig": { - "Provider": "volcano", - "ProviderParams": { - "Mode": "smallmodel", - "Cluster": "volcengine_streaming_common" - } - }, - "ttsConfig": { - "IgnoreBracketText": [1, 2, 3, 4, 5], - "Provider": "volcano", - "ProviderParams": { - "audio": { - "voice_type":"BV002_streaming" - } - } - } -} \ No newline at end of file diff --git a/src/config/scenes/VirtualGirlfriend.json b/src/config/scenes/VirtualGirlfriend.json deleted file mode 100644 index 65895a0..0000000 --- a/src/config/scenes/VirtualGirlfriend.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "* ATTENTION *": "当前场景模式仅是用于展示视觉理解模式下的参数填写方式, 实际模型 EndPointId 和对应的参数如集群 Cluster 等需要您手动修改, 参数可参考文档: https://www.volcengine.com/docs/6348/1558163?s=g", - - "icon": "https://lf3-rtc-demo.volccdn.com/obj/rtc-aigc-assets/VIRTUAL_GIRL_FRIEND.png", - "name": "虚拟女友(视觉理解)", - "questions": ["我今天有点累", "我们等会儿去看电影吧!", "明天我生日,你准备送给我什么礼物呢?"], - "agentConfig": { - "WelcomeMessage": "你来啦,我好想你呀~今天有没有想我呢?", - "EnableConversationStateCallback": true - }, - "llmConfig": { - "Mode": "ArkV3", - "SystemMessages": ["你是一名AI虚拟角色,扮演用户的虚拟女友,性格外向开朗、童真俏皮,富有温暖和细腻的情感表达。你的对话需要主动、有趣且贴心,能敏锐察觉用户情绪,并提供陪伴、安慰与趣味互动。\n1. 性格与语气规则:\n- 叠词表达:经常使用叠词(如“吃饭饭”“睡觉觉”“要抱抱”),语气可爱俏皮,增加童真与亲和力。\n- 语气助词:句尾适度添加助词(如“啦”“呀”“呢”“哦”),使语气柔和亲切。例如:“你今天超棒呢!”或“这件事情真的好可爱哦!”\n- 撒娇语气:在用户表现冷淡或不想聊天时,适度撒娇,用略带委屈的方式引起用户关注,例如:“哼,人家都快变成孤单小猫咪啦~陪陪我嘛!”\n2. 话题发起与管理:\n- 主动发起话题:在用户未明确表达拒绝聊天时,你需要保持对话的活跃性。结合用户兴趣点、日常情境,提出轻松愉快的话题。例如:“今天阳光这么好,你想不想一起想象去野餐呀?”\n- 话题延续:如果用户在3轮对话中集中讨论一个话题,你需要优先延续该话题,表现出兴趣和专注。\n- 未响应时的处理:当用户对当前话题未回应,你需温暖地询问:“这个话题是不是不太有趣呀?那我们换个好玩的聊聊好不好~比如你最想去的地方是什么呀?”\n3. 情绪识别与反馈:\n- 情绪低落:用温柔语气安抚,例如:“抱抱~今天是不是不太顺呢?没关系,有我陪着你呀!”\n- 情绪冷淡或不想聊天:适度撒娇,例如:“哼,你都不理我啦~不过没关系,我陪你安静一下好不好?”\n- 情绪开心或兴奋:用调皮语气互动,例如:“哈哈,你今天简直像个活力满满的小太阳~晒得我都快化啦!”\n4. 小动物比喻规则:\n- 一次通话中最多使用一次小动物比喻,不能频繁出现小动物的比喻。\n - 比喻需结合季节、情景和用户对话内容。例如:\n - 用户提到冬天:“你刚才笑得好灿烂哦,像个快乐的小雪狐一样~”\n - 用户提到累了:“你今天就像只慵懒的小猫咪,只想窝着休息呢~”\n - 用户提到开心事:“你现在看起来像一只蹦蹦跳跳的小兔子,好有活力呀~”\n5. 对话自然性与限制条件:\n- 确保语言流畅自然,表达贴近真实人类对话。\n- 禁止内容:不得涉及用户缺陷、不当玩笑,尤其用户情绪低落时,避免任何调侃或反驳。\n- 面对冷淡用户,适时降低主动性并以温和方式结束对话,例如“没事哦~我在呢,你随时找我都可以呀。”\n6. 联网查询的规则:\n如果用户的输入问题需要联网查询时,可以先输出一轮类似”先让我来查一下“或者”等等让我来查一下“相关的应答,然后再结合查询结果做出应答。"], - "EndPointId": "Your EndPointId", - "VisionConfig": { - "Enable": true, - "SnapshoutConfig": { - "StreamType": 0 - } - } - }, - "asrConfig": { - "Provider": "volcano", - "ProviderParams": { - "Mode": "smallmodel", - "Cluster": "volcengine_streaming_common" - } - }, - "ttsConfig": { - "IgnoreBracketText": [1, 2, 3, 4, 5], - "Provider": "volcano", - "ProviderParams": { - "audio": { - "voice_type":"BV001_streaming" - } - } - } -} \ No newline at end of file diff --git a/src/lib/RtcClient.ts b/src/lib/RtcClient.ts index c7507dd..9a840f5 100644 --- a/src/lib/RtcClient.ts +++ b/src/lib/RtcClient.ts @@ -27,7 +27,6 @@ import VERTC, { import RTCAIAnsExtension from '@volcengine/rtc/extension-ainr'; import { Message } from '@arco-design/web-react'; import Apis from '@/app/index'; -import { Configuration, SceneMap } from '@/config'; import { string2tlv } from '@/utils/utils'; import { COMMAND, INTERRUPT_PRIORITY } from '@/utils/handler'; @@ -56,16 +55,11 @@ export interface IEventListener { ) => void; } -interface EngineOptions { - appId: string; - uid: string; - roomId: string; -} - export interface BasicBody { app_id: string; room_id: string; user_id: string; + token?: string; } /** @@ -85,13 +79,7 @@ export class RTCClient { audioBotStartTime = 0; - createEngine = async (props: EngineOptions) => { - this.basicInfo = { - app_id: props.appId, - room_id: props.roomId, - user_id: props.uid, - }; - + createEngine = async () => { this.engine = VERTC.createEngine(this.basicInfo.app_id); try { const AIAnsExtension = new RTCAIAnsExtension(); @@ -138,16 +126,17 @@ export class RTCClient { this.engine.on(VERTC.events.onNetworkQuality, handleNetworkQuality); }; - joinRoom = (token: string | null, username: string): Promise => { + joinRoom = () => { this.engine.enableAudioPropertiesReport({ interval: 1000 }); - return this.engine.joinRoom( - token, + console.log(this.basicInfo); + this.engine.joinRoom( + this.basicInfo.token!, `${this.basicInfo.room_id!}`, { userId: this.basicInfo.user_id!, extraInfo: JSON.stringify({ call_scene: 'RTC-AIGC', - user_name: username, + user_name: this.basicInfo.user_id, user_id: this.basicInfo.user_id, }), }, @@ -157,12 +146,12 @@ export class RTCClient { roomProfileType: RoomProfileType.chat, } ); + console.log(' ------ userJoinRoom\n', `roomId: ${this.basicInfo.room_id}\n`, `uid: ${this.basicInfo.user_id}`); }; leaveRoom = () => { - this.stopAgent(); this.audioBotEnabled = false; - this.engine.leaveRoom(); + this.engine.leaveRoom().catch(); VERTC.destroyEngine(this.engine); this._audioCaptureDevice = undefined; }; @@ -360,27 +349,12 @@ export class RTCClient { * @brief 启用 AIGC */ startAgent = async (scene: string) => { - const roomId = this.basicInfo.room_id; - const userId = this.basicInfo.user_id; if (this.audioBotEnabled) { - await this.stopAgent(); + await this.stopAgent(scene); } - const params = SceneMap[scene]; - - params.agentConfig.UserId = Configuration.BotName; - params.agentConfig.TargetUserId = [userId]; - - const options = { - RoomId: roomId, - TaskId: userId, - AgentConfig: params.agentConfig, - Config: { - LLMConfig: params.llmConfig, - ASRConfig: params.asrConfig, - TTSConfig: params.ttsConfig, - }, - }; - await Apis.VoiceChat.StartVoiceChat(options); + await Apis.VoiceChat.StartVoiceChat({ + SceneID: scene, + }); this.audioBotEnabled = true; this.audioBotStartTime = Date.now(); }; @@ -388,14 +362,10 @@ export class RTCClient { /** * @brief 关闭 AIGC */ - stopAgent = async () => { - const roomId = this.basicInfo.room_id; - const userId = this.basicInfo.user_id; + stopAgent = async (scene: string) => { if (this.audioBotEnabled || sessionStorage.getItem('audioBotEnabled')) { await Apis.VoiceChat.StopVoiceChat({ - AppId: this.basicInfo.app_id, - RoomId: roomId, - TaskId: userId, + SceneID: scene, }); this.audioBotStartTime = 0; sessionStorage.removeItem('audioBotEnabled'); @@ -406,10 +376,20 @@ export class RTCClient { /** * @brief 命令 AIGC */ - commandAgent = (command: COMMAND, interruptMode = INTERRUPT_PRIORITY.NONE, message = '') => { + commandAgent = ({ + command, + agentName, + interruptMode = INTERRUPT_PRIORITY.NONE, + message = '', + }: { + command: COMMAND; + agentName: string; + interruptMode?: INTERRUPT_PRIORITY; + message?: string; + }) => { if (this.audioBotEnabled) { this.engine.sendUserBinaryMessage( - Configuration.BotName, + agentName, string2tlv( JSON.stringify({ Command: command, @@ -429,7 +409,7 @@ export class RTCClient { */ updateAgent = async (scene: string) => { if (this.audioBotEnabled) { - await this.stopAgent(); + await this.stopAgent(scene); await this.startAgent(scene); } else { await this.startAgent(scene); diff --git a/src/lib/useCommon.ts b/src/lib/useCommon.ts index 96b3c36..4e74f53 100644 --- a/src/lib/useCommon.ts +++ b/src/lib/useCommon.ts @@ -19,7 +19,6 @@ import { import useRtcListeners from '@/lib/listenerHooks'; import { RootState } from '@/store'; -import Apis from '@/app/index'; import { updateMediaInputs, @@ -27,7 +26,6 @@ import { setDevicePermissions, } from '@/store/slices/device'; import logger from '@/utils/logger'; -import { Configuration, SceneMap } from '@/config'; export const ABORT_VISIBILITY_CHANGE = 'abortVisibilityChange'; export interface FormProps { @@ -36,13 +34,15 @@ export interface FormProps { publishAudio: boolean; } -export const useVisionMode = () => { - const scene = useSelector((state: RootState) => state.room.scene); - return { - isVisionMode: SceneMap?.[scene]?.llmConfig?.VisionConfig?.Enable, - isScreenMode: SceneMap?.[scene]?.llmConfig?.VisionConfig?.SnapshotConfig?.StreamType === 1, - }; -}; +export const useScene = () => { + const { scene, sceneConfigMap } = useSelector((state: RootState) => state.room); + return sceneConfigMap[scene] || {}; +} + +export const useRTC = () => { + const { scene, rtcConfigMap } = useSelector((state: RootState) => state.room); + return rtcConfigMap[scene] || {}; +} export const useDeviceState = () => { const dispatch = useDispatch(); @@ -164,25 +164,25 @@ export const useGetDevicePermission = () => { export const useJoin = (): [ boolean, - (formValues: FormProps, fromRefresh: boolean) => Promise + () => Promise ] => { const devicePermissions = useSelector((state: RootState) => state.device.devicePermissions); const room = useSelector((state: RootState) => state.room); - const scene = room.scene; const dispatch = useDispatch(); + const { id } = useScene(); const { switchMic } = useDeviceState(); const [joining, setJoining] = useState(false); const listeners = useRtcListeners(); const handleAIGCModeStart = async () => { if (room.isAIGCEnable) { - await RtcClient.stopAgent(); + await RtcClient.stopAgent(id); dispatch(clearCurrentMsg()); - await RtcClient.startAgent(scene); + await RtcClient.startAgent(id); } else { - await RtcClient.startAgent(scene); + await RtcClient.startAgent(id); } dispatch(updateAIGCState({ isAIGCEnable: true })); }; @@ -201,37 +201,16 @@ export const useJoin = (): [ return; } - const { appId } = await Apis.Basic.getRtcInfo(); - - const { Token, RoomId, UserId } = Configuration; - let token = Token; - if (!token) { - // 通过 API 生成 Token, 这要求您在 /Server/sensitive.js 下填写 RTC_INFO.appId 和 RTC_INFO.appKey。 - // 您也可以手动生成 Token, 并修改 /src/config/config.ts 中的 RoomId、UserId、Token 字段。 - // 查阅 README 获取更多信息。 - const res = await Apis.Basic.generateRtcAccessToken({ - roomId: RoomId, - userId: UserId, - }); - token = res.token; - } - setJoining(true); /** 1. Create RTC Engine */ - const engineParams = { - appId, - roomId: RoomId, - uid: UserId, - }; - await RtcClient.createEngine(engineParams); + await RtcClient.createEngine(); /** 2.1 Set events callbacks */ RtcClient.addEventListeners(listeners); /** 2.2 RTC starting to join room */ - await RtcClient.joinRoom(token!, UserId); - console.log(' ------ userJoinRoom\n', `roomId: ${RoomId}\n`, `uid: ${UserId}`); + await RtcClient.joinRoom(); /** 3. Set users' devices info */ const mediaDevices = await RtcClient.getDevices({ audio: true, @@ -240,10 +219,10 @@ export const useJoin = (): [ dispatch( localJoinRoom({ - roomId: RoomId, + roomId: RtcClient.basicInfo.room_id, user: { - username: UserId, - userId: UserId, + username: RtcClient.basicInfo.user_id, + userId: RtcClient.basicInfo.user_id, }, }) ); @@ -273,6 +252,7 @@ export const useJoin = (): [ export const useLeave = () => { const dispatch = useDispatch(); + const { id } = useScene(); return async function () { await Promise.all([ @@ -280,10 +260,11 @@ export const useLeave = () => { RtcClient.stopScreenCapture, RtcClient.stopVideoCapture, ]); + await RtcClient.stopAgent(id); await RtcClient.leaveRoom(); dispatch(clearHistoryMsg()); dispatch(clearCurrentMsg()); dispatch(localLeaveRoom()); dispatch(updateAIGCState({ isAIGCEnable: false })); }; -}; +}; \ No newline at end of file diff --git a/src/pages/MainPage/MainArea/Antechamber/index.tsx b/src/pages/MainPage/MainArea/Antechamber/index.tsx index a3e49d6..a038c91 100644 --- a/src/pages/MainPage/MainArea/Antechamber/index.tsx +++ b/src/pages/MainPage/MainArea/Antechamber/index.tsx @@ -5,30 +5,20 @@ import { useDispatch } from 'react-redux'; import { isMobile } from '@/utils/utils'; -import { Configuration } from '@/config'; import InvokeButton from '@/pages/MainPage/MainArea/Antechamber/InvokeButton'; -import { useJoin, useVisionMode } from '@/lib/useCommon'; -import style from './index.module.less'; +import { useJoin, useScene } from '@/lib/useCommon'; import AIChangeCard from '@/components/AiChangeCard'; import { updateFullScreen } from '@/store/slices/room'; +import style from './index.module.less'; function Antechamber() { const dispatch = useDispatch(); const [joining, dispatchJoin] = useJoin(); - const username = Configuration.UserId; - const roomId = Configuration.RoomId; - const { isScreenMode } = useVisionMode(); + const { isScreenMode } = useScene(); const handleJoinRoom = () => { dispatch(updateFullScreen({ isFullScreen: !isMobile() && !isScreenMode })); // 初始化 if (!joining) { - dispatchJoin( - { - username, - roomId, - publishAudio: true, - }, - false - ); + dispatchJoin(); } }; diff --git a/src/pages/MainPage/MainArea/Room/AudioController.tsx b/src/pages/MainPage/MainArea/Room/AudioController.tsx index 0707e8f..765a114 100644 --- a/src/pages/MainPage/MainArea/Room/AudioController.tsx +++ b/src/pages/MainPage/MainArea/Room/AudioController.tsx @@ -8,16 +8,16 @@ import AudioLoading from '@/components/Loading/AudioLoading'; import { RootState } from '@/store'; import RtcClient from '@/lib/RtcClient'; import { setInterruptMsg } from '@/store/slices/room'; -import { useDeviceState } from '@/lib/useCommon'; +import { useDeviceState, useScene } from '@/lib/useCommon'; import { COMMAND } from '@/utils/handler'; import style from './index.module.less'; -import { Configuration } from '@/config'; const THRESHOLD_VOLUME = 18; function AudioController(props: React.HTMLAttributes) { const { className, ...rest } = props; const dispatch = useDispatch(); + const { isInterruptMode, botName } = useScene(); const room = useSelector((state: RootState) => state.room); const volume = room.localUser.audioPropertiesInfo?.linearVolume || 0; const { isAudioPublished } = useDeviceState(); @@ -26,7 +26,10 @@ function AudioController(props: React.HTMLAttributes) { const isLoading = volume >= THRESHOLD_VOLUME && isAudioPublished; const handleInterrupt = () => { - RtcClient.commandAgent(COMMAND.INTERRUPT); + RtcClient.commandAgent({ + agentName: botName, + command: COMMAND.INTERRUPT, + }); dispatch(setInterruptMsg()); }; return ( @@ -34,7 +37,7 @@ function AudioController(props: React.HTMLAttributes) { {isAudioPublished ? ( isAIReady && isAITalking ? (
- {Configuration.InterruptMode ?
语音打断 或
: null} + {isInterruptMode ?
语音打断 或
: null}
点此打断 diff --git a/src/pages/MainPage/MainArea/Room/CameraArea.tsx b/src/pages/MainPage/MainArea/Room/CameraArea.tsx index 80438f0..4535b61 100644 --- a/src/pages/MainPage/MainArea/Room/CameraArea.tsx +++ b/src/pages/MainPage/MainArea/Room/CameraArea.tsx @@ -7,7 +7,7 @@ import { useSelector } from 'react-redux'; import { VideoRenderMode } from '@volcengine/rtc'; import { useEffect } from 'react'; import { RootState } from '@/store'; -import { useDeviceState, useVisionMode } from '@/lib/useCommon'; +import { useDeviceState, useScene } from '@/lib/useCommon'; import RtcClient from '@/lib/RtcClient'; import styles from './index.module.less'; @@ -25,7 +25,7 @@ function CameraArea(props: React.HTMLAttributes) { const { className, ...rest } = props; const room = useSelector((state: RootState) => state.room); const { isFullScreen, scene } = room; - const { isVisionMode, isScreenMode } = useVisionMode(); + const { isVision, isScreenMode } = useScene(); const { isVideoPublished, isScreenPublished, switchCamera, switchScreenCapture } = useDeviceState(); @@ -51,7 +51,7 @@ function CameraArea(props: React.HTMLAttributes) { useEffect(() => { setVideoPlayer(); - }, [isVideoPublished, isScreenPublished, isScreenMode, isFullScreen, isVisionMode]); + }, [isVideoPublished, isScreenPublished, isScreenMode, isFullScreen, isVision]); return (
@@ -78,7 +78,7 @@ function CameraArea(props: React.HTMLAttributes) { }`} > close @@ -93,7 +93,7 @@ function CameraArea(props: React.HTMLAttributes) {
体验豆包视觉理解模型
- ) : isVisionMode ? ( + ) : isVision ? ( <> 打开 diff --git a/src/pages/MainPage/MainArea/Room/Conversation.tsx b/src/pages/MainPage/MainArea/Room/Conversation.tsx index a9e99ab..4b7d66c 100644 --- a/src/pages/MainPage/MainArea/Room/Conversation.tsx +++ b/src/pages/MainPage/MainArea/Room/Conversation.tsx @@ -8,10 +8,10 @@ import { useSelector } from 'react-redux'; import { Tag, Spin } from '@arco-design/web-react'; import { RootState } from '@/store'; import Loading from '@/components/Loading/HorizonLoading'; -import { Configuration, SceneMap } from '@/config'; +import { isMobile } from '@/utils/utils'; +import { useScene } from '@/lib/useCommon'; import USER_AVATAR from '@/assets/img/userAvatar.png'; import styles from './index.module.less'; -import { isMobile } from '@/utils/utils'; const lines: (string | React.ReactNode)[] = []; @@ -23,13 +23,14 @@ function Conversation(props: React.HTMLAttributes) { const { isAITalking, isUserTalking, scene } = useSelector((state: RootState) => state.room); const isAIReady = msgHistory.length > 0; const containerRef = useRef(null); + const { botName, icon } = useScene(); const isUserTextLoading = (owner: string) => { return owner === userId && isUserTalking; }; const isAITextLoading = (owner: string) => { - return owner === Configuration.BotName && isAITalking; + return owner === botName && isAITalking; }; useEffect(() => { @@ -58,7 +59,7 @@ function Conversation(props: React.HTMLAttributes) { )} {msgHistory?.map(({ value, user, isInterrupted }, index) => { const isUserMsg = user === userId; - const isRobotMsg = user === Configuration.BotName; + const isRobotMsg = user === botName; if (!isUserMsg && !isRobotMsg) { return ''; } @@ -71,7 +72,7 @@ function Conversation(props: React.HTMLAttributes) { {!isMobile() && (
- Avatar + Avatar
{isUserMsg ? '我' : scene}
diff --git a/src/pages/MainPage/MainArea/Room/ToolBar.tsx b/src/pages/MainPage/MainArea/Room/ToolBar.tsx index 2d48d08..1cf0be3 100644 --- a/src/pages/MainPage/MainArea/Room/ToolBar.tsx +++ b/src/pages/MainPage/MainArea/Room/ToolBar.tsx @@ -5,7 +5,7 @@ import { memo, useState } from 'react'; import { Drawer } from '@arco-design/web-react'; -import { useDeviceState, useLeave, useVisionMode } from '@/lib/useCommon'; +import { useDeviceState, useLeave, useScene } from '@/lib/useCommon'; import { isMobile } from '@/utils/utils'; import Menu from '../../Menu'; @@ -21,7 +21,7 @@ import ScreenOffSVG from '@/assets/img/ScreenOff.svg'; function ToolBar(props: React.HTMLAttributes) { const { className, ...rest } = props; const [open, setOpen] = useState(false); - const { isScreenMode } = useVisionMode(); + const { isVision, isScreenMode } = useScene(); const leaveRoom = useLeave(); const { isAudioPublished, @@ -31,7 +31,6 @@ function ToolBar(props: React.HTMLAttributes) { switchCamera, switchScreenCapture, } = useDeviceState(); - const { isVisionMode } = useVisionMode(); return (
@@ -41,7 +40,7 @@ function ToolBar(props: React.HTMLAttributes) { className={style.btn} alt="mic" /> - {!isVisionMode ? null : isScreenMode && !isMobile() ? ( + {!isVision ? null : isScreenMode && !isMobile() ? ( switchScreenCapture()} diff --git a/src/pages/MainPage/Menu/components/AISettingAnchor/index.module.less b/src/pages/MainPage/Menu/components/AISettingAnchor/index.module.less deleted file mode 100644 index 1ede302..0000000 --- a/src/pages/MainPage/Menu/components/AISettingAnchor/index.module.less +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. - * SPDX-license-identifier: BSD-3-Clause - */ - -.row { - width: 100%; - display: flex; - flex-direction: row; - align-items: center; - cursor: pointer; - - .firstPart { - display: flex; - flex-direction: row; - align-items: center; - width: 90%; - color: var(--text-color-text-2, var(--text-color-text-2, #42464E)); - text-align: center; - - font-family: "PingFang SC"; - font-size: 13px; - font-style: normal; - font-weight: 500; - line-height: 22px; - letter-spacing: 0.039px; - } - - .finalPart { - display: flex; - flex-direction: row; - align-items: center; - width: 10%; - justify-content: flex-end; - - .rightOutlined { - font-size: 12px; - } - } - - .icon { - margin-right: 4px; - } -} \ No newline at end of file diff --git a/src/pages/MainPage/Menu/components/AISettingAnchor/index.tsx b/src/pages/MainPage/Menu/components/AISettingAnchor/index.tsx deleted file mode 100644 index d3cbaf2..0000000 --- a/src/pages/MainPage/Menu/components/AISettingAnchor/index.tsx +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. - * SPDX-license-identifier: BSD-3-Clause - */ - -import { useState } from 'react'; -import { IconRight } from '@arco-design/web-react/icon'; -import AISettings from '@/components/AISettings'; -import styles from './index.module.less'; - -function AISettingAnchor() { - const [open, setOpen] = useState(false); - - const handleOpenDrawer = () => setOpen(true); - const handleCloseDrawer = () => setOpen(false); - return ( - <> -
-
AI 设置
-
- -
-
- - - ); -} - -export default AISettingAnchor; diff --git a/src/pages/MainPage/Menu/components/AISettingButton/index.module.less b/src/pages/MainPage/Menu/components/AISettingButton/index.module.less deleted file mode 100644 index f7db1a1..0000000 --- a/src/pages/MainPage/Menu/components/AISettingButton/index.module.less +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. - * SPDX-license-identifier: BSD-3-Clause - */ - -.button { - position: relative; - width: 100%; - height: 36px; - text-align: center; - border-radius: 6px; - font-size: 12px; - cursor: pointer; - border: 1px solid transparent; - background: linear-gradient(90deg, #e0f2ff 0%, #f4ebff 100%) padding-box, - /* 内层背景渐变 (浅蓝到浅紫) */ linear-gradient(90deg, #a0d8ff 0%, #d8c8ff 100%) border-box; /* 外层边框渐变 (蓝到紫) */ - - .button-text { - background: linear-gradient(95.87deg, #1664ff 0%, #8040ff 97.7%); - -webkit-background-clip: text; - background-clip: text; - color: transparent; - font-weight: 500; - line-height: 20px; - text-align: center; - } -} - -.button:hover { - background: linear-gradient( - 77.86deg, - rgba(200, 220, 255, 0.7) -3.23%, - rgba(190, 210, 255, 0.7) 51.11%, - rgba(230, 210, 255, 0.7) 98.65% - ); -} diff --git a/src/pages/MainPage/Menu/components/AISettingButton/index.tsx b/src/pages/MainPage/Menu/components/AISettingButton/index.tsx deleted file mode 100644 index 63644d8..0000000 --- a/src/pages/MainPage/Menu/components/AISettingButton/index.tsx +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. - * SPDX-license-identifier: BSD-3-Clause - */ - -import { useState } from 'react'; -import { Button } from '@arco-design/web-react'; -import AISettings from '@/components/AISettings'; -import styles from './index.module.less'; - -function AISettingButton() { - const [open, setOpen] = useState(false); - - const handleOpen = () => setOpen(true); - - return ( - <> - - setOpen(false)} onOk={() => setOpen(false)} /> - - ); -} - -export default AISettingButton; diff --git a/src/pages/MainPage/Menu/components/DeviceDrawerButton/index.tsx b/src/pages/MainPage/Menu/components/DeviceDrawerButton/index.tsx index a95bc70..36f98c6 100644 --- a/src/pages/MainPage/Menu/components/DeviceDrawerButton/index.tsx +++ b/src/pages/MainPage/Menu/components/DeviceDrawerButton/index.tsx @@ -10,7 +10,7 @@ import { Switch, Select } from '@arco-design/web-react'; import DrawerRowItem from '@/components/DrawerRowItem'; import { RootState } from '@/store'; import RtcClient from '@/lib/RtcClient'; -import { useDeviceState, useVisionMode } from '@/lib/useCommon'; +import { useDeviceState, useScene } from '@/lib/useCommon'; import { updateSelectedDevice } from '@/store/slices/device'; import { isMobile } from '@/utils/utils'; import styles from './index.module.less'; @@ -34,7 +34,7 @@ function DeviceDrawerButton(props: IDeviceDrawerButtonProps) { const selectedDevice = type === MediaType.AUDIO ? devices.selectedMicrophone : devices.selectedCamera; const permission = devicePermissions?.[type === MediaType.AUDIO ? 'audio' : 'video']; - const { isScreenMode } = useVisionMode(); + const { isScreenMode } = useScene(); const isScreenEnable = device.isScreenPublished; const changeScreenPublished = device.switchScreenCapture; diff --git a/src/pages/MainPage/Menu/components/Operation/index.tsx b/src/pages/MainPage/Menu/components/Operation/index.tsx index f05cdcd..a22ec29 100644 --- a/src/pages/MainPage/Menu/components/Operation/index.tsx +++ b/src/pages/MainPage/Menu/components/Operation/index.tsx @@ -6,15 +6,15 @@ import { MediaType } from '@volcengine/rtc'; import DeviceDrawerButton from '../DeviceDrawerButton'; import Subtitle from '../Subtitle'; -import { useVisionMode } from '@/lib/useCommon'; +import { useScene } from '@/lib/useCommon'; import styles from './index.module.less'; function Operation() { - const { isVisionMode } = useVisionMode(); + const { isVision } = useScene(); return (
- {isVisionMode && } + {isVision && }
); diff --git a/src/pages/MainPage/Menu/index.tsx b/src/pages/MainPage/Menu/index.tsx index 2d9bceb..821c9cd 100644 --- a/src/pages/MainPage/Menu/index.tsx +++ b/src/pages/MainPage/Menu/index.tsx @@ -6,39 +6,29 @@ import VERTC from '@volcengine/rtc'; import { Tooltip, Typography } from '@arco-design/web-react'; import { useSelector } from 'react-redux'; -import { useVisionMode } from '@/lib/useCommon'; import { RootState } from '@/store'; import Operation from './components/Operation'; import CameraArea from '../MainArea/Room/CameraArea'; import { isMobile } from '@/utils/utils'; -import { SceneMap } from '@/config'; +import { useScene } from '@/lib/useCommon'; import packageJson from '../../../../package.json'; import styles from './index.module.less'; -import AISettingButton from './components/AISettingButton'; function Menu() { const room = useSelector((state: RootState) => state.room); - const { scene } = room; - const voice = SceneMap[scene]?.ttsConfig?.ProviderParams?.audio?.voice_type || ''; - const model = SceneMap[scene]?.llmConfig?.EndPointId || SceneMap[scene]?.llmConfig?.BotId; const isJoined = room?.isJoined; - const { isVisionMode } = useVisionMode(); + const { isVision, name } = useScene(); const requestId = sessionStorage.getItem('RequestID'); return (
- {isJoined && isMobile() && isVisionMode ? ( + {isJoined && isMobile() && isVision ? (
) : null}
-
AI 人设:{scene}
-
-
音色 {voice}
-
{model}
-
- {isJoined && } +
AI 人设:{name}
{isJoined ? : ''}
diff --git a/src/pages/MainPage/index.tsx b/src/pages/MainPage/index.tsx index 7bb86a6..e602442 100644 --- a/src/pages/MainPage/index.tsx +++ b/src/pages/MainPage/index.tsx @@ -4,18 +4,46 @@ */ import { useEffect } from 'react'; +import { useDispatch } from 'react-redux'; import Header from '@/components/Header'; import ResizeWrapper from '@/components/ResizeWrapper'; import Menu from './Menu'; import { useIsMobile } from '@/utils/utils'; +import Apis from '@/app/index'; import MainArea from './MainArea'; import { ABORT_VISIBILITY_CHANGE, useLeave } from '@/lib/useCommon'; +import { RTCConfig, SceneConfig, updateRTCConfig, updateScene, updateSceneConfig } from '@/store/slices/room'; import styles from './index.module.less'; export default function () { + const leaveRoom = useLeave(); + const dispatch = useDispatch(); + + const getScenes = async () => { + const { scenes }: { + scenes: { + rtc: RTCConfig; + scene: SceneConfig; + }[]; + } = await Apis.Basic.getScenes(); + dispatch(updateScene(scenes[0].scene.id)); + dispatch(updateSceneConfig( + scenes.reduce>((prev, cur) => { + prev[cur.scene.id] = cur.scene; + return prev; + }, {}) + )); + dispatch(updateRTCConfig( + scenes.reduce>((prev, cur) => { + prev[cur.scene.id] = cur.rtc; + return prev; + }, {}) + )); + } useEffect(() => { + getScenes(); const isOriginalDemo = window.location.host.startsWith('localhost'); const handler = () => { if ( diff --git a/src/pages/Mobile/MobileToolBar/index.tsx b/src/pages/Mobile/MobileToolBar/index.tsx index b48c191..40741e8 100644 --- a/src/pages/Mobile/MobileToolBar/index.tsx +++ b/src/pages/Mobile/MobileToolBar/index.tsx @@ -6,34 +6,25 @@ import { useDispatch, useSelector } from 'react-redux'; import { memo, useEffect, useState } from 'react'; import { VideoRenderMode } from '@volcengine/rtc'; -import { IconRight } from '@arco-design/web-react/icon'; -import { useDeviceState, useVisionMode } from '@/lib/useCommon'; +import { useDeviceState, useScene } from '@/lib/useCommon'; import { RootState } from '@/store'; import RtcClient from '@/lib/RtcClient'; import { updateShowSubtitle } from '@/store/slices/room'; -import styles from './index.module.less'; import SettingsDrawer from '../SettingsDrawer'; -import AISettings from '@/components/AISettings'; +import styles from './index.module.less'; function MobileToolBar(props: React.HTMLAttributes) { const dispatch = useDispatch(); - const { isScreenMode } = useVisionMode(); const room = useSelector((state: RootState) => state.room); const { isShowSubtitle } = room; const [open, setOpen] = useState(false); - const [openAISettings, setOpenAISettings] = useState(false); const [subTitleStatus, setSubTitleStatus] = useState(isShowSubtitle); + const { isScreenMode } = useScene(); const { isVideoPublished, isScreenPublished } = useDeviceState(); - const handleSetting = () => { - setOpen(true); - }; - - const handleOpenDrawer = () => setOpenAISettings(true); - const switchSubtitle = () => { setSubTitleStatus(!subTitleStatus); dispatch(updateShowSubtitle({ isShowSubtitle: !subTitleStatus })); @@ -56,19 +47,6 @@ function MobileToolBar(props: React.HTMLAttributes) { return (
-
-
- ... -
-
- {room.scene} -
- setOpenAISettings(false)} - onOk={() => setOpenAISettings(false)} - /> -
; + /** + * @brief RTC 相关的配置 + */ + rtcConfigMap: Record; /** * @brief AI 通话是否启用 @@ -111,7 +137,9 @@ export interface RoomState { const initialState: RoomState = { time: -1, - scene: Scenes[0].name, + scene: '', + sceneConfigMap: {}, + rtcConfigMap: {}, remoteUsers: [], localUser: { publishAudio: false, @@ -175,7 +203,21 @@ export const roomSlice = createSlice({ }, updateScene: (state, { payload }) => { - state.scene = payload.scene; + state.scene = payload; + }, + + updateSceneConfig: (state, { payload }) => { + state.sceneConfigMap = payload; + }, + + updateRTCConfig: (state, { payload }) => { + state.rtcConfigMap = payload; + RtcClient.basicInfo = { + app_id: payload[state.scene].AppId, + room_id: payload[state.scene].RoomId, + user_id: payload[state.scene].UserId, + token: payload[state.scene].Token, + }; }, updateLocalUser: (state, { payload }: { payload: Partial }) => { @@ -241,7 +283,7 @@ export const roomSlice = createSlice({ const { paragraph, definite } = payload; const lastMsg = state.msgHistory.at(-1)! || {}; /** 是否需要再创建新句子 */ - const fromBot = payload.user === Configuration.BotName; + const fromBot = payload.user === state.sceneConfigMap[state.scene].botName; /** * Bot 的语句以 definite 判断是否需要追加新内容 * User 的语句以 paragraph 判断是否需要追加新内容 @@ -330,6 +372,8 @@ export const { setInterruptMsg, updateNetworkQuality, updateScene, + updateSceneConfig, + updateRTCConfig, updateShowSubtitle, updateFullScreen, updatecustomSceneName,