feat: Simplify the code & remove useless components.
This commit is contained in:
parent
0b4a06d73d
commit
fcf8b920dd
42
README.md
42
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 和参数配置方式
|
||||
|
||||
34
Server/README.md
Normal file
34
Server/README.md
Normal file
@ -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 是否展示。
|
||||
- 使用时请留意相关服务已开通。
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
66
Server/scenes/Custom.json
Normal file
66
Server/scenes/Custom.json
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
@ -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"
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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 (
|
||||
<Modal
|
||||
closable={false}
|
||||
maskClosable={false}
|
||||
title={null}
|
||||
className={styles.container}
|
||||
style={{
|
||||
width: isMobile() ? '100%' : '500px',
|
||||
}}
|
||||
footer={
|
||||
<div className={styles.footer}>
|
||||
<div className={styles.suffix}>人设修改后,对话将重新启动。</div>
|
||||
<Button loading={loading} className={styles.cancel} onClick={onCancel}>
|
||||
取消
|
||||
</Button>
|
||||
<Button loading={loading} className={styles.confirm} onClick={handleUpdateConfig}>
|
||||
确定
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
visible={open}
|
||||
onCancel={onCancel}
|
||||
>
|
||||
<div className={styles.title}>
|
||||
选择你所需要的
|
||||
<span className={styles['special-text']}> AI 人设</span>
|
||||
</div>
|
||||
<div className={styles['sub-title']}>
|
||||
我们已为您配置好对应人设的基本参数,您也可以修改 JSON 配置来修改参数。
|
||||
</div>
|
||||
<div className={isMobile() ? styles['scenes-mobile'] : styles.scenes}>
|
||||
{Scenes.map(({ name, icon }) =>
|
||||
name ? (
|
||||
<CheckIcon
|
||||
key={name}
|
||||
icon={icon}
|
||||
title={name}
|
||||
checked={name === scene}
|
||||
onClick={() => handleChecked(name)}
|
||||
/>
|
||||
) : isMobile() ? (
|
||||
<div style={{ width: '100px', height: '100px' }} />
|
||||
) : null
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default AISettings;
|
||||
@ -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 (
|
||||
<div className={`${style.card} ${className} ${isFullScreen ? style.fullScreen : ''}`}>
|
||||
<div className={style.avatar}>
|
||||
<img id="avatar-card" src={avatar} alt="Avatar" />
|
||||
<img id="avatar-card" src={icon} alt="Avatar" />
|
||||
{showStatus ? (
|
||||
isAITalking ? (
|
||||
<div className={style.aiStatus}>
|
||||
|
||||
@ -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 (
|
||||
<div className={style.card}>
|
||||
<div className={style.avatar}>
|
||||
<img id="avatar-card" src={avatar} alt="Avatar" />
|
||||
<img id="avatar-card" src={icon} alt="Avatar" />
|
||||
</div>
|
||||
<div className={style.title}>
|
||||
<div>Hi,欢迎体验实时对话式 AI</div>
|
||||
<div className={style.desc}>
|
||||
{isVisionMode ? <>支持豆包 Vision 模型和 深度思考模型,</> : ''}
|
||||
{isVision ? <>支持豆包 Vision 模型和 深度思考模型,</> : ''}
|
||||
超多对话场景等你开启
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.sceneContainer}>
|
||||
{Scenes.map((key) =>
|
||||
key ? (
|
||||
<CheckScene
|
||||
key={key.name}
|
||||
icon={key.icon}
|
||||
title={key.name}
|
||||
checked={key.name === scene}
|
||||
onClick={() => handleChecked(key.name)}
|
||||
/>
|
||||
) : null
|
||||
{Scenes.map((key: SceneConfig) =>
|
||||
<CheckScene
|
||||
key={key.name}
|
||||
icon={key.icon}
|
||||
title={key.name}
|
||||
checked={key.id === scene}
|
||||
onClick={() => handleChecked(key.id)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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 (
|
||||
<div className={`${className} ${styles.noStyle}`}>
|
||||
{icon ? <img className={styles.icon} src={icon} alt="icon" /> : ''}
|
||||
<div className={styles.content}>
|
||||
<div className={styles.label}>{label}</div>
|
||||
<div className={styles.description}>{description}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${className} ${styles.wrapper} ${checked ? styles.active : ''}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
{icon ? <img className={styles.icon} src={icon} alt="icon" /> : ''}
|
||||
<div className={styles.content}>
|
||||
<div className={styles.label}>{label}</div>
|
||||
<div className={styles.description}>{description}</div>
|
||||
</div>
|
||||
{suffix}
|
||||
{checked ? <img className={styles.checkIcon} src={CheckedSVG} alt="checked" /> : ''}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CheckBox;
|
||||
@ -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;
|
||||
}
|
||||
@ -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<string>(value!);
|
||||
const selectedOne = useMemo(() => data.find((item) => item.key === value), [data, value]);
|
||||
const handleSeeMore = () => {
|
||||
setVisible(true);
|
||||
};
|
||||
useEffect(() => {
|
||||
setSelected(value!);
|
||||
}, [visible]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.wrapper}>
|
||||
{selectedOne ? (
|
||||
<CheckBox
|
||||
className={styles.box}
|
||||
icon={selectedOne?.icon}
|
||||
label={selectedOne?.label || ''}
|
||||
description={selectedOne?.description}
|
||||
noStyle
|
||||
/>
|
||||
) : (
|
||||
<div className={styles.placeholder}>{placeHolder}</div>
|
||||
)}
|
||||
<Button type="text" className={styles.seeMore} onClick={handleSeeMore}>
|
||||
{moreIcon ? <img src={moreIcon} alt="moreIcon" /> : ''}
|
||||
<span className={styles.seeMoreText}>{moreText || '查看更多'}</span>
|
||||
</Button>
|
||||
</div>
|
||||
<Drawer
|
||||
style={{
|
||||
width: isMobile() ? '100%' : '650px',
|
||||
}}
|
||||
closable={false}
|
||||
className={styles.modal}
|
||||
title={label}
|
||||
visible={visible}
|
||||
footer={
|
||||
<div className={styles.footer}>
|
||||
<Button className={styles.cancel} onClick={() => setVisible(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
className={styles.confirm}
|
||||
onClick={() => {
|
||||
onChange?.(selected);
|
||||
setVisible(false);
|
||||
}}
|
||||
>
|
||||
确定
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className={styles.modalInner}>
|
||||
{data.map((item) => (
|
||||
<CheckBox
|
||||
className={styles.box}
|
||||
key={item.key}
|
||||
icon={item.icon}
|
||||
label={item.label}
|
||||
description={item.description}
|
||||
checked={item.key === selected}
|
||||
onClick={() => setSelected(item.key)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(CheckBoxSelector);
|
||||
@ -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;
|
||||
}
|
||||
@ -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 (
|
||||
<div className={`${checked ? styles.active : wrapperStyle} ${className}`} onClick={onClick}>
|
||||
{tag ? <div className={styles.tag}>{tag}</div> : ''}
|
||||
<div className={styles.content}>
|
||||
{icon ? <img className={styles.icon} src={icon} alt="icon" /> : ''}
|
||||
<div className={styles['checked-text']}>{title}</div>
|
||||
</div>
|
||||
{checked ? <img className={styles.checkIcon} src={CheckedSVG} alt="checked" /> : ''}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CheckIcon;
|
||||
@ -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(() => {
|
||||
|
||||
@ -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%;
|
||||
}
|
||||
}
|
||||
@ -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<HTMLDivElement> {
|
||||
title: string;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
function TitleCard(props: ITitleCardProps) {
|
||||
const { required, title, children, className, ...rest } = props;
|
||||
return (
|
||||
<div className={`${styles.wrapper} ${className}`} {...rest}>
|
||||
<div className={styles.title}>
|
||||
{required ? <div className={styles.required}>* </div> : ''}
|
||||
{title}
|
||||
</div>
|
||||
<div className={styles.children}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default TitleCard;
|
||||
@ -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,
|
||||
};
|
||||
@ -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<string, any>;
|
||||
ttsConfig: Record<string, any>;
|
||||
}
|
||||
|
||||
export const Scenes: IScene[] = [CustomScene, VirtualGirlfriend];
|
||||
export const SceneMap: Record<string, IScene> = {
|
||||
[CustomScene.name]: CustomScene,
|
||||
[VirtualGirlfriend.name]: VirtualGirlfriend,
|
||||
};
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<void> => {
|
||||
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);
|
||||
|
||||
@ -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<void | boolean>
|
||||
() => Promise<void | boolean>
|
||||
] => {
|
||||
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 }));
|
||||
};
|
||||
};
|
||||
};
|
||||
@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -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<HTMLDivElement>) {
|
||||
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<HTMLDivElement>) {
|
||||
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<HTMLDivElement>) {
|
||||
{isAudioPublished ? (
|
||||
isAIReady && isAITalking ? (
|
||||
<div className={style.interruptContainer}>
|
||||
{Configuration.InterruptMode ? <div>语音打断 或 </div> : null}
|
||||
{isInterruptMode ? <div>语音打断 或 </div> : null}
|
||||
<div onClick={handleInterrupt} className={style.interrupt}>
|
||||
<div className={style.interruptIcon} />
|
||||
<span>点此打断</span>
|
||||
|
||||
@ -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<HTMLDivElement>) {
|
||||
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<HTMLDivElement>) {
|
||||
|
||||
useEffect(() => {
|
||||
setVideoPlayer();
|
||||
}, [isVideoPublished, isScreenPublished, isScreenMode, isFullScreen, isVisionMode]);
|
||||
}, [isVideoPublished, isScreenPublished, isScreenMode, isFullScreen, isVision]);
|
||||
|
||||
return (
|
||||
<div className={`${styles['camera-wrapper']} ${className}`} {...rest}>
|
||||
@ -78,7 +78,7 @@ function CameraArea(props: React.HTMLAttributes<HTMLDivElement>) {
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
src={isScreenMode ? ScreenCloseNoteSVG : isVisionMode ? CameraCloseNoteSVG : UserAvatar}
|
||||
src={isScreenMode ? ScreenCloseNoteSVG : isVision ? CameraCloseNoteSVG : UserAvatar}
|
||||
alt="close"
|
||||
className={styles['camera-placeholder-close-note']}
|
||||
/>
|
||||
@ -93,7 +93,7 @@ function CameraArea(props: React.HTMLAttributes<HTMLDivElement>) {
|
||||
</span>
|
||||
<div>体验豆包视觉理解模型</div>
|
||||
</>
|
||||
) : isVisionMode ? (
|
||||
) : isVision ? (
|
||||
<>
|
||||
打开
|
||||
<span onClick={handleOperateCamera} className={styles['camera-open-btn']}>
|
||||
|
||||
@ -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<HTMLDivElement>) {
|
||||
const { isAITalking, isUserTalking, scene } = useSelector((state: RootState) => state.room);
|
||||
const isAIReady = msgHistory.length > 0;
|
||||
const containerRef = useRef<HTMLDivElement>(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<HTMLDivElement>) {
|
||||
)}
|
||||
{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<HTMLDivElement>) {
|
||||
{!isMobile() && (
|
||||
<div className={styles.msgName}>
|
||||
<div className={styles.avatar}>
|
||||
<img src={isUserMsg ? USER_AVATAR : SceneMap[scene].icon} alt="Avatar" />
|
||||
<img src={isUserMsg ? USER_AVATAR : icon} alt="Avatar" />
|
||||
</div>
|
||||
{isUserMsg ? '我' : scene}
|
||||
</div>
|
||||
|
||||
@ -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<HTMLDivElement>) {
|
||||
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<HTMLDivElement>) {
|
||||
switchCamera,
|
||||
switchScreenCapture,
|
||||
} = useDeviceState();
|
||||
const { isVisionMode } = useVisionMode();
|
||||
|
||||
return (
|
||||
<div className={`${className} ${style.btns} ${isMobile() ? style.column : ''}`} {...rest}>
|
||||
@ -41,7 +40,7 @@ function ToolBar(props: React.HTMLAttributes<HTMLDivElement>) {
|
||||
className={style.btn}
|
||||
alt="mic"
|
||||
/>
|
||||
{!isVisionMode ? null : isScreenMode && !isMobile() ? (
|
||||
{!isVision ? null : isScreenMode && !isMobile() ? (
|
||||
<img
|
||||
src={isScreenPublished ? 'new-screen-off.svg' : 'new-screen-on.svg'}
|
||||
onClick={() => switchScreenCapture()}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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 (
|
||||
<>
|
||||
<div className={styles.row} onClick={handleOpenDrawer}>
|
||||
<div className={styles.firstPart}>AI 设置</div>
|
||||
<div className={styles.finalPart}>
|
||||
<IconRight className={styles.rightOutlined} />
|
||||
</div>
|
||||
</div>
|
||||
<AISettings open={open} onOk={handleCloseDrawer} onCancel={handleCloseDrawer} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default AISettingAnchor;
|
||||
@ -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%
|
||||
);
|
||||
}
|
||||
@ -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 (
|
||||
<>
|
||||
<Button className={styles.button} onClick={handleOpen}>
|
||||
<div className={styles['button-text']}>修改 AI 人设</div>
|
||||
</Button>
|
||||
<AISettings open={open} onCancel={() => setOpen(false)} onOk={() => setOpen(false)} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default AISettingButton;
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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 (
|
||||
<div className={`${styles.box} ${styles.device}`}>
|
||||
<Subtitle />
|
||||
{isVisionMode && <DeviceDrawerButton type={MediaType.VIDEO} />}
|
||||
{isVision && <DeviceDrawerButton type={MediaType.VIDEO} />}
|
||||
<DeviceDrawerButton />
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -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 (
|
||||
<div className={styles.wrapper}>
|
||||
{isJoined && isMobile() && isVisionMode ? (
|
||||
{isJoined && isMobile() && isVision ? (
|
||||
<div className={styles['mobile-camera-wrapper']}>
|
||||
<CameraArea className={styles['mobile-camera']} />
|
||||
</div>
|
||||
) : null}
|
||||
<div className={`${styles.box} ${styles.info}`}>
|
||||
<div className={styles.title}>AI 人设:{scene}</div>
|
||||
<div>
|
||||
<div className={styles.desc}>音色 {voice}</div>
|
||||
<div className={styles.desc}>{model}</div>
|
||||
</div>
|
||||
{isJoined && <AISettingButton />}
|
||||
<div className={styles.title}>AI 人设:{name}</div>
|
||||
</div>
|
||||
{isJoined ? <Operation /> : ''}
|
||||
<div className={`${styles.box} ${styles.info}`}>
|
||||
|
||||
@ -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<Record<string, SceneConfig>>((prev, cur) => {
|
||||
prev[cur.scene.id] = cur.scene;
|
||||
return prev;
|
||||
}, {})
|
||||
));
|
||||
dispatch(updateRTCConfig(
|
||||
scenes.reduce<Record<string, RTCConfig>>((prev, cur) => {
|
||||
prev[cur.scene.id] = cur.rtc;
|
||||
return prev;
|
||||
}, {})
|
||||
));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getScenes();
|
||||
const isOriginalDemo = window.location.host.startsWith('localhost');
|
||||
const handler = () => {
|
||||
if (
|
||||
|
||||
@ -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<HTMLDivElement>) {
|
||||
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<HTMLDivElement>) {
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<div>
|
||||
<div className={styles.setting} onClick={handleSetting}>
|
||||
...
|
||||
</div>
|
||||
<div className={styles.aiSetting} onClick={handleOpenDrawer}>
|
||||
{room.scene} <IconRight />
|
||||
</div>
|
||||
<AISettings
|
||||
open={openAISettings}
|
||||
onCancel={() => setOpenAISettings(false)}
|
||||
onOk={() => setOpenAISettings(false)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className={`${styles.subtitle} ${subTitleStatus ? styles.showSubTitle : ''}`}
|
||||
|
||||
@ -10,7 +10,7 @@ import {
|
||||
NetworkQuality,
|
||||
RemoteAudioStats,
|
||||
} from '@volcengine/rtc';
|
||||
import { Configuration, Scenes } from '@/config';
|
||||
import RtcClient from '@/lib/RtcClient';
|
||||
|
||||
export interface IUser {
|
||||
username?: string;
|
||||
@ -36,6 +36,24 @@ export interface Msg {
|
||||
isInterrupted?: boolean;
|
||||
}
|
||||
|
||||
export interface SceneConfig {
|
||||
id: string;
|
||||
icon?: string;
|
||||
name?: string;
|
||||
questions?: string[];
|
||||
botName: string;
|
||||
isVision: boolean;
|
||||
isScreenMode: boolean;
|
||||
isInterruptMode: boolean;
|
||||
}
|
||||
|
||||
export interface RTCConfig {
|
||||
AppId: string;
|
||||
RoomId: string;
|
||||
UserId: string;
|
||||
Token: string;
|
||||
}
|
||||
|
||||
export interface RoomState {
|
||||
time: number;
|
||||
roomId?: string;
|
||||
@ -47,9 +65,17 @@ export interface RoomState {
|
||||
*/
|
||||
isJoined: boolean;
|
||||
/**
|
||||
* @brief 选择的模式
|
||||
* @brief 选择的场景
|
||||
*/
|
||||
scene: string;
|
||||
/**
|
||||
* @brief 场景下的配置
|
||||
*/
|
||||
sceneConfigMap: Record<string, SceneConfig>;
|
||||
/**
|
||||
* @brief RTC 相关的配置
|
||||
*/
|
||||
rtcConfigMap: Record<string, RTCConfig>;
|
||||
|
||||
/**
|
||||
* @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<LocalUser> }) => {
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user