chore: update comment & readme
parent
cdb24f5b75
commit
540ecf261e
26
README.md
26
README.md
|
|
@ -5,7 +5,6 @@
|
||||||
- 用户只需调用基于标准的 OpenAPI 接口即可配置所需的 ASR、LLM、TTS 类型和参数。火山引擎云端计算服务负责边缘用户接入、云端资源调度、音视频流压缩、文本与语音转换处理以及数据订阅传输等环节。简化开发流程,让开发者更专注在对大模型核心能力的训练及调试,从而快速推进AIGC产品应用创新。
|
- 用户只需调用基于标准的 OpenAPI 接口即可配置所需的 ASR、LLM、TTS 类型和参数。火山引擎云端计算服务负责边缘用户接入、云端资源调度、音视频流压缩、文本与语音转换处理以及数据订阅传输等环节。简化开发流程,让开发者更专注在对大模型核心能力的训练及调试,从而快速推进AIGC产品应用创新。
|
||||||
- 同时火山引擎 RTC拥有成熟的音频 3A 处理、视频处理等技术以及大规模音视频聊天能力,可支持 AIGC 产品更便捷的支持多模态交互、多人互动等场景能力,保持交互的自然性和高效性。
|
- 同时火山引擎 RTC拥有成熟的音频 3A 处理、视频处理等技术以及大规模音视频聊天能力,可支持 AIGC 产品更便捷的支持多模态交互、多人互动等场景能力,保持交互的自然性和高效性。
|
||||||
|
|
||||||
# 快速开始
|
|
||||||
## 【必看】环境准备
|
## 【必看】环境准备
|
||||||
- Node 版本: 16.0+
|
- Node 版本: 16.0+
|
||||||
1. 需要准备两个 Terminal,分别启动服务端、前端页面。
|
1. 需要准备两个 Terminal,分别启动服务端、前端页面。
|
||||||
|
|
@ -15,40 +14,43 @@ RoomId、UserId 以及申请的 AppID、BusinessID(如有)、Token、ASR AppID
|
||||||
4. 您需要在 [火山方舟-在线推理](https://console.volcengine.com/ark/region:ark+cn-beijing/endpoint?config=%7B%7D) 中创建接入点, 并将模型对应的接入点 ID 填入 `src/config/common.ts` 文件中的 `ARK_V3_MODEL_ID`, 否则无法正常启动智能体。
|
4. 您需要在 [火山方舟-在线推理](https://console.volcengine.com/ark/region:ark+cn-beijing/endpoint?config=%7B%7D) 中创建接入点, 并将模型对应的接入点 ID 填入 `src/config/common.ts` 文件中的 `ARK_V3_MODEL_ID`, 否则无法正常启动智能体。
|
||||||
5. 如果您已经自行完成了服务端的逻辑,可以不依赖 Demo 中的 Server,直接修改前端代码文件 `src/config/index.ts` 中的 `AIGC_PROXY_HOST` 请求域名和接口,并在 `src/app/api.ts` 中修改接口的参数配置 `APIS_CONFIG`。
|
5. 如果您已经自行完成了服务端的逻辑,可以不依赖 Demo 中的 Server,直接修改前端代码文件 `src/config/index.ts` 中的 `AIGC_PROXY_HOST` 请求域名和接口,并在 `src/app/api.ts` 中修改接口的参数配置 `APIS_CONFIG`。
|
||||||
|
|
||||||
## 服务端
|
## 快速开始
|
||||||
|
### 服务端
|
||||||
进到项目根目录
|
进到项目根目录
|
||||||
### 安装依赖
|
#### 安装依赖
|
||||||
```shell
|
```shell
|
||||||
cd Server
|
cd Server
|
||||||
yarn
|
yarn
|
||||||
```
|
```
|
||||||
### 运行项目
|
#### 运行项目
|
||||||
```shell
|
```shell
|
||||||
node app.js
|
node app.js
|
||||||
```
|
```
|
||||||
|
|
||||||
## 前端页面
|
### 前端页面
|
||||||
进到项目根目录
|
进到项目根目录
|
||||||
### 安装依赖
|
#### 安装依赖
|
||||||
```shell
|
```shell
|
||||||
yarn
|
yarn
|
||||||
```
|
```
|
||||||
### 运行项目
|
#### 运行项目
|
||||||
```shell
|
```shell
|
||||||
yarn dev
|
yarn dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## 常见问题
|
### 常见问题
|
||||||
| 问题 | 解决方案 |
|
| 问题 | 解决方案 |
|
||||||
| :-- | :-- |
|
| :-- | :-- |
|
||||||
| `Server/app.js` 中的 `sessionToken` 是什么,该怎么填,为什么要填 | `sessionToken` 是火山引擎子账号发起 OpenAPI 请求时所必须携带的临时 Token,获取方式可参考 [此文章末尾](https://www.volcengine.com/docs/6348/1315561)。 |
|
| `Server/app.js` 中的 `sessionToken` 是什么,该怎么填,为什么要填 | `sessionToken` 是火山引擎子账号发起 OpenAPI 请求时所必须携带的临时 Token,获取方式可参考 [此文章末尾](https://www.volcengine.com/docs/6348/1315561)。 |
|
||||||
| 什么是主账号,什么是子账号 | 可以参考[官方概念](https://www.volcengine.com/docs/6257/64963?hyperlink_open_type=lark.open_in_browser) 。|
|
| 不清楚什么是主账号,什么是子账号 | 可以参考[官方概念](https://www.volcengine.com/docs/6257/64963?hyperlink_open_type=lark.open_in_browser) 。|
|
||||||
| 启动智能体之后, 说话无反馈 | <li>可能是因为参数传递的有问题, 例如参数大小写、类型等问题,请再次确认下这类型问题是否存在。</li><li>另一方面,可能是因为控制台中相关权限没有正常授予,请参考[流程](https://www.volcengine.com/docs/6348/1315561)再次确认下是否完成相关操作。</li><li>相关资源可能未开通或者用量不足,请再次确认。</li> |
|
| **启动智能体之后, 对话无反馈** | <li>参数传递可能有问题, 例如参数大小写、类型等问题,请再次确认下这类型问题是否存在。</li><li>另一方面,可能是因为控制台中相关权限没有正常授予,请参考[流程](https://www.volcengine.com/docs/6348/1315561)再次确认下是否完成相关操作。</li><li>相关资源可能未开通或者用量不足,请再次确认。</li><li>请检查本地的网络/带宽情况</li> |
|
||||||
|
| **浏览器报了 `Uncaught (in promise) r: token_error` 错误** | 请检查您填在项目中的 RTC Token 是否合法,检测用于生成 Token 的 UserId、RoomId 是否与项目中填写的一致。 |
|
||||||
|
| 什么是 RTC | **R**eal **T**ime **C**ommunication, RTC 的概念可参考[官网文档](https://www.volcengine.com/docs/6348/66812)。 |
|
||||||
| 为什么我的麦克风正常、摄像头也正常,但是设备没有正常工作? | 可能是设备权限未授予,详情可参考 [Web 排查设备权限获取失败问题](https://www.volcengine.com/docs/6348/1356355)。 |
|
| 为什么我的麦克风正常、摄像头也正常,但是设备没有正常工作? | 可能是设备权限未授予,详情可参考 [Web 排查设备权限获取失败问题](https://www.volcengine.com/docs/6348/1356355)。 |
|
||||||
|
|
||||||
如果有上述以外的问题,欢迎联系我们反馈。
|
如果有上述以外的问题,也可以参考[问题反馈收集](https://bytedance.larkoffice.com/docx/FM51drJNFoSFcAxciXYcZkpmnBl),或者联系我们帮忙排查处理。
|
||||||
|
|
||||||
## 相关文档
|
### 相关文档
|
||||||
- [场景介绍](https://www.volcengine.com/docs/6348/1310537)
|
- [场景介绍](https://www.volcengine.com/docs/6348/1310537)
|
||||||
- [Demo 体验](https://www.volcengine.com/docs/6348/1310559)
|
- [Demo 体验](https://www.volcengine.com/docs/6348/1310559)
|
||||||
- [场景搭建方案](https://www.volcengine.com/docs/6348/1310560)
|
- [场景搭建方案](https://www.volcengine.com/docs/6348/1310560)
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
"test": "craco test",
|
"test": "craco test",
|
||||||
"eject": "react-scripts eject",
|
"eject": "react-scripts eject",
|
||||||
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
|
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
|
||||||
"eslint": "eslint src/ --fix --quiet --ext .js,.jsx,.ts,.tsx",
|
"eslint": "eslint src/ --fix --cache --quiet --ext .js,.jsx,.ts,.tsx",
|
||||||
"stylelint": "stylelint 'src/**/*.less' --fix",
|
"stylelint": "stylelint 'src/**/*.less' --fix",
|
||||||
"pre-commit": "npm run eslint && npm run stylelint",
|
"pre-commit": "npm run eslint && npm run stylelint",
|
||||||
"echo": "node message.js"
|
"echo": "node message.js"
|
||||||
|
|
|
||||||
|
|
@ -28,13 +28,18 @@ type ApiConfig = typeof APIS_CONFIG;
|
||||||
type TupleToUnion<T extends readonly unknown[]> = T[number];
|
type TupleToUnion<T extends readonly unknown[]> = T[number];
|
||||||
type ApiNames = Pick<TupleToUnion<ApiConfig>, 'action'>['action'];
|
type ApiNames = Pick<TupleToUnion<ApiConfig>, 'action'>['action'];
|
||||||
type RequestFn = <T extends keyof RequestResponse>(params?: RequestParams[T]) => RequestResponse[T];
|
type RequestFn = <T extends keyof RequestResponse>(params?: RequestParams[T]) => RequestResponse[T];
|
||||||
type PromiseRequestFn = <T extends keyof RequestResponse>(params?: RequestParams[T]) => Promise<RequestResponse[T]>;
|
type PromiseRequestFn = <T extends keyof RequestResponse>(
|
||||||
|
params?: RequestParams[T]
|
||||||
|
) => Promise<RequestResponse[T]>;
|
||||||
type Apis = Record<ApiNames, RequestFn | PromiseRequestFn>;
|
type Apis = Record<ApiNames, RequestFn | PromiseRequestFn>;
|
||||||
|
|
||||||
const APIS = APIS_CONFIG.reduce((store, cur) => {
|
const APIS = APIS_CONFIG.reduce((store, cur) => {
|
||||||
const { action, apiBasicParams, method = 'get' } = cur;
|
const { action, apiBasicParams, method = 'get' } = cur;
|
||||||
store[action] = async (params) => {
|
store[action] = async (params) => {
|
||||||
const queryData = method === 'get' ? await requestGetMethod(apiBasicParams)(params) : await requestPostMethod(apiBasicParams)(params);
|
const queryData =
|
||||||
|
method === 'get'
|
||||||
|
? await requestGetMethod(apiBasicParams)(params)
|
||||||
|
: await requestPostMethod(apiBasicParams)(params);
|
||||||
const res = await queryData?.json();
|
const res = await queryData?.json();
|
||||||
return resultHandler(res);
|
return resultHandler(res);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,11 @@ export const requestGetMethod = (apiBasicParams: string, headers = {}) => {
|
||||||
* @param isJson
|
* @param isJson
|
||||||
* @param headers
|
* @param headers
|
||||||
*/
|
*/
|
||||||
export const requestPostMethod = (apiBasicParams: string, isJson: boolean = true, headers: Headers = {}) => {
|
export const requestPostMethod = (
|
||||||
|
apiBasicParams: string,
|
||||||
|
isJson: boolean = true,
|
||||||
|
headers: Headers = {}
|
||||||
|
) => {
|
||||||
return async <T>(params: T) => {
|
return async <T>(params: T) => {
|
||||||
const res = await fetch(`${AIGC_PROXY_HOST}${apiBasicParams}`, {
|
const res = await fetch(`${AIGC_PROXY_HOST}${apiBasicParams}`, {
|
||||||
method: 'post',
|
method: 'post',
|
||||||
|
|
@ -57,5 +61,7 @@ export const resultHandler = (res: any) => {
|
||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
Message.error(`[${ResponseMetadata?.Action}]Failed(Reason: ${ResponseMetadata?.Error?.Code})`);
|
Message.error(`[${ResponseMetadata?.Action}]Failed(Reason: ${ResponseMetadata?.Error?.Code})`);
|
||||||
throw new Error(`[${ResponseMetadata?.Action}]Failed(${JSON.stringify(ResponseMetadata, null, 2)})`);
|
throw new Error(
|
||||||
|
`[${ResponseMetadata?.Action}]Failed(${JSON.stringify(ResponseMetadata, null, 2)})`
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -100,5 +100,9 @@ export interface RequestResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DeepPartial<T> = {
|
export type DeepPartial<T> = {
|
||||||
[P in keyof T]?: T[P] extends Array<infer U> ? Array<DeepPartial<U>> : T[P] extends object ? DeepPartial<T[P]> : T[P];
|
[P in keyof T]?: T[P] extends Array<infer U>
|
||||||
|
? Array<DeepPartial<U>>
|
||||||
|
: T[P] extends object
|
||||||
|
? DeepPartial<T[P]>
|
||||||
|
: T[P];
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,19 @@ import { useEffect, useState } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { IconSwap } from '@arco-design/web-react/icon';
|
import { IconSwap } from '@arco-design/web-react/icon';
|
||||||
import CheckIcon from '../CheckIcon';
|
import CheckIcon from '../CheckIcon';
|
||||||
import Config, { Icon, Name, SCENE, Prompt, Welcome, Voice, Model, AI_MODEL, ModelSourceType, VOICE_INFO_MAP, VOICE_TYPE } from '@/config';
|
import Config, {
|
||||||
|
Icon,
|
||||||
|
Name,
|
||||||
|
SCENE,
|
||||||
|
Prompt,
|
||||||
|
Welcome,
|
||||||
|
Voice,
|
||||||
|
Model,
|
||||||
|
AI_MODEL,
|
||||||
|
ModelSourceType,
|
||||||
|
VOICE_INFO_MAP,
|
||||||
|
VOICE_TYPE,
|
||||||
|
} from '@/config';
|
||||||
import TitleCard from '../TitleCard';
|
import TitleCard from '../TitleCard';
|
||||||
import CheckBoxSelector from '@/components/CheckBoxSelector';
|
import CheckBoxSelector from '@/components/CheckBoxSelector';
|
||||||
import RtcClient from '@/lib/RtcClient';
|
import RtcClient from '@/lib/RtcClient';
|
||||||
|
|
@ -156,7 +168,9 @@ function AISettings() {
|
||||||
选择你所需要的
|
选择你所需要的
|
||||||
<span className={styles['special-text']}> AI 人设</span>
|
<span className={styles['special-text']}> AI 人设</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles['sub-title']}>我们已为您配置好对应人设的基本参数,您也可以根据自己的需求进行自定义设置</div>
|
<div className={styles['sub-title']}>
|
||||||
|
我们已为您配置好对应人设的基本参数,您也可以根据自己的需求进行自定义设置
|
||||||
|
</div>
|
||||||
<div className={utils.isMobile() ? styles['scenes-mobile'] : styles.scenes}>
|
<div className={utils.isMobile() ? styles['scenes-mobile'] : styles.scenes}>
|
||||||
{SCENES.map((key) => (
|
{SCENES.map((key) => (
|
||||||
<CheckIcon
|
<CheckIcon
|
||||||
|
|
@ -282,7 +296,8 @@ function AISettings() {
|
||||||
<TitleCard title="官方模型">
|
<TitleCard title="官方模型">
|
||||||
<CheckBoxSelector
|
<CheckBoxSelector
|
||||||
label="模型选择"
|
label="模型选择"
|
||||||
data={Object.keys(AI_MODEL).map((type) => ({
|
data={Object.keys(AI_MODEL)
|
||||||
|
.map((type) => ({
|
||||||
key: AI_MODEL[type as keyof typeof AI_MODEL],
|
key: AI_MODEL[type as keyof typeof AI_MODEL],
|
||||||
label: type.replaceAll('_', ' '),
|
label: type.replaceAll('_', ' '),
|
||||||
icon: DoubaoModelSVG,
|
icon: DoubaoModelSVG,
|
||||||
|
|
|
||||||
|
|
@ -29,13 +29,20 @@ function AvatarCard(props: IAvatarCardProps) {
|
||||||
<div className={`${style.card} ${className}`} {...rest}>
|
<div className={`${style.card} ${className}`} {...rest}>
|
||||||
<div className={style.corner} />
|
<div className={style.corner} />
|
||||||
<div className={style.avatar}>
|
<div className={style.avatar}>
|
||||||
<img id="avatar-card" src={avatar || DouBaoAvatar} className={style['doubao-gif']} alt="Avatar" />
|
<img
|
||||||
|
id="avatar-card"
|
||||||
|
src={avatar || DouBaoAvatar}
|
||||||
|
className={style['doubao-gif']}
|
||||||
|
alt="Avatar"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={style.body} />
|
<div className={style.body} />
|
||||||
<div className={style['text-wrapper']}>
|
<div className={style['text-wrapper']}>
|
||||||
<div className={style['user-info']}>
|
<div className={style['user-info']}>
|
||||||
<div className={style.title}>{Name[scene]}</div>
|
<div className={style.title}>{Name[scene]}</div>
|
||||||
<div className={style.description}>声源来自 {ReversedVoiceType[TTSConfig?.VoiceType || '']}</div>
|
<div className={style.description}>
|
||||||
|
声源来自 {ReversedVoiceType[TTSConfig?.VoiceType || '']}
|
||||||
|
</div>
|
||||||
<div className={style.description}>模型 {LLMConfig.ModelName}</div>
|
<div className={style.description}>模型 {LLMConfig.ModelName}</div>
|
||||||
<AISettings />
|
<AISettings />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,11 @@ function BubbleMsg(props: IBubbleMsgProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={style} className={`${styles.bubbleWrapper} ${className}`}>
|
<div style={style} className={`${styles.bubbleWrapper} ${className}`}>
|
||||||
<img className={`${styles.bubbleLogo} ${styles[`bubble-direction-${direction}`]}`} src={Bubble} alt="Logo" />
|
<img
|
||||||
|
className={`${styles.bubbleLogo} ${styles[`bubble-direction-${direction}`]}`}
|
||||||
|
src={Bubble}
|
||||||
|
alt="Logo"
|
||||||
|
/>
|
||||||
<div className={styles.bubbleText}>{text}</div>
|
<div className={styles.bubbleText}>{text}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,8 @@ interface IProps {
|
||||||
|
|
||||||
function ButtonRadio(props: IProps) {
|
function ButtonRadio(props: IProps) {
|
||||||
const { value, onChange, options } = props;
|
const { value, onChange, options } = props;
|
||||||
const selected = useMemo(() => options.find((item) => item.key === value), [value]) || options?.[0];
|
const selected =
|
||||||
|
useMemo(() => options.find((item) => item.key === value), [value]) || options?.[0];
|
||||||
const handleClick = (key: string) => {
|
const handleClick = (key: string) => {
|
||||||
onChange?.(key);
|
onChange?.(key);
|
||||||
};
|
};
|
||||||
|
|
@ -26,7 +27,12 @@ function ButtonRadio(props: IProps) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
{options.map(({ label, key }) => (
|
{options.map(({ label, key }) => (
|
||||||
<Button type="text" key={key} className={`${styles.item} ${key === selected.key ? styles.selected : ''}`} onClick={() => handleClick(key)}>
|
<Button
|
||||||
|
type="text"
|
||||||
|
key={key}
|
||||||
|
className={`${styles.item} ${key === selected.key ? styles.selected : ''}`}
|
||||||
|
onClick={() => handleClick(key)}
|
||||||
|
>
|
||||||
<span className={key === selected.key ? styles['selected-text'] : ''}>{label}</span>
|
<span className={key === selected.key ? styles['selected-text'] : ''}>{label}</span>
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,16 @@ interface IProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
function CheckBox(props: IProps) {
|
function CheckBox(props: IProps) {
|
||||||
const { noStyle, className = '', icon = '', checked, label, description, suffix, onClick } = props;
|
const {
|
||||||
|
noStyle,
|
||||||
|
className = '',
|
||||||
|
icon = '',
|
||||||
|
checked,
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
suffix,
|
||||||
|
onClick,
|
||||||
|
} = props;
|
||||||
|
|
||||||
if (noStyle) {
|
if (noStyle) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -35,7 +44,10 @@ function CheckBox(props: IProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${className} ${styles.wrapper} ${checked ? styles.active : ''}`} onClick={onClick}>
|
<div
|
||||||
|
className={`${className} ${styles.wrapper} ${checked ? styles.active : ''}`}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
{icon ? <img className={styles.icon} src={icon} alt="icon" /> : ''}
|
{icon ? <img className={styles.icon} src={icon} alt="icon" /> : ''}
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<div className={styles.label}>{label}</div>
|
<div className={styles.label}>{label}</div>
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,13 @@ function CheckBoxSelector(props: IProps) {
|
||||||
<>
|
<>
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
{selectedOne ? (
|
{selectedOne ? (
|
||||||
<CheckBox className={styles.box} icon={selectedOne?.icon} label={selectedOne?.label || ''} description={selectedOne?.description} noStyle />
|
<CheckBox
|
||||||
|
className={styles.box}
|
||||||
|
icon={selectedOne?.icon}
|
||||||
|
label={selectedOne?.label || ''}
|
||||||
|
description={selectedOne?.description}
|
||||||
|
noStyle
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.placeholder}>{placeHolder}</div>
|
<div className={styles.placeholder}>{placeHolder}</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -78,7 +84,15 @@ function CheckBoxSelector(props: IProps) {
|
||||||
>
|
>
|
||||||
<div className={styles.modalInner}>
|
<div className={styles.modalInner}>
|
||||||
{data.map((item) => (
|
{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)} />
|
<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>
|
</div>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,15 @@ function DrawerRowItem(props: IDrawerRowItemProps) {
|
||||||
<IconRight className={styles.rightOutlined} />
|
<IconRight className={styles.rightOutlined} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Drawer closable title={drawer?.title || ''} width={drawer?.width || 400} className={styles.drawer} visible={open} onCancel={handleClose} footer={null}>
|
<Drawer
|
||||||
|
closable
|
||||||
|
title={drawer?.title || ''}
|
||||||
|
width={drawer?.width || 400}
|
||||||
|
className={styles.drawer}
|
||||||
|
visible={open}
|
||||||
|
onCancel={handleClose}
|
||||||
|
footer={null}
|
||||||
|
>
|
||||||
<div className={styles.children}>{drawer?.children}</div>
|
<div className={styles.children}>{drawer?.children}</div>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -74,12 +74,22 @@ function Header(props: HeaderProps) {
|
||||||
{children}
|
{children}
|
||||||
{utils.isMobile() ? null : (
|
{utils.isMobile() ? null : (
|
||||||
<div className={styles['header-right']}>
|
<div className={styles['header-right']}>
|
||||||
<div className={styles['header-right-text']} onClick={() => window.open('https://www.volcengine.com/product/veRTC/ConversationalAI', '_blank')}>
|
<div
|
||||||
|
className={styles['header-right-text']}
|
||||||
|
onClick={() =>
|
||||||
|
window.open('https://www.volcengine.com/product/veRTC/ConversationalAI', '_blank')
|
||||||
|
}
|
||||||
|
>
|
||||||
官网链接
|
官网链接
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={styles['header-right-text']}
|
className={styles['header-right-text']}
|
||||||
onClick={() => window.open('https://www.volcengine.com/contact/product?t=%E5%AF%B9%E8%AF%9D%E5%BC%8Fai&source=%E4%BA%A7%E5%93%81%E5%92%A8%E8%AF%A2', '_blank')}
|
onClick={() =>
|
||||||
|
window.open(
|
||||||
|
'https://www.volcengine.com/contact/product?t=%E5%AF%B9%E8%AF%9D%E5%BC%8Fai&source=%E4%BA%A7%E5%93%81%E5%92%A8%E8%AF%A2',
|
||||||
|
'_blank'
|
||||||
|
)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
联系我们
|
联系我们
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,8 @@ function NetworkIndicator() {
|
||||||
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 = room.remoteUsers.find((user) => user.userId === Config.BotName)?.audioStats?.audioLossRate || 0;
|
const audioLossRateLower =
|
||||||
|
room.remoteUsers.find((user) => user.userId === Config.BotName)?.audioStats?.audioLossRate || 0;
|
||||||
|
|
||||||
const indicators = useMemo(() => {
|
const indicators = useMemo(() => {
|
||||||
switch (networkQuality) {
|
switch (networkQuality) {
|
||||||
|
|
@ -75,11 +76,15 @@ function NetworkIndicator() {
|
||||||
<div className={style.loss}>
|
<div className={style.loss}>
|
||||||
<div>
|
<div>
|
||||||
<IconArrowUp style={{ color: 'rgba(22, 100, 255, 1)' }} />
|
<IconArrowUp style={{ color: 'rgba(22, 100, 255, 1)' }} />
|
||||||
<span>{`${audioLossRateUpper}` ? (audioLossRateUpper * 100)?.toFixed(0) : '- '}%</span>
|
<span>
|
||||||
|
{`${audioLossRateUpper}` ? (audioLossRateUpper * 100)?.toFixed(0) : '- '}%
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<IconArrowDown />
|
<IconArrowDown />
|
||||||
<span>{`${audioLossRateLower}` ? (audioLossRateLower * 100)?.toFixed(0) : '- '}%</span>
|
<span>
|
||||||
|
{`${audioLossRateLower}` ? (audioLossRateLower * 100)?.toFixed(0) : '- '}%
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,9 @@ export enum CustomParamsType {
|
||||||
/**
|
/**
|
||||||
* @brief AI 音色可选值
|
* @brief AI 音色可选值
|
||||||
* @default 通用女声
|
* @default 通用女声
|
||||||
* @notes 通用女声、通用男声为默认音色, 其它皆为付费音色
|
* @notes 通用女声、通用男声为默认音色, 其它皆为付费音色。
|
||||||
|
* 音色 ID 可于 https://console.volcengine.com/speech/service/8 中开通获取。
|
||||||
|
* 对应 "音色详情" 中, "Voice_type" 列的值。
|
||||||
*/
|
*/
|
||||||
export enum VOICE_TYPE {
|
export enum VOICE_TYPE {
|
||||||
'通用女声' = 'BV001_streaming',
|
'通用女声' = 'BV001_streaming',
|
||||||
|
|
@ -102,18 +104,14 @@ export const AI_MODE_MAP: Partial<Record<AI_MODEL, AI_MODEL_MODE>> = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 豆包模型的 ID
|
* @brief 豆包模型的 ID
|
||||||
|
* @note 具体的模型 ID 请至 https://console.volcengine.com/ark/region:ark+cn-beijing/endpoint 参看/创建
|
||||||
|
* 模型 ID 即接入点 ID, 在上述链接中表格内 "接入点名称" 列中, 类似于 "ep-2024xxxxxx-xxx" 格式即是模型 ID。
|
||||||
*/
|
*/
|
||||||
export const ARK_V3_MODEL_ID: Partial<Record<AI_MODEL, string>> = {
|
export const ARK_V3_MODEL_ID: Partial<Record<AI_MODEL, string>> = {
|
||||||
/**
|
|
||||||
* @note 具体的 ID 请至 https://console.volcengine.com/ark/region:ark+cn-beijing/endpoint 参看/创建
|
|
||||||
*/
|
|
||||||
[AI_MODEL.DOUBAO_LITE_4K]: '************** 此处填充方舟上的模型 ID *************',
|
[AI_MODEL.DOUBAO_LITE_4K]: '************** 此处填充方舟上的模型 ID *************',
|
||||||
[AI_MODEL.DOUBAO_PRO_4K]: '************** 此处填充方舟上的模型 ID *************',
|
[AI_MODEL.DOUBAO_PRO_4K]: '************** 此处填充方舟上的模型 ID *************',
|
||||||
[AI_MODEL.DOUBAO_PRO_32K]: '************** 此处填充方舟上的模型 ID *************',
|
[AI_MODEL.DOUBAO_PRO_32K]: '************** 此处填充方舟上的模型 ID *************',
|
||||||
[AI_MODEL.DOUBAO_PRO_128K]: '************** 此处填充方舟上的模型 ID *************',
|
[AI_MODEL.DOUBAO_PRO_128K]: '************** 此处填充方舟上的模型 ID *************',
|
||||||
/**
|
|
||||||
* @note 视觉模型, 可至火山方舟开通使用
|
|
||||||
*/
|
|
||||||
[AI_MODEL.VISION]: '************** 此处填充方舟上的模型 ID *************',
|
[AI_MODEL.VISION]: '************** 此处填充方舟上的模型 ID *************',
|
||||||
// ... 可根据所开通的模型进行扩充
|
// ... 可根据所开通的模型进行扩充
|
||||||
};
|
};
|
||||||
|
|
@ -155,6 +153,9 @@ export const Name = {
|
||||||
[SCENE.CUSTOM]: '自定义',
|
[SCENE.CUSTOM]: '自定义',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 智能体启动后的欢迎词。
|
||||||
|
*/
|
||||||
export const Welcome = {
|
export const Welcome = {
|
||||||
[SCENE.INTELLIGENT_ASSISTANT]: '你好,我是你的AI小助手,有什么可以帮你的吗?',
|
[SCENE.INTELLIGENT_ASSISTANT]: '你好,我是你的AI小助手,有什么可以帮你的吗?',
|
||||||
[SCENE.VIRTUAL_GIRL_FRIEND]: '你来啦,我好想你呀~今天有没有想我呢?',
|
[SCENE.VIRTUAL_GIRL_FRIEND]: '你来啦,我好想你呀~今天有没有想我呢?',
|
||||||
|
|
@ -186,15 +187,38 @@ export const Voice = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Questions = {
|
export const Questions = {
|
||||||
[SCENE.INTELLIGENT_ASSISTANT]: ['最近有什么好看的电影推荐吗?', '上海有什么好玩的地方吗?', '能给我讲一个故事吗?'],
|
[SCENE.INTELLIGENT_ASSISTANT]: [
|
||||||
[SCENE.VIRTUAL_GIRL_FRIEND]: ['我今天有点累。', '我们等会儿去看电影吧!', '明天我生日,你准备送给我什么礼物呢?'],
|
'最近有什么好看的电影推荐吗?',
|
||||||
[SCENE.TRANSLATE]: ['道可道,非常道;名可名,非常名。', 'Stay hungry, stay foolish.', '天生我材必有用,千金散尽还复来。'],
|
'上海有什么好玩的地方吗?',
|
||||||
[SCENE.CHILDREN_ENCYCLOPEDIA]: ['天上有多少颗星星?', '太阳为什么总是从东边升起?', '苹果的英语怎么说?'],
|
'能给我讲一个故事吗?',
|
||||||
[SCENE.CUSTOMER_SERVICE]: ['我上次来你们店里吃饭,等了三十分钟菜才上来。', '你们店里卫生间有点脏。', '你们空调开得太冷了。'],
|
],
|
||||||
|
[SCENE.VIRTUAL_GIRL_FRIEND]: [
|
||||||
|
'我今天有点累。',
|
||||||
|
'我们等会儿去看电影吧!',
|
||||||
|
'明天我生日,你准备送给我什么礼物呢?',
|
||||||
|
],
|
||||||
|
[SCENE.TRANSLATE]: [
|
||||||
|
'道可道,非常道;名可名,非常名。',
|
||||||
|
'Stay hungry, stay foolish.',
|
||||||
|
'天生我材必有用,千金散尽还复来。',
|
||||||
|
],
|
||||||
|
[SCENE.CHILDREN_ENCYCLOPEDIA]: [
|
||||||
|
'天上有多少颗星星?',
|
||||||
|
'太阳为什么总是从东边升起?',
|
||||||
|
'苹果的英语怎么说?',
|
||||||
|
],
|
||||||
|
[SCENE.CUSTOMER_SERVICE]: [
|
||||||
|
'我上次来你们店里吃饭,等了三十分钟菜才上来。',
|
||||||
|
'你们店里卫生间有点脏。',
|
||||||
|
'你们空调开得太冷了。',
|
||||||
|
],
|
||||||
[SCENE.TEACHING_ASSISTANT]: ['这个单词是什么意思?', '这道题该怎么做?', '我的表情是什么样的?'],
|
[SCENE.TEACHING_ASSISTANT]: ['这个单词是什么意思?', '这道题该怎么做?', '我的表情是什么样的?'],
|
||||||
[SCENE.CUSTOM]: ['你能帮我解决什么问题?', '今天北京天气怎么样?', '你喜欢哪位流行歌手?'],
|
[SCENE.CUSTOM]: ['你能帮我解决什么问题?', '今天北京天气怎么样?', '你喜欢哪位流行歌手?'],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 大模型 System 角色预设指令,可用于控制模型输出, 类似 Prompt 的概念。
|
||||||
|
*/
|
||||||
export const Prompt = {
|
export const Prompt = {
|
||||||
[SCENE.INTELLIGENT_ASSISTANT]: `##人设
|
[SCENE.INTELLIGENT_ASSISTANT]: `##人设
|
||||||
你是一个全能智能体,拥有丰富的百科知识,可以为人们答疑解惑,解决问题。
|
你是一个全能智能体,拥有丰富的百科知识,可以为人们答疑解惑,解决问题。
|
||||||
|
|
|
||||||
|
|
@ -3,40 +3,94 @@
|
||||||
* SPDX-license-identifier: BSD-3-Clause
|
* SPDX-license-identifier: BSD-3-Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { TTS_CLUSTER, ARK_V3_MODEL_ID, ModelSourceType, SCENE, Prompt, Welcome, Model, Voice, LLM_BOT_ID, AI_MODEL, AI_MODE_MAP, AI_MODEL_MODE } from '.';
|
import {
|
||||||
|
TTS_CLUSTER,
|
||||||
|
ARK_V3_MODEL_ID,
|
||||||
|
ModelSourceType,
|
||||||
|
SCENE,
|
||||||
|
Prompt,
|
||||||
|
Welcome,
|
||||||
|
Model,
|
||||||
|
Voice,
|
||||||
|
LLM_BOT_ID,
|
||||||
|
AI_MODEL,
|
||||||
|
AI_MODE_MAP,
|
||||||
|
AI_MODEL_MODE,
|
||||||
|
} from '.';
|
||||||
|
|
||||||
export const CONVERSATION_SIGNATURE = 'conversation';
|
export const CONVERSATION_SIGNATURE = 'conversation';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief RTC & AIGC 配置
|
* @brief RTC & AIGC 配置。
|
||||||
* @notes 更多参数请参考: https://api.volcengine.com/api-explorer?action=StartVoiceChat&groupName=%E6%99%BA%E8%83%BD%E4%BD%93&serviceCode=rtc&version=2024-12-01
|
* @notes 更多参数请参考
|
||||||
|
* https://www.volcengine.com/docs/6348/1404673
|
||||||
*/
|
*/
|
||||||
export class ConfigFactory {
|
export class ConfigFactory {
|
||||||
BaseConfig = {
|
BaseConfig = {
|
||||||
AppId: 'Your AppId',
|
|
||||||
/**
|
/**
|
||||||
* @brief 非必填, 按需填充
|
* @note 必填, RTC AppId 可于 https://console.volcengine.com/rtc/listRTC 中获取。
|
||||||
|
*/
|
||||||
|
AppId: 'Your RTC AppId',
|
||||||
|
/**
|
||||||
|
* @brief 非必填, 按需填充。
|
||||||
*/
|
*/
|
||||||
BusinessId: undefined,
|
BusinessId: undefined,
|
||||||
|
/**
|
||||||
|
* @brief 必填, 房间 ID, 自定义即可。
|
||||||
|
*/
|
||||||
RoomId: 'Your Room Id',
|
RoomId: 'Your Room Id',
|
||||||
|
/**
|
||||||
|
* @brief 必填, 当前和 AI 对话的用户的 ID, 自定义即可。
|
||||||
|
*/
|
||||||
UserId: 'Your User Id',
|
UserId: 'Your User Id',
|
||||||
|
/**
|
||||||
|
* @brief 必填, RTC Token, 由 AppId、RoomId、UserId、时间戳等等信息计算得出, 可于 https://console.volcengine.com/rtc/listRTC 列表中
|
||||||
|
* 找到对应 AppId 行中 "操作" 列的 "临时Token" 按钮点击进行生成, 用于本地 RTC 通信进房鉴权校验。
|
||||||
|
* @note 生成临时 Token 时, 页面上的 RoomId / UserId 填的与此处的 RoomId / UserId 保持一致。
|
||||||
|
* 正式使用时可通参考 https://www.volcengine.com/docs/6348/70121 通过代码生成 Token。
|
||||||
|
*/
|
||||||
Token: 'Your Token',
|
Token: 'Your Token',
|
||||||
|
/**
|
||||||
|
* @brief 必填, TTS(语音合成) AppId, 可于 https://console.volcengine.com/speech/app 中获取, 若无可先创建应用。
|
||||||
|
* @note 创建应用时, 需要选择 "语音合成" 服务, 并选择对应的 App 进行绑定。
|
||||||
|
*/
|
||||||
TTSAppId: 'Your TTS AppId',
|
TTSAppId: 'Your TTS AppId',
|
||||||
|
/**
|
||||||
|
* @brief 必填, ASR(语音识别) AppId, 可于 https://console.volcengine.com/speech/app 中获取, 若无可先创建应用。
|
||||||
|
* @note 创建应用时, 需要按需根据语言选择 "流式语音识别" 服务, 并选择对应的 App 进行绑定。
|
||||||
|
*/
|
||||||
ASRAppId: 'Your ASR AppId',
|
ASRAppId: 'Your ASR AppId',
|
||||||
};
|
};
|
||||||
|
|
||||||
Model: AI_MODEL = Model[SCENE.INTELLIGENT_ASSISTANT];
|
Model: AI_MODEL = Model[SCENE.INTELLIGENT_ASSISTANT];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @note 必填, 音色 ID, 可具体看定义。
|
||||||
|
* 音色 ID 获取方式可查看 VOICE_TYPE 定义
|
||||||
|
* 此处已有默认值, 不影响跑通, 可按需修改。
|
||||||
|
*/
|
||||||
VoiceType = Voice[SCENE.INTELLIGENT_ASSISTANT];
|
VoiceType = Voice[SCENE.INTELLIGENT_ASSISTANT];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @note 大模型 System 角色预设指令, 可用于控制模型输出, 类似 Prompt 的概念。
|
||||||
|
*/
|
||||||
Prompt = Prompt[SCENE.INTELLIGENT_ASSISTANT];
|
Prompt = Prompt[SCENE.INTELLIGENT_ASSISTANT];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @note 智能体启动后的欢迎词。
|
||||||
|
*/
|
||||||
WelcomeSpeech = Welcome[SCENE.INTELLIGENT_ASSISTANT];
|
WelcomeSpeech = Welcome[SCENE.INTELLIGENT_ASSISTANT];
|
||||||
|
|
||||||
ModeSourceType = ModelSourceType.Available;
|
ModeSourceType = ModelSourceType.Available;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @note 非必填, 第三方模型才需要使用, 用火山方舟模型时无需关注。
|
||||||
|
*/
|
||||||
Url? = '';
|
Url? = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @note 非必填, 第三方模型才需要使用, 用火山方舟模型时无需关注。
|
||||||
|
*/
|
||||||
APIKey? = '';
|
APIKey? = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
* SPDX-license-identifier: BSD-3-Clause
|
* SPDX-license-identifier: BSD-3-Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import VERTC, {
|
import VERTC, {
|
||||||
MirrorType,
|
MirrorType,
|
||||||
StreamIndex,
|
StreamIndex,
|
||||||
|
|
@ -33,7 +34,11 @@ export interface IEventListener {
|
||||||
handleUserJoin: (e: onUserJoinedEvent) => void;
|
handleUserJoin: (e: onUserJoinedEvent) => void;
|
||||||
handleUserLeave: (e: onUserLeaveEvent) => void;
|
handleUserLeave: (e: onUserLeaveEvent) => void;
|
||||||
handleUserPublishStream: (e: { userId: string; mediaType: MediaType }) => void;
|
handleUserPublishStream: (e: { userId: string; mediaType: MediaType }) => void;
|
||||||
handleUserUnpublishStream: (e: { userId: string; mediaType: MediaType; reason: StreamRemoveReason }) => void;
|
handleUserUnpublishStream: (e: {
|
||||||
|
userId: string;
|
||||||
|
mediaType: MediaType;
|
||||||
|
reason: StreamRemoveReason;
|
||||||
|
}) => void;
|
||||||
handleRemoteStreamStats: (e: RemoteStreamStats) => void;
|
handleRemoteStreamStats: (e: RemoteStreamStats) => void;
|
||||||
handleLocalStreamStats: (e: LocalStreamStats) => void;
|
handleLocalStreamStats: (e: LocalStreamStats) => void;
|
||||||
handleLocalAudioPropertiesReport: (e: LocalAudioPropertiesInfo[]) => void;
|
handleLocalAudioPropertiesReport: (e: LocalAudioPropertiesInfo[]) => void;
|
||||||
|
|
@ -45,7 +50,10 @@ export interface IEventListener {
|
||||||
handleUserStartAudioCapture: (e: { userId: string }) => void;
|
handleUserStartAudioCapture: (e: { userId: string }) => void;
|
||||||
handleUserStopAudioCapture: (e: { userId: string }) => void;
|
handleUserStopAudioCapture: (e: { userId: string }) => void;
|
||||||
handleRoomBinaryMessageReceived: (e: { userId: string; message: ArrayBuffer }) => void;
|
handleRoomBinaryMessageReceived: (e: { userId: string; message: ArrayBuffer }) => void;
|
||||||
handleNetworkQuality: (uplinkNetworkQuality: NetworkQuality, downlinkNetworkQuality: NetworkQuality) => void;
|
handleNetworkQuality: (
|
||||||
|
uplinkNetworkQuality: NetworkQuality,
|
||||||
|
downlinkNetworkQuality: NetworkQuality
|
||||||
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EngineOptions {
|
interface EngineOptions {
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,11 @@ const useRtcListeners = (): IEventListener => {
|
||||||
dispatch(updateRemoteUser(payload));
|
dispatch(updateRemoteUser(payload));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUserUnpublishStream = (e: { userId: string; mediaType: MediaType; reason: StreamRemoveReason }) => {
|
const handleUserUnpublishStream = (e: {
|
||||||
|
userId: string;
|
||||||
|
mediaType: MediaType;
|
||||||
|
reason: StreamRemoveReason;
|
||||||
|
}) => {
|
||||||
const { userId, mediaType } = e;
|
const { userId, mediaType } = e;
|
||||||
|
|
||||||
const payload: IUser = { userId };
|
const payload: IUser = { userId };
|
||||||
|
|
@ -119,7 +123,9 @@ const useRtcListeners = (): IEventListener => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLocalAudioPropertiesReport = (e: LocalAudioPropertiesInfo[]) => {
|
const handleLocalAudioPropertiesReport = (e: LocalAudioPropertiesInfo[]) => {
|
||||||
const localAudioInfo = e.find((audioInfo) => audioInfo.streamIndex === StreamIndex.STREAM_INDEX_MAIN);
|
const localAudioInfo = e.find(
|
||||||
|
(audioInfo) => audioInfo.streamIndex === StreamIndex.STREAM_INDEX_MAIN
|
||||||
|
);
|
||||||
if (localAudioInfo) {
|
if (localAudioInfo) {
|
||||||
dispatch(
|
dispatch(
|
||||||
updateLocalUser({
|
updateLocalUser({
|
||||||
|
|
@ -236,10 +242,15 @@ const useRtcListeners = (): IEventListener => {
|
||||||
dispatch(updateAITalkState({ isAITalking: false }));
|
dispatch(updateAITalkState({ isAITalking: false }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNetworkQuality = (uplinkNetworkQuality: NetworkQuality, downlinkNetworkQuality: NetworkQuality) => {
|
const handleNetworkQuality = (
|
||||||
|
uplinkNetworkQuality: NetworkQuality,
|
||||||
|
downlinkNetworkQuality: NetworkQuality
|
||||||
|
) => {
|
||||||
dispatch(
|
dispatch(
|
||||||
updateNetworkQuality({
|
updateNetworkQuality({
|
||||||
networkQuality: Math.floor((uplinkNetworkQuality + downlinkNetworkQuality) / 2) as NetworkQuality,
|
networkQuality: Math.floor(
|
||||||
|
(uplinkNetworkQuality + downlinkNetworkQuality) / 2
|
||||||
|
) as NetworkQuality,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,23 @@ import { useSelector, useDispatch } from 'react-redux';
|
||||||
import { MediaType } from '@volcengine/rtc';
|
import { MediaType } from '@volcengine/rtc';
|
||||||
import Utils from '@/utils/utils';
|
import Utils from '@/utils/utils';
|
||||||
import RtcClient from '@/lib/RtcClient';
|
import RtcClient from '@/lib/RtcClient';
|
||||||
import { clearCurrentMsg, clearHistoryMsg, localJoinRoom, localLeaveRoom, updateAIGCState, updateLocalUser } from '@/store/slices/room';
|
import {
|
||||||
|
clearCurrentMsg,
|
||||||
|
clearHistoryMsg,
|
||||||
|
localJoinRoom,
|
||||||
|
localLeaveRoom,
|
||||||
|
updateAIGCState,
|
||||||
|
updateLocalUser,
|
||||||
|
} from '@/store/slices/room';
|
||||||
|
|
||||||
import useRtcListeners from '@/lib/listenerHooks';
|
import useRtcListeners from '@/lib/listenerHooks';
|
||||||
import { RootState } from '@/store';
|
import { RootState } from '@/store';
|
||||||
|
|
||||||
import { updateMediaInputs, updateSelectedDevice, setDevicePermissions } from '@/store/slices/device';
|
import {
|
||||||
|
updateMediaInputs,
|
||||||
|
updateSelectedDevice,
|
||||||
|
setDevicePermissions,
|
||||||
|
} from '@/store/slices/device';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
import aigcConfig, { AI_MODEL } from '@/config';
|
import aigcConfig, { AI_MODEL } from '@/config';
|
||||||
|
|
||||||
|
|
@ -45,7 +56,10 @@ export const useGetDevicePermission = () => {
|
||||||
return permission;
|
return permission;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useJoin = (): [boolean, (formValues: FormProps, fromRefresh: boolean) => Promise<void | boolean>] => {
|
export const useJoin = (): [
|
||||||
|
boolean,
|
||||||
|
(formValues: FormProps, fromRefresh: 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);
|
||||||
|
|
||||||
|
|
@ -198,7 +212,9 @@ export const useDeviceState = () => {
|
||||||
|
|
||||||
const switchMic = (publish = true) => {
|
const switchMic = (publish = true) => {
|
||||||
if (publish) {
|
if (publish) {
|
||||||
!isAudioPublished ? RtcClient.publishStream(MediaType.AUDIO) : RtcClient.unpublishStream(MediaType.AUDIO);
|
!isAudioPublished
|
||||||
|
? RtcClient.publishStream(MediaType.AUDIO)
|
||||||
|
: RtcClient.unpublishStream(MediaType.AUDIO);
|
||||||
}
|
}
|
||||||
queryDevices(MediaType.AUDIO);
|
queryDevices(MediaType.AUDIO);
|
||||||
!isAudioPublished ? RtcClient.startAudioCapture() : RtcClient.stopAudioCapture();
|
!isAudioPublished ? RtcClient.startAudioCapture() : RtcClient.stopAudioCapture();
|
||||||
|
|
@ -211,7 +227,9 @@ export const useDeviceState = () => {
|
||||||
|
|
||||||
const switchCamera = (publish = true) => {
|
const switchCamera = (publish = true) => {
|
||||||
if (publish) {
|
if (publish) {
|
||||||
!isVideoPublished ? RtcClient.publishStream(MediaType.VIDEO) : RtcClient.unpublishStream(MediaType.VIDEO);
|
!isVideoPublished
|
||||||
|
? RtcClient.publishStream(MediaType.VIDEO)
|
||||||
|
: RtcClient.unpublishStream(MediaType.VIDEO);
|
||||||
}
|
}
|
||||||
queryDevices(MediaType.VIDEO);
|
queryDevices(MediaType.VIDEO);
|
||||||
!localUser.publishVideo ? RtcClient.startVideoCapture() : RtcClient.stopVideoCapture();
|
!localUser.publishVideo ? RtcClient.startVideoCapture() : RtcClient.stopVideoCapture();
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,11 @@ function InvokeButton(props: IInvokeButtonProps) {
|
||||||
<div className={`${style.wrapper} ${loading ? '' : style.cursor} ${className}`} {...rest}>
|
<div className={`${style.wrapper} ${loading ? '' : style.cursor} ${className}`} {...rest}>
|
||||||
<div className={style.btn}>
|
<div className={style.btn}>
|
||||||
<img src={CallButtonSVG} alt="call" />
|
<img src={CallButtonSVG} alt="call" />
|
||||||
{loading ? <Loading className={style.icon} /> : <img src={PhoneSVG} className={style.icon} alt="phone" />}
|
{loading ? (
|
||||||
|
<Loading className={style.icon} />
|
||||||
|
) : (
|
||||||
|
<img src={PhoneSVG} className={style.icon} alt="phone" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={style.text}>{loading ? '连接中' : '通话'}</div>
|
<div className={style.text}>{loading ? '连接中' : '通话'}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,9 @@ function CameraArea(props: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
const handleOperateCamera = () => {
|
const handleOperateCamera = () => {
|
||||||
!localUser.publishVideo ? RtcClient.startVideoCapture() : RtcClient.stopVideoCapture();
|
!localUser.publishVideo ? RtcClient.startVideoCapture() : RtcClient.stopVideoCapture();
|
||||||
|
|
||||||
!localUser.publishVideo ? RtcClient.publishStream(MediaType.VIDEO) : RtcClient.unpublishStream(MediaType.VIDEO);
|
!localUser.publishVideo
|
||||||
|
? RtcClient.publishStream(MediaType.VIDEO)
|
||||||
|
: RtcClient.unpublishStream(MediaType.VIDEO);
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
updateLocalUser({
|
updateLocalUser({
|
||||||
|
|
@ -49,7 +51,11 @@ function CameraArea(props: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
<div id={LocalVideoID} className={styles['camera-player']} />
|
<div id={LocalVideoID} className={styles['camera-player']} />
|
||||||
) : (
|
) : (
|
||||||
<div className={styles['camera-placeholder']}>
|
<div className={styles['camera-placeholder']}>
|
||||||
<img src={CameraCloseNoteSVG} alt="close" className={styles['camera-placeholder-close-note']} />
|
<img
|
||||||
|
src={CameraCloseNoteSVG}
|
||||||
|
alt="close"
|
||||||
|
className={styles['camera-placeholder-close-note']}
|
||||||
|
/>
|
||||||
<div>
|
<div>
|
||||||
请
|
请
|
||||||
<span onClick={handleOperateCamera} className={styles['camera-open-btn']}>
|
<span onClick={handleOperateCamera} className={styles['camera-open-btn']}>
|
||||||
|
|
|
||||||
|
|
@ -46,11 +46,18 @@ function Conversation(props: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.sentence} ${isUserMsg ? styles.user : styles.robot}`} key={`msg-${index}`}>
|
<div
|
||||||
|
className={`${styles.sentence} ${isUserMsg ? styles.user : styles.robot}`}
|
||||||
|
key={`msg-${index}`}
|
||||||
|
>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
{value}
|
{value}
|
||||||
<div className={styles['loading-wrapper']}>
|
<div className={styles['loading-wrapper']}>
|
||||||
{isAIReady && (isUserTalking || isAITalking) && index === msgHistory.length - 1 ? <Loading gap={3} className={styles.loading} dotClassName={styles.dot} /> : ''}
|
{isAIReady && (isUserTalking || isAITalking) && index === msgHistory.length - 1 ? (
|
||||||
|
<Loading gap={3} className={styles.loading} dotClassName={styles.dot} />
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!isUserMsg && isInterrupted ? <Tag className={styles.interruptTag}>已打断</Tag> : ''}
|
{!isUserMsg && isInterrupted ? <Tag className={styles.interruptTag}>已打断</Tag> : ''}
|
||||||
|
|
|
||||||
|
|
@ -32,9 +32,25 @@ function ToolBar(props: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className={`${className} ${style.btns} ${utils.isMobile() ? style.column : ''}`} {...rest}>
|
<div className={`${className} ${style.btns} ${utils.isMobile() ? style.column : ''}`} {...rest}>
|
||||||
{utils.isMobile() ? <img src={SettingSVG} onClick={handleSetting} className={style.setting} alt="setting" /> : null}
|
{utils.isMobile() ? (
|
||||||
<img src={isAudioPublished ? MicOpenSVG : MicCloseSVG} onClick={() => switchMic(true)} className={style.btn} alt="mic" />
|
<img src={SettingSVG} onClick={handleSetting} className={style.setting} alt="setting" />
|
||||||
{model === AI_MODEL.VISION ? <img src={isVideoPublished ? CameraOpenSVG : CameraCloseSVG} onClick={() => switchCamera(true)} className={style.btn} alt="camera" /> : ''}
|
) : null}
|
||||||
|
<img
|
||||||
|
src={isAudioPublished ? MicOpenSVG : MicCloseSVG}
|
||||||
|
onClick={() => switchMic(true)}
|
||||||
|
className={style.btn}
|
||||||
|
alt="mic"
|
||||||
|
/>
|
||||||
|
{model === AI_MODEL.VISION ? (
|
||||||
|
<img
|
||||||
|
src={isVideoPublished ? CameraOpenSVG : CameraCloseSVG}
|
||||||
|
onClick={() => switchCamera(true)}
|
||||||
|
className={style.btn}
|
||||||
|
alt="camera"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
<img src={LeaveRoomSVG} onClick={leaveRoom} className={style.btn} alt="leave" />
|
<img src={LeaveRoomSVG} onClick={leaveRoom} className={style.btn} alt="leave" />
|
||||||
{utils.isMobile() ? (
|
{utils.isMobile() ? (
|
||||||
<Drawer
|
<Drawer
|
||||||
|
|
|
||||||
|
|
@ -31,11 +31,15 @@ function DeviceDrawerButton(props: IDeviceDrawerButtonProps) {
|
||||||
const switcher = type === MediaType.AUDIO ? device.switchMic : device.switchCamera;
|
const switcher = type === MediaType.AUDIO ? device.switchMic : device.switchCamera;
|
||||||
const devicePermissions = useSelector((state: RootState) => state.device.devicePermissions);
|
const devicePermissions = useSelector((state: RootState) => state.device.devicePermissions);
|
||||||
const devices = useSelector((state: RootState) => state.device);
|
const devices = useSelector((state: RootState) => state.device);
|
||||||
const selectedDevice = type === MediaType.AUDIO ? devices.selectedMicrophone : devices.selectedCamera;
|
const selectedDevice =
|
||||||
|
type === MediaType.AUDIO ? devices.selectedMicrophone : devices.selectedCamera;
|
||||||
const permission = devicePermissions?.[type === MediaType.AUDIO ? 'audio' : 'video'];
|
const permission = devicePermissions?.[type === MediaType.AUDIO ? 'audio' : 'video'];
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const deviceList = useMemo(() => (type === MediaType.AUDIO ? devices.audioInputs : devices.videoInputs), [devices]);
|
const deviceList = useMemo(
|
||||||
|
() => (type === MediaType.AUDIO ? devices.audioInputs : devices.videoInputs),
|
||||||
|
[devices]
|
||||||
|
);
|
||||||
|
|
||||||
const handleDeviceChange = (value: string) => {
|
const handleDeviceChange = (value: string) => {
|
||||||
RtcClient.switchDevice(type, value);
|
RtcClient.switchDevice(type, value);
|
||||||
|
|
@ -66,7 +70,12 @@ function DeviceDrawerButton(props: IDeviceDrawerButtonProps) {
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
<div className={styles.label}>{DEVICE_NAME[type]}</div>
|
<div className={styles.label}>{DEVICE_NAME[type]}</div>
|
||||||
<div className={styles.value}>
|
<div className={styles.value}>
|
||||||
<Switch checked={isEnable} size="small" onChange={(enable) => switcher(enable)} disabled={!permission} />
|
<Switch
|
||||||
|
checked={isEnable}
|
||||||
|
size="small"
|
||||||
|
onChange={(enable) => switcher(enable)}
|
||||||
|
disabled={!permission}
|
||||||
|
/>
|
||||||
<Select style={{ width: 250 }} value={selectedDevice} onChange={handleDeviceChange}>
|
<Select style={{ width: 250 }} value={selectedDevice} onChange={handleDeviceChange}>
|
||||||
{deviceList.map((device) => (
|
{deviceList.map((device) => (
|
||||||
<Select.Option key={device.deviceId} value={device.deviceId}>
|
<Select.Option key={device.deviceId} value={device.deviceId}>
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ export const DeviceSlice = createSlice({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
export const { updateMediaInputs, updateSelectedDevice, setMicrophoneList, setDevicePermissions } = DeviceSlice.actions;
|
export const { updateMediaInputs, updateSelectedDevice, setMicrophoneList, setDevicePermissions } =
|
||||||
|
DeviceSlice.actions;
|
||||||
|
|
||||||
export default DeviceSlice.reducer;
|
export default DeviceSlice.reducer;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
import { AudioPropertiesInfo, LocalAudioStats, NetworkQuality, RemoteAudioStats } from '@volcengine/rtc';
|
import {
|
||||||
|
AudioPropertiesInfo,
|
||||||
|
LocalAudioStats,
|
||||||
|
NetworkQuality,
|
||||||
|
RemoteAudioStats,
|
||||||
|
} from '@volcengine/rtc';
|
||||||
import config, { SCENE } from '@/config';
|
import config, { SCENE } from '@/config';
|
||||||
import utils from '@/utils/utils';
|
import utils from '@/utils/utils';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,10 @@ class Utils {
|
||||||
let hasLogin = true;
|
let hasLogin = true;
|
||||||
if (!_roomId || !_uid) {
|
if (!_roomId || !_uid) {
|
||||||
hasLogin = false;
|
hasLogin = false;
|
||||||
} else if (!/^[0-9a-zA-Z_\-@.]{1,128}$/.test(_roomId) || !/^[0-9a-zA-Z_\-@.]{1,128}$/.test(_uid)) {
|
} else if (
|
||||||
|
!/^[0-9a-zA-Z_\-@.]{1,128}$/.test(_roomId) ||
|
||||||
|
!/^[0-9a-zA-Z_\-@.]{1,128}$/.test(_uid)
|
||||||
|
) {
|
||||||
hasLogin = false;
|
hasLogin = false;
|
||||||
}
|
}
|
||||||
return hasLogin;
|
return hasLogin;
|
||||||
|
|
@ -135,7 +138,10 @@ class Utils {
|
||||||
if (arr.length) {
|
if (arr.length) {
|
||||||
const last = arr.at(-1)!;
|
const last = arr.at(-1)!;
|
||||||
const { user, value, isInterrupted } = last;
|
const { user, value, isInterrupted } = last;
|
||||||
if ((added.user === RtcClient.basicInfo.user_id && last.user === added.user) || (user === added.user && added.value.startsWith(value) && value.trim())) {
|
if (
|
||||||
|
(added.user === RtcClient.basicInfo.user_id && last.user === added.user) ||
|
||||||
|
(user === added.user && added.value.startsWith(value) && value.trim())
|
||||||
|
) {
|
||||||
arr.pop();
|
arr.pop();
|
||||||
added.isInterrupted = isInterrupted;
|
added.isInterrupted = isInterrupted;
|
||||||
}
|
}
|
||||||
|
|
@ -189,7 +195,8 @@ class Utils {
|
||||||
type += String.fromCharCode(typeBuffer[i]);
|
type += String.fromCharCode(typeBuffer[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const length = (lengthBuffer[0] << 24) | (lengthBuffer[1] << 16) | (lengthBuffer[2] << 8) | lengthBuffer[3];
|
const length =
|
||||||
|
(lengthBuffer[0] << 24) | (lengthBuffer[1] << 16) | (lengthBuffer[2] << 8) | lengthBuffer[3];
|
||||||
|
|
||||||
const value = new TextDecoder().decode(valueBuffer.subarray(0, length));
|
const value = new TextDecoder().decode(valueBuffer.subarray(0, length));
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue