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 (
-

+

{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 (
-

+
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 ?

: ''}
-
-
{label}
-
{description}
-
-
- );
- }
-
- return (
-
- {icon ?

: ''}
-
-
{label}
-
{description}
-
- {suffix}
- {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 ?

: ''}
-
{title}
-
- {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
) {
}`}
>
@@ -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() && (
-

+
{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 (
- <>
-
-
- >
- );
-}
-
-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}
-
- {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,