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
|
# 交互式AIGC场景 AIGC Demo
|
||||||
|
|
||||||
此 Demo 为简化版本, 如您有 1.5.x 版本 UI 的诉求, 可切换至 1.5.1 分支。
|
此 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能力链路。
|
- 在 AIGC 对话场景下,火山引擎 AIGC-RTC Server 云端服务,通过整合 RTC 音视频流处理,ASR 语音识别,大模型接口调用集成,以及 TTS 语音生成等能力,提供基于流式语音的端到端AIGC能力链路。
|
||||||
@ -17,32 +17,22 @@
|
|||||||
### 2. 服务开通
|
### 2. 服务开通
|
||||||
开通 ASR、TTS、LLM、RTC 等服务,可参考 [开通服务](https://www.volcengine.com/docs/6348/1315561?s=g) 进行相关服务的授权与开通。
|
开通 ASR、TTS、LLM、RTC 等服务,可参考 [开通服务](https://www.volcengine.com/docs/6348/1315561?s=g) 进行相关服务的授权与开通。
|
||||||
|
|
||||||
### 3. 参数配置
|
### 3. 场景配置
|
||||||
#### 3.1 服务端配置(`Server/sensitive.js`)
|
`Server/scenes/*.json`
|
||||||
- 必填参数:
|
|
||||||
- `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.2 场景配置(`src/config/scenes/*.json`)
|
您可以自定义具体场景, 并按需根据模版填充 `SceneConfig`、`AccountConfig`、`RTCConfig`、`VoiceChat` 中需要的参数。
|
||||||
您可以自定义具体场景, 并按需根据模版填充 OpenAPI 需要的参数。
|
|
||||||
|
|
||||||
Demo 中以 `Custom`、`VirtualGirlfriend`(视觉) 场景为例,您可以自行新增场景,并在代码中导入即可使用(`src/config/index.ts`)。
|
Demo 中以 `Custom` 场景为例,您可以自行新增场景。
|
||||||
|
|
||||||
注意:
|
注意:
|
||||||
- `EndPointId`:在 [火山方舟-在线推理](https://console.volcengine.com/ark/region:ark+cn-beijing/endpoint) 中创建接入点获取。
|
- `SceneConfig`:场景的信息,例如名称、头像等。
|
||||||
- 敏感信息建议填充到 `Server/sensitive.js` 中。
|
- `AccountConfig`:场景下的账号信息,https://console.volcengine.com/iam/keymanage/ 获取 AK/SK。
|
||||||
- Demo 会根据 JSON 参数中是否包含视觉理解相关的参数从而展示 视频采集/屏幕采集 相关的 UI。
|
- `RTCConfig`:场景下的 RTC 配置。
|
||||||
|
- AppId、AppKey 可从 https://console.volcengine.com/rtc/aigc/listRTC 中获取。
|
||||||
### 4. 自定义服务端
|
- RoomId、UserId 可自定义也可不填,交由服务端生成。
|
||||||
如果您已自行完成服务端逻辑,可以:
|
- `VoiceChat`: 场景下的 AIGC 配置。
|
||||||
1. 修改 `src/config/index.ts` 中的 `AIGC_PROXY_HOST` 请求域名和接口
|
- 可参考 https://www.volcengine.com/docs/6348/1558163 中参数描述,完整填写参数内容。
|
||||||
2. 在 `src/app/api.ts` 中修改接口参数配置 `APIS_CONFIG`
|
- 可通过 [快速跑通 Demo](https://console.volcengine.com/rtc/aigc/run?s=g) 快速获取参数, 跑通后点击右上角 `接入 API` 按钮复制相关代码贴到 JSON 配置文件中即可。
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
请注意,服务端和 Web 端都需要启动, 启动步骤如下:
|
请注意,服务端和 Web 端都需要启动, 启动步骤如下:
|
||||||
@ -92,11 +82,15 @@ yarn dev
|
|||||||
## 更新日志
|
## 更新日志
|
||||||
|
|
||||||
### OpenAPI 更新
|
### OpenAPI 更新
|
||||||
参考 [OpenAPI 更新](https://www.volcengine.com/docs/6348/1544162) 中与 实时对话式 AI 相关的更新内容。
|
参考 [OpenAPI 更新](https://www.volcengine.com/docs/6348/116363?s=g) 中与 实时对话式 AI 相关的更新内容。
|
||||||
|
|
||||||
### Demo 更新
|
### Demo 更新
|
||||||
|
|
||||||
#### [1.6.0]
|
#### [1.6.0]
|
||||||
|
- 2025-06-23
|
||||||
|
- 简化 Demo 使用, 配置归一化。
|
||||||
|
- 删除无用组件。
|
||||||
|
- 追加服务端 README。
|
||||||
- 2025-06-18
|
- 2025-06-18
|
||||||
- 更新 RTC Web SDK 版本至 4.66.16
|
- 更新 RTC Web SDK 版本至 4.66.16
|
||||||
- 更新 UI 和参数配置方式
|
- 更新 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 Koa = require('koa');
|
||||||
|
const uuid = require('uuid');
|
||||||
const bodyParser = require('koa-bodyparser');
|
const bodyParser = require('koa-bodyparser');
|
||||||
const cors = require('koa2-cors');
|
const cors = require('koa2-cors');
|
||||||
const { Signer } = require('@volcengine/openapi');
|
const { Signer } = require('@volcengine/openapi');
|
||||||
const fetch = require('node-fetch');
|
const fetch = require('node-fetch');
|
||||||
const { wrapper, assert, sensitiveInjector } = require('./util');
|
const { wrapper, assert, readFiles } = require('./util');
|
||||||
const { ACCOUNT_INFO, RTC_INFO } = require('./sensitive');
|
|
||||||
const TokenManager = require('./token');
|
const TokenManager = require('./token');
|
||||||
const Privileges = require('./token').privileges;
|
const Privileges = require('./token').privileges;
|
||||||
|
|
||||||
|
const Scenes = readFiles('./scenes', '.json');
|
||||||
|
|
||||||
const app = new Koa();
|
const app = new Koa();
|
||||||
|
|
||||||
app.use(cors({
|
app.use(cors({
|
||||||
@ -31,13 +33,37 @@ app.use(async ctx => {
|
|||||||
containResponseMetadata: false,
|
containResponseMetadata: false,
|
||||||
logic: async () => {
|
logic: async () => {
|
||||||
const { Action, Version = '2024-12-01' } = ctx.query || {};
|
const { Action, Version = '2024-12-01' } = ctx.query || {};
|
||||||
const body = ctx.request.body;
|
|
||||||
assert(Action, 'Action 不能为空');
|
assert(Action, 'Action 不能为空');
|
||||||
assert(Version, 'Version 不能为空');
|
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 的使用方式 */
|
/** 参考 https://github.com/volcengine/volc-sdk-nodejs 可获取更多 火山 TOP 网关 SDK 的使用方式 */
|
||||||
const openApiRequestData = {
|
const openApiRequestData = {
|
||||||
@ -54,7 +80,7 @@ app.use(async ctx => {
|
|||||||
body,
|
body,
|
||||||
};
|
};
|
||||||
const signer = new Signer(openApiRequestData, "rtc");
|
const signer = new Signer(openApiRequestData, "rtc");
|
||||||
signer.addAuthorization(ACCOUNT_INFO);
|
signer.addAuthorization(AccountConfig);
|
||||||
|
|
||||||
/** 参考 https://www.volcengine.com/docs/6348/69828 可获取更多 OpenAPI 的信息 */
|
/** 参考 https://www.volcengine.com/docs/6348/69828 可获取更多 OpenAPI 的信息 */
|
||||||
const result = await fetch(`https://rtc.volcengineapi.com?Action=${Action}&Version=${Version}`, {
|
const result = await fetch(`https://rtc.volcengineapi.com?Action=${Action}&Version=${Version}`, {
|
||||||
@ -68,33 +94,36 @@ app.use(async ctx => {
|
|||||||
|
|
||||||
wrapper({
|
wrapper({
|
||||||
ctx,
|
ctx,
|
||||||
apiName: 'rtc-info',
|
apiName: 'getScenes',
|
||||||
logic: () => {
|
logic: () => {
|
||||||
return {
|
const scenes = Object.keys(Scenes).map((scene) => {
|
||||||
appId: RTC_INFO.appId,
|
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();
|
||||||
|
|
||||||
/**
|
assert(AppKey, `自动生成 Token 时, ${scene} 场景的 AppKey 不可为空`);
|
||||||
* @brief 生成 RTC Token
|
const key = new TokenManager.AccessToken(AppId, AppKey, RTCConfig.RoomId, RTCConfig.UserId);
|
||||||
* @refer https://www.volcengine.com/docs/6348/70121
|
key.addPrivilege(Privileges.PrivSubscribeStream, 0);
|
||||||
*/
|
key.addPrivilege(Privileges.PrivPublishStream, 0);
|
||||||
await wrapper({
|
key.expireTime(Math.floor(new Date() / 1000) + (24 * 3600));
|
||||||
ctx,
|
RTCConfig.Token = key.serialize();
|
||||||
apiName: 'rtc-token',
|
}
|
||||||
logic: async () => {
|
SceneConfig.id = scene;
|
||||||
const { roomId, userId } = ctx.request.body || {};
|
SceneConfig.botName = VoiceChat?.AgentConfig?.UserId;
|
||||||
assert(RTC_INFO.appId, 'AppID 不能为空, 请修改 /Server/sensitive.js');
|
SceneConfig.isInterruptMode = VoiceChat?.Config?.InterruptMode === 0;
|
||||||
assert(RTC_INFO.appKey, 'AppKey 不能为空, 请修改 /Server/sensitive.js');
|
SceneConfig.isVision = VoiceChat?.Config?.LLMConfig?.VisionConfig?.Enable;
|
||||||
assert(roomId, 'RoomID 不能为空');
|
SceneConfig.isScreenMode = VoiceChat?.Config?.LLMConfig?.VisionConfig?.SnapshoutConfig?.StreamType === 1;
|
||||||
assert(userId, 'UserID 不能为空');
|
delete RTCConfig.AppKey;
|
||||||
const key = new TokenManager.AccessToken(RTC_INFO.appId, RTC_INFO.appKey, roomId, userId);
|
return {
|
||||||
key.addPrivilege(Privileges.PrivSubscribeStream, 0);
|
scene: SceneConfig || {},
|
||||||
key.addPrivilege(Privileges.PrivPublishStream, 0);
|
rtc: RTCConfig,
|
||||||
key.expireTime(Math.floor(new Date() / 1000) + (24 * 3600));
|
};
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
token: key.serialize(),
|
scenes,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -11,12 +11,14 @@
|
|||||||
"koa-bodyparser": "^4.4.1",
|
"koa-bodyparser": "^4.4.1",
|
||||||
"koa2-cors": "^2.0.6",
|
"koa2-cors": "^2.0.6",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"node-fetch": "^2.3.2"
|
"node-fetch": "^2.3.2",
|
||||||
|
"uuid": "^11.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^3.1.10"
|
"nodemon": "^3.1.10"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"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.
|
* Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.
|
||||||
* SPDX-license-identifier: BSD-3-Clause
|
* SPDX-license-identifier: BSD-3-Clause
|
||||||
*/
|
*/
|
||||||
|
const fs = require('fs');
|
||||||
const merge = require('lodash/merge');
|
const path = require('path');
|
||||||
const { LLMConfig, RTC_INFO, TTSConfig, ASRConfig } = require("./sensitive");
|
|
||||||
|
|
||||||
const judgeMethodPath = (method) => {
|
const judgeMethodPath = (method) => {
|
||||||
return (ctx, pathname) => ctx.method.toLowerCase() === method && ctx.url.startsWith(`/${pathname}`);
|
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) => {
|
const assert = (expression, msg) => {
|
||||||
if (!!!expression || expression?.includes?.(' ')) {
|
if (!!!expression || expression?.includes?.(' ')) {
|
||||||
console.log(`\x1b[31m校验失败: ${msg}\x1b[0m`)
|
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 = {
|
module.exports = {
|
||||||
wrapper,
|
wrapper,
|
||||||
assert,
|
assert,
|
||||||
sensitiveInjector,
|
readFiles,
|
||||||
}
|
}
|
||||||
@ -840,6 +840,11 @@ unpipe@1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
||||||
integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==
|
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:
|
uuid@^8.3.2:
|
||||||
version "8.3.2"
|
version "8.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
||||||
|
|||||||
@ -8,13 +8,8 @@
|
|||||||
*/
|
*/
|
||||||
export const BasicAPIs = [
|
export const BasicAPIs = [
|
||||||
{
|
{
|
||||||
action: 'getRtcInfo',
|
action: 'getScenes',
|
||||||
apiPath: '/rtc-info',
|
apiPath: '/getScenes',
|
||||||
method: 'post',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
action: 'generateRtcAccessToken',
|
|
||||||
apiPath: '/rtc-token',
|
|
||||||
method: 'post',
|
method: 'post',
|
||||||
},
|
},
|
||||||
] as const;
|
] 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 { useSelector } from 'react-redux';
|
||||||
import { RootState } from '@/store';
|
import { RootState } from '@/store';
|
||||||
import UserTag from '../UserTag';
|
import UserTag from '../UserTag';
|
||||||
|
import { useDeviceState, useScene } from '@/lib/useCommon';
|
||||||
import style from './index.module.less';
|
import style from './index.module.less';
|
||||||
import { useDeviceState } from '@/lib/useCommon';
|
|
||||||
import { SceneMap } from '@/config';
|
|
||||||
|
|
||||||
interface IAiAvatarCardProps {
|
interface IAiAvatarCardProps {
|
||||||
showStatus: boolean;
|
showStatus: boolean;
|
||||||
@ -21,8 +20,8 @@ const THRESHOLD_VOLUME = 18;
|
|||||||
function AiAvatarCard(props: IAiAvatarCardProps) {
|
function AiAvatarCard(props: IAiAvatarCardProps) {
|
||||||
const { showStatus, showUserTag, className } = props;
|
const { showStatus, showUserTag, className } = props;
|
||||||
const room = useSelector((state: RootState) => state.room);
|
const room = useSelector((state: RootState) => state.room);
|
||||||
|
const { icon } = useScene();
|
||||||
const { scene, isAITalking, isFullScreen } = room;
|
const { scene, isAITalking, isFullScreen } = room;
|
||||||
const avatar = SceneMap[scene]?.icon;
|
|
||||||
const volume = room.localUser.audioPropertiesInfo?.linearVolume || 0;
|
const volume = room.localUser.audioPropertiesInfo?.linearVolume || 0;
|
||||||
const { isAudioPublished } = useDeviceState();
|
const { isAudioPublished } = useDeviceState();
|
||||||
const isLoading = volume >= THRESHOLD_VOLUME && isAudioPublished;
|
const isLoading = volume >= THRESHOLD_VOLUME && isAudioPublished;
|
||||||
@ -30,7 +29,7 @@ function AiAvatarCard(props: IAiAvatarCardProps) {
|
|||||||
return (
|
return (
|
||||||
<div className={`${style.card} ${className} ${isFullScreen ? style.fullScreen : ''}`}>
|
<div className={`${style.card} ${className} ${isFullScreen ? style.fullScreen : ''}`}>
|
||||||
<div className={style.avatar}>
|
<div className={style.avatar}>
|
||||||
<img id="avatar-card" src={avatar} alt="Avatar" />
|
<img id="avatar-card" src={icon} alt="Avatar" />
|
||||||
{showStatus ? (
|
{showStatus ? (
|
||||||
isAITalking ? (
|
isAITalking ? (
|
||||||
<div className={style.aiStatus}>
|
<div className={style.aiStatus}>
|
||||||
|
|||||||
@ -3,50 +3,44 @@
|
|||||||
* SPDX-license-identifier: BSD-3-Clause
|
* SPDX-license-identifier: BSD-3-Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { RootState } from '@/store';
|
import { RootState } from '@/store';
|
||||||
import CheckScene from './CheckScene';
|
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 style from './index.module.less';
|
||||||
import { Scenes, SceneMap } from '@/config';
|
|
||||||
import { useVisionMode } from '@/lib/useCommon';
|
|
||||||
|
|
||||||
function AIChangeCard() {
|
function AIChangeCard() {
|
||||||
const room = useSelector((state: RootState) => state.room);
|
const { scene, sceneConfigMap } = useSelector((state: RootState) => state.room);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [scene, setScene] = useState(room.scene);
|
const { icon, isVision } = useScene();
|
||||||
const { isVisionMode } = useVisionMode();
|
const Scenes = Object.keys(sceneConfigMap).map(key => sceneConfigMap[key]);
|
||||||
const avatar = SceneMap[scene]?.icon;
|
|
||||||
|
|
||||||
const handleChecked = (checkedScene: string) => {
|
const handleChecked = (checkedScene: string) => {
|
||||||
setScene(checkedScene);
|
dispatch(updateScene(checkedScene));
|
||||||
dispatch(updateScene({ scene: checkedScene }));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.card}>
|
<div className={style.card}>
|
||||||
<div className={style.avatar}>
|
<div className={style.avatar}>
|
||||||
<img id="avatar-card" src={avatar} alt="Avatar" />
|
<img id="avatar-card" src={icon} alt="Avatar" />
|
||||||
</div>
|
</div>
|
||||||
<div className={style.title}>
|
<div className={style.title}>
|
||||||
<div>Hi,欢迎体验实时对话式 AI</div>
|
<div>Hi,欢迎体验实时对话式 AI</div>
|
||||||
<div className={style.desc}>
|
<div className={style.desc}>
|
||||||
{isVisionMode ? <>支持豆包 Vision 模型和 深度思考模型,</> : ''}
|
{isVision ? <>支持豆包 Vision 模型和 深度思考模型,</> : ''}
|
||||||
超多对话场景等你开启
|
超多对话场景等你开启
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={style.sceneContainer}>
|
<div className={style.sceneContainer}>
|
||||||
{Scenes.map((key) =>
|
{Scenes.map((key: SceneConfig) =>
|
||||||
key ? (
|
<CheckScene
|
||||||
<CheckScene
|
key={key.name}
|
||||||
key={key.name}
|
icon={key.icon}
|
||||||
icon={key.icon}
|
title={key.name}
|
||||||
title={key.name}
|
checked={key.id === scene}
|
||||||
checked={key.name === scene}
|
onClick={() => handleChecked(key.id)}
|
||||||
onClick={() => handleChecked(key.name)}
|
/>
|
||||||
/>
|
|
||||||
) : null
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</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 { IconArrowDown, IconArrowUp } from '@arco-design/web-react/icon';
|
||||||
import { NetworkQuality } from '@volcengine/rtc';
|
import { NetworkQuality } from '@volcengine/rtc';
|
||||||
import { RootState } from '@/store';
|
import { RootState } from '@/store';
|
||||||
|
import { useScene } from '@/lib/useCommon';
|
||||||
import style from './index.module.less';
|
import style from './index.module.less';
|
||||||
import { Configuration } from '@/config';
|
|
||||||
|
|
||||||
enum INDICATOR_COLORS {
|
enum INDICATOR_COLORS {
|
||||||
GREAT = 'rgba(35, 195, 67, 1)',
|
GREAT = 'rgba(35, 195, 67, 1)',
|
||||||
@ -31,11 +31,12 @@ const INDICATOR_TEXT = {
|
|||||||
|
|
||||||
function NetworkIndicator() {
|
function NetworkIndicator() {
|
||||||
const room = useSelector((state: RootState) => state.room);
|
const room = useSelector((state: RootState) => state.room);
|
||||||
|
const { botName } = useScene();
|
||||||
const networkQuality = room.networkQuality;
|
const networkQuality = room.networkQuality;
|
||||||
const delay = room.localUser.audioStats?.rtt;
|
const delay = room.localUser.audioStats?.rtt;
|
||||||
const audioLossRateUpper = room.localUser.audioStats?.audioLossRate || 0;
|
const audioLossRateUpper = room.localUser.audioStats?.audioLossRate || 0;
|
||||||
const audioLossRateLower =
|
const audioLossRateLower =
|
||||||
room.remoteUsers.find((user) => user.userId === Configuration.BotName)?.audioStats
|
room.remoteUsers.find((user) => user.userId === botName)?.audioStats
|
||||||
?.audioLossRate || 0;
|
?.audioLossRate || 0;
|
||||||
|
|
||||||
const indicators = useMemo(() => {
|
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
|
* 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 Disclaimer = 'https://www.volcengine.com/docs/6348/68916';
|
||||||
export const ReversoContext = 'https://www.volcengine.com/docs/6348/68918';
|
export const ReversoContext = 'https://www.volcengine.com/docs/6348/68918';
|
||||||
export const UserAgreement = 'https://www.volcengine.com/docs/6348/128955';
|
export const UserAgreement = 'https://www.volcengine.com/docs/6348/128955';
|
||||||
@ -27,9 +22,3 @@ export interface IScene {
|
|||||||
asrConfig: Record<string, any>;
|
asrConfig: Record<string, any>;
|
||||||
ttsConfig: 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 RTCAIAnsExtension from '@volcengine/rtc/extension-ainr';
|
||||||
import { Message } from '@arco-design/web-react';
|
import { Message } from '@arco-design/web-react';
|
||||||
import Apis from '@/app/index';
|
import Apis from '@/app/index';
|
||||||
import { Configuration, SceneMap } from '@/config';
|
|
||||||
import { string2tlv } from '@/utils/utils';
|
import { string2tlv } from '@/utils/utils';
|
||||||
import { COMMAND, INTERRUPT_PRIORITY } from '@/utils/handler';
|
import { COMMAND, INTERRUPT_PRIORITY } from '@/utils/handler';
|
||||||
|
|
||||||
@ -56,16 +55,11 @@ export interface IEventListener {
|
|||||||
) => void;
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EngineOptions {
|
|
||||||
appId: string;
|
|
||||||
uid: string;
|
|
||||||
roomId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BasicBody {
|
export interface BasicBody {
|
||||||
app_id: string;
|
app_id: string;
|
||||||
room_id: string;
|
room_id: string;
|
||||||
user_id: string;
|
user_id: string;
|
||||||
|
token?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -85,13 +79,7 @@ export class RTCClient {
|
|||||||
|
|
||||||
audioBotStartTime = 0;
|
audioBotStartTime = 0;
|
||||||
|
|
||||||
createEngine = async (props: EngineOptions) => {
|
createEngine = async () => {
|
||||||
this.basicInfo = {
|
|
||||||
app_id: props.appId,
|
|
||||||
room_id: props.roomId,
|
|
||||||
user_id: props.uid,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.engine = VERTC.createEngine(this.basicInfo.app_id);
|
this.engine = VERTC.createEngine(this.basicInfo.app_id);
|
||||||
try {
|
try {
|
||||||
const AIAnsExtension = new RTCAIAnsExtension();
|
const AIAnsExtension = new RTCAIAnsExtension();
|
||||||
@ -138,16 +126,17 @@ export class RTCClient {
|
|||||||
this.engine.on(VERTC.events.onNetworkQuality, handleNetworkQuality);
|
this.engine.on(VERTC.events.onNetworkQuality, handleNetworkQuality);
|
||||||
};
|
};
|
||||||
|
|
||||||
joinRoom = (token: string | null, username: string): Promise<void> => {
|
joinRoom = () => {
|
||||||
this.engine.enableAudioPropertiesReport({ interval: 1000 });
|
this.engine.enableAudioPropertiesReport({ interval: 1000 });
|
||||||
return this.engine.joinRoom(
|
console.log(this.basicInfo);
|
||||||
token,
|
this.engine.joinRoom(
|
||||||
|
this.basicInfo.token!,
|
||||||
`${this.basicInfo.room_id!}`,
|
`${this.basicInfo.room_id!}`,
|
||||||
{
|
{
|
||||||
userId: this.basicInfo.user_id!,
|
userId: this.basicInfo.user_id!,
|
||||||
extraInfo: JSON.stringify({
|
extraInfo: JSON.stringify({
|
||||||
call_scene: 'RTC-AIGC',
|
call_scene: 'RTC-AIGC',
|
||||||
user_name: username,
|
user_name: this.basicInfo.user_id,
|
||||||
user_id: this.basicInfo.user_id,
|
user_id: this.basicInfo.user_id,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
@ -157,12 +146,12 @@ export class RTCClient {
|
|||||||
roomProfileType: RoomProfileType.chat,
|
roomProfileType: RoomProfileType.chat,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
console.log(' ------ userJoinRoom\n', `roomId: ${this.basicInfo.room_id}\n`, `uid: ${this.basicInfo.user_id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
leaveRoom = () => {
|
leaveRoom = () => {
|
||||||
this.stopAgent();
|
|
||||||
this.audioBotEnabled = false;
|
this.audioBotEnabled = false;
|
||||||
this.engine.leaveRoom();
|
this.engine.leaveRoom().catch();
|
||||||
VERTC.destroyEngine(this.engine);
|
VERTC.destroyEngine(this.engine);
|
||||||
this._audioCaptureDevice = undefined;
|
this._audioCaptureDevice = undefined;
|
||||||
};
|
};
|
||||||
@ -360,27 +349,12 @@ export class RTCClient {
|
|||||||
* @brief 启用 AIGC
|
* @brief 启用 AIGC
|
||||||
*/
|
*/
|
||||||
startAgent = async (scene: string) => {
|
startAgent = async (scene: string) => {
|
||||||
const roomId = this.basicInfo.room_id;
|
|
||||||
const userId = this.basicInfo.user_id;
|
|
||||||
if (this.audioBotEnabled) {
|
if (this.audioBotEnabled) {
|
||||||
await this.stopAgent();
|
await this.stopAgent(scene);
|
||||||
}
|
}
|
||||||
const params = SceneMap[scene];
|
await Apis.VoiceChat.StartVoiceChat({
|
||||||
|
SceneID: 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);
|
|
||||||
this.audioBotEnabled = true;
|
this.audioBotEnabled = true;
|
||||||
this.audioBotStartTime = Date.now();
|
this.audioBotStartTime = Date.now();
|
||||||
};
|
};
|
||||||
@ -388,14 +362,10 @@ export class RTCClient {
|
|||||||
/**
|
/**
|
||||||
* @brief 关闭 AIGC
|
* @brief 关闭 AIGC
|
||||||
*/
|
*/
|
||||||
stopAgent = async () => {
|
stopAgent = async (scene: string) => {
|
||||||
const roomId = this.basicInfo.room_id;
|
|
||||||
const userId = this.basicInfo.user_id;
|
|
||||||
if (this.audioBotEnabled || sessionStorage.getItem('audioBotEnabled')) {
|
if (this.audioBotEnabled || sessionStorage.getItem('audioBotEnabled')) {
|
||||||
await Apis.VoiceChat.StopVoiceChat({
|
await Apis.VoiceChat.StopVoiceChat({
|
||||||
AppId: this.basicInfo.app_id,
|
SceneID: scene,
|
||||||
RoomId: roomId,
|
|
||||||
TaskId: userId,
|
|
||||||
});
|
});
|
||||||
this.audioBotStartTime = 0;
|
this.audioBotStartTime = 0;
|
||||||
sessionStorage.removeItem('audioBotEnabled');
|
sessionStorage.removeItem('audioBotEnabled');
|
||||||
@ -406,10 +376,20 @@ export class RTCClient {
|
|||||||
/**
|
/**
|
||||||
* @brief 命令 AIGC
|
* @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) {
|
if (this.audioBotEnabled) {
|
||||||
this.engine.sendUserBinaryMessage(
|
this.engine.sendUserBinaryMessage(
|
||||||
Configuration.BotName,
|
agentName,
|
||||||
string2tlv(
|
string2tlv(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
Command: command,
|
Command: command,
|
||||||
@ -429,7 +409,7 @@ export class RTCClient {
|
|||||||
*/
|
*/
|
||||||
updateAgent = async (scene: string) => {
|
updateAgent = async (scene: string) => {
|
||||||
if (this.audioBotEnabled) {
|
if (this.audioBotEnabled) {
|
||||||
await this.stopAgent();
|
await this.stopAgent(scene);
|
||||||
await this.startAgent(scene);
|
await this.startAgent(scene);
|
||||||
} else {
|
} else {
|
||||||
await this.startAgent(scene);
|
await this.startAgent(scene);
|
||||||
|
|||||||
@ -19,7 +19,6 @@ import {
|
|||||||
|
|
||||||
import useRtcListeners from '@/lib/listenerHooks';
|
import useRtcListeners from '@/lib/listenerHooks';
|
||||||
import { RootState } from '@/store';
|
import { RootState } from '@/store';
|
||||||
import Apis from '@/app/index';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
updateMediaInputs,
|
updateMediaInputs,
|
||||||
@ -27,7 +26,6 @@ import {
|
|||||||
setDevicePermissions,
|
setDevicePermissions,
|
||||||
} from '@/store/slices/device';
|
} from '@/store/slices/device';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
import { Configuration, SceneMap } from '@/config';
|
|
||||||
|
|
||||||
export const ABORT_VISIBILITY_CHANGE = 'abortVisibilityChange';
|
export const ABORT_VISIBILITY_CHANGE = 'abortVisibilityChange';
|
||||||
export interface FormProps {
|
export interface FormProps {
|
||||||
@ -36,13 +34,15 @@ export interface FormProps {
|
|||||||
publishAudio: boolean;
|
publishAudio: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useVisionMode = () => {
|
export const useScene = () => {
|
||||||
const scene = useSelector((state: RootState) => state.room.scene);
|
const { scene, sceneConfigMap } = useSelector((state: RootState) => state.room);
|
||||||
return {
|
return sceneConfigMap[scene] || {};
|
||||||
isVisionMode: SceneMap?.[scene]?.llmConfig?.VisionConfig?.Enable,
|
}
|
||||||
isScreenMode: SceneMap?.[scene]?.llmConfig?.VisionConfig?.SnapshotConfig?.StreamType === 1,
|
|
||||||
};
|
export const useRTC = () => {
|
||||||
};
|
const { scene, rtcConfigMap } = useSelector((state: RootState) => state.room);
|
||||||
|
return rtcConfigMap[scene] || {};
|
||||||
|
}
|
||||||
|
|
||||||
export const useDeviceState = () => {
|
export const useDeviceState = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@ -164,25 +164,25 @@ export const useGetDevicePermission = () => {
|
|||||||
|
|
||||||
export const useJoin = (): [
|
export const useJoin = (): [
|
||||||
boolean,
|
boolean,
|
||||||
(formValues: FormProps, fromRefresh: boolean) => Promise<void | boolean>
|
() => Promise<void | boolean>
|
||||||
] => {
|
] => {
|
||||||
const devicePermissions = useSelector((state: RootState) => state.device.devicePermissions);
|
const devicePermissions = useSelector((state: RootState) => state.device.devicePermissions);
|
||||||
const room = useSelector((state: RootState) => state.room);
|
const room = useSelector((state: RootState) => state.room);
|
||||||
const scene = room.scene;
|
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const { id } = useScene();
|
||||||
const { switchMic } = useDeviceState();
|
const { switchMic } = useDeviceState();
|
||||||
const [joining, setJoining] = useState(false);
|
const [joining, setJoining] = useState(false);
|
||||||
const listeners = useRtcListeners();
|
const listeners = useRtcListeners();
|
||||||
|
|
||||||
const handleAIGCModeStart = async () => {
|
const handleAIGCModeStart = async () => {
|
||||||
if (room.isAIGCEnable) {
|
if (room.isAIGCEnable) {
|
||||||
await RtcClient.stopAgent();
|
await RtcClient.stopAgent(id);
|
||||||
dispatch(clearCurrentMsg());
|
dispatch(clearCurrentMsg());
|
||||||
await RtcClient.startAgent(scene);
|
await RtcClient.startAgent(id);
|
||||||
} else {
|
} else {
|
||||||
await RtcClient.startAgent(scene);
|
await RtcClient.startAgent(id);
|
||||||
}
|
}
|
||||||
dispatch(updateAIGCState({ isAIGCEnable: true }));
|
dispatch(updateAIGCState({ isAIGCEnable: true }));
|
||||||
};
|
};
|
||||||
@ -201,37 +201,16 @@ export const useJoin = (): [
|
|||||||
return;
|
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);
|
setJoining(true);
|
||||||
|
|
||||||
/** 1. Create RTC Engine */
|
/** 1. Create RTC Engine */
|
||||||
const engineParams = {
|
await RtcClient.createEngine();
|
||||||
appId,
|
|
||||||
roomId: RoomId,
|
|
||||||
uid: UserId,
|
|
||||||
};
|
|
||||||
await RtcClient.createEngine(engineParams);
|
|
||||||
|
|
||||||
/** 2.1 Set events callbacks */
|
/** 2.1 Set events callbacks */
|
||||||
RtcClient.addEventListeners(listeners);
|
RtcClient.addEventListeners(listeners);
|
||||||
|
|
||||||
/** 2.2 RTC starting to join room */
|
/** 2.2 RTC starting to join room */
|
||||||
await RtcClient.joinRoom(token!, UserId);
|
await RtcClient.joinRoom();
|
||||||
console.log(' ------ userJoinRoom\n', `roomId: ${RoomId}\n`, `uid: ${UserId}`);
|
|
||||||
/** 3. Set users' devices info */
|
/** 3. Set users' devices info */
|
||||||
const mediaDevices = await RtcClient.getDevices({
|
const mediaDevices = await RtcClient.getDevices({
|
||||||
audio: true,
|
audio: true,
|
||||||
@ -240,10 +219,10 @@ export const useJoin = (): [
|
|||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
localJoinRoom({
|
localJoinRoom({
|
||||||
roomId: RoomId,
|
roomId: RtcClient.basicInfo.room_id,
|
||||||
user: {
|
user: {
|
||||||
username: UserId,
|
username: RtcClient.basicInfo.user_id,
|
||||||
userId: UserId,
|
userId: RtcClient.basicInfo.user_id,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -273,6 +252,7 @@ export const useJoin = (): [
|
|||||||
|
|
||||||
export const useLeave = () => {
|
export const useLeave = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const { id } = useScene();
|
||||||
|
|
||||||
return async function () {
|
return async function () {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@ -280,10 +260,11 @@ export const useLeave = () => {
|
|||||||
RtcClient.stopScreenCapture,
|
RtcClient.stopScreenCapture,
|
||||||
RtcClient.stopVideoCapture,
|
RtcClient.stopVideoCapture,
|
||||||
]);
|
]);
|
||||||
|
await RtcClient.stopAgent(id);
|
||||||
await RtcClient.leaveRoom();
|
await RtcClient.leaveRoom();
|
||||||
dispatch(clearHistoryMsg());
|
dispatch(clearHistoryMsg());
|
||||||
dispatch(clearCurrentMsg());
|
dispatch(clearCurrentMsg());
|
||||||
dispatch(localLeaveRoom());
|
dispatch(localLeaveRoom());
|
||||||
dispatch(updateAIGCState({ isAIGCEnable: false }));
|
dispatch(updateAIGCState({ isAIGCEnable: false }));
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -5,30 +5,20 @@
|
|||||||
|
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { isMobile } from '@/utils/utils';
|
import { isMobile } from '@/utils/utils';
|
||||||
import { Configuration } from '@/config';
|
|
||||||
import InvokeButton from '@/pages/MainPage/MainArea/Antechamber/InvokeButton';
|
import InvokeButton from '@/pages/MainPage/MainArea/Antechamber/InvokeButton';
|
||||||
import { useJoin, useVisionMode } from '@/lib/useCommon';
|
import { useJoin, useScene } from '@/lib/useCommon';
|
||||||
import style from './index.module.less';
|
|
||||||
import AIChangeCard from '@/components/AiChangeCard';
|
import AIChangeCard from '@/components/AiChangeCard';
|
||||||
import { updateFullScreen } from '@/store/slices/room';
|
import { updateFullScreen } from '@/store/slices/room';
|
||||||
|
import style from './index.module.less';
|
||||||
|
|
||||||
function Antechamber() {
|
function Antechamber() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [joining, dispatchJoin] = useJoin();
|
const [joining, dispatchJoin] = useJoin();
|
||||||
const username = Configuration.UserId;
|
const { isScreenMode } = useScene();
|
||||||
const roomId = Configuration.RoomId;
|
|
||||||
const { isScreenMode } = useVisionMode();
|
|
||||||
const handleJoinRoom = () => {
|
const handleJoinRoom = () => {
|
||||||
dispatch(updateFullScreen({ isFullScreen: !isMobile() && !isScreenMode })); // 初始化
|
dispatch(updateFullScreen({ isFullScreen: !isMobile() && !isScreenMode })); // 初始化
|
||||||
if (!joining) {
|
if (!joining) {
|
||||||
dispatchJoin(
|
dispatchJoin();
|
||||||
{
|
|
||||||
username,
|
|
||||||
roomId,
|
|
||||||
publishAudio: true,
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -8,16 +8,16 @@ import AudioLoading from '@/components/Loading/AudioLoading';
|
|||||||
import { RootState } from '@/store';
|
import { RootState } from '@/store';
|
||||||
import RtcClient from '@/lib/RtcClient';
|
import RtcClient from '@/lib/RtcClient';
|
||||||
import { setInterruptMsg } from '@/store/slices/room';
|
import { setInterruptMsg } from '@/store/slices/room';
|
||||||
import { useDeviceState } from '@/lib/useCommon';
|
import { useDeviceState, useScene } from '@/lib/useCommon';
|
||||||
import { COMMAND } from '@/utils/handler';
|
import { COMMAND } from '@/utils/handler';
|
||||||
import style from './index.module.less';
|
import style from './index.module.less';
|
||||||
import { Configuration } from '@/config';
|
|
||||||
|
|
||||||
const THRESHOLD_VOLUME = 18;
|
const THRESHOLD_VOLUME = 18;
|
||||||
|
|
||||||
function AudioController(props: React.HTMLAttributes<HTMLDivElement>) {
|
function AudioController(props: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
const { className, ...rest } = props;
|
const { className, ...rest } = props;
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const { isInterruptMode, botName } = useScene();
|
||||||
const room = useSelector((state: RootState) => state.room);
|
const room = useSelector((state: RootState) => state.room);
|
||||||
const volume = room.localUser.audioPropertiesInfo?.linearVolume || 0;
|
const volume = room.localUser.audioPropertiesInfo?.linearVolume || 0;
|
||||||
const { isAudioPublished } = useDeviceState();
|
const { isAudioPublished } = useDeviceState();
|
||||||
@ -26,7 +26,10 @@ function AudioController(props: React.HTMLAttributes<HTMLDivElement>) {
|
|||||||
const isLoading = volume >= THRESHOLD_VOLUME && isAudioPublished;
|
const isLoading = volume >= THRESHOLD_VOLUME && isAudioPublished;
|
||||||
|
|
||||||
const handleInterrupt = () => {
|
const handleInterrupt = () => {
|
||||||
RtcClient.commandAgent(COMMAND.INTERRUPT);
|
RtcClient.commandAgent({
|
||||||
|
agentName: botName,
|
||||||
|
command: COMMAND.INTERRUPT,
|
||||||
|
});
|
||||||
dispatch(setInterruptMsg());
|
dispatch(setInterruptMsg());
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
@ -34,7 +37,7 @@ function AudioController(props: React.HTMLAttributes<HTMLDivElement>) {
|
|||||||
{isAudioPublished ? (
|
{isAudioPublished ? (
|
||||||
isAIReady && isAITalking ? (
|
isAIReady && isAITalking ? (
|
||||||
<div className={style.interruptContainer}>
|
<div className={style.interruptContainer}>
|
||||||
{Configuration.InterruptMode ? <div>语音打断 或 </div> : null}
|
{isInterruptMode ? <div>语音打断 或 </div> : null}
|
||||||
<div onClick={handleInterrupt} className={style.interrupt}>
|
<div onClick={handleInterrupt} className={style.interrupt}>
|
||||||
<div className={style.interruptIcon} />
|
<div className={style.interruptIcon} />
|
||||||
<span>点此打断</span>
|
<span>点此打断</span>
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { useSelector } from 'react-redux';
|
|||||||
import { VideoRenderMode } from '@volcengine/rtc';
|
import { VideoRenderMode } from '@volcengine/rtc';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { RootState } from '@/store';
|
import { RootState } from '@/store';
|
||||||
import { useDeviceState, useVisionMode } from '@/lib/useCommon';
|
import { useDeviceState, useScene } from '@/lib/useCommon';
|
||||||
import RtcClient from '@/lib/RtcClient';
|
import RtcClient from '@/lib/RtcClient';
|
||||||
|
|
||||||
import styles from './index.module.less';
|
import styles from './index.module.less';
|
||||||
@ -25,7 +25,7 @@ function CameraArea(props: React.HTMLAttributes<HTMLDivElement>) {
|
|||||||
const { className, ...rest } = props;
|
const { className, ...rest } = props;
|
||||||
const room = useSelector((state: RootState) => state.room);
|
const room = useSelector((state: RootState) => state.room);
|
||||||
const { isFullScreen, scene } = room;
|
const { isFullScreen, scene } = room;
|
||||||
const { isVisionMode, isScreenMode } = useVisionMode();
|
const { isVision, isScreenMode } = useScene();
|
||||||
const { isVideoPublished, isScreenPublished, switchCamera, switchScreenCapture } =
|
const { isVideoPublished, isScreenPublished, switchCamera, switchScreenCapture } =
|
||||||
useDeviceState();
|
useDeviceState();
|
||||||
|
|
||||||
@ -51,7 +51,7 @@ function CameraArea(props: React.HTMLAttributes<HTMLDivElement>) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setVideoPlayer();
|
setVideoPlayer();
|
||||||
}, [isVideoPublished, isScreenPublished, isScreenMode, isFullScreen, isVisionMode]);
|
}, [isVideoPublished, isScreenPublished, isScreenMode, isFullScreen, isVision]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${styles['camera-wrapper']} ${className}`} {...rest}>
|
<div className={`${styles['camera-wrapper']} ${className}`} {...rest}>
|
||||||
@ -78,7 +78,7 @@ function CameraArea(props: React.HTMLAttributes<HTMLDivElement>) {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={isScreenMode ? ScreenCloseNoteSVG : isVisionMode ? CameraCloseNoteSVG : UserAvatar}
|
src={isScreenMode ? ScreenCloseNoteSVG : isVision ? CameraCloseNoteSVG : UserAvatar}
|
||||||
alt="close"
|
alt="close"
|
||||||
className={styles['camera-placeholder-close-note']}
|
className={styles['camera-placeholder-close-note']}
|
||||||
/>
|
/>
|
||||||
@ -93,7 +93,7 @@ function CameraArea(props: React.HTMLAttributes<HTMLDivElement>) {
|
|||||||
</span>
|
</span>
|
||||||
<div>体验豆包视觉理解模型</div>
|
<div>体验豆包视觉理解模型</div>
|
||||||
</>
|
</>
|
||||||
) : isVisionMode ? (
|
) : isVision ? (
|
||||||
<>
|
<>
|
||||||
打开
|
打开
|
||||||
<span onClick={handleOperateCamera} className={styles['camera-open-btn']}>
|
<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 { Tag, Spin } from '@arco-design/web-react';
|
||||||
import { RootState } from '@/store';
|
import { RootState } from '@/store';
|
||||||
import Loading from '@/components/Loading/HorizonLoading';
|
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 USER_AVATAR from '@/assets/img/userAvatar.png';
|
||||||
import styles from './index.module.less';
|
import styles from './index.module.less';
|
||||||
import { isMobile } from '@/utils/utils';
|
|
||||||
|
|
||||||
const lines: (string | React.ReactNode)[] = [];
|
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 { isAITalking, isUserTalking, scene } = useSelector((state: RootState) => state.room);
|
||||||
const isAIReady = msgHistory.length > 0;
|
const isAIReady = msgHistory.length > 0;
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const { botName, icon } = useScene();
|
||||||
|
|
||||||
const isUserTextLoading = (owner: string) => {
|
const isUserTextLoading = (owner: string) => {
|
||||||
return owner === userId && isUserTalking;
|
return owner === userId && isUserTalking;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isAITextLoading = (owner: string) => {
|
const isAITextLoading = (owner: string) => {
|
||||||
return owner === Configuration.BotName && isAITalking;
|
return owner === botName && isAITalking;
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -58,7 +59,7 @@ function Conversation(props: React.HTMLAttributes<HTMLDivElement>) {
|
|||||||
)}
|
)}
|
||||||
{msgHistory?.map(({ value, user, isInterrupted }, index) => {
|
{msgHistory?.map(({ value, user, isInterrupted }, index) => {
|
||||||
const isUserMsg = user === userId;
|
const isUserMsg = user === userId;
|
||||||
const isRobotMsg = user === Configuration.BotName;
|
const isRobotMsg = user === botName;
|
||||||
if (!isUserMsg && !isRobotMsg) {
|
if (!isUserMsg && !isRobotMsg) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@ -71,7 +72,7 @@ function Conversation(props: React.HTMLAttributes<HTMLDivElement>) {
|
|||||||
{!isMobile() && (
|
{!isMobile() && (
|
||||||
<div className={styles.msgName}>
|
<div className={styles.msgName}>
|
||||||
<div className={styles.avatar}>
|
<div className={styles.avatar}>
|
||||||
<img src={isUserMsg ? USER_AVATAR : SceneMap[scene].icon} alt="Avatar" />
|
<img src={isUserMsg ? USER_AVATAR : icon} alt="Avatar" />
|
||||||
</div>
|
</div>
|
||||||
{isUserMsg ? '我' : scene}
|
{isUserMsg ? '我' : scene}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { memo, useState } from 'react';
|
import { memo, useState } from 'react';
|
||||||
import { Drawer } from '@arco-design/web-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 { isMobile } from '@/utils/utils';
|
||||||
import Menu from '../../Menu';
|
import Menu from '../../Menu';
|
||||||
|
|
||||||
@ -21,7 +21,7 @@ import ScreenOffSVG from '@/assets/img/ScreenOff.svg';
|
|||||||
function ToolBar(props: React.HTMLAttributes<HTMLDivElement>) {
|
function ToolBar(props: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
const { className, ...rest } = props;
|
const { className, ...rest } = props;
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const { isScreenMode } = useVisionMode();
|
const { isVision, isScreenMode } = useScene();
|
||||||
const leaveRoom = useLeave();
|
const leaveRoom = useLeave();
|
||||||
const {
|
const {
|
||||||
isAudioPublished,
|
isAudioPublished,
|
||||||
@ -31,7 +31,6 @@ function ToolBar(props: React.HTMLAttributes<HTMLDivElement>) {
|
|||||||
switchCamera,
|
switchCamera,
|
||||||
switchScreenCapture,
|
switchScreenCapture,
|
||||||
} = useDeviceState();
|
} = useDeviceState();
|
||||||
const { isVisionMode } = useVisionMode();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${className} ${style.btns} ${isMobile() ? style.column : ''}`} {...rest}>
|
<div className={`${className} ${style.btns} ${isMobile() ? style.column : ''}`} {...rest}>
|
||||||
@ -41,7 +40,7 @@ function ToolBar(props: React.HTMLAttributes<HTMLDivElement>) {
|
|||||||
className={style.btn}
|
className={style.btn}
|
||||||
alt="mic"
|
alt="mic"
|
||||||
/>
|
/>
|
||||||
{!isVisionMode ? null : isScreenMode && !isMobile() ? (
|
{!isVision ? null : isScreenMode && !isMobile() ? (
|
||||||
<img
|
<img
|
||||||
src={isScreenPublished ? 'new-screen-off.svg' : 'new-screen-on.svg'}
|
src={isScreenPublished ? 'new-screen-off.svg' : 'new-screen-on.svg'}
|
||||||
onClick={() => switchScreenCapture()}
|
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 DrawerRowItem from '@/components/DrawerRowItem';
|
||||||
import { RootState } from '@/store';
|
import { RootState } from '@/store';
|
||||||
import RtcClient from '@/lib/RtcClient';
|
import RtcClient from '@/lib/RtcClient';
|
||||||
import { useDeviceState, useVisionMode } from '@/lib/useCommon';
|
import { useDeviceState, useScene } from '@/lib/useCommon';
|
||||||
import { updateSelectedDevice } from '@/store/slices/device';
|
import { updateSelectedDevice } from '@/store/slices/device';
|
||||||
import { isMobile } from '@/utils/utils';
|
import { isMobile } from '@/utils/utils';
|
||||||
import styles from './index.module.less';
|
import styles from './index.module.less';
|
||||||
@ -34,7 +34,7 @@ function DeviceDrawerButton(props: IDeviceDrawerButtonProps) {
|
|||||||
const selectedDevice =
|
const selectedDevice =
|
||||||
type === MediaType.AUDIO ? devices.selectedMicrophone : devices.selectedCamera;
|
type === MediaType.AUDIO ? devices.selectedMicrophone : devices.selectedCamera;
|
||||||
const permission = devicePermissions?.[type === MediaType.AUDIO ? 'audio' : 'video'];
|
const permission = devicePermissions?.[type === MediaType.AUDIO ? 'audio' : 'video'];
|
||||||
const { isScreenMode } = useVisionMode();
|
const { isScreenMode } = useScene();
|
||||||
const isScreenEnable = device.isScreenPublished;
|
const isScreenEnable = device.isScreenPublished;
|
||||||
const changeScreenPublished = device.switchScreenCapture;
|
const changeScreenPublished = device.switchScreenCapture;
|
||||||
|
|
||||||
|
|||||||
@ -6,15 +6,15 @@
|
|||||||
import { MediaType } from '@volcengine/rtc';
|
import { MediaType } from '@volcengine/rtc';
|
||||||
import DeviceDrawerButton from '../DeviceDrawerButton';
|
import DeviceDrawerButton from '../DeviceDrawerButton';
|
||||||
import Subtitle from '../Subtitle';
|
import Subtitle from '../Subtitle';
|
||||||
import { useVisionMode } from '@/lib/useCommon';
|
import { useScene } from '@/lib/useCommon';
|
||||||
import styles from './index.module.less';
|
import styles from './index.module.less';
|
||||||
|
|
||||||
function Operation() {
|
function Operation() {
|
||||||
const { isVisionMode } = useVisionMode();
|
const { isVision } = useScene();
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.box} ${styles.device}`}>
|
<div className={`${styles.box} ${styles.device}`}>
|
||||||
<Subtitle />
|
<Subtitle />
|
||||||
{isVisionMode && <DeviceDrawerButton type={MediaType.VIDEO} />}
|
{isVision && <DeviceDrawerButton type={MediaType.VIDEO} />}
|
||||||
<DeviceDrawerButton />
|
<DeviceDrawerButton />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -6,39 +6,29 @@
|
|||||||
import VERTC from '@volcengine/rtc';
|
import VERTC from '@volcengine/rtc';
|
||||||
import { Tooltip, Typography } from '@arco-design/web-react';
|
import { Tooltip, Typography } from '@arco-design/web-react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { useVisionMode } from '@/lib/useCommon';
|
|
||||||
import { RootState } from '@/store';
|
import { RootState } from '@/store';
|
||||||
import Operation from './components/Operation';
|
import Operation from './components/Operation';
|
||||||
import CameraArea from '../MainArea/Room/CameraArea';
|
import CameraArea from '../MainArea/Room/CameraArea';
|
||||||
import { isMobile } from '@/utils/utils';
|
import { isMobile } from '@/utils/utils';
|
||||||
import { SceneMap } from '@/config';
|
import { useScene } from '@/lib/useCommon';
|
||||||
import packageJson from '../../../../package.json';
|
import packageJson from '../../../../package.json';
|
||||||
import styles from './index.module.less';
|
import styles from './index.module.less';
|
||||||
import AISettingButton from './components/AISettingButton';
|
|
||||||
|
|
||||||
function Menu() {
|
function Menu() {
|
||||||
const room = useSelector((state: RootState) => state.room);
|
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 isJoined = room?.isJoined;
|
||||||
const { isVisionMode } = useVisionMode();
|
const { isVision, name } = useScene();
|
||||||
const requestId = sessionStorage.getItem('RequestID');
|
const requestId = sessionStorage.getItem('RequestID');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
{isJoined && isMobile() && isVisionMode ? (
|
{isJoined && isMobile() && isVision ? (
|
||||||
<div className={styles['mobile-camera-wrapper']}>
|
<div className={styles['mobile-camera-wrapper']}>
|
||||||
<CameraArea className={styles['mobile-camera']} />
|
<CameraArea className={styles['mobile-camera']} />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className={`${styles.box} ${styles.info}`}>
|
<div className={`${styles.box} ${styles.info}`}>
|
||||||
<div className={styles.title}>AI 人设:{scene}</div>
|
<div className={styles.title}>AI 人设:{name}</div>
|
||||||
<div>
|
|
||||||
<div className={styles.desc}>音色 {voice}</div>
|
|
||||||
<div className={styles.desc}>{model}</div>
|
|
||||||
</div>
|
|
||||||
{isJoined && <AISettingButton />}
|
|
||||||
</div>
|
</div>
|
||||||
{isJoined ? <Operation /> : ''}
|
{isJoined ? <Operation /> : ''}
|
||||||
<div className={`${styles.box} ${styles.info}`}>
|
<div className={`${styles.box} ${styles.info}`}>
|
||||||
|
|||||||
@ -4,18 +4,46 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
import Header from '@/components/Header';
|
import Header from '@/components/Header';
|
||||||
import ResizeWrapper from '@/components/ResizeWrapper';
|
import ResizeWrapper from '@/components/ResizeWrapper';
|
||||||
import Menu from './Menu';
|
import Menu from './Menu';
|
||||||
import { useIsMobile } from '@/utils/utils';
|
import { useIsMobile } from '@/utils/utils';
|
||||||
|
import Apis from '@/app/index';
|
||||||
import MainArea from './MainArea';
|
import MainArea from './MainArea';
|
||||||
import { ABORT_VISIBILITY_CHANGE, useLeave } from '@/lib/useCommon';
|
import { ABORT_VISIBILITY_CHANGE, useLeave } from '@/lib/useCommon';
|
||||||
|
import { RTCConfig, SceneConfig, updateRTCConfig, updateScene, updateSceneConfig } from '@/store/slices/room';
|
||||||
import styles from './index.module.less';
|
import styles from './index.module.less';
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
|
|
||||||
const leaveRoom = useLeave();
|
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(() => {
|
useEffect(() => {
|
||||||
|
getScenes();
|
||||||
const isOriginalDemo = window.location.host.startsWith('localhost');
|
const isOriginalDemo = window.location.host.startsWith('localhost');
|
||||||
const handler = () => {
|
const handler = () => {
|
||||||
if (
|
if (
|
||||||
|
|||||||
@ -6,34 +6,25 @@
|
|||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { memo, useEffect, useState } from 'react';
|
import { memo, useEffect, useState } from 'react';
|
||||||
import { VideoRenderMode } from '@volcengine/rtc';
|
import { VideoRenderMode } from '@volcengine/rtc';
|
||||||
import { IconRight } from '@arco-design/web-react/icon';
|
import { useDeviceState, useScene } from '@/lib/useCommon';
|
||||||
import { useDeviceState, useVisionMode } from '@/lib/useCommon';
|
|
||||||
import { RootState } from '@/store';
|
import { RootState } from '@/store';
|
||||||
import RtcClient from '@/lib/RtcClient';
|
import RtcClient from '@/lib/RtcClient';
|
||||||
|
|
||||||
import { updateShowSubtitle } from '@/store/slices/room';
|
import { updateShowSubtitle } from '@/store/slices/room';
|
||||||
import styles from './index.module.less';
|
|
||||||
import SettingsDrawer from '../SettingsDrawer';
|
import SettingsDrawer from '../SettingsDrawer';
|
||||||
import AISettings from '@/components/AISettings';
|
import styles from './index.module.less';
|
||||||
|
|
||||||
function MobileToolBar(props: React.HTMLAttributes<HTMLDivElement>) {
|
function MobileToolBar(props: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const { isScreenMode } = useVisionMode();
|
|
||||||
const room = useSelector((state: RootState) => state.room);
|
const room = useSelector((state: RootState) => state.room);
|
||||||
const { isShowSubtitle } = room;
|
const { isShowSubtitle } = room;
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [openAISettings, setOpenAISettings] = useState(false);
|
|
||||||
const [subTitleStatus, setSubTitleStatus] = useState(isShowSubtitle);
|
const [subTitleStatus, setSubTitleStatus] = useState(isShowSubtitle);
|
||||||
|
|
||||||
|
const { isScreenMode } = useScene();
|
||||||
const { isVideoPublished, isScreenPublished } = useDeviceState();
|
const { isVideoPublished, isScreenPublished } = useDeviceState();
|
||||||
|
|
||||||
const handleSetting = () => {
|
|
||||||
setOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOpenDrawer = () => setOpenAISettings(true);
|
|
||||||
|
|
||||||
const switchSubtitle = () => {
|
const switchSubtitle = () => {
|
||||||
setSubTitleStatus(!subTitleStatus);
|
setSubTitleStatus(!subTitleStatus);
|
||||||
dispatch(updateShowSubtitle({ isShowSubtitle: !subTitleStatus }));
|
dispatch(updateShowSubtitle({ isShowSubtitle: !subTitleStatus }));
|
||||||
@ -56,19 +47,6 @@ function MobileToolBar(props: React.HTMLAttributes<HTMLDivElement>) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<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>
|
||||||
<div
|
<div
|
||||||
className={`${styles.subtitle} ${subTitleStatus ? styles.showSubTitle : ''}`}
|
className={`${styles.subtitle} ${subTitleStatus ? styles.showSubTitle : ''}`}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import {
|
|||||||
NetworkQuality,
|
NetworkQuality,
|
||||||
RemoteAudioStats,
|
RemoteAudioStats,
|
||||||
} from '@volcengine/rtc';
|
} from '@volcengine/rtc';
|
||||||
import { Configuration, Scenes } from '@/config';
|
import RtcClient from '@/lib/RtcClient';
|
||||||
|
|
||||||
export interface IUser {
|
export interface IUser {
|
||||||
username?: string;
|
username?: string;
|
||||||
@ -36,6 +36,24 @@ export interface Msg {
|
|||||||
isInterrupted?: boolean;
|
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 {
|
export interface RoomState {
|
||||||
time: number;
|
time: number;
|
||||||
roomId?: string;
|
roomId?: string;
|
||||||
@ -47,9 +65,17 @@ export interface RoomState {
|
|||||||
*/
|
*/
|
||||||
isJoined: boolean;
|
isJoined: boolean;
|
||||||
/**
|
/**
|
||||||
* @brief 选择的模式
|
* @brief 选择的场景
|
||||||
*/
|
*/
|
||||||
scene: string;
|
scene: string;
|
||||||
|
/**
|
||||||
|
* @brief 场景下的配置
|
||||||
|
*/
|
||||||
|
sceneConfigMap: Record<string, SceneConfig>;
|
||||||
|
/**
|
||||||
|
* @brief RTC 相关的配置
|
||||||
|
*/
|
||||||
|
rtcConfigMap: Record<string, RTCConfig>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief AI 通话是否启用
|
* @brief AI 通话是否启用
|
||||||
@ -111,7 +137,9 @@ export interface RoomState {
|
|||||||
|
|
||||||
const initialState: RoomState = {
|
const initialState: RoomState = {
|
||||||
time: -1,
|
time: -1,
|
||||||
scene: Scenes[0].name,
|
scene: '',
|
||||||
|
sceneConfigMap: {},
|
||||||
|
rtcConfigMap: {},
|
||||||
remoteUsers: [],
|
remoteUsers: [],
|
||||||
localUser: {
|
localUser: {
|
||||||
publishAudio: false,
|
publishAudio: false,
|
||||||
@ -175,7 +203,21 @@ export const roomSlice = createSlice({
|
|||||||
},
|
},
|
||||||
|
|
||||||
updateScene: (state, { payload }) => {
|
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> }) => {
|
updateLocalUser: (state, { payload }: { payload: Partial<LocalUser> }) => {
|
||||||
@ -241,7 +283,7 @@ export const roomSlice = createSlice({
|
|||||||
const { paragraph, definite } = payload;
|
const { paragraph, definite } = payload;
|
||||||
const lastMsg = state.msgHistory.at(-1)! || {};
|
const lastMsg = state.msgHistory.at(-1)! || {};
|
||||||
/** 是否需要再创建新句子 */
|
/** 是否需要再创建新句子 */
|
||||||
const fromBot = payload.user === Configuration.BotName;
|
const fromBot = payload.user === state.sceneConfigMap[state.scene].botName;
|
||||||
/**
|
/**
|
||||||
* Bot 的语句以 definite 判断是否需要追加新内容
|
* Bot 的语句以 definite 判断是否需要追加新内容
|
||||||
* User 的语句以 paragraph 判断是否需要追加新内容
|
* User 的语句以 paragraph 判断是否需要追加新内容
|
||||||
@ -330,6 +372,8 @@ export const {
|
|||||||
setInterruptMsg,
|
setInterruptMsg,
|
||||||
updateNetworkQuality,
|
updateNetworkQuality,
|
||||||
updateScene,
|
updateScene,
|
||||||
|
updateSceneConfig,
|
||||||
|
updateRTCConfig,
|
||||||
updateShowSubtitle,
|
updateShowSubtitle,
|
||||||
updateFullScreen,
|
updateFullScreen,
|
||||||
updatecustomSceneName,
|
updatecustomSceneName,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user