├── .eslintrc ├── .gitignore ├── .npmrc ├── .prettierrc ├── .stylelintrc ├── .yarnrc ├── LICENSE ├── README.md ├── Server ├── .npmrc ├── app.js ├── package.json └── yarn.lock ├── craco.config.js ├── message.js ├── package.json ├── public ├── favicon.png ├── index.html └── robots.txt ├── src ├── App.tsx ├── app │ ├── api.ts │ ├── base.ts │ └── type.ts ├── assets │ └── img │ │ ├── CHILDREN_ENCYCLOPEDIA.png │ │ ├── CUSTOMER_SERVICE.png │ │ ├── CallWrapper.svg │ │ ├── CameraClose.svg │ │ ├── CameraCloseNote.svg │ │ ├── CameraOpen.svg │ │ ├── Checked.svg │ │ ├── Close.svg │ │ ├── Dot.svg │ │ ├── DoubaoAvatar.png │ │ ├── DoubaoAvatarGIF.webp │ │ ├── DoubaoModel.svg │ │ ├── DoubaoProfile.svg │ │ ├── INTELLIGENT_ASSISTANT.png │ │ ├── LeaveRoom.svg │ │ ├── Logo.svg │ │ ├── MicClose.svg │ │ ├── MicOpen.svg │ │ ├── MicrophoneOff.svg │ │ ├── MicrophoneOn.svg │ │ ├── ModelChange.svg │ │ ├── MsgBubble.svg │ │ ├── Phone.svg │ │ ├── SCREEN_READER.png │ │ ├── ScreenCloseNote.svg │ │ ├── ScreenOff.svg │ │ ├── ScreenOn.svg │ │ ├── Setting.svg │ │ ├── Stop.svg │ │ ├── StopRobotBtn.svg │ │ ├── StopWave.jpeg │ │ ├── TEACHER.png │ │ ├── TEACHING_ASSISTANT.png │ │ ├── TRANSLATE.png │ │ ├── VIRTUAL_GIRL_FRIEND.png │ │ ├── VoiceTypeChange.svg │ │ ├── doubao.svg │ │ ├── huoponvsheng.jpeg │ │ ├── jingqiangkanye.jpeg │ │ ├── magicTool.svg │ │ ├── menu.svg │ │ ├── tongyongnansheng.jpeg │ │ ├── tongyongnvsheng.jpeg │ │ ├── wankuqingnian.jpeg │ │ ├── wanwanxiaohe.jpeg │ │ └── wennuanahu.jpeg ├── components │ ├── AISettings │ │ ├── index.module.less │ │ └── index.tsx │ ├── AvatarCard │ │ ├── index.module.less │ │ └── index.tsx │ ├── CheckBox │ │ ├── index.module.less │ │ └── index.tsx │ ├── CheckBoxSelector │ │ ├── index.module.less │ │ └── index.tsx │ ├── CheckIcon │ │ ├── index.module.less │ │ └── index.tsx │ ├── DrawerRowItem │ │ ├── index.module.less │ │ └── index.tsx │ ├── Header │ │ ├── index.module.less │ │ └── index.tsx │ ├── Loading │ │ ├── AudioLoading │ │ │ ├── index.module.less │ │ │ └── index.tsx │ │ ├── HorizonLoading │ │ │ ├── index.module.less │ │ │ └── index.tsx │ │ └── VerticalLoading │ │ │ ├── index.module.less │ │ │ └── index.tsx │ ├── NetworkIndicator │ │ ├── index.module.less │ │ └── index.tsx │ ├── ResizeWrapper │ │ ├── index.module.less │ │ └── index.tsx │ └── TitleCard │ │ ├── index.module.less │ │ └── index.tsx ├── config │ ├── common.ts │ ├── config.ts │ └── index.ts ├── index.less ├── index.tsx ├── interface.ts ├── lib │ ├── RtcClient.ts │ ├── listenerHooks.ts │ └── useCommon.ts ├── pages │ └── MainPage │ │ ├── MainArea │ │ ├── Antechamber │ │ │ ├── InvokeButton │ │ │ │ ├── index.module.less │ │ │ │ ├── index.tsx │ │ │ │ └── loading.tsx │ │ │ ├── index.module.less │ │ │ └── index.tsx │ │ ├── Room │ │ │ ├── AudioController.tsx │ │ │ ├── CameraArea.tsx │ │ │ ├── Conversation.tsx │ │ │ ├── ToolBar.tsx │ │ │ ├── index.module.less │ │ │ └── index.tsx │ │ ├── index.module.less │ │ └── index.tsx │ │ ├── Menu │ │ ├── components │ │ │ ├── AISettingAnchor │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ │ ├── DeviceDrawerButton │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ │ ├── Interrupt │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ │ └── Operation │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ ├── index.module.less │ │ └── index.tsx │ │ ├── index.module.less │ │ └── index.tsx ├── react-app-env.d.ts ├── store │ ├── index.ts │ └── slices │ │ ├── device.ts │ │ └── room.ts ├── theme.less └── utils │ ├── handler.ts │ ├── logger.ts │ ├── utils.less │ └── utils.ts └── tsconfig.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true, 5 | "commonjs": true, 6 | "es6": true, 7 | "node": true, 8 | "jest": true 9 | }, 10 | "parser": "@typescript-eslint/parser", 11 | "extends": [ 12 | "airbnb", 13 | "plugin:react/recommended", 14 | "plugin:prettier/recommended", 15 | "plugin:react-hooks/recommended" 16 | ], 17 | "parserOptions": { 18 | "ecmaFeatures": { 19 | "experimentalObjectRestSpread": true 20 | }, 21 | "sourceType": "module" 22 | }, 23 | "plugins": ["react", "babel", "@typescript-eslint/eslint-plugin"], 24 | "globals": { 25 | "ActiveXObject": false, 26 | "describe": false, 27 | "it": false, 28 | "expect": false, 29 | "jest": false, 30 | "$": false, 31 | "afterEach": false, 32 | "beforeEach": false 33 | }, 34 | "overrides": [ 35 | { 36 | "files": ["*.ts", "*.tsx"], 37 | "rules": { 38 | "@typescript-eslint/no-unused-vars": [2, { "args": "none" }], 39 | "@typescript-eslint/no-use-before-define": [2, { "functions": false, "classes": false }] 40 | } 41 | } 42 | ], 43 | "rules": { 44 | "prettier/prettier": ["warn", { "trailingComma": "es5", "printWidth": 100 }], 45 | "linebreak-style": "off", 46 | "no-console": ["warn", { "allow": ["warn", "error", "log"] }], 47 | "no-case-declarations": 0, 48 | "no-param-reassign": 0, 49 | "no-underscore-dangle": 0, 50 | "no-useless-constructor": 0, 51 | "no-unused-vars": [2, { "vars": "all", "args": "none" }], 52 | "no-restricted-syntax": 0, 53 | "no-unused-expressions": ["error", { "allowShortCircuit": true, "allowTernary": true }], 54 | "no-plusplus": 0, 55 | "no-return-assign": 0, 56 | "no-script-url": 0, 57 | "no-extend-native": 0, 58 | "no-restricted-globals": 0, 59 | "no-nested-ternary": 0, 60 | "no-empty": 0, 61 | "no-void": 0, 62 | "no-useless-escape": 0, 63 | "no-bitwise": 0, 64 | "no-mixed-operators": 0, 65 | "consistent-return": 0, 66 | "one-var": 0, 67 | "prefer-promise-reject-errors": 0, 68 | "prefer-destructuring": 0, 69 | "global-require": 0, 70 | "guard-for-in": 0, 71 | "func-names": 0, 72 | "strict": 0, 73 | "radix": 0, 74 | "no-prototype-builtins": 0, 75 | "class-methods-use-this": 0, 76 | "import/no-dynamic-require": 0, 77 | "import/no-unresolved": 0, 78 | "import/extensions": 0, 79 | "import/no-extraneous-dependencies": 0, 80 | "import/prefer-default-export": 0, 81 | "import/no-absolute-path": 0, 82 | "react/no-danger": 0, 83 | "react/forbid-prop-types": 0, 84 | "react/prop-types": 0, 85 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx", "ts", "tsx"] }], 86 | "react/sort-comp": 0, 87 | "react/no-did-update-set-state": 0, 88 | "react/prefer-stateless-function": 0, 89 | "react/jsx-closing-tag-location": 0, 90 | "react/jsx-no-bind": 0, 91 | "react/no-array-index-key": 0, 92 | "react/no-children-prop": 0, 93 | "react/no-did-mount-set-state": 0, 94 | "react/no-find-dom-node": 0, 95 | "react/default-props-match-prop-types": 0, 96 | "react/jsx-one-expression-per-line": 0, 97 | "react/react-in-jsx-scope": 0, 98 | "react/jsx-props-no-spreading": 0, 99 | "jsx-a11y/anchor-is-valid": 0, 100 | "jsx-a11y/no-static-element-interactions": 0, 101 | "jsx-a11y/click-events-have-key-events": 0, 102 | "jsx-a11y/no-noninteractive-element-interactions": 0, 103 | "jsx-a11y/alt-text": 0, 104 | "jsx-a11y/label-has-for": 0, 105 | "jsx-a11y/label-has-associated-control": 0, 106 | "jsx-a11y/no-noninteractive-tabindex": 0, 107 | "jsx-a11y/tabindex-no-positive": 0, 108 | "react/jsx-indent": 0, 109 | "react/display-name": 0, 110 | "react/no-multi-comp": 0, 111 | "react/destructuring-assignment": 0, 112 | "react/no-access-state-in-setstate": 0, 113 | "react/button-has-type": 0, 114 | "react/require-default-props": 0, 115 | "react/jsx-wrap-multilines": 0, 116 | "react/no-render-return-value": 0, 117 | "array-callback-return": 0, 118 | "no-cond-assign": 0, 119 | "@typescript-eslint/explicit-function-return-type": 0, 120 | "no-use-before-define": 0, 121 | "@typescript-eslint/no-use-before-define": 2, 122 | "@typescript-eslint/no-var-requires": 0, 123 | "@typescript-eslint/no-empty-function": 0, 124 | "no-shadow": 0, 125 | "no-continue": 0, 126 | "no-loop-func": 0, 127 | "prefer-spread": 0, 128 | "react-hooks/rules-of-hooks": "error", 129 | "react-hooks/exhaustive-deps": "warn", 130 | "no-undef": 0 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .vscode 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .eslintcache 25 | 26 | /Server/node_modules 27 | /Server/log 28 | /log 29 | yarn.lock -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry = 'https://registry.npmjs.org/' 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "semi": true, 4 | "singleQuote": true, 5 | "jsxSingleQuote": false, 6 | "printWidth": 200, 7 | "useTabs": false, 8 | "tabWidth": 2, 9 | "trailingComma": "es5" 10 | } 11 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["stylelint-config-standard", "stylelint-config-prettier"], 3 | "customSyntax": "postcss-less", 4 | "rules": { 5 | "no-descending-specificity": null, 6 | "no-duplicate-selectors": null, 7 | "font-family-no-missing-generic-family-keyword": null, 8 | "block-opening-brace-space-before": "always", 9 | "declaration-block-trailing-semicolon": null, 10 | "declaration-colon-newline-after": null, 11 | "indentation": null, 12 | "selector-descendant-combinator-no-non-space": null, 13 | "selector-class-pattern": null, 14 | "keyframes-name-pattern": null, 15 | "no-invalid-position-at-import-rule": null, 16 | "number-max-precision": 6, 17 | "color-function-notation": null, 18 | "selector-pseudo-class-no-unknown": [ 19 | true, 20 | { 21 | "ignorePseudoClasses": ["global"] 22 | } 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | registry="https://registry.yarnpkg.com" 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 2 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 3 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 4 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 5 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 6 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 7 | ====== 8 | ====== 9 | THE FOLLOWING SETS FORTH ATTRIBUTION NOTICES FOR THIRD PARTY SOFTWARE THAT MAY BE CONTAINED IN PORTIONS OF RTC AIGC DEMO. 10 | ====== 11 | WebRTC 12 | Copyright (c) 2011, The WebRTC project authors. All rights reserved. 13 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 14 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 15 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 16 | 3. Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 18 | ====== -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 交互式AIGC场景 AIGC Demo 2 | 3 | ## 简介 4 | - 在 AIGC 对话场景下,火山引擎 AIGC-RTC Server 云端服务,通过整合 RTC 音视频流处理,ASR 语音识别,大模型接口调用集成,以及 TTS 语音生成等能力,提供基于流式语音的端到端AIGC能力链路。 5 | - 用户只需调用基于标准的 OpenAPI 接口即可配置所需的 ASR、LLM、TTS 类型和参数。火山引擎云端计算服务负责边缘用户接入、云端资源调度、音视频流压缩、文本与语音转换处理以及数据订阅传输等环节。简化开发流程,让开发者更专注在对大模型核心能力的训练及调试,从而快速推进AIGC产品应用创新。 6 | - 同时火山引擎 RTC拥有成熟的音频 3A 处理、视频处理等技术以及大规模音视频聊天能力,可支持 AIGC 产品更便捷的支持多模态交互、多人互动等场景能力,保持交互的自然性和高效性。 7 | 8 | ## 【必看】环境准备 9 | - **Node 版本: 16.0+** 10 | 1. 需要准备两个 Terminal,分别启动服务端、前端页面。 11 | 2. 开通 ASR、TTS、LLM、RTC 等服务,可参考 [开通服务](https://www.volcengine.com/docs/6348/1315561?s=g) 进行相关服务的授权与开通。 12 | 3. **根据你自定义的 13 | RoomId、UserId 以及申请的 AppID、BusinessID(如有)、Token、ASR AppID、TTS AppID,修改 `src/config/config.ts` 文件中 `ConfigFactory` 中 `BaseConfig` 的配置信息**。 14 | 4. 使用火山引擎控制台账号的 [AK、SK](https://console.volcengine.com/iam/keymanage?s=g), 修改 `Server/app.js` 文件中的 `ACCOUNT_INFO`。 15 | 5. 若您使用的是官方模型, 需要在 [火山方舟-在线推理](https://console.volcengine.com/ark/region:ark+cn-beijing/endpoint?config=%7B%7D&s=g) 中创建接入点, 并将模型对应的接入点 ID 填入 `src/config/common.ts` 文件中的 `ARK_V3_MODEL_ID`, 否则无法正常启动智能体。 16 | 6. 如果您已经自行完成了服务端的逻辑,可以不依赖 Demo 中的 Server,直接修改前端代码文件 `src/config/index.ts` 中的 `AIGC_PROXY_HOST` 请求域名和接口,并在 `src/app/api.ts` 中修改接口的参数配置 `APIS_CONFIG`。 17 | 18 | ## 快速开始 19 | 请注意,服务端和 Web 端都需要启动, 启动步骤如下: 20 | ### 服务端 21 | 进到项目根目录 22 | #### 安装依赖 23 | ```shell 24 | cd Server 25 | yarn 26 | ``` 27 | #### 运行项目 28 | ```shell 29 | node app.js 30 | ``` 31 | 32 | ### 前端页面 33 | 进到项目根目录 34 | #### 安装依赖 35 | ```shell 36 | yarn 37 | ``` 38 | #### 运行项目 39 | ```shell 40 | yarn dev 41 | ``` 42 | 43 | ### 常见问题 44 | | 问题 | 解决方案 | 45 | | :-- | :-- | 46 | | 如何使用第三方模型、Coze Bot | 点击页面上的 "修改 AI 设定" 进入配置页,可切换 官方模型/Coze/第三方模型,填写对应参数即可,相关代码对应 `src/components/AISettings/index.tsx` 文件。 | 47 | | **启动智能体之后, 对话无反馈,或者一直停留在 "AI 准备中, 请稍侯"** |
  • 可能因为控制台中相关权限没有正常授予,请参考[流程](https://www.volcengine.com/docs/6348/1315561?s=g)再次确认下是否完成相关操作。此问题的可能性较大,建议仔细对照是否已经将相应的权限开通。
  • 参数传递可能有问题, 例如参数大小写、类型等问题,请再次确认下这类型问题是否存在。
  • 相关资源可能未开通或者用量不足/欠费,请再次确认。
  • **请检查当前使用的模型 ID 等内容都是正确且可用的。**
  • | 48 | | **浏览器报了 `Uncaught (in promise) r: token_error` 错误** | 请检查您填在项目中的 RTC Token 是否合法,检测用于生成 Token 的 UserId、RoomId 以及 Token 本身是否与项目中填写的一致;或者 Token 可能过期, 可尝试重新生成下。 | 49 | | **[StartVoiceChat]Failed(Reason: The task has been started. Please do not call the startup task interface repeatedly.)** 报错 | 由于目前设置的 RoomId、UserId 为固定值,重复调用 startAudioBot 会导致出错,只需先调用 stopAudioBot 后再重新 startAudioBot 即可。 | 50 | | 为什么我的麦克风正常、摄像头也正常,但是设备没有正常工作? | 可能是设备权限未授予,详情可参考 [Web 排查设备权限获取失败问题](https://www.volcengine.com/docs/6348/1356355?s=g)。 | 51 | | 接口调用时, 返回 "Invalid 'Authorization' header, Pls check your authorization header" 错误 | `Server/app.js` 中的 AK/SK 不正确 | 52 | | 什么是 RTC | **R**eal **T**ime **C**ommunication, RTC 的概念可参考[官网文档](https://www.volcengine.com/docs/6348/66812?s=g)。 | 53 | | 不清楚什么是主账号,什么是子账号 | 可以参考[官方概念](https://www.volcengine.com/docs/6257/64963?hyperlink_open_type=lark.open_in_browser&s=g) 。| 54 | 55 | 如果有上述以外的问题,欢迎联系我们反馈。 56 | 57 | ### 相关文档 58 | - [场景介绍](https://www.volcengine.com/docs/6348/1310537?s=g) 59 | - [Demo 体验](https://www.volcengine.com/docs/6348/1310559?s=g) 60 | - [场景搭建方案](https://www.volcengine.com/docs/6348/1310560?s=g) 61 | 62 | ## 更新日志 63 | 64 | ### OpenAPI 更新 65 | 参考 [OpenAPI 更新](https://www.volcengine.com/docs/6348/116363?s=g) 中与 实时对话式 AI 相关的更新内容。 66 | 67 | ### Demo 更新 68 | 69 | #### [1.6.0] 70 | - 2025-05-28 71 | - 更新 RTC Web SDK 版本至 4.66.14 72 | - 2025-05-22 73 | - 更新 RTC Web SDK 版本至 4.66.13 74 | - 删除无用依赖 75 | - 更新 Readme 文档 76 | - 2025-04-16 77 | - 支持 Coze Bot 78 | - 更新部分注释和文档内容 79 | - 删除子账号的 SessionToken 配置, 子账号调用无须 SessionToken 80 | - 修复通话前修改内容,在通话后配置消失的问题 81 | 82 | #### [1.5.1] 83 | - 2025-04-11 84 | - 移除无用代码和依赖 85 | - 修复字幕逻辑 86 | 87 | #### [1.5.0] 88 | - 2025-03-31 89 | - 修复部分 UI 问题 90 | - 追加屏幕共享能力 (视觉模型可用,**读屏助手** 人设下可使用) 91 | - 修改字幕逻辑,避免字幕回调中标点符号、大小写不一致引起的字幕重复问题 92 | - 更新 RTC Web SDK 版本至 4.66.1 93 | - 追加设备权限未授予时的提示 -------------------------------------------------------------------------------- /Server/.npmrc: -------------------------------------------------------------------------------- 1 | registry = 'https://registry.npmjs.org/' -------------------------------------------------------------------------------- /Server/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | const Koa = require('koa'); 7 | const bodyParser = require('koa-bodyparser'); 8 | const cors = require('koa2-cors'); 9 | const { Signer } = require('@volcengine/openapi'); 10 | const fetch = require('node-fetch'); 11 | 12 | const app = new Koa(); 13 | 14 | app.use(cors({ 15 | origin: '*' 16 | })); 17 | 18 | /** 19 | * @notes 在 https://console.volcengine.com/iam/keymanage/ 获取 AK/SK 20 | */ 21 | const ACCOUNT_INFO = { 22 | /** 23 | * @notes 必填, 在 https://console.volcengine.com/iam/keymanage/ 获取 24 | */ 25 | accessKeyId: 'Your AK', 26 | /** 27 | * @notes 必填, 在 https://console.volcengine.com/iam/keymanage/ 获取 28 | */ 29 | secretKey: 'Your SK', 30 | } 31 | 32 | app.use(bodyParser()); 33 | 34 | 35 | app.use(async ctx => { 36 | /** 37 | * @brief 代理 AIGC 的 OpenAPI 请求 38 | */ 39 | if (ctx.url.startsWith('/proxyAIGCFetch') && ctx.method.toLowerCase() === 'post') { 40 | const { Action, Version } = ctx.query || {}; 41 | const body = ctx.request.body; 42 | 43 | /** 44 | * 参考 https://github.com/volcengine/volc-sdk-nodejs 可获取更多 火山 TOP 网关 SDK 的使用方式 45 | */ 46 | const openApiRequestData = { 47 | region: 'cn-north-1', 48 | method: 'POST', 49 | params: { 50 | Action, 51 | Version, 52 | }, 53 | headers: { 54 | Host: 'rtc.volcengineapi.com', 55 | 'Content-type': 'application/json', 56 | }, 57 | body, 58 | }; 59 | const signer = new Signer(openApiRequestData, "rtc"); 60 | signer.addAuthorization(ACCOUNT_INFO); 61 | 62 | /** 参考 https://www.volcengine.com/docs/6348/69828 可获取更多 OpenAPI 的信息 */ 63 | const result = await fetch(`https://rtc.volcengineapi.com?Action=${Action}&Version=${Version}`, { 64 | method: 'POST', 65 | headers: openApiRequestData.headers, 66 | body: JSON.stringify(body), 67 | }); 68 | const volcResponse = await result.json(); 69 | ctx.body = volcResponse; 70 | } else { 71 | ctx.body = '

    404 Not Found

    '; 72 | } 73 | }); 74 | 75 | app.listen(3001, () => { 76 | console.log('AIGC Server is running at http://localhost:3001'); 77 | }); 78 | 79 | -------------------------------------------------------------------------------- /Server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "AIGCServer", 3 | "version": "1.0.0", 4 | "description": "Server for demo to call open api", 5 | "main": "app.js", 6 | "license": "BSD-3-Clause", 7 | "private": true, 8 | "dependencies": { 9 | "@volcengine/openapi": "^1.22.0", 10 | "koa": "^2.15.3", 11 | "koa-bodyparser": "^4.4.1", 12 | "koa2-cors": "^2.0.6", 13 | "node-fetch": "^2.3.2" 14 | }, 15 | "scripts": { 16 | "dev": "node app.js" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /craco.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | const CracoLessPlugin = require('craco-less'); 7 | const path = require('path'); 8 | 9 | module.exports = { 10 | webpack: { 11 | alias: { 12 | '@': path.resolve(__dirname, 'src'), 13 | }, 14 | }, 15 | plugins: [ 16 | { 17 | plugin: CracoLessPlugin, 18 | options: { 19 | lessLoaderOptions: { 20 | lessOptions: { 21 | javascriptEnabled: true, 22 | }, 23 | }, 24 | }, 25 | }, 26 | ], 27 | }; 28 | -------------------------------------------------------------------------------- /message.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | const reset = '\x1b[0m'; 7 | const bright = '\x1b[1m'; 8 | const green = '\x1b[32m'; 9 | 10 | console.log(`${bright}${bright}===================================================`); 11 | console.log(`${bright}${green}| 请查看目录下的 README.md 内容, 否则启动可能失败 |`); 12 | console.log(`${bright}${reset}===================================================${reset}`); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aigc", 3 | "version": "1.6.0", 4 | "license": "BSD-3-Clause", 5 | "private": true, 6 | "dependencies": { 7 | "@reduxjs/toolkit": "^1.8.3", 8 | "@volcengine/rtc": "~4.66.14", 9 | "@arco-design/web-react": "^2.65.0", 10 | "react": "^18.2.0", 11 | "react-dom": "^18.2.0", 12 | "react-redux": "^8.0.2", 13 | "react-router": "^6.3.0", 14 | "react-router-dom": "^6.3.0", 15 | "redux": "^4.2.0", 16 | "uuid": "^8.3.2" 17 | }, 18 | "scripts": { 19 | "dev": "npm run echo && npm run start", 20 | "start": "cross-env REACT_APP_LOCAL=cn craco start", 21 | "server:start": "node Server/app.js", 22 | "build": "craco build", 23 | "test": "craco test", 24 | "eject": "react-scripts eject", 25 | "prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'", 26 | "eslint": "eslint src/ --fix --cache --quiet --ext .js,.jsx,.ts,.tsx", 27 | "stylelint": "stylelint 'src/**/*.less' --fix", 28 | "pre-commit": "npm run eslint && npm run stylelint", 29 | "echo": "node message.js" 30 | }, 31 | "eslintConfig": { 32 | "extends": [ 33 | "react-app", 34 | "react-app/jest" 35 | ] 36 | }, 37 | "browserslist": { 38 | "production": [ 39 | ">0.2%", 40 | "not dead", 41 | "not op_mini all" 42 | ], 43 | "development": [ 44 | "last 1 chrome version", 45 | "last 1 firefox version", 46 | "last 1 safari version" 47 | ] 48 | }, 49 | "devDependencies": { 50 | "@craco/craco": "^6.4.5", 51 | "@types/lodash": "^4.17.4", 52 | "@types/node": "^16.11.45", 53 | "@types/react": "^18.0.15", 54 | "@types/react-dom": "^18.0.6", 55 | "@types/react-helmet": "^6.1.11", 56 | "@types/uuid": "^8.3.4", 57 | "craco-less": "^2.0.0", 58 | "cross-env": "^7.0.3", 59 | "eslint-config-airbnb": "^19.0.4", 60 | "eslint-config-prettier": "^8.5.0", 61 | "eslint-plugin-babel": "^5.3.1", 62 | "eslint-plugin-prettier": "^4.2.1", 63 | "postcss-less": "^6.0.0", 64 | "prettier": "^2.7.1", 65 | "react-scripts": "5.0.1", 66 | "stylelint": "^14.9.1", 67 | "stylelint-config-prettier": "^9.0.3", 68 | "stylelint-config-standard": "^26.0.0", 69 | "typescript": "^4.7.4" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volcengine/rtc-aigc-demo/64c1a16d13c959c8b806e3dbda5fd0219e60675a/public/favicon.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 22 | RTC AIGC Demo 23 | 24 | 25 | 26 |
    27 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | import { BrowserRouter, Routes, Route } from 'react-router-dom'; 6 | import MainPage from './pages/MainPage'; 7 | import '@arco-design/web-react/dist/css/arco.css'; 8 | 9 | function App() { 10 | console.warn('运行问题可参考 README 内容进行排查'); 11 | return ( 12 | 13 | 14 | 15 | } /> 16 | } /> 17 | 18 | 19 | 20 | ); 21 | } 22 | 23 | export default App; 24 | -------------------------------------------------------------------------------- /src/app/api.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | import { requestGetMethod, requestPostMethod, resultHandler } from './base'; 7 | import { ACTIONS, RequestParams, RequestResponse } from './type'; 8 | 9 | const APIS_CONFIG = [ 10 | { 11 | action: ACTIONS.StartVoiceChat, 12 | apiBasicParams: `?Name=start&Action=${ACTIONS.StartVoiceChat}&Version=2024-12-01`, 13 | method: 'post', 14 | }, 15 | { 16 | action: ACTIONS.UpdateVoiceChat, 17 | apiBasicParams: `?Name=update&Action=${ACTIONS.UpdateVoiceChat}&Version=2024-12-01`, 18 | method: 'post', 19 | }, 20 | { 21 | action: ACTIONS.StopVoiceChat, 22 | apiBasicParams: `?Name=stop&Action=${ACTIONS.StopVoiceChat}&Version=2024-12-01`, 23 | method: 'post', 24 | }, 25 | ] as const; 26 | 27 | type ApiConfig = typeof APIS_CONFIG; 28 | type TupleToUnion = T[number]; 29 | type ApiNames = Pick, 'action'>['action']; 30 | type RequestFn = (params?: RequestParams[T]) => RequestResponse[T]; 31 | type PromiseRequestFn = ( 32 | params?: RequestParams[T] 33 | ) => Promise; 34 | type Apis = Record; 35 | 36 | const APIS = APIS_CONFIG.reduce((store, cur) => { 37 | const { action, apiBasicParams, method = 'get' } = cur; 38 | store[action] = async (params) => { 39 | const queryData = 40 | method === 'get' 41 | ? await requestGetMethod(apiBasicParams)(params) 42 | : await requestPostMethod(apiBasicParams)(params); 43 | const res = await queryData?.json(); 44 | return resultHandler(res); 45 | }; 46 | return store; 47 | }, {} as Apis); 48 | 49 | export default APIS; 50 | -------------------------------------------------------------------------------- /src/app/base.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | import { Modal } from '@arco-design/web-react'; 7 | import { AIGC_PROXY_HOST } from '@/config'; 8 | 9 | type Headers = Record; 10 | 11 | /** 12 | * @brief Get 13 | * @param apiName 14 | * @param headers 15 | */ 16 | export const requestGetMethod = (apiBasicParams: string, headers = {}) => { 17 | return async (params: Record = {}) => { 18 | const url = `${AIGC_PROXY_HOST}${apiBasicParams}&${Object.keys(params) 19 | .map((key) => `${key}=${params[key]}`) 20 | .join('&')}`; 21 | const res = await fetch(url, { 22 | headers: { 23 | ...headers, 24 | }, 25 | }); 26 | return res; 27 | }; 28 | }; 29 | 30 | /** 31 | * @brief Post 32 | * @param apiName 33 | * @param isJson 34 | * @param headers 35 | */ 36 | export const requestPostMethod = ( 37 | apiBasicParams: string, 38 | isJson: boolean = true, 39 | headers: Headers = {} 40 | ) => { 41 | return async (params: T) => { 42 | const res = await fetch(`${AIGC_PROXY_HOST}${apiBasicParams}`, { 43 | method: 'post', 44 | headers: { 45 | 'content-type': 'application/json', 46 | ...headers, 47 | }, 48 | body: (isJson ? JSON.stringify(params) : params) as BodyInit, 49 | }); 50 | return res; 51 | }; 52 | }; 53 | 54 | /** 55 | * @brief Handler 56 | * @param res 57 | */ 58 | export const resultHandler = (res: any) => { 59 | const { Result, ResponseMetadata } = res || {}; 60 | if (Result === 'ok') { 61 | return Result; 62 | } 63 | const error = ResponseMetadata?.Error?.Message || Result; 64 | Modal.error({ 65 | title: '接口调用错误', 66 | content: `[${ResponseMetadata?.Action}]Failed(Reason: ${error}), 请参考 README 文档排查问题。`, 67 | }); 68 | }; 69 | -------------------------------------------------------------------------------- /src/app/type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | export enum ACTIONS { 7 | StartVoiceChat = 'StartVoiceChat', 8 | UpdateVoiceChat = 'UpdateVoiceChat', 9 | StopVoiceChat = 'StopVoiceChat', 10 | } 11 | 12 | /** 13 | * @brief 请求参数类型 14 | * @note OpenAPI 接口参数结构可能更新, 请参阅最新文档内容。 15 | * https://www.volcengine.com/docs/6348/1404673?s=g 16 | */ 17 | export interface RequestParams { 18 | /** 19 | * @brief 通过接口开启数字人,使用前需要开 ASR、LLM、TTS 等服务。 20 | */ 21 | [ACTIONS.StartVoiceChat]: { 22 | AppId: string; 23 | BusinessId?: string; 24 | RoomId: string; 25 | TaskId: string; 26 | Config: Partial<{ 27 | BotName: string; 28 | ASRConfig: { 29 | AppId: string; 30 | Cluster?: string; 31 | }; 32 | TTSConfig: Partial<{ 33 | AppId: string; 34 | VoiceType: string; 35 | Cluster?: string; 36 | IgnoreBracketText?: number[]; 37 | }>; 38 | LLMConfig: Partial<{ 39 | AppId: string; 40 | ModelName?: string; 41 | ModelVersion: string; 42 | Mode?: string; 43 | Host?: string; 44 | Region?: string; 45 | MaxTokens?: number; 46 | MinTokens?: number; 47 | Temperature?: number; 48 | TopP?: number; 49 | TopK?: number; 50 | MaxPromptTokens?: number; 51 | SystemMessages?: string[]; 52 | UserMessages?: string[]; 53 | HistoryLength?: number; 54 | WelcomeSpeech?: string; 55 | EndPointId?: string; 56 | BotId?: string; 57 | }>; 58 | }>; 59 | /** 60 | * @brief 智能体基本配置。 61 | */ 62 | AgentConfig: { 63 | TargetUserId: string[]; 64 | WelcomeMessage: string; 65 | UserId: string; 66 | EnableConversationStateCallback?: boolean; 67 | ServerMessageSignatureForRTS?: string; 68 | ServerMessageURLForRTS?: string; 69 | }; 70 | }; 71 | /** 72 | * @brief 控制数字人行为,目前支持行为见 Command 参数。 73 | */ 74 | [ACTIONS.UpdateVoiceChat]: { 75 | AppId: string; 76 | BusinessId?: string; 77 | RoomId: string; 78 | TaskId: string; 79 | Command: string; 80 | Message?: string; 81 | }; 82 | /** 83 | * @brief 关闭数字人任务。 84 | */ 85 | [ACTIONS.StopVoiceChat]: { 86 | AppId: string; 87 | BusinessId?: string; 88 | RoomId: string; 89 | TaskId: string; 90 | }; 91 | } 92 | 93 | /** 94 | * @brief 返回参数类型 95 | */ 96 | export interface RequestResponse { 97 | [ACTIONS.StartVoiceChat]: string; 98 | [ACTIONS.UpdateVoiceChat]: string; 99 | [ACTIONS.StopVoiceChat]: string; 100 | } 101 | 102 | export type DeepPartial = { 103 | [P in keyof T]?: T[P] extends Array 104 | ? Array> 105 | : T[P] extends object 106 | ? DeepPartial 107 | : T[P]; 108 | }; 109 | -------------------------------------------------------------------------------- /src/assets/img/CHILDREN_ENCYCLOPEDIA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volcengine/rtc-aigc-demo/64c1a16d13c959c8b806e3dbda5fd0219e60675a/src/assets/img/CHILDREN_ENCYCLOPEDIA.png -------------------------------------------------------------------------------- /src/assets/img/CUSTOMER_SERVICE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volcengine/rtc-aigc-demo/64c1a16d13c959c8b806e3dbda5fd0219e60675a/src/assets/img/CUSTOMER_SERVICE.png -------------------------------------------------------------------------------- /src/assets/img/CallWrapper.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/assets/img/CameraClose.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/img/CameraCloseNote.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/assets/img/CameraOpen.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/img/Checked.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/img/Close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/img/Dot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/assets/img/DoubaoAvatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volcengine/rtc-aigc-demo/64c1a16d13c959c8b806e3dbda5fd0219e60675a/src/assets/img/DoubaoAvatar.png -------------------------------------------------------------------------------- /src/assets/img/DoubaoAvatarGIF.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volcengine/rtc-aigc-demo/64c1a16d13c959c8b806e3dbda5fd0219e60675a/src/assets/img/DoubaoAvatarGIF.webp -------------------------------------------------------------------------------- /src/assets/img/INTELLIGENT_ASSISTANT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volcengine/rtc-aigc-demo/64c1a16d13c959c8b806e3dbda5fd0219e60675a/src/assets/img/INTELLIGENT_ASSISTANT.png -------------------------------------------------------------------------------- /src/assets/img/LeaveRoom.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/img/Logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/assets/img/MicClose.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/img/MicOpen.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/assets/img/MicrophoneOff.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/img/MicrophoneOn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/img/ModelChange.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/assets/img/MsgBubble.svg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volcengine/rtc-aigc-demo/64c1a16d13c959c8b806e3dbda5fd0219e60675a/src/assets/img/MsgBubble.svg -------------------------------------------------------------------------------- /src/assets/img/Phone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/img/SCREEN_READER.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volcengine/rtc-aigc-demo/64c1a16d13c959c8b806e3dbda5fd0219e60675a/src/assets/img/SCREEN_READER.png -------------------------------------------------------------------------------- /src/assets/img/ScreenCloseNote.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/img/ScreenOff.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/img/ScreenOn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/img/Setting.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/img/Stop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/img/StopRobotBtn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/img/StopWave.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volcengine/rtc-aigc-demo/64c1a16d13c959c8b806e3dbda5fd0219e60675a/src/assets/img/StopWave.jpeg -------------------------------------------------------------------------------- /src/assets/img/TEACHER.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volcengine/rtc-aigc-demo/64c1a16d13c959c8b806e3dbda5fd0219e60675a/src/assets/img/TEACHER.png -------------------------------------------------------------------------------- /src/assets/img/TEACHING_ASSISTANT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volcengine/rtc-aigc-demo/64c1a16d13c959c8b806e3dbda5fd0219e60675a/src/assets/img/TEACHING_ASSISTANT.png -------------------------------------------------------------------------------- /src/assets/img/TRANSLATE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volcengine/rtc-aigc-demo/64c1a16d13c959c8b806e3dbda5fd0219e60675a/src/assets/img/TRANSLATE.png -------------------------------------------------------------------------------- /src/assets/img/VIRTUAL_GIRL_FRIEND.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volcengine/rtc-aigc-demo/64c1a16d13c959c8b806e3dbda5fd0219e60675a/src/assets/img/VIRTUAL_GIRL_FRIEND.png -------------------------------------------------------------------------------- /src/assets/img/VoiceTypeChange.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/assets/img/huoponvsheng.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volcengine/rtc-aigc-demo/64c1a16d13c959c8b806e3dbda5fd0219e60675a/src/assets/img/huoponvsheng.jpeg -------------------------------------------------------------------------------- /src/assets/img/jingqiangkanye.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volcengine/rtc-aigc-demo/64c1a16d13c959c8b806e3dbda5fd0219e60675a/src/assets/img/jingqiangkanye.jpeg -------------------------------------------------------------------------------- /src/assets/img/magicTool.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/img/menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/img/tongyongnansheng.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volcengine/rtc-aigc-demo/64c1a16d13c959c8b806e3dbda5fd0219e60675a/src/assets/img/tongyongnansheng.jpeg -------------------------------------------------------------------------------- /src/assets/img/tongyongnvsheng.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volcengine/rtc-aigc-demo/64c1a16d13c959c8b806e3dbda5fd0219e60675a/src/assets/img/tongyongnvsheng.jpeg -------------------------------------------------------------------------------- /src/assets/img/wankuqingnian.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volcengine/rtc-aigc-demo/64c1a16d13c959c8b806e3dbda5fd0219e60675a/src/assets/img/wankuqingnian.jpeg -------------------------------------------------------------------------------- /src/assets/img/wanwanxiaohe.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volcengine/rtc-aigc-demo/64c1a16d13c959c8b806e3dbda5fd0219e60675a/src/assets/img/wanwanxiaohe.jpeg -------------------------------------------------------------------------------- /src/assets/img/wennuanahu.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volcengine/rtc-aigc-demo/64c1a16d13c959c8b806e3dbda5fd0219e60675a/src/assets/img/wennuanahu.jpeg -------------------------------------------------------------------------------- /src/components/AISettings/index.module.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | .container { 7 | padding: 16px 8px; 8 | background: linear-gradient(0deg, #F0F2FF 0%, #E0E4FF 100%); 9 | 10 | :global { 11 | .arco-drawer-scroll { 12 | .arco-drawer-content { 13 | overflow-x: hidden; 14 | overflow-y: auto; 15 | 16 | scrollbar-width: thin; /* 设置滚动条宽度为细 */ 17 | scrollbar-color: rgba(0, 0, 0, 0) rgba(0, 0, 0, 0); /* 设置滚动条和轨道的颜色 */ 18 | } 19 | 20 | ::-webkit-scrollbar { 21 | width: 0px; 22 | height: 0px; 23 | } 24 | 25 | ::-webkit-scrollbar-thumb { 26 | background: rgba(0,0,0,0); 27 | border-radius: 0px; 28 | } 29 | 30 | ::-webkit-scrollbar-track { 31 | background: rgba(0,0,0,0); 32 | border-radius: 0px; 33 | } 34 | } 35 | } 36 | 37 | .title { 38 | font-size: 20px; 39 | font-weight: 500; 40 | line-height: 28px; 41 | 42 | .special-text { 43 | background: linear-gradient(90deg, #004FFF 38.86%, #9865FF 100%); 44 | -webkit-background-clip: text; 45 | background-clip: text; 46 | color: transparent; 47 | } 48 | } 49 | 50 | .sub-title { 51 | font-size: 12px; 52 | font-weight: 400; 53 | line-height: 20px; 54 | color: var(--text-color-text-3, rgba(115, 122, 135, 1)); 55 | margin-top: 6px; 56 | } 57 | 58 | .scenes { 59 | width: 100%; 60 | display: flex; 61 | flex-direction: row; 62 | gap: 14px; 63 | margin-top: 32px; 64 | } 65 | 66 | .scenes-mobile { 67 | width: 100%; 68 | display: flex; 69 | flex-direction: row; 70 | flex-wrap: wrap; 71 | justify-content: center; 72 | align-items: center; 73 | gap: 14px; 74 | margin-top: 32px; 75 | overflow-x: auto; 76 | padding-bottom: 8px; 77 | } 78 | 79 | .configuration { 80 | position: relative; 81 | min-height: calc(100% - 300px); 82 | height: max-content; 83 | width: 100%; 84 | background: white; 85 | box-sizing: border-box; 86 | padding: 32px 24px; 87 | margin-top: 24px; 88 | margin-bottom: 12px; 89 | border-radius: 12px; 90 | display: flex; 91 | flex-direction: column; 92 | gap: 36px; 93 | 94 | .ai-settings-radio { 95 | display: flex; 96 | flex-direction: row; 97 | justify-content: flex-end; 98 | } 99 | 100 | .anchor { 101 | position: absolute; 102 | border-bottom: 12px solid white; 103 | border-left: 12px solid transparent; 104 | border-right: 12px solid transparent; 105 | top: 0px; 106 | transform: translate(-50%, -99%); 107 | } 108 | 109 | .ai-settings { 110 | width: 100%; 111 | display: flex; 112 | flex-direction: row; 113 | margin-top: -16px; 114 | gap: 24px; 115 | 116 | .ai-settings-wrapper { 117 | display: flex; 118 | width: 100%; 119 | flex-direction: row; 120 | justify-content: space-between; 121 | align-items: center; 122 | } 123 | 124 | .ai-settings-model { 125 | width: 100%; 126 | display: flex; 127 | flex-direction: column; 128 | align-items: flex-end; 129 | gap: 12px; 130 | } 131 | } 132 | 133 | :global { 134 | .arco-textarea { 135 | background: white !important; 136 | width: 100%; 137 | height: max-content; 138 | } 139 | .arco-textarea:focus { 140 | outline: none !important; 141 | } 142 | } 143 | 144 | textarea { 145 | border-radius: 4px; 146 | resize: none; 147 | -webkit-resizer: none; 148 | border: 0px; 149 | outline: none; 150 | box-shadow: none; 151 | } 152 | 153 | textarea:focus { 154 | border: 0px; 155 | outline: none; 156 | box-shadow: none; 157 | } 158 | } 159 | } 160 | 161 | 162 | .footer { 163 | width: calc(100% - 12px); 164 | display: flex; 165 | flex-direction: row; 166 | justify-content: flex-end; 167 | align-items: center; 168 | padding-bottom: 16px; 169 | gap: 12px; 170 | 171 | .suffix { 172 | font-size: 12px; 173 | font-weight: 400; 174 | line-height: 20px; 175 | margin-right: 12px; 176 | color: var(--text-color-text-3, rgba(115, 122, 135, 1)); 177 | } 178 | 179 | .cancel { 180 | width: 88px; 181 | height: 32px; 182 | border-radius: 6px; 183 | border: 1px solid var(--line-color-border-3, rgba(221, 226, 233, 1)); 184 | background-color: white !important; 185 | } 186 | 187 | .confirm { 188 | width: 88px; 189 | height: 32px; 190 | border-radius: 6px; 191 | background: linear-gradient(95.87deg, #1664FF 0%, #8040FF 97.7%); 192 | color: white !important; 193 | } 194 | 195 | .confirm:hover { 196 | opacity: .8; 197 | } 198 | 199 | .confirm:active { 200 | opacity: 1; 201 | } 202 | } -------------------------------------------------------------------------------- /src/components/AvatarCard/index.module.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | .card { 7 | display: grid; 8 | position: relative; 9 | width: 370px; 10 | height: 128px; 11 | 12 | .avatar { 13 | position: absolute; 14 | box-sizing: border-box; 15 | border-radius: 50% 0% 0 50%; 16 | width: 128px; 17 | height: 128px; 18 | margin-right: 16px; 19 | border-left: 1px solid #EAEDF1; 20 | border-top: 1px solid #EAEDF1; 21 | border-bottom: 1px solid #EAEDF1; 22 | background-color: white; 23 | z-index: 2; 24 | display: flex; 25 | flex-direction: row; 26 | justify-content: center; 27 | align-items: center; 28 | 29 | .doubao-gif { 30 | height: 127px; 31 | transform: scale(0.95); 32 | } 33 | } 34 | 35 | .body { 36 | width: 100%; 37 | height: 100%; 38 | padding: 16px 16px 16px calc(64px + 16px); 39 | position: relative; 40 | display: flex; 41 | align-items: center; 42 | border: 1px solid var(--line-color-border-2, #EAEDF1); 43 | box-sizing: border-box; 44 | box-shadow: 0px 2px 6px 0px rgba(0, 0, 0, 0.05); 45 | transform:translateX(64px); 46 | } 47 | 48 | .body::after { 49 | content: ''; 50 | position: absolute; 51 | top: -1px; 52 | right: -1px; 53 | width: 20px; 54 | height: 20px; 55 | background-color: white; 56 | clip-path: polygon(0 0, 100% 0, 100% 100%); 57 | } 58 | 59 | .body::before { 60 | content: ''; 61 | position: absolute; 62 | top: 0px; 63 | right: 0px; 64 | width: 20px; 65 | height: 20px; 66 | background-color: #EAEDF1; 67 | clip-path: polygon(0 0, 100% 0, 100% 100%); 68 | } 69 | 70 | .text-wrapper { 71 | position: absolute; 72 | left: 128px; 73 | margin-left: 16px; 74 | width: max-content; 75 | height: 100%; 76 | display: flex; 77 | flex-direction: column; 78 | justify-content: center; 79 | z-index: 4; 80 | 81 | .user-info { 82 | display: flex; 83 | flex-direction: column; 84 | justify-content: center; 85 | 86 | .title { 87 | color: var(--text-color-text-1, #0C0D0E); 88 | font-size: 14px; 89 | font-weight: 500; 90 | line-height: 22px; 91 | } 92 | 93 | .description { 94 | font-size: 12px; 95 | font-weight: 400; 96 | line-height: 20px; 97 | color: #737A87; 98 | } 99 | } 100 | } 101 | 102 | .corner { 103 | position: absolute; 104 | top: -6px; 105 | right: -6px; 106 | width: 0px; 107 | height: 0px; 108 | border-right: 10px solid transparent; 109 | border-top: 10px solid transparent; 110 | border-bottom: 10px solid transparent; 111 | border-left: 10px solid #EAEDF1; 112 | z-index: 3; 113 | transform: translateX(64px) rotate(-45deg); 114 | } 115 | 116 | .corner::before { 117 | content: ''; 118 | position: absolute; 119 | top: 0px; 120 | right: 0px; 121 | width: 0px; 122 | height: 0px; 123 | border-right: 8px solid transparent; 124 | border-top: 8px solid transparent; 125 | border-bottom: 8px solid transparent; 126 | border-left: 8px solid white; 127 | transform: translate(7px, -8px); 128 | } 129 | 130 | .corner::after { 131 | content: ''; 132 | position: absolute; 133 | top: 0px; 134 | right: 4px; 135 | width: 5px; 136 | height: 1px; 137 | background-color: #EAEDF1; 138 | transform: rotate(-90deg); 139 | } 140 | } 141 | 142 | .button { 143 | position: relative; 144 | width: max-content !important; 145 | height: 24px !important; 146 | margin-top: 8px; 147 | border-radius: 4px !important; 148 | font-size: 12px !important; 149 | background: linear-gradient(77.86deg, rgba(229, 242, 255, 0.5) -3.23%, rgba(217, 229, 255, 0.5) 51.11%, rgba(246, 226, 255, 0.5) 98.65%); 150 | cursor: pointer; 151 | 152 | .button-text { 153 | background: linear-gradient(77.86deg, #3384FF -3.23%, #014BDE 51.11%, #A945FB 98.65%); 154 | -webkit-background-clip: text; 155 | background-clip: text; 156 | color: transparent; 157 | font-weight: 500; 158 | line-height: 20px; 159 | text-align: center; 160 | } 161 | } 162 | 163 | .button::after { 164 | content: ''; 165 | position: absolute; 166 | border-radius: 3px; 167 | top: 0px; 168 | left: 0px; 169 | width: 100%; 170 | height: 22px; 171 | background: white; 172 | z-index: -1; 173 | } 174 | 175 | .button::before { 176 | content: ''; 177 | position: absolute; 178 | border-radius: 5px; 179 | top: -2px; 180 | left: -2px; 181 | width: calc(100% + 4px); 182 | height: 26px; 183 | background: linear-gradient(90deg, rgba(0, 139, 255, 0.5) 0%, rgba(0, 98, 255, 0.5) 49.5%, rgba(207, 92, 255, 0.5) 100%); 184 | z-index: -2; 185 | } 186 | 187 | .button:hover { 188 | 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%); 189 | } 190 | 191 | .button:active { 192 | background: linear-gradient(77.86deg, rgba(170, 190, 255, 0.9) -3.23%, rgba(160, 180, 255, 0.9) 51.11%, rgba(210, 180, 255, 0.9) 98.65%); 193 | } -------------------------------------------------------------------------------- /src/components/AvatarCard/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | import { useSelector } from 'react-redux'; 7 | import { Button } from '@arco-design/web-react'; 8 | import { useState } from 'react'; 9 | import AISettings from '../AISettings'; 10 | import style from './index.module.less'; 11 | import DouBaoAvatar from '@/assets/img/DoubaoAvatarGIF.webp'; 12 | import { RootState } from '@/store'; 13 | import { MODEL_MODE, Name, VOICE_TYPE } from '@/config'; 14 | 15 | interface IAvatarCardProps extends React.HTMLAttributes { 16 | avatar?: string; 17 | } 18 | 19 | const ReversedVoiceType = Object.entries(VOICE_TYPE).reduce>( 20 | (acc, [key, value]) => { 21 | acc[value] = key; 22 | return acc; 23 | }, 24 | {} 25 | ); 26 | 27 | const SourceName = { 28 | [MODEL_MODE.VENDOR]: '第三方模型', 29 | [MODEL_MODE.COZE]: 'Coze', 30 | }; 31 | 32 | function AvatarCard(props: IAvatarCardProps) { 33 | const room = useSelector((state: RootState) => state.room); 34 | const { scene, aiConfig, modelMode } = room; 35 | const [open, setOpen] = useState(false); 36 | const { LLMConfig, TTSConfig } = aiConfig.Config || {}; 37 | const { avatar, className, ...rest } = props; 38 | const voice = TTSConfig.ProviderParams.audio.voice_type; 39 | 40 | const handleOpenDrawer = () => setOpen(true); 41 | const handleCloseDrawer = () => setOpen(false); 42 | 43 | return ( 44 |
    45 |
    46 |
    47 | Avatar 53 |
    54 |
    55 |
    56 |
    57 |
    {Name[scene]}
    58 |
    声源来自 {ReversedVoiceType[voice || '']}
    59 |
    60 | {modelMode === MODEL_MODE.ORIGINAL 61 | ? `模型 ${LLMConfig.ModelName}` 62 | : `模型来源 ${SourceName[modelMode]}`} 63 |
    64 | 65 | 68 |
    69 |
    70 |
    71 | ); 72 | } 73 | 74 | export default AvatarCard; 75 | -------------------------------------------------------------------------------- /src/components/CheckBox/index.module.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | .noStyle { 7 | display: flex; 8 | flex-direction: row; 9 | justify-content: flex-start; 10 | align-items: center; 11 | 12 | .icon { 13 | margin-right: 12px; 14 | width: 48px; 15 | height: 48px; 16 | } 17 | .content { 18 | display: flex; 19 | flex-direction: column; 20 | justify-content: center; 21 | align-items: flex-start; 22 | 23 | .label { 24 | font-size: 14px; 25 | font-weight: 500; 26 | line-height: 22px; 27 | } 28 | 29 | .description { 30 | font-size: 12px; 31 | font-weight: 400; 32 | line-height: 20px; 33 | text-align: left; 34 | } 35 | } 36 | } 37 | 38 | 39 | 40 | .wrapper { 41 | width: 260px; 42 | height: 88px; 43 | padding: 3px 16px 3px 16px; 44 | border-radius: 12px; 45 | border: 1px solid; 46 | 47 | border-color: rgba(22, 100, 255, 0.3); 48 | display: flex; 49 | flex-direction: row; 50 | justify-content: flex-start; 51 | align-items: center; 52 | cursor: pointer; 53 | 54 | .icon { 55 | border-radius: 50%; 56 | margin-right: 12px; 57 | width: 48px; 58 | height: 48px; 59 | } 60 | 61 | .content { 62 | display: flex; 63 | flex-direction: column; 64 | justify-content: center; 65 | align-items: flex-start; 66 | 67 | .label { 68 | font-family: PingFang SC; 69 | font-size: 14px; 70 | font-weight: 500; 71 | line-height: 22px; 72 | } 73 | 74 | .description { 75 | font-family: PingFang SC; 76 | font-size: 12px; 77 | font-weight: 400; 78 | line-height: 20px; 79 | text-align: left; 80 | } 81 | } 82 | } 83 | 84 | .wrapper:hover { 85 | box-shadow: 0px 5px 6px 0px rgba(82, 102, 133, 0.15); 86 | } 87 | 88 | .active { 89 | position: relative; 90 | border: 1px solid; 91 | border-color: rgba(0, 104, 255, 1); 92 | 93 | .checkIcon { 94 | position: absolute; 95 | bottom: 0px; 96 | right: 0px; 97 | } 98 | } -------------------------------------------------------------------------------- /src/components/CheckBox/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | import { ReactNode } from 'react'; 7 | // import { Button } from '@arco-design/web-react'; 8 | import CheckedSVG from '@/assets/img/Checked.svg'; 9 | import styles from './index.module.less'; 10 | 11 | interface IProps { 12 | className?: string; 13 | checked?: boolean; 14 | onClick?: () => void; 15 | icon?: string; 16 | label?: string | ReactNode; 17 | description?: string | ReactNode; 18 | suffix?: string | ReactNode; 19 | noStyle?: boolean; 20 | } 21 | 22 | function CheckBox(props: IProps) { 23 | const { 24 | noStyle, 25 | className = '', 26 | icon = '', 27 | checked, 28 | label, 29 | description, 30 | suffix, 31 | onClick, 32 | } = props; 33 | 34 | if (noStyle) { 35 | return ( 36 |
    37 | {icon ? icon : ''} 38 |
    39 |
    {label}
    40 |
    {description}
    41 |
    42 |
    43 | ); 44 | } 45 | 46 | return ( 47 |
    51 | {icon ? icon : ''} 52 |
    53 |
    {label}
    54 |
    {description}
    55 |
    56 | {suffix} 57 | {checked ? checked : ''} 58 |
    59 | ); 60 | } 61 | 62 | export default CheckBox; 63 | -------------------------------------------------------------------------------- /src/components/CheckBoxSelector/index.module.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | .wrapper { 7 | width: 100%; 8 | display: flex; 9 | flex-direction: row; 10 | align-items: center; 11 | 12 | .placeholder { 13 | font-size: 14px; 14 | font-weight: 400; 15 | line-height: 22px; 16 | color: var(--text-color-text-3, rgba(115, 122, 135, 1)); 17 | } 18 | 19 | .box { 20 | margin-right: 16px; 21 | } 22 | 23 | .seeMore { 24 | display: flex; 25 | padding: 6px 12px; 26 | justify-content: center; 27 | align-items: center; 28 | gap: var(--border-radius-small, 4px); 29 | border-radius: 6px; 30 | background: linear-gradient(96deg, rgba(22, 100, 255, 0.10) 0%, rgba(128, 64, 255, 0.10) 97.7%); 31 | 32 | .seeMoreText { 33 | font-family: "PingFang SC"; 34 | font-size: 13px; 35 | font-style: normal; 36 | font-weight: 500; 37 | line-height: 22px; /* 169.231% */ 38 | letter-spacing: 0.039px; 39 | background: var(--Linear, linear-gradient(90deg, #004FFF 38.86%, #9865FF 100%)); 40 | background-clip: text; 41 | -webkit-background-clip: text; 42 | -webkit-text-fill-color: transparent; 43 | } 44 | } 45 | } 46 | 47 | :global { 48 | 49 | .ant-modal-content { 50 | border-radius: 8px; 51 | } 52 | .ant-modal-footer { 53 | border-top: 0px; 54 | } 55 | 56 | .ant-modal-body { 57 | padding-top: 8px; 58 | padding-bottom: 16px; 59 | } 60 | 61 | .ant-modal-header { 62 | border-bottom: 0px; 63 | border-radius: 8px; 64 | } 65 | } 66 | 67 | .footer { 68 | width: calc(100% - 12px); 69 | display: flex; 70 | flex-direction: row; 71 | justify-content: flex-end; 72 | align-items: center; 73 | gap: 12px; 74 | 75 | .cancel { 76 | width: 88px; 77 | height: 32px; 78 | border-radius: 6px; 79 | border: 1px solid var(--line-color-border-3, rgba(221, 226, 233, 1)) 80 | } 81 | 82 | .confirm { 83 | width: 88px; 84 | height: 32px; 85 | border-radius: 6px; 86 | background: linear-gradient(95.87deg, #1664FF 0%, #8040FF 97.7%); 87 | color: white !important; 88 | } 89 | 90 | .confirm:hover { 91 | opacity: .8; 92 | } 93 | 94 | .confirm:active { 95 | opacity: 1; 96 | } 97 | } 98 | 99 | .modalInner { 100 | width: 100%; 101 | // max-height: 500px; 102 | display: flex; 103 | flex: row; 104 | flex-wrap: wrap; 105 | overflow: auto; 106 | gap: 12px; 107 | } 108 | 109 | .modal { 110 | // max-height: 650px; 111 | overflow: hidden; 112 | } 113 | 114 | .modalInner::-webkit-scrollbar { 115 | width: 8px; 116 | height: 8px; 117 | border-radius: 5px; 118 | } 119 | 120 | .modalInner::-webkit-scrollbar-thumb { 121 | background: rgb(205, 204, 204); 122 | border-radius: 0px; 123 | border-radius: 5px; 124 | } 125 | 126 | .modalInner::-webkit-scrollbar-track { 127 | background: rgb(255, 255, 255); 128 | border-radius: 0px; 129 | } -------------------------------------------------------------------------------- /src/components/CheckBoxSelector/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | import { useEffect, useMemo, useState, memo } from 'react'; 7 | import { Button, Drawer } from '@arco-design/web-react'; 8 | import CheckBox from '@/components/CheckBox'; 9 | import styles from './index.module.less'; 10 | import utils from '@/utils/utils'; 11 | 12 | export interface ICheckBoxItemProps { 13 | icon?: string; 14 | label: string; 15 | description?: string; 16 | key: string; 17 | } 18 | 19 | interface IProps { 20 | data?: ICheckBoxItemProps[]; 21 | onChange?: (key: string) => void; 22 | value?: string; 23 | label?: string; 24 | moreIcon?: string; 25 | moreText?: string; 26 | placeHolder?: string; 27 | } 28 | 29 | function CheckBoxSelector(props: IProps) { 30 | const { placeHolder, label = '', data = [], value, onChange, moreIcon, moreText } = props; 31 | const [visible, setVisible] = useState(false); 32 | const [selected, setSelected] = useState(value!); 33 | const selectedOne = useMemo(() => data.find((item) => item.key === value), [data, value]); 34 | const handleSeeMore = () => { 35 | setVisible(true); 36 | }; 37 | useEffect(() => { 38 | setSelected(value!); 39 | }, [visible]); 40 | 41 | return ( 42 | <> 43 |
    44 | {selectedOne ? ( 45 | 52 | ) : ( 53 |
    {placeHolder}
    54 | )} 55 | 59 |
    60 | 70 | 73 | 82 |
    83 | } 84 | > 85 |
    86 | {data.map((item) => ( 87 | setSelected(item.key)} 95 | /> 96 | ))} 97 |
    98 | 99 | 100 | ); 101 | } 102 | 103 | export default memo(CheckBoxSelector); 104 | -------------------------------------------------------------------------------- /src/components/CheckIcon/index.module.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | .wrapper { 7 | position: relative; 8 | width: 100px; 9 | height: 100px; 10 | box-sizing: border-box; 11 | border-radius: 12px; 12 | display: flex; 13 | flex-direction: row; 14 | justify-content: flex-start; 15 | align-items: center; 16 | cursor: pointer; 17 | 18 | .content { 19 | width: 100%; 20 | height: 100%; 21 | display: flex; 22 | flex-direction: column; 23 | justify-content: center; 24 | align-items: center; 25 | z-index: 1; 26 | gap: 3px; 27 | 28 | .icon { 29 | border-radius: 50%; 30 | width: 55%; 31 | height: max-content; 32 | } 33 | 34 | .checked-text { 35 | font-size: 13px; 36 | line-height: 22px; 37 | } 38 | } 39 | } 40 | 41 | .wrapper:hover { 42 | box-shadow: 0px 5px 6px 0px rgba(82, 102, 133, 0.15); 43 | } 44 | 45 | .wrapper::after { 46 | content: ''; 47 | position: absolute; 48 | border-radius: 11px; 49 | top: 1px; 50 | left: 1px; 51 | width: 100%; 52 | height: 100px; 53 | background: white; 54 | } 55 | 56 | .wrapper::before { 57 | content: ''; 58 | position: absolute; 59 | border-radius: 12px; 60 | top: 0px; 61 | left: 0px; 62 | width: 102px; 63 | height: 102px; 64 | background: linear-gradient(99.97deg, rgba(22, 100, 255, 0.2) 20.8%, rgba(132, 97, 251, 0.2) 100.66%); 65 | // background: linear-gradient(99.97deg, #1664FF 20.8%, #8461FB 100.66%); 66 | } 67 | 68 | 69 | .active { 70 | position: relative; 71 | width: 100px; 72 | height: 100px; 73 | box-sizing: border-box; 74 | border-radius: 12px; 75 | display: flex; 76 | flex-direction: row; 77 | justify-content: flex-start; 78 | align-items: center; 79 | cursor: pointer; 80 | 81 | .checkIcon { 82 | position: absolute; 83 | bottom: -1px; 84 | right: -1px; 85 | z-index: 2; 86 | width: 20px; 87 | height: 20px; 88 | } 89 | 90 | .content { 91 | width: 100%; 92 | height: 100%; 93 | display: flex; 94 | flex-direction: column; 95 | justify-content: center; 96 | align-items: center; 97 | z-index: 1; 98 | gap: 3px; 99 | 100 | .icon { 101 | border-radius: 50%; 102 | width: 55%; 103 | height: max-content; 104 | } 105 | 106 | .checked-text { 107 | background: linear-gradient(90deg, #004FFF 38.86%, #9865FF 100%); 108 | background-clip: text; 109 | -webkit-background-clip: text; 110 | -webkit-text-fill-color: transparent; 111 | font-size: 13px; 112 | font-weight: 500; 113 | line-height: 22px; 114 | } 115 | } 116 | } 117 | 118 | .active:hover { 119 | box-shadow: 0px 5px 6px 0px rgba(82, 102, 133, 0.15); 120 | } 121 | 122 | .active::after { 123 | content: ''; 124 | position: absolute; 125 | border-radius: 11px; 126 | top: 1px; 127 | left: 1px; 128 | width: 100%; 129 | height: 100px; 130 | background: white; 131 | } 132 | 133 | .active::before { 134 | content: ''; 135 | position: absolute; 136 | border-radius: 12px; 137 | top: 0px; 138 | left: 0px; 139 | width: 102px; 140 | height: 102px; 141 | background: linear-gradient(99.97deg, #1664FF 20.8%, #8461FB 100.66%); 142 | } 143 | 144 | .blur { 145 | position: relative; 146 | width: 100px; 147 | height: 100px; 148 | box-sizing: border-box; 149 | border-radius: 12px; 150 | display: flex; 151 | flex-direction: row; 152 | justify-content: flex-start; 153 | align-items: center; 154 | cursor: pointer; 155 | 156 | .content { 157 | width: 100%; 158 | height: 100%; 159 | display: flex; 160 | flex-direction: column; 161 | justify-content: center; 162 | align-items: center; 163 | z-index: 1; 164 | gap: 3px; 165 | 166 | .icon { 167 | border-radius: 50%; 168 | width: 55%; 169 | height: max-content; 170 | opacity: .5; 171 | } 172 | 173 | .checked-text { 174 | font-size: 13px; 175 | line-height: 22px; 176 | } 177 | } 178 | } 179 | 180 | .blur:hover { 181 | box-shadow: 0px 5px 6px 0px rgba(82, 102, 133, 0.15); 182 | } 183 | 184 | .blur::after { 185 | content: ''; 186 | position: absolute; 187 | border-radius: 11px; 188 | top: 1px; 189 | left: 1px; 190 | width: 100%; 191 | height: 100px; 192 | background: white; 193 | opacity: .8; 194 | } 195 | 196 | .blur::before { 197 | content: ''; 198 | position: absolute; 199 | border-radius: 12px; 200 | top: 0px; 201 | left: 0px; 202 | width: 100px; 203 | height: 100px; 204 | border: dashed 1px rgba(132, 97, 251, 0.2); 205 | } 206 | 207 | .tag { 208 | position: absolute; 209 | top: 0; 210 | right: 0; 211 | z-index: 3; 212 | font-size: 10px; 213 | font-weight: 500; 214 | line-height: 18px; 215 | transform: translate(20%, -50%); 216 | background: rgba(134, 123, 227, 1); 217 | padding: 0px 6px 0px 6px; 218 | border-radius: 20px 20px 20px 0px; 219 | color: white; 220 | } -------------------------------------------------------------------------------- /src/components/CheckIcon/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | import CheckedSVG from '@/assets/img/Checked.svg'; 7 | import styles from './index.module.less'; 8 | 9 | interface IProps { 10 | className?: string; 11 | blur?: boolean; 12 | checked: boolean; 13 | title?: string; 14 | onClick?: () => void; 15 | icon?: string; 16 | tag?: string; 17 | } 18 | 19 | function CheckIcon(props: IProps) { 20 | const { tag, blur, className = '', icon, title, checked, onClick } = props; 21 | const wrapperStyle = blur ? styles.blur : styles.wrapper; 22 | return ( 23 |
    24 | {tag ?
    {tag}
    : ''} 25 |
    26 | {icon ? icon : ''} 27 |
    {title}
    28 |
    29 | {checked ? checked : ''} 30 |
    31 | ); 32 | } 33 | 34 | export default CheckIcon; 35 | -------------------------------------------------------------------------------- /src/components/DrawerRowItem/index.module.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | .row { 7 | width: 100%; 8 | display: flex; 9 | flex-direction: row; 10 | align-items: center; 11 | cursor: pointer; 12 | 13 | .firstPart { 14 | display: flex; 15 | flex-direction: row; 16 | align-items: center; 17 | width: 90%; 18 | color: var(--text-color-text-2, var(--text-color-text-2, #42464E)); 19 | text-align: center; 20 | 21 | /* Body/body-2 medium */ 22 | font-family: "PingFang SC"; 23 | font-size: 13px; 24 | font-style: normal; 25 | font-weight: 500; 26 | line-height: 22px; /* 169.231% */ 27 | letter-spacing: 0.039px; 28 | } 29 | 30 | .finalPart { 31 | display: flex; 32 | flex-direction: row; 33 | align-items: center; 34 | width: 10%; 35 | justify-content: flex-end; 36 | 37 | .rightOutlined { 38 | font-size: 12px; 39 | } 40 | } 41 | 42 | .icon { 43 | margin-right: 4px; 44 | } 45 | } 46 | 47 | 48 | 49 | .footer { 50 | width: calc(100% - 12px); 51 | display: flex; 52 | flex-direction: row; 53 | justify-content: flex-end; 54 | align-items: center; 55 | 56 | .cancel { 57 | width: 88px; 58 | height: 32px; 59 | border-radius: 6px; 60 | border: 1px solid var(--line-color-border-3, rgba(221, 226, 233, 1)); 61 | margin-right: 12px; 62 | } 63 | 64 | .confirm { 65 | width: 88px; 66 | height: 32px; 67 | border-radius: 6px; 68 | background: linear-gradient(95.87deg, #1664FF 0%, #8040FF 97.7%); 69 | color: white; 70 | } 71 | } 72 | 73 | .children { 74 | width: 100%; 75 | height: 100%; 76 | position: relative; 77 | overflow: hidden; 78 | } 79 | 80 | :global { 81 | .ant-drawer-body { 82 | padding: 12px 24px 0px 24px; 83 | } 84 | } -------------------------------------------------------------------------------- /src/components/DrawerRowItem/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | import React, { useState } from 'react'; 7 | import { Drawer } from '@arco-design/web-react'; 8 | import { IconRight } from '@arco-design/web-react/icon'; 9 | import styles from './index.module.less'; 10 | 11 | type IDrawerRowItemProps = { 12 | btnSrc?: string; 13 | btnText: string; 14 | suffix?: React.ReactNode; 15 | drawer?: { 16 | title: string; 17 | width?: string | number; 18 | onOpen?: () => void; 19 | onClose?: () => void; 20 | onCancel?: () => void; 21 | onConfirm?: (handleClose: () => void) => void; 22 | children?: React.ReactNode; 23 | footer?: boolean; 24 | }; 25 | } & React.HTMLAttributes; 26 | 27 | function DrawerRowItem(props: IDrawerRowItemProps) { 28 | const { btnSrc, btnText, suffix, drawer, style, className = '' } = props; 29 | const [open, setOpen] = useState(false); 30 | const { onClose, onOpen } = drawer!; 31 | 32 | const handleClose = () => { 33 | drawer?.onCancel?.(); 34 | setOpen(false); 35 | onClose?.(); 36 | }; 37 | 38 | const handleOpen = () => { 39 | setOpen(true); 40 | if (drawer) { 41 | onOpen?.(); 42 | } 43 | }; 44 | 45 | return ( 46 | <> 47 |
    48 |
    49 | {btnSrc ? svg : ''} 50 | {btnText} 51 | {suffix} 52 |
    53 |
    54 | 55 |
    56 |
    57 | 66 |
    {drawer?.children}
    67 |
    68 | 69 | ); 70 | } 71 | 72 | export default DrawerRowItem; 73 | -------------------------------------------------------------------------------- /src/components/Header/index.module.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | .header { 7 | height: 48px; 8 | background: white; 9 | width: 100%; 10 | display: flex; 11 | align-items: center; 12 | justify-content: space-between; 13 | position: relative; 14 | 15 | :global { 16 | .arco-popover-content-top { 17 | padding: 0px; 18 | } 19 | } 20 | } 21 | 22 | .header-logo { 23 | display: flex; 24 | align-items: center; 25 | justify-content: space-between; 26 | margin-left: 24px; 27 | 28 | :global { 29 | img { 30 | height: 24px; 31 | } 32 | .arco-popover-content { 33 | padding: 0; 34 | } 35 | } 36 | } 37 | 38 | .menu-wrapper { 39 | display: flex; 40 | flex-direction: column; 41 | align-items: center; 42 | row-gap: 8px; 43 | justify-content: space-between; 44 | } 45 | 46 | .header-logo-text { 47 | background: linear-gradient(90deg, #004FFF 38.86%, #9865FF 100%); 48 | -webkit-background-clip: text; 49 | background-clip: text; 50 | color: transparent; 51 | font-size: 16px; 52 | } 53 | 54 | .header-right { 55 | z-index: 2; 56 | color: #fff; 57 | display: flex; 58 | align-items: center; 59 | :global { 60 | span { 61 | height: 24px; 62 | } 63 | } 64 | } 65 | 66 | .header-setting-btn { 67 | background-color: transparent; 68 | border: none; 69 | margin-right: 24px; 70 | color: #000000; 71 | font-size: 16px; 72 | cursor: pointer; 73 | } 74 | 75 | .header-pop { 76 | :global { 77 | .ant-popover-arrow { 78 | // display: none; 79 | left: 16px; 80 | .ant-popover-arrow-content { 81 | &:before { 82 | background-color: white; 83 | } 84 | } 85 | } 86 | .ant-popover-content { 87 | margin-left: 12px; 88 | } 89 | .ant-popover-inner { 90 | margin-right: 12px; 91 | } 92 | .ant-popover-inner-content { 93 | padding: 0; 94 | background-color: white; 95 | position: relative; 96 | width: 100px; 97 | height: 100px; 98 | display: flex; 99 | align-items: center; 100 | flex-direction: column; 101 | justify-content: space-between; 102 | padding-bottom: 7px; 103 | padding-top: 7px; 104 | cursor: pointer; 105 | color: black; 106 | 107 | div { 108 | font-size: 13px; 109 | font-weight: 400; 110 | line-height: 20px; 111 | &:hover { 112 | color: #1664ff; 113 | } 114 | } 115 | } 116 | } 117 | } 118 | 119 | .divider { 120 | margin-top: 2px; 121 | margin-bottom: 2px; 122 | min-width: 70%; 123 | width: 70%; 124 | } 125 | 126 | .header-right-text { 127 | color: #000000; 128 | margin-right: 24px; 129 | cursor: pointer; 130 | } 131 | -------------------------------------------------------------------------------- /src/components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | import { Button, Divider, Popover } from '@arco-design/web-react'; 7 | import { IconMenu } from '@arco-design/web-react/icon'; 8 | import NetworkIndicator from '@/components/NetworkIndicator'; 9 | import utils from '@/utils/utils'; 10 | import Logo from '@/assets/img/Logo.svg'; 11 | import styles from './index.module.less'; 12 | 13 | const Disclaimer = 'https://www.volcengine.com/docs/6348/68916'; 14 | const ReversoContext = 'https://www.volcengine.com/docs/6348/68918'; 15 | const UserAgreement = 'https://www.volcengine.com/docs/6348/128955'; 16 | 17 | interface HeaderProps { 18 | children?: React.ReactNode; 19 | hide?: boolean; 20 | } 21 | 22 | function Header(props: HeaderProps) { 23 | const { children, hide } = props; 24 | 25 | const MenuProps = [ 26 | { 27 | name: '免责声明', 28 | url: Disclaimer, 29 | }, 30 | { 31 | name: '隐私政策', 32 | url: ReversoContext, 33 | }, 34 | { 35 | name: '用户协议', 36 | url: UserAgreement, 37 | }, 38 | ]; 39 | 40 | return ( 41 |
    47 |
    48 | {utils.isMobile() ? null : ( 49 | 52 | {MenuProps.map((menuItem) => ( 53 | 62 | ))} 63 |
    64 | } 65 | > 66 | 67 | 68 | )} 69 | Logo 70 | 71 | 实时对话式 AI 体验馆 72 | 73 |
    74 | {children} 75 | {utils.isMobile() ? null : ( 76 |
    77 |
    80 | window.open('https://www.volcengine.com/product/veRTC/ConversationalAI', '_blank') 81 | } 82 | > 83 | 官网链接 84 |
    85 |
    88 | window.open( 89 | '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', 90 | '_blank' 91 | ) 92 | } 93 | > 94 | 联系我们 95 |
    96 |
    97 | )} 98 |
    99 | ); 100 | } 101 | 102 | export default Header; 103 | -------------------------------------------------------------------------------- /src/components/Loading/AudioLoading/index.module.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | .loader { 7 | display: flex; 8 | flex-direction: row; 9 | justify-content: center; 10 | align-items: center; 11 | gap: 6px; 12 | height: 36px; 13 | margin-top: 4px; 14 | } 15 | 16 | .dot { 17 | width: 20px; 18 | height: 20px; 19 | border-radius: 12px; 20 | background-color: rgba(148, 116, 255, 1); 21 | } 22 | 23 | .dotter { 24 | animation: glow 0.9s infinite; 25 | } 26 | 27 | @keyframes glow { 28 | 0% { 29 | height: 20px; 30 | } 31 | 50% { 32 | height: 36px; 33 | } 34 | 100% { 35 | height: 20px; 36 | } 37 | } -------------------------------------------------------------------------------- /src/components/Loading/AudioLoading/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | import { memo } from 'react'; 7 | import style from './index.module.less'; 8 | 9 | interface IAudioLoadingProps extends React.HTMLAttributes { 10 | loading?: boolean; 11 | } 12 | 13 | function AudioLoading(props: IAudioLoadingProps) { 14 | const { loading = false, className = '', ...rest } = props; 15 | return ( 16 |
    17 | {Array(3) 18 | .fill(0) 19 | .map((_, index) => ( 20 |
    27 | ))} 28 |
    29 | ); 30 | } 31 | 32 | export default memo(AudioLoading); 33 | -------------------------------------------------------------------------------- /src/components/Loading/HorizonLoading/index.module.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | .loader { 7 | display: flex; 8 | } 9 | 10 | .dot { 11 | width: 10px; 12 | height: 10px; 13 | border-radius: 50%; 14 | background-color: white; 15 | animation: glow 0.9s infinite; 16 | } -------------------------------------------------------------------------------- /src/components/Loading/HorizonLoading/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | import { memo } from 'react'; 7 | import style from './index.module.less'; 8 | 9 | interface ILoadingProps extends React.HTMLAttributes { 10 | dotClassName?: string; 11 | speed?: number; 12 | gap?: number; 13 | } 14 | 15 | function Loading(props: ILoadingProps) { 16 | const { dotClassName, gap = 5, speed = 0.9, className = '', ...rest } = props; 17 | return ( 18 |
    25 | {Array(3) 26 | .fill(0) 27 | .map((_, index) => ( 28 |
    36 | ))} 37 |
    38 | ); 39 | } 40 | 41 | export default memo(Loading); 42 | -------------------------------------------------------------------------------- /src/components/Loading/VerticalLoading/index.module.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | .loader { 7 | width: 40px; 8 | height: 10px; 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | margin: 6px 0px; 13 | } 14 | 15 | .bar { 16 | width: 3px; 17 | height: 12px; 18 | margin: 1px; 19 | display: inline-block; 20 | animation: shake 0.6s ease infinite; 21 | } 22 | 23 | /* 为每个 bar 指定不同的延迟来实现抖动效果 */ 24 | .bar:nth-child(1) { 25 | animation-delay: -0.2s; 26 | } 27 | 28 | .bar:nth-child(2) { 29 | animation-delay: -0.1s; 30 | } 31 | 32 | .bar:nth-child(3) { 33 | } 34 | 35 | @keyframes shake { 36 | 0% { 37 | transform: scaleY(1); 38 | background-color: var(--primary-color-primary-7, rgba(23, 89, 221, 1)); 39 | } 40 | 50% { 41 | transform: scaleY(0.5); 42 | background-color: var(--primary-color-primary-3, rgba(151, 188, 255, 1)); 43 | } 44 | 100% { 45 | transform: scaleY(1); 46 | background-color: var(--primary-color-primary-7, rgba(23, 89, 221, 1)); 47 | } 48 | } -------------------------------------------------------------------------------- /src/components/Loading/VerticalLoading/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | import { memo } from 'react'; 7 | import styles from './index.module.less'; 8 | 9 | function Loading() { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | } 18 | 19 | export default memo(Loading); 20 | -------------------------------------------------------------------------------- /src/components/NetworkIndicator/index.module.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | .panel { 7 | display: flex; 8 | 9 | .label { 10 | width: 90px; 11 | display: flex; 12 | flex-direction: column; 13 | gap: 4px; 14 | 15 | .state { 16 | font-weight: bold; 17 | } 18 | } 19 | 20 | .value { 21 | display: flex; 22 | flex-direction: column; 23 | gap: 4px; 24 | width: max-content; 25 | 26 | .state { 27 | font-weight: bold; 28 | } 29 | 30 | .loss { 31 | display: flex; 32 | flex-direction: row; 33 | justify-content: space-between; 34 | gap: 12px; 35 | } 36 | } 37 | } 38 | 39 | .wrapper { 40 | display: flex; 41 | align-items: flex-end; 42 | height: 14px; 43 | width: 14px; 44 | margin: 14px; 45 | column-gap: 1.5px; 46 | background-color: rgba(142, 142, 142, 0.05); 47 | border-radius: 3px; 48 | padding: 2px; 49 | 50 | .indicator { 51 | width: 30%; 52 | border-color: rgba(127, 127, 127, 0.184); 53 | border-width: 1px; 54 | border-radius: 1px; 55 | border-style: solid; 56 | opacity: 0.8; 57 | transition: height 0.3s; 58 | box-sizing: border-box; 59 | } 60 | } -------------------------------------------------------------------------------- /src/components/NetworkIndicator/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | import { useMemo } from 'react'; 7 | import { Popover } from '@arco-design/web-react'; 8 | import { useSelector } from 'react-redux'; 9 | import { IconArrowDown, IconArrowUp } from '@arco-design/web-react/icon'; 10 | import { NetworkQuality } from '@volcengine/rtc'; 11 | import { RootState } from '@/store'; 12 | import style from './index.module.less'; 13 | import Config from '@/config'; 14 | 15 | enum INDICATOR_COLORS { 16 | GREAT = 'rgba(35, 195, 67, 1)', 17 | FAIR = 'rgba(208, 141, 6, 1)', 18 | BAD = 'rgba(245, 78, 78, 1)', 19 | PLACE_HOLDER = 'transparent', 20 | } 21 | 22 | const INDICATOR_TEXT = { 23 | [NetworkQuality.UNKNOWN]: '正常', 24 | [NetworkQuality.EXCELLENT]: '正常', 25 | [NetworkQuality.GOOD]: '正常', 26 | [NetworkQuality.POOR]: '一般', 27 | [NetworkQuality.BAD]: '一般', 28 | [NetworkQuality.VBAD]: '较差', 29 | [NetworkQuality.DOWN]: '较差', 30 | }; 31 | 32 | function NetworkIndicator() { 33 | const room = useSelector((state: RootState) => state.room); 34 | const networkQuality = room.networkQuality; 35 | const delay = room.localUser.audioStats?.rtt; 36 | const audioLossRateUpper = room.localUser.audioStats?.audioLossRate || 0; 37 | const audioLossRateLower = 38 | room.remoteUsers.find((user) => user.userId === Config.BotName)?.audioStats?.audioLossRate || 0; 39 | 40 | const indicators = useMemo(() => { 41 | switch (networkQuality) { 42 | case NetworkQuality.UNKNOWN: 43 | case NetworkQuality.EXCELLENT: 44 | case NetworkQuality.GOOD: 45 | return Array(3).fill(INDICATOR_COLORS.GREAT); 46 | case NetworkQuality.POOR: 47 | case NetworkQuality.BAD: 48 | return Array(2).fill(INDICATOR_COLORS.FAIR).concat(INDICATOR_COLORS.PLACE_HOLDER); 49 | case NetworkQuality.VBAD: 50 | case NetworkQuality.DOWN: 51 | default: 52 | return [INDICATOR_COLORS.BAD].concat(...Array(2).fill(INDICATOR_COLORS.PLACE_HOLDER)); 53 | } 54 | }, [networkQuality]); 55 | 56 | return ( 57 | 61 |
    62 |
    网络状态
    63 |
    延迟
    64 |
    丢包率
    65 |
    66 |
    67 |
    73 | {INDICATOR_TEXT[networkQuality]} 74 |
    75 |
    {delay ? delay.toFixed(0) : '- '}ms
    76 |
    77 |
    78 | 79 | 80 | {`${audioLossRateUpper}` ? (audioLossRateUpper * 100)?.toFixed(0) : '- '}% 81 | 82 |
    83 |
    84 | 85 | 86 | {`${audioLossRateLower}` ? (audioLossRateLower * 100)?.toFixed(0) : '- '}% 87 | 88 |
    89 |
    90 |
    91 |
    92 | } 93 | > 94 |
    95 | {indicators.map((color, index) => ( 96 |
    104 | ))} 105 |
    106 | 107 | ); 108 | } 109 | 110 | export default NetworkIndicator; 111 | -------------------------------------------------------------------------------- /src/components/ResizeWrapper/index.module.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | .container { 7 | position: relative; 8 | } -------------------------------------------------------------------------------- /src/components/ResizeWrapper/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | import { useEffect, useRef } from 'react'; 7 | import styles from './index.module.less'; 8 | 9 | export type IWrapperProps = React.PropsWithChildren & { 10 | className?: string; 11 | }; 12 | 13 | export default function (props: IWrapperProps) { 14 | const { children, className = '' } = props; 15 | 16 | const ref = useRef(null); 17 | 18 | const resize = () => { 19 | if (ref.current) { 20 | ref.current.style.height = `${window.innerHeight}px`; 21 | } 22 | }; 23 | 24 | useEffect(() => { 25 | resize(); 26 | window.addEventListener('resize', resize); 27 | return () => { 28 | window.removeEventListener('resize', resize); 29 | }; 30 | }, []); 31 | 32 | return ( 33 |
    34 | {children} 35 |
    36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/components/TitleCard/index.module.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | .wrapper { 7 | width: 100%; 8 | box-sizing: border-box; 9 | height: max-content; 10 | display: flex; 11 | flex-direction: row; 12 | align-items: center; 13 | min-height: 54px; 14 | padding: 20px 16px; 15 | border-radius: 8px; 16 | border: 1px solid rgba(229, 238, 255, 1); 17 | backdrop-filter: blur(28px); 18 | box-shadow: 0px 0px 16px 0px 0px 4px 4px 0px rgba(255, 255, 255, 0.15) inset; 19 | backdrop-filter: blur(28px); 20 | 21 | .title { 22 | position: absolute; 23 | font-size: 12px; 24 | left: 10px; 25 | top: 0px; 26 | transform: translateY(-50%); 27 | padding: 0px 6px; 28 | z-index: 1; 29 | color: var(--text-color-text-3, rgba(115, 122, 135, 1)); 30 | background-color: white; 31 | width: max-content; 32 | display: flex; 33 | flex-direction: row; 34 | justify-content: center; 35 | align-items: center; 36 | 37 | .required { 38 | height: max-content; 39 | width: max-content; 40 | color: red; 41 | margin-right: 6px; 42 | padding-top: 4.5px; 43 | font-size: 14px; 44 | } 45 | } 46 | 47 | div { 48 | width: 100%; 49 | } 50 | } -------------------------------------------------------------------------------- /src/components/TitleCard/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | import styles from './index.module.less'; 7 | 8 | interface ITitleCardProps extends React.HTMLAttributes { 9 | title: string; 10 | required?: boolean; 11 | } 12 | 13 | function TitleCard(props: ITitleCardProps) { 14 | const { required, title, children, className, ...rest } = props; 15 | return ( 16 |
    17 |
    18 | {required ?
    *
    : ''} 19 | {title} 20 |
    21 |
    {children}
    22 |
    23 | ); 24 | } 25 | export default TitleCard; 26 | -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | import { ConfigFactory } from './config'; 7 | 8 | export * from './common'; 9 | 10 | export const AIGC_PROXY_HOST = 'http://localhost:3001/proxyAIGCFetch'; 11 | 12 | export const Config = ConfigFactory; 13 | export default new ConfigFactory(); 14 | -------------------------------------------------------------------------------- /src/index.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | @import './theme.less'; 7 | 8 | body { 9 | margin: 0; 10 | overflow: hidden; 11 | width: 100% !important; 12 | background: linear-gradient(109.22deg, rgba(116, 37, 255, 0.05) 0.27%, rgba(39, 88, 255, 0.05) 51.39%, rgba(0, 102, 255, 0.05) 99.54%); 13 | 14 | img { 15 | user-drag: none; 16 | -webkit-user-drag: none; 17 | user-select: none; 18 | -webkit-user-select: none; 19 | -moz-user-select: none; 20 | -ms-user-select: none; 21 | } 22 | } 23 | 24 | @keyframes glow { 25 | 0% { 26 | opacity: 1; 27 | } 28 | 40% { 29 | opacity: 0.7; 30 | } 31 | 100% { 32 | opacity: 0.3; 33 | } 34 | } -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | import ReactDOM from 'react-dom/client'; 7 | import { Provider } from 'react-redux'; 8 | import App from './App'; 9 | import store from './store'; 10 | import './index.less'; 11 | 12 | const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); 13 | root.render( 14 | 15 | 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /src/interface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | export enum DeviceType { 7 | Camera = 'camera', 8 | Microphone = 'microphone', 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/listenerHooks.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | import VERTC, { 7 | LocalAudioPropertiesInfo, 8 | RemoteAudioPropertiesInfo, 9 | LocalStreamStats, 10 | MediaType, 11 | onUserJoinedEvent, 12 | onUserLeaveEvent, 13 | RemoteStreamStats, 14 | StreamRemoveReason, 15 | StreamIndex, 16 | DeviceInfo, 17 | AutoPlayFailedEvent, 18 | PlayerEvent, 19 | NetworkQuality, 20 | } from '@volcengine/rtc'; 21 | import { useDispatch } from 'react-redux'; 22 | import { useRef } from 'react'; 23 | 24 | import { 25 | IUser, 26 | remoteUserJoin, 27 | remoteUserLeave, 28 | updateLocalUser, 29 | updateRemoteUser, 30 | addAutoPlayFail, 31 | removeAutoPlayFail, 32 | updateAITalkState, 33 | updateNetworkQuality, 34 | } from '@/store/slices/room'; 35 | import RtcClient, { IEventListener } from './RtcClient'; 36 | 37 | import { setMicrophoneList, updateSelectedDevice } from '@/store/slices/device'; 38 | import { useMessageHandler } from '@/utils/handler'; 39 | 40 | const useRtcListeners = (): IEventListener => { 41 | const dispatch = useDispatch(); 42 | const { parser } = useMessageHandler(); 43 | const playStatus = useRef<{ [key: string]: { audio: boolean; video: boolean } }>({}); 44 | 45 | const handleTrackEnded = async (event: { kind: string; isScreen: boolean }) => { 46 | const { kind, isScreen } = event; 47 | /** 浏览器自带的屏幕共享关闭触发方式,通过 onTrackEnd 事件去关闭 */ 48 | if (isScreen && kind === 'video') { 49 | await RtcClient.stopScreenCapture(); 50 | await RtcClient.unpublishScreenStream(MediaType.VIDEO); 51 | dispatch( 52 | updateLocalUser({ 53 | publishScreen: false, 54 | }) 55 | ); 56 | } 57 | }; 58 | 59 | const handleUserJoin = (e: onUserJoinedEvent) => { 60 | const extraInfo = JSON.parse(e.userInfo.extraInfo || '{}'); 61 | const userId = extraInfo.user_id || e.userInfo.userId; 62 | const username = extraInfo.user_name || e.userInfo.userId; 63 | dispatch( 64 | remoteUserJoin({ 65 | userId, 66 | username, 67 | }) 68 | ); 69 | }; 70 | 71 | const handleError = (e: { errorCode: typeof VERTC.ErrorCode.DUPLICATE_LOGIN }) => { 72 | const { errorCode } = e; 73 | if (errorCode === VERTC.ErrorCode.DUPLICATE_LOGIN) { 74 | console.log('踢人'); 75 | } 76 | }; 77 | 78 | const handleUserLeave = (e: onUserLeaveEvent) => { 79 | dispatch(remoteUserLeave(e.userInfo)); 80 | dispatch(removeAutoPlayFail(e.userInfo)); 81 | }; 82 | 83 | const handleUserPublishStream = (e: { userId: string; mediaType: MediaType }) => { 84 | const { userId, mediaType } = e; 85 | const payload: IUser = { userId }; 86 | if (mediaType === MediaType.AUDIO) { 87 | /** 暂不需要 */ 88 | } 89 | payload.publishAudio = true; 90 | dispatch(updateRemoteUser(payload)); 91 | }; 92 | 93 | const handleUserUnpublishStream = (e: { 94 | userId: string; 95 | mediaType: MediaType; 96 | reason: StreamRemoveReason; 97 | }) => { 98 | const { userId, mediaType } = e; 99 | 100 | const payload: IUser = { userId }; 101 | if (mediaType === MediaType.AUDIO) { 102 | payload.publishAudio = false; 103 | } 104 | 105 | if (mediaType === MediaType.AUDIO_AND_VIDEO) { 106 | payload.publishAudio = false; 107 | } 108 | 109 | dispatch(updateRemoteUser(payload)); 110 | }; 111 | 112 | const handleRemoteStreamStats = (e: RemoteStreamStats) => { 113 | dispatch( 114 | updateRemoteUser({ 115 | userId: e.userId, 116 | audioStats: e.audioStats, 117 | }) 118 | ); 119 | }; 120 | 121 | const handleLocalStreamStats = (e: LocalStreamStats) => { 122 | dispatch( 123 | updateLocalUser({ 124 | audioStats: e.audioStats, 125 | }) 126 | ); 127 | }; 128 | 129 | const handleLocalAudioPropertiesReport = (e: LocalAudioPropertiesInfo[]) => { 130 | const localAudioInfo = e.find( 131 | (audioInfo) => audioInfo.streamIndex === StreamIndex.STREAM_INDEX_MAIN 132 | ); 133 | if (localAudioInfo) { 134 | dispatch( 135 | updateLocalUser({ 136 | audioPropertiesInfo: localAudioInfo.audioPropertiesInfo, 137 | }) 138 | ); 139 | } 140 | }; 141 | 142 | const handleRemoteAudioPropertiesReport = (e: RemoteAudioPropertiesInfo[]) => { 143 | const remoteAudioInfo = e 144 | .filter((audioInfo) => audioInfo.streamKey.streamIndex === StreamIndex.STREAM_INDEX_MAIN) 145 | .map((audioInfo) => ({ 146 | userId: audioInfo.streamKey.userId, 147 | audioPropertiesInfo: audioInfo.audioPropertiesInfo, 148 | })); 149 | 150 | if (remoteAudioInfo.length) { 151 | dispatch(updateRemoteUser(remoteAudioInfo)); 152 | } 153 | }; 154 | 155 | const handleAudioDeviceStateChanged = async (device: DeviceInfo) => { 156 | const devices = await RtcClient.getDevices(); 157 | 158 | if (device.mediaDeviceInfo.kind === 'audioinput') { 159 | let deviceId = device.mediaDeviceInfo.deviceId; 160 | if (device.deviceState === 'inactive') { 161 | deviceId = devices.audioInputs?.[0].deviceId || ''; 162 | } 163 | RtcClient.switchDevice(MediaType.AUDIO, deviceId); 164 | dispatch(setMicrophoneList(devices.audioInputs)); 165 | 166 | dispatch( 167 | updateSelectedDevice({ 168 | selectedMicrophone: deviceId, 169 | }) 170 | ); 171 | } 172 | }; 173 | 174 | const handleAutoPlayFail = (event: AutoPlayFailedEvent) => { 175 | const { userId, kind } = event; 176 | let playUser = playStatus.current?.[userId] || {}; 177 | playUser = { ...playUser, [kind]: false }; 178 | playStatus.current[userId] = playUser; 179 | 180 | dispatch( 181 | addAutoPlayFail({ 182 | userId, 183 | }) 184 | ); 185 | }; 186 | 187 | const addFailUser = (userId: string) => { 188 | dispatch(addAutoPlayFail({ userId })); 189 | }; 190 | 191 | const playerFail = (params: { type: 'audio' | 'video'; userId: string }) => { 192 | const { type, userId } = params; 193 | let playUser = playStatus.current?.[userId] || {}; 194 | 195 | playUser = { ...playUser, [type]: false }; 196 | 197 | const { audio, video } = playUser; 198 | 199 | if (audio === false || video === false) { 200 | addFailUser(userId); 201 | } 202 | 203 | return playUser; 204 | }; 205 | 206 | const handlePlayerEvent = (event: PlayerEvent) => { 207 | const { userId, rawEvent, type } = event; 208 | let playUser = playStatus.current?.[userId] || {}; 209 | 210 | if (!playStatus.current) return; 211 | 212 | if (rawEvent.type === 'playing') { 213 | playUser = { ...playUser, [type]: true }; 214 | const { audio, video } = playUser; 215 | if (audio !== false && video !== false) { 216 | dispatch(removeAutoPlayFail({ userId })); 217 | } 218 | } else if (rawEvent.type === 'pause') { 219 | playUser = playerFail({ type, userId }); 220 | } 221 | 222 | playStatus.current[userId] = playUser; 223 | }; 224 | 225 | const handleUserStartAudioCapture = (_: { userId: string }) => { 226 | dispatch(updateAITalkState({ isAITalking: true })); 227 | }; 228 | 229 | const handleUserStopAudioCapture = (_: { userId: string }) => { 230 | dispatch(updateAITalkState({ isAITalking: false })); 231 | }; 232 | 233 | const handleNetworkQuality = ( 234 | uplinkNetworkQuality: NetworkQuality, 235 | downlinkNetworkQuality: NetworkQuality 236 | ) => { 237 | dispatch( 238 | updateNetworkQuality({ 239 | networkQuality: Math.floor( 240 | (uplinkNetworkQuality + downlinkNetworkQuality) / 2 241 | ) as NetworkQuality, 242 | }) 243 | ); 244 | }; 245 | 246 | const handleRoomBinaryMessageReceived = (event: { userId: string; message: ArrayBuffer }) => { 247 | const { message } = event; 248 | parser(message); 249 | }; 250 | 251 | return { 252 | handleError, 253 | handleUserJoin, 254 | handleUserLeave, 255 | handleTrackEnded, 256 | handleUserPublishStream, 257 | handleUserUnpublishStream, 258 | handleRemoteStreamStats, 259 | handleLocalStreamStats, 260 | handleLocalAudioPropertiesReport, 261 | handleRemoteAudioPropertiesReport, 262 | handleAudioDeviceStateChanged, 263 | handleAutoPlayFail, 264 | handlePlayerEvent, 265 | handleUserStartAudioCapture, 266 | handleUserStopAudioCapture, 267 | handleRoomBinaryMessageReceived, 268 | handleNetworkQuality, 269 | }; 270 | }; 271 | 272 | export default useRtcListeners; 273 | -------------------------------------------------------------------------------- /src/lib/useCommon.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | import { useEffect, useState } from 'react'; 7 | import { useSelector, useDispatch } from 'react-redux'; 8 | import VERTC, { MediaType } from '@volcengine/rtc'; 9 | import { Modal } from '@arco-design/web-react'; 10 | import Utils from '@/utils/utils'; 11 | import RtcClient from '@/lib/RtcClient'; 12 | import { 13 | clearCurrentMsg, 14 | clearHistoryMsg, 15 | localJoinRoom, 16 | localLeaveRoom, 17 | updateAIGCState, 18 | updateLocalUser, 19 | } from '@/store/slices/room'; 20 | 21 | import useRtcListeners from '@/lib/listenerHooks'; 22 | import { RootState } from '@/store'; 23 | 24 | import { 25 | updateMediaInputs, 26 | updateSelectedDevice, 27 | setDevicePermissions, 28 | } from '@/store/slices/device'; 29 | import logger from '@/utils/logger'; 30 | import aigcConfig, { ScreenShareScene, isVisionMode } from '@/config'; 31 | 32 | export interface FormProps { 33 | username: string; 34 | roomId: string; 35 | publishAudio: boolean; 36 | } 37 | 38 | export const useVisionMode = () => { 39 | const room = useSelector((state: RootState) => state.room); 40 | return { 41 | isVisionMode: isVisionMode(room.aiConfig?.Config?.LLMConfig.ModelName), 42 | isScreenMode: ScreenShareScene.includes(room.scene), 43 | }; 44 | }; 45 | 46 | export const useDeviceState = () => { 47 | const dispatch = useDispatch(); 48 | const room = useSelector((state: RootState) => state.room); 49 | const localUser = room.localUser; 50 | const isAudioPublished = localUser.publishAudio; 51 | const isVideoPublished = localUser.publishVideo; 52 | const isScreenPublished = localUser.publishScreen; 53 | const queryDevices = async (type: MediaType) => { 54 | const mediaDevices = await RtcClient.getDevices({ 55 | audio: type === MediaType.AUDIO, 56 | video: type === MediaType.VIDEO, 57 | }); 58 | if (type === MediaType.AUDIO) { 59 | dispatch( 60 | updateMediaInputs({ 61 | audioInputs: mediaDevices.audioInputs, 62 | }) 63 | ); 64 | dispatch( 65 | updateSelectedDevice({ 66 | selectedMicrophone: mediaDevices.audioInputs[0]?.deviceId, 67 | }) 68 | ); 69 | } else { 70 | dispatch( 71 | updateMediaInputs({ 72 | videoInputs: mediaDevices.videoInputs, 73 | }) 74 | ); 75 | dispatch( 76 | updateSelectedDevice({ 77 | selectedCamera: mediaDevices.videoInputs[0]?.deviceId, 78 | }) 79 | ); 80 | } 81 | return mediaDevices; 82 | }; 83 | 84 | const switchMic = async (controlPublish = true) => { 85 | if (controlPublish) { 86 | await (!isAudioPublished 87 | ? RtcClient.publishStream(MediaType.AUDIO) 88 | : RtcClient.unpublishStream(MediaType.AUDIO)); 89 | } 90 | queryDevices(MediaType.AUDIO); 91 | await (!isAudioPublished ? RtcClient.startAudioCapture() : RtcClient.stopAudioCapture()); 92 | dispatch( 93 | updateLocalUser({ 94 | publishAudio: !isAudioPublished, 95 | }) 96 | ); 97 | }; 98 | 99 | const switchCamera = async (controlPublish = true) => { 100 | if (controlPublish) { 101 | await (!isVideoPublished 102 | ? RtcClient.publishStream(MediaType.VIDEO) 103 | : RtcClient.unpublishStream(MediaType.VIDEO)); 104 | } 105 | queryDevices(MediaType.VIDEO); 106 | await (!isVideoPublished ? RtcClient.startVideoCapture() : RtcClient.stopVideoCapture()); 107 | dispatch( 108 | updateLocalUser({ 109 | publishVideo: !isVideoPublished, 110 | }) 111 | ); 112 | }; 113 | 114 | const switchScreenCapture = async (controlPublish = true) => { 115 | try { 116 | if (controlPublish) { 117 | await (!isScreenPublished 118 | ? RtcClient.publishScreenStream(MediaType.VIDEO) 119 | : RtcClient.unpublishScreenStream(MediaType.VIDEO)); 120 | } 121 | await (!isScreenPublished ? RtcClient.startScreenCapture() : RtcClient.stopScreenCapture()); 122 | dispatch( 123 | updateLocalUser({ 124 | publishScreen: !isScreenPublished, 125 | }) 126 | ); 127 | } catch { 128 | console.warn('Not Authorized.'); 129 | } 130 | }; 131 | 132 | return { 133 | isAudioPublished, 134 | isVideoPublished, 135 | isScreenPublished, 136 | switchMic, 137 | switchCamera, 138 | switchScreenCapture, 139 | }; 140 | }; 141 | 142 | export const useGetDevicePermission = () => { 143 | const [permission, setPermission] = useState<{ 144 | audio: boolean; 145 | }>(); 146 | 147 | const dispatch = useDispatch(); 148 | 149 | useEffect(() => { 150 | (async () => { 151 | const permission = await RtcClient.checkPermission(); 152 | dispatch(setDevicePermissions(permission)); 153 | setPermission(permission); 154 | })(); 155 | }, [dispatch]); 156 | return permission; 157 | }; 158 | 159 | export const useJoin = (): [ 160 | boolean, 161 | (formValues: FormProps, fromRefresh: boolean) => Promise 162 | ] => { 163 | const devicePermissions = useSelector((state: RootState) => state.device.devicePermissions); 164 | const room = useSelector((state: RootState) => state.room); 165 | 166 | const dispatch = useDispatch(); 167 | 168 | const { switchCamera, switchMic } = useDeviceState(); 169 | const [joining, setJoining] = useState(false); 170 | const listeners = useRtcListeners(); 171 | 172 | const handleAIGCModeStart = async () => { 173 | if (room.isAIGCEnable) { 174 | await RtcClient.stopAudioBot(); 175 | dispatch(clearCurrentMsg()); 176 | await RtcClient.startAudioBot(); 177 | } else { 178 | await RtcClient.startAudioBot(); 179 | } 180 | dispatch(updateAIGCState({ isAIGCEnable: true })); 181 | }; 182 | 183 | async function disPatchJoin(formValues: FormProps): Promise { 184 | if (joining) { 185 | return; 186 | } 187 | 188 | const isSupported = await VERTC.isSupported(); 189 | if (!isSupported) { 190 | Modal.error({ 191 | title: '不支持 RTC', 192 | content: '您的浏览器可能不支持 RTC 功能,请尝试更换浏览器或升级浏览器后再重试。', 193 | }); 194 | return; 195 | } 196 | 197 | setJoining(true); 198 | const { username, roomId } = formValues; 199 | const isVision = isVisionMode(aigcConfig.Model); 200 | const shouldGetVideoPermission = isVision && !ScreenShareScene.includes(room.scene); 201 | 202 | const token = aigcConfig.BaseConfig.Token; 203 | 204 | /** 1. Create RTC Engine */ 205 | const engineParams = { 206 | appId: aigcConfig.BaseConfig.AppId, 207 | roomId, 208 | uid: username, 209 | }; 210 | await RtcClient.createEngine(engineParams); 211 | 212 | /** 2.1 Set events callbacks */ 213 | RtcClient.addEventListeners(listeners); 214 | 215 | /** 2.2 RTC starting to join room */ 216 | await RtcClient.joinRoom(token!, username); 217 | console.log(' ------ userJoinRoom\n', `roomId: ${roomId}\n`, `uid: ${username}`); 218 | /** 3. Set users' devices info */ 219 | const mediaDevices = await RtcClient.getDevices({ 220 | audio: true, 221 | video: shouldGetVideoPermission, 222 | }); 223 | 224 | dispatch( 225 | localJoinRoom({ 226 | roomId, 227 | user: { 228 | username, 229 | userId: username, 230 | }, 231 | }) 232 | ); 233 | dispatch( 234 | updateSelectedDevice({ 235 | selectedMicrophone: mediaDevices.audioInputs[0]?.deviceId, 236 | selectedCamera: mediaDevices.videoInputs[0]?.deviceId, 237 | }) 238 | ); 239 | dispatch(updateMediaInputs(mediaDevices)); 240 | 241 | setJoining(false); 242 | 243 | if (devicePermissions.audio) { 244 | try { 245 | await switchMic(); 246 | // RtcClient.setAudioVolume(30); 247 | } catch (e) { 248 | logger.debug('No permission for mic'); 249 | } 250 | } 251 | 252 | if (devicePermissions.video && shouldGetVideoPermission) { 253 | try { 254 | await switchCamera(); 255 | } catch (e) { 256 | logger.debug('No permission for camera'); 257 | } 258 | } 259 | 260 | Utils.setSessionInfo({ 261 | username, 262 | roomId, 263 | publishAudio: true, 264 | }); 265 | 266 | handleAIGCModeStart(); 267 | } 268 | 269 | return [joining, disPatchJoin]; 270 | }; 271 | 272 | export const useLeave = () => { 273 | const dispatch = useDispatch(); 274 | 275 | return async function () { 276 | await Promise.all([ 277 | RtcClient.stopAudioCapture, 278 | RtcClient.stopScreenCapture, 279 | RtcClient.stopVideoCapture, 280 | ]); 281 | await RtcClient.leaveRoom(); 282 | dispatch(clearHistoryMsg()); 283 | dispatch(clearCurrentMsg()); 284 | dispatch(localLeaveRoom()); 285 | dispatch(updateAIGCState({ isAIGCEnable: false })); 286 | }; 287 | }; 288 | -------------------------------------------------------------------------------- /src/pages/MainPage/MainArea/Antechamber/InvokeButton/index.module.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | .wrapper { 7 | position: relative; 8 | display: flex; 9 | flex-direction: column; 10 | align-items: center; 11 | justify-content: center; 12 | 13 | 14 | .btn { 15 | width: max-content; 16 | height: max-content; 17 | border-radius: 50%; 18 | display: flex; 19 | justify-content: center; 20 | align-items: center; 21 | 22 | .icon { 23 | position: absolute; 24 | } 25 | } 26 | 27 | .text { 28 | margin-top: 8px; 29 | color: rgba(115, 122, 135, 1); 30 | } 31 | } 32 | 33 | .cursor { 34 | cursor: pointer; 35 | } 36 | 37 | .cursor:hover { 38 | opacity: 0.8; 39 | } 40 | 41 | .cursor:active { 42 | opacity: 1; 43 | } 44 | 45 | .loader { 46 | display: flex; 47 | gap: 5px; 48 | } 49 | 50 | .dot { 51 | width: 10px; 52 | height: 10px; 53 | border-radius: 50%; 54 | background-color: white; 55 | animation: glow 0.9s infinite; 56 | } 57 | 58 | @keyframes glow { 59 | 0% { 60 | opacity: 1; 61 | } 62 | 40% { 63 | opacity: 0.7; 64 | } 65 | 100% { 66 | opacity: 0.3; 67 | } 68 | } -------------------------------------------------------------------------------- /src/pages/MainPage/MainArea/Antechamber/InvokeButton/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | import Loading from './loading'; 7 | import style from './index.module.less'; 8 | import CallButtonSVG from '@/assets/img/CallWrapper.svg'; 9 | import PhoneSVG from '@/assets/img/Phone.svg'; 10 | 11 | interface IInvokeButtonProps extends React.HTMLAttributes { 12 | loading?: boolean; 13 | } 14 | 15 | function InvokeButton(props: IInvokeButtonProps) { 16 | const { loading, className, ...rest } = props; 17 | 18 | return ( 19 |
    20 |
    21 | call 22 | {loading ? ( 23 | 24 | ) : ( 25 | phone 26 | )} 27 |
    28 |
    {loading ? '连接中' : '通话'}
    29 |
    30 | ); 31 | } 32 | 33 | export default InvokeButton; 34 | -------------------------------------------------------------------------------- /src/pages/MainPage/MainArea/Antechamber/InvokeButton/loading.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | import style from './index.module.less'; 7 | 8 | function Loading(props: React.HTMLAttributes) { 9 | const { className = '', ...rest } = props; 10 | return ( 11 |
    12 | {Array(3) 13 | .fill(0) 14 | .map((_, index) => ( 15 |
    22 | ))} 23 |
    24 | ); 25 | } 26 | 27 | export default Loading; 28 | -------------------------------------------------------------------------------- /src/pages/MainPage/MainArea/Antechamber/index.module.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | .wrapper { 7 | position: relative; 8 | width: 100%; 9 | height: 100%; 10 | border-radius: 16px; 11 | display: flex; 12 | flex-direction: column; 13 | align-items: center; 14 | justify-content: center; 15 | 16 | .avatar { 17 | /** 18 | * height = 128px in AvatarCard.avatar 19 | * 20 | */ 21 | margin-top: -128px; 22 | /** 23 | * width = 128px in AvatarCard.avatar 24 | * 128px / 2 = 64px 25 | * 26 | */ 27 | transform: translateX(calc(50% - 64px)); 28 | } 29 | 30 | .mobile { 31 | transform: none !important; 32 | } 33 | 34 | .title { 35 | font-size: 24px; 36 | font-weight: 500; 37 | line-height: 32px; 38 | text-align: center; 39 | margin-top: 24px; 40 | } 41 | 42 | .description { 43 | font-size: 12px; 44 | font-weight: 400; 45 | line-height: 20px; 46 | text-align: center; 47 | color: rgba(66, 70, 78, 1); 48 | margin-top: 4px; 49 | } 50 | 51 | .invoke-btn { 52 | position: absolute; 53 | bottom: 120px; 54 | } 55 | } -------------------------------------------------------------------------------- /src/pages/MainPage/MainArea/Antechamber/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | import AvatarCard from '@/components/AvatarCard'; 7 | import Utils from '@/utils/utils'; 8 | import aigcConfig from '@/config'; 9 | import InvokeButton from '@/pages/MainPage/MainArea/Antechamber/InvokeButton'; 10 | import { useJoin } from '@/lib/useCommon'; 11 | import style from './index.module.less'; 12 | 13 | function Antechamber() { 14 | const [joining, dispatchJoin] = useJoin(); 15 | const username = aigcConfig.BaseConfig.UserId; 16 | const roomId = aigcConfig.BaseConfig.RoomId; 17 | 18 | const handleJoinRoom = () => { 19 | if (!joining) { 20 | dispatchJoin( 21 | { 22 | username, 23 | roomId, 24 | publishAudio: true, 25 | }, 26 | false 27 | ); 28 | } 29 | }; 30 | 31 | return ( 32 |
    33 | 34 |
    AI 语音助手
    35 |
    Powered by 豆包大模型和火山引擎视频云 RTC
    36 | 37 |
    38 | ); 39 | } 40 | 41 | export default Antechamber; 42 | -------------------------------------------------------------------------------- /src/pages/MainPage/MainArea/Room/AudioController.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | import { useDispatch, useSelector } from 'react-redux'; 7 | import AudioLoading from '@/components/Loading/AudioLoading'; 8 | import { RootState } from '@/store'; 9 | import RtcClient from '@/lib/RtcClient'; 10 | import { setInterruptMsg } from '@/store/slices/room'; 11 | import { useDeviceState } from '@/lib/useCommon'; 12 | import { COMMAND } from '@/utils/handler'; 13 | import style from './index.module.less'; 14 | import StopRobotBtn from '@/assets/img/StopRobotBtn.svg'; 15 | 16 | const THRESHOLD_VOLUME = 18; 17 | 18 | function AudioController(props: React.HTMLAttributes) { 19 | const { className, ...rest } = props; 20 | const dispatch = useDispatch(); 21 | const room = useSelector((state: RootState) => state.room); 22 | const volume = room.localUser.audioPropertiesInfo?.linearVolume || 0; 23 | const { isAudioPublished } = useDeviceState(); 24 | const isAITalking = room.isAITalking; 25 | const isLoading = volume >= THRESHOLD_VOLUME && isAudioPublished; 26 | 27 | const handleInterrupt = () => { 28 | RtcClient.commandAudioBot(COMMAND.INTERRUPT); 29 | dispatch(setInterruptMsg()); 30 | }; 31 | return ( 32 |
    33 | {isAudioPublished ? ( 34 | isAITalking ? ( 35 |
    36 | StopRobotBtn 37 | 点击打断 38 |
    39 | ) : ( 40 |
    正在听...
    41 | ) 42 | ) : ( 43 |
    你已关闭麦克风
    44 | )} 45 | 46 |
    47 | ); 48 | } 49 | export default AudioController; 50 | -------------------------------------------------------------------------------- /src/pages/MainPage/MainArea/Room/CameraArea.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | import { useSelector } from 'react-redux'; 7 | import { useEffect } from 'react'; 8 | import { RootState } from '@/store'; 9 | import { useDeviceState, useVisionMode } from '@/lib/useCommon'; 10 | import RtcClient from '@/lib/RtcClient'; 11 | import { ScreenShareScene } from '@/config'; 12 | 13 | import styles from './index.module.less'; 14 | import CameraCloseNoteSVG from '@/assets/img/CameraCloseNote.svg'; 15 | import ScreenCloseNoteSVG from '@/assets/img/ScreenCloseNote.svg'; 16 | 17 | const LocalVideoID = 'local-video-player'; 18 | const LocalScreenID = 'local-screen-player'; 19 | 20 | function CameraArea(props: React.HTMLAttributes) { 21 | const { className, ...rest } = props; 22 | const room = useSelector((state: RootState) => state.room); 23 | const { isVisionMode } = useVisionMode(); 24 | const isScreenMode = ScreenShareScene.includes(room.scene); 25 | const { isVideoPublished, isScreenPublished, switchCamera, switchScreenCapture } = 26 | useDeviceState(); 27 | 28 | const setVideoPlayer = () => { 29 | if (isVisionMode && (isVideoPublished || isScreenPublished)) { 30 | RtcClient.setLocalVideoPlayer( 31 | room.localUser.username!, 32 | isScreenMode ? LocalScreenID : LocalVideoID, 33 | isScreenPublished 34 | ); 35 | } 36 | }; 37 | 38 | const handleOperateCamera = () => { 39 | switchCamera(); 40 | }; 41 | 42 | const handleOperateScreenShare = () => { 43 | switchScreenCapture(); 44 | }; 45 | 46 | useEffect(() => { 47 | setVideoPlayer(); 48 | }, [isVideoPublished, isScreenPublished, isScreenMode]); 49 | 50 | return isVisionMode ? ( 51 |
    52 |
    58 |
    64 |
    69 | close 74 |
    75 | 请 76 | {isScreenMode ? ( 77 | 78 | 打开屏幕采集 79 | 80 | ) : ( 81 | 82 | 打开摄像头 83 | 84 | )} 85 |
    86 |
    体验豆包视觉理解模型
    87 |
    88 |
    89 | ) : null; 90 | } 91 | 92 | export default CameraArea; 93 | -------------------------------------------------------------------------------- /src/pages/MainPage/MainArea/Room/Conversation.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | import React, { useRef, useEffect } from 'react'; 7 | import { useSelector } from 'react-redux'; 8 | import { Tag, Spin } from '@arco-design/web-react'; 9 | import { RootState } from '@/store'; 10 | import Loading from '@/components/Loading/HorizonLoading'; 11 | import Config from '@/config'; 12 | import styles from './index.module.less'; 13 | 14 | const lines: (string | React.ReactNode)[] = []; 15 | 16 | function Conversation(props: React.HTMLAttributes) { 17 | const { className, ...rest } = props; 18 | const msgHistory = useSelector((state: RootState) => state.room.msgHistory); 19 | const { userId } = useSelector((state: RootState) => state.room.localUser); 20 | const { isAITalking, isUserTalking } = useSelector((state: RootState) => state.room); 21 | const isAIReady = msgHistory.length > 0; 22 | const containerRef = useRef(null); 23 | 24 | const isUserTextLoading = (owner: string) => { 25 | return owner === userId && isUserTalking; 26 | }; 27 | 28 | const isAITextLoading = (owner: string) => { 29 | return owner === Config.BotName && isAITalking; 30 | }; 31 | 32 | useEffect(() => { 33 | const container = containerRef.current; 34 | if (container) { 35 | container.scrollTop = container.scrollHeight - container.clientHeight; 36 | } 37 | }, [msgHistory.length]); 38 | 39 | return ( 40 |
    41 | {lines.map((line) => line)} 42 | {!isAIReady ? ( 43 |
    44 | 45 | AI 准备中, 请稍侯 46 |
    47 | ) : ( 48 | '' 49 | )} 50 | {msgHistory?.map(({ value, user, isInterrupted }, index) => { 51 | const isUserMsg = user === userId; 52 | const isRobotMsg = user === Config.BotName; 53 | if (!isUserMsg && !isRobotMsg) { 54 | return ''; 55 | } 56 | return ( 57 |
    61 |
    62 | {value} 63 |
    64 | {isAIReady && 65 | (isUserTextLoading(user) || isAITextLoading(user)) && 66 | index === msgHistory.length - 1 ? ( 67 | 68 | ) : ( 69 | '' 70 | )} 71 |
    72 |
    73 | {!isUserMsg && isInterrupted ? 已打断 : ''} 74 |
    75 | ); 76 | })} 77 |
    78 | ); 79 | } 80 | 81 | export default Conversation; 82 | -------------------------------------------------------------------------------- /src/pages/MainPage/MainArea/Room/ToolBar.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | import { useSelector } from 'react-redux'; 7 | import { memo, useState } from 'react'; 8 | import { Drawer } from '@arco-design/web-react'; 9 | import { useDeviceState, useLeave } from '@/lib/useCommon'; 10 | import { RootState } from '@/store'; 11 | import { isVisionMode } from '@/config/common'; 12 | import { ScreenShareScene } from '@/config'; 13 | import utils from '@/utils/utils'; 14 | import Menu from '../../Menu'; 15 | 16 | import style from './index.module.less'; 17 | import CameraOpenSVG from '@/assets/img/CameraOpen.svg'; 18 | import CameraCloseSVG from '@/assets/img/CameraClose.svg'; 19 | import MicOpenSVG from '@/assets/img/MicOpen.svg'; 20 | import SettingSVG from '@/assets/img/Setting.svg'; 21 | import MicCloseSVG from '@/assets/img/MicClose.svg'; 22 | import LeaveRoomSVG from '@/assets/img/LeaveRoom.svg'; 23 | import ScreenOnSVG from '@/assets/img/ScreenOn.svg'; 24 | import ScreenOffSVG from '@/assets/img/ScreenOff.svg'; 25 | 26 | function ToolBar(props: React.HTMLAttributes) { 27 | const { className, ...rest } = props; 28 | const room = useSelector((state: RootState) => state.room); 29 | const [open, setOpen] = useState(false); 30 | const model = room.aiConfig.Config.LLMConfig?.ModelName; 31 | const isScreenMode = ScreenShareScene.includes(room.scene); 32 | const leaveRoom = useLeave(); 33 | const { 34 | isAudioPublished, 35 | isVideoPublished, 36 | isScreenPublished, 37 | switchMic, 38 | switchCamera, 39 | switchScreenCapture, 40 | } = useDeviceState(); 41 | 42 | const handleSetting = () => { 43 | setOpen(true); 44 | }; 45 | return ( 46 |
    47 | {utils.isMobile() ? ( 48 | setting 49 | ) : null} 50 | switchMic(true)} 53 | className={style.btn} 54 | alt="mic" 55 | /> 56 | {isVisionMode(model) ? ( 57 | isScreenMode ? ( 58 | switchScreenCapture()} 61 | className={style.btn} 62 | alt="screenShare" 63 | /> 64 | ) : ( 65 | switchCamera(true)} 68 | className={style.btn} 69 | alt="camera" 70 | /> 71 | ) 72 | ) : ( 73 | '' 74 | )} 75 | leave 76 | {utils.isMobile() ? ( 77 | setOpen(false)} 81 | style={{ 82 | width: 'max-content', 83 | }} 84 | footer={null} 85 | > 86 | 87 | 88 | ) : null} 89 |
    90 | ); 91 | } 92 | export default memo(ToolBar); 93 | -------------------------------------------------------------------------------- /src/pages/MainPage/MainArea/Room/index.module.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | .wrapper { 7 | position: relative; 8 | width: 100%; 9 | height: 100%; 10 | border-radius: 16px; 11 | padding: 36px 72px; 12 | box-sizing: border-box; 13 | 14 | .conversation { 15 | width: 100%; 16 | position: relative; 17 | height: 100%; 18 | /** 19 | * 100% 为容器高度 20 | * 128px 为上层 DouBao Card Height 21 | * 24px 为 margin top 22 | * 36px * 2 为容器 padding 23 | * 128 + 24 + 36 * 2 = 224px 24 | */ 25 | max-height: calc(100% - 224px - 8px); 26 | display: flex; 27 | flex-direction: column; 28 | padding-bottom: 12px; 29 | // background-color: black; 30 | overflow-x: hidden; 31 | overflow-y: auto; 32 | margin-top: 24px; 33 | 34 | .sentence { 35 | position: relative; 36 | display: flex; 37 | flex-direction: row; 38 | justify-content: flex-start; 39 | align-items: center; 40 | width: max-content; 41 | white-space: normal; 42 | max-width: 100%; 43 | line-height: 28px; 44 | 45 | .content { 46 | width: max-content; 47 | } 48 | } 49 | 50 | .user { 51 | width: max-content; 52 | border: 0px solid; 53 | align-self: flex-end; 54 | padding: 8px 12px 8px 12px; 55 | border-radius: 12px 0px 12px 12px; 56 | background: var(--background-color-bg-5, rgba(241, 243, 245, 1)); 57 | margin-top: 12px; 58 | margin-bottom: 12px; 59 | } 60 | .robot { 61 | font-family: PingFang SC; 62 | font-size: 14px; 63 | font-weight: 400; 64 | letter-spacing: 0.003em; 65 | 66 | border: 0px solid; 67 | align-self: flex-start; 68 | padding: 3px 12px 3px 0px; 69 | } 70 | 71 | .loading-wrapper { 72 | width: max-content; 73 | display: inline-block; 74 | 75 | .loading { 76 | margin-left: 8px; 77 | width: max-content; 78 | } 79 | 80 | .dot { 81 | background-color: rgba(193, 163, 237, 1); 82 | width: 8px; 83 | height: 8px; 84 | } 85 | } 86 | 87 | .aiReadying { 88 | font-family: PingFang SC; 89 | font-size: 16px; 90 | font-weight: 500; 91 | color: rgba(27, 30, 61, 0.6); 92 | text-align: center; 93 | display: flex; 94 | flex-direction: row; 95 | justify-content: flex-start; 96 | align-items: center; 97 | line-height: 28px; 98 | } 99 | 100 | .aiReading-spin { 101 | margin-right: 12px; 102 | line-height: 16px; 103 | } 104 | 105 | .interruptTag { 106 | width: max-content; 107 | height: 22px; 108 | padding: 0px 6px 0px 6px; 109 | border-radius: 4px; 110 | margin-left: 6px; 111 | font-family: PingFang SC; 112 | font-size: 12px; 113 | font-weight: 400; 114 | line-height: 22px; 115 | letter-spacing: 0.003em; 116 | color: var(--text-color-text-3, rgba(115, 122, 135, 1)); 117 | background: var(--security-unknown-tag-unknown-1, rgba(241, 243, 245, 1)); 118 | } 119 | } 120 | 121 | .conversation::-webkit-scrollbar { 122 | width: 0px; 123 | height: 0px; 124 | } 125 | 126 | .conversation::-webkit-scrollbar-thumb { 127 | background: rgba(0,0,0,0); 128 | border-radius: 0px; 129 | } 130 | 131 | .conversation::-webkit-scrollbar-track { 132 | background: rgba(0,0,0,0); 133 | border-radius: 0px; 134 | } 135 | 136 | .toolBar { 137 | position: absolute; 138 | right: 0px; 139 | margin-right: 36px; 140 | bottom: 36px; 141 | } 142 | 143 | .controller { 144 | position: absolute; 145 | left: 0px; 146 | bottom: 36px; 147 | margin-left: 50%; 148 | transform: translateX(-50%); 149 | } 150 | 151 | .declare { 152 | position: absolute; 153 | bottom: 8px; 154 | left: 12px; 155 | color: var(--text-color-text-4, rgba(199, 204, 214, 1)); 156 | font-size: 10px; 157 | font-weight: 400; 158 | line-height: 20px; 159 | } 160 | } 161 | 162 | .mobile { 163 | padding: 12px 28px; 164 | } 165 | 166 | .text { 167 | width: 100%; 168 | text-align: center; 169 | color: rgba(148, 116, 255, 1); 170 | font-size: 14px; 171 | font-weight: 500; 172 | line-height: 22px; 173 | } 174 | 175 | .closed { 176 | width: 100%; 177 | text-align: center; 178 | color: #737A87; 179 | font-size: 14px; 180 | font-weight: 400; 181 | line-height: 19.6px; 182 | } 183 | 184 | .btns { 185 | width: 100%; 186 | display: flex; 187 | flex-direction: row; 188 | justify-content: flex-end; 189 | align-items: center; 190 | gap: 16px; 191 | 192 | .setting { 193 | background-color: rgba(111, 111, 111, 0.497); 194 | border-radius: 50%; 195 | width: 48px; 196 | height: 48px; 197 | padding: 12px; 198 | box-sizing: border-box; 199 | cursor: pointer; 200 | } 201 | 202 | .btn { 203 | cursor: pointer; 204 | } 205 | 206 | .btn:hover { 207 | opacity: 0.8; 208 | } 209 | 210 | .btn:active { 211 | opacity: 1; 212 | } 213 | } 214 | 215 | .column { 216 | flex-direction: column !important; 217 | align-items: flex-end !important; 218 | } 219 | 220 | .interrupt { 221 | display: flex; 222 | flex-direction: row; 223 | justify-content: center; 224 | align-items: center; 225 | background: #FFFFFF; 226 | border-radius: 4px; 227 | box-shadow: 0px 2px 1px 0px rgba(0, 0, 0, 0.08), 0px 0px 0px 1px rgba(221, 226, 233, 1); 228 | border-color: #d9d9d9; 229 | width: 81px; 230 | height: 24px; 231 | gap: 4px; 232 | cursor: pointer; 233 | user-select: none; 234 | -webkit-user-select: none; /* Safari */ 235 | -moz-user-select: none; /* Firefox */ 236 | -ms-user-select: none; /* Internet Explorer/Edge */ 237 | 238 | .interrupt-text { 239 | color: var(--text-color-text-3, rgba(115, 122, 135, 1)); 240 | font-size: 12px; 241 | } 242 | 243 | &:hover { 244 | opacity: 0.8; 245 | } 246 | 247 | &:active { 248 | opacity: 1; 249 | } 250 | } 251 | 252 | .camera-wrapper { 253 | position: absolute; 254 | top: 16px; 255 | right: 16px; 256 | width: 320px; 257 | height: 200px; 258 | border-radius: 8px; 259 | background: var(--line-color-border-2, rgba(234, 237, 241, 1)); 260 | display: flex; 261 | flex-direction: column; 262 | justify-content: center; 263 | align-items: center; 264 | border: 0.81px solid var(--line-color-border-3, rgba(221, 226, 233, 1)); 265 | overflow: hidden; 266 | z-index: 4; 267 | 268 | .camera-player { 269 | width: 100%; 270 | height: 100%; 271 | border-radius: 8px; 272 | } 273 | 274 | .camera-player-hidden { 275 | display: none !important; 276 | } 277 | 278 | .camera-placeholder { 279 | width: 100%; 280 | display: flex; 281 | flex-direction: column; 282 | justify-content: center; 283 | align-items: center; 284 | font-size: 12px; 285 | color: var(--text-color-text-3, rgba(115, 122, 135, 1)); 286 | 287 | .camera-placeholder-close-note { 288 | margin-bottom: 8px; 289 | width: 60px; 290 | height: 60px; 291 | } 292 | 293 | .camera-open-btn { 294 | color: var(--primary-color-primary-6, rgba(22, 100, 255, 1)); 295 | cursor: pointer; 296 | } 297 | } 298 | } -------------------------------------------------------------------------------- /src/pages/MainPage/MainArea/Room/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | import AvatarCard from '@/components/AvatarCard'; 7 | import Conversation from './Conversation'; 8 | import ToolBar from './ToolBar'; 9 | import CameraArea from './CameraArea'; 10 | import AudioController from './AudioController'; 11 | import utils from '@/utils/utils'; 12 | import style from './index.module.less'; 13 | import DoubaoAvatar from '@/assets/img/DoubaoAvatar.png'; 14 | 15 | function Room() { 16 | return ( 17 |
    18 | 19 | {utils.isMobile() ? null : } 20 | 21 | 22 | 23 |
    AI生成内容由大模型生成,不能完全保障真实
    24 |
    25 | ); 26 | } 27 | 28 | export default Room; 29 | -------------------------------------------------------------------------------- /src/pages/MainPage/MainArea/index.module.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | .wrapper { 7 | width: 100%; 8 | height: 100%; 9 | background-color: white; 10 | border: 1px solid var(--line-color-border-2, rgba(234, 237, 241, 1)); 11 | border-radius: 16px; 12 | padding: 20px 12.5%; 13 | 14 | .space { 15 | width: 100%; 16 | min-height: 40px; 17 | } 18 | 19 | .doubaoIcon { 20 | width: 111px; 21 | height: 111px; 22 | min-height: 111px; 23 | overflow: hidden; 24 | } 25 | 26 | .interruptTag { 27 | width: max-content; 28 | height: 22px; 29 | padding: 0px 6px 0px 6px; 30 | border-radius: 4px; 31 | margin-left: 4px; 32 | font-family: PingFang SC; 33 | font-size: 12px; 34 | font-weight: 400; 35 | line-height: 22px; 36 | letter-spacing: 0.003em; 37 | color: var(--text-color-text-3, rgba(115, 122, 135, 1)); 38 | background: var(--security-unknown-tag-unknown-1, rgba(241, 243, 245, 1)); 39 | } 40 | 41 | .welcome { 42 | font-family: PingFang SC; 43 | font-size: 24px; 44 | font-weight: 500; 45 | line-height: 32px; 46 | letter-spacing: 0.003em; 47 | text-align: left; 48 | margin-top: 8px; 49 | } 50 | 51 | .weight { 52 | background: linear-gradient(90deg, #004FFF 38.86%, #9865FF 100%); 53 | -webkit-background-clip: text; 54 | background-clip: text; 55 | color: transparent; 56 | } 57 | 58 | .tip { 59 | font-family: PingFang SC; 60 | font-size: 13px; 61 | font-weight: 400; 62 | line-height: 22px; 63 | letter-spacing: 0.003em; 64 | text-align: left; 65 | color: rgba(27, 30, 61, 0.6); 66 | margin-top: 18px; 67 | margin-bottom: 18px; 68 | } 69 | 70 | .tagProblem { 71 | width: max-content; 72 | border-radius: 4px; 73 | font-family: PingFang SC; 74 | font-size: 12px; 75 | font-weight: 500; 76 | line-height: 20px; 77 | letter-spacing: 0.003em; 78 | text-align: center; 79 | margin-bottom: 12px; 80 | color: rgba(66, 70, 78, 1); 81 | } 82 | 83 | .conversation { 84 | overflow-x: hidden; 85 | overflow-y: auto; 86 | width: 100%; 87 | position: relative; 88 | height: calc(75% - 12px); 89 | display: flex; 90 | flex-direction: column; 91 | padding-bottom: 12px; 92 | 93 | .aiReadying { 94 | font-family: PingFang SC; 95 | font-size: 16px; 96 | font-weight: 500; 97 | line-height: 18px; 98 | letter-spacing: 0.003em; 99 | color: rgba(27, 30, 61, 0.6); 100 | margin-top: 12px; 101 | text-align: center; 102 | display: flex; 103 | flex-direction: row; 104 | justify-content: flex-start; 105 | align-items: center; 106 | } 107 | 108 | .aiReading-spin { 109 | margin-right: 12px; 110 | } 111 | } 112 | 113 | .conversation::-webkit-scrollbar { 114 | width: 0px; 115 | height: 0px; 116 | } 117 | 118 | .conversation::-webkit-scrollbar-thumb { 119 | background: rgba(0,0,0,0); 120 | border-radius: 0px; 121 | } 122 | 123 | .conversation::-webkit-scrollbar-track { 124 | background: rgba(0,0,0,0); 125 | border-radius: 0px; 126 | } 127 | 128 | .sentence { 129 | display: flex; 130 | flex-direction: row; 131 | justify-content: flex-start; 132 | align-items: center; 133 | width: 100%; 134 | } 135 | .user { 136 | width: max-content; 137 | border: 0px solid; 138 | align-self: flex-end; 139 | padding: 8px 12px 8px 12px; 140 | border-radius: 12px 0px 12px 12px; 141 | background: var(--background-color-bg-5, rgba(241, 243, 245, 1)); 142 | margin-top: 12px; 143 | } 144 | .robot { 145 | font-family: PingFang SC; 146 | font-size: 14px; 147 | font-weight: 400; 148 | letter-spacing: 0.003em; 149 | 150 | border: 0px solid; 151 | align-self: flex-start; 152 | padding: 3px 12px 3px 0px; 153 | } 154 | 155 | .userTalkingWave { 156 | height: 100px; 157 | } 158 | 159 | .userStopTalkingWave { 160 | height: 100px; 161 | transform: scaleY(.5); 162 | } 163 | 164 | .status { 165 | overflow: hidden; 166 | width: 100%; 167 | height: 25%; 168 | display: flex; 169 | flex-direction: column; 170 | justify-content: flex-end; 171 | align-items: center; 172 | gap: 8px; 173 | 174 | .status-row { 175 | display: flex; 176 | flex-direction: row; 177 | justify-content: center; 178 | align-items: center; 179 | 180 | .status-icon { 181 | width: 24px; 182 | height: 24px; 183 | margin-right: 6px; 184 | } 185 | 186 | .status-text { 187 | font-family: PingFang SC; 188 | font-size: 14px; 189 | font-weight: 500; 190 | line-height: 22px; 191 | letter-spacing: 0.003em; 192 | } 193 | } 194 | 195 | .desc { 196 | font-family: PingFang SC; 197 | font-size: 10px; 198 | font-weight: 400; 199 | line-height: 18px; 200 | letter-spacing: 0.003em; 201 | text-align: center; 202 | color: var(--text-color-text-4, rgba(199, 204, 214, 1)); 203 | } 204 | 205 | .micNotify { 206 | display: flex; 207 | flex-direction: row; 208 | justify-content: center; 209 | align-items: center; 210 | } 211 | 212 | .micReopen { 213 | position: relative; 214 | width: 107px; 215 | height: 40px; 216 | padding: 5px 16px 5px 16px; 217 | margin-left: 12px; 218 | margin-right: 12px; 219 | background-clip: padding-box; /* 确保背景不覆盖边框 */ 220 | border-radius: 12px; 221 | 222 | &:hover, 223 | &:active, 224 | &:focus { 225 | opacity: 1; 226 | color: rgba(0, 0, 0, 0.85); 227 | border-color: #d9d9d9; 228 | } 229 | } 230 | } 231 | 232 | .interrupt { 233 | display: flex; 234 | flex-direction: row; 235 | justify-content: center; 236 | align-items: center; 237 | margin-top: 12px; 238 | width: max-content; 239 | line-height: 28px; 240 | padding: 1px 6px 1px 6px; 241 | border-radius: 4px; 242 | margin-left: 4px; 243 | font-family: PingFang SC; 244 | font-size: 12px; 245 | font-weight: 400; 246 | letter-spacing: 0.003em; 247 | text-align: left; 248 | box-shadow: 0px 0px 0px 1px rgba(221, 226, 233, 1); 249 | color: var(--text-color-text-3, rgba(115, 122, 135, 1)); 250 | 251 | &:hover, 252 | &:active, 253 | &:focus { 254 | opacity: 1; 255 | border-color: #d9d9d9; 256 | } 257 | 258 | img { 259 | margin-right: 8px; 260 | } 261 | } 262 | } -------------------------------------------------------------------------------- /src/pages/MainPage/MainArea/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | import { useSelector } from 'react-redux'; 7 | import Antechamber from './Antechamber'; 8 | import Room from './Room'; 9 | 10 | function MainArea() { 11 | const room = useSelector((state: any) => state.room); 12 | const isJoined = room.isJoined; 13 | return isJoined ? : ; 14 | } 15 | 16 | export default MainArea; 17 | -------------------------------------------------------------------------------- /src/pages/MainPage/Menu/components/AISettingAnchor/index.module.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | .row { 7 | width: 100%; 8 | display: flex; 9 | flex-direction: row; 10 | align-items: center; 11 | cursor: pointer; 12 | 13 | .firstPart { 14 | display: flex; 15 | flex-direction: row; 16 | align-items: center; 17 | width: 90%; 18 | color: var(--text-color-text-2, var(--text-color-text-2, #42464E)); 19 | text-align: center; 20 | 21 | font-family: "PingFang SC"; 22 | font-size: 13px; 23 | font-style: normal; 24 | font-weight: 500; 25 | line-height: 22px; 26 | letter-spacing: 0.039px; 27 | } 28 | 29 | .finalPart { 30 | display: flex; 31 | flex-direction: row; 32 | align-items: center; 33 | width: 10%; 34 | justify-content: flex-end; 35 | 36 | .rightOutlined { 37 | font-size: 12px; 38 | } 39 | } 40 | 41 | .icon { 42 | margin-right: 4px; 43 | } 44 | } -------------------------------------------------------------------------------- /src/pages/MainPage/Menu/components/AISettingAnchor/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | import { useState } from 'react'; 7 | import { IconRight } from '@arco-design/web-react/icon'; 8 | import AISettings from '@/components/AISettings'; 9 | import styles from './index.module.less'; 10 | 11 | function AISettingAnchor() { 12 | const [open, setOpen] = useState(false); 13 | 14 | const handleOpenDrawer = () => setOpen(true); 15 | const handleCloseDrawer = () => setOpen(false); 16 | return ( 17 | <> 18 |
    19 |
    AI 设置
    20 |
    21 | 22 |
    23 |
    24 | 25 | 26 | ); 27 | } 28 | 29 | export default AISettingAnchor; 30 | -------------------------------------------------------------------------------- /src/pages/MainPage/Menu/components/DeviceDrawerButton/index.module.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | .wrapper { 7 | width: 100%; 8 | display: flex; 9 | flex-direction: row; 10 | gap: 24px; 11 | padding: 8px 16px; 12 | 13 | 14 | .label { 15 | display: flex; 16 | flex-direction: column; 17 | align-items: flex-start; 18 | line-height: 16px; 19 | gap: 12px; 20 | 21 | .label-text { 22 | font-family: PingFang SC; 23 | font-size: 14px; 24 | font-weight: 500; 25 | line-height: 22px; 26 | letter-spacing: 0.003em; 27 | text-align: left; 28 | } 29 | } 30 | 31 | .value { 32 | display: flex; 33 | flex-direction: column; 34 | justify-content: center; 35 | align-items: flex-start; 36 | gap: 18px; 37 | } 38 | } -------------------------------------------------------------------------------- /src/pages/MainPage/Menu/components/DeviceDrawerButton/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | import { useMemo } from 'react'; 7 | import { useSelector, useDispatch } from 'react-redux'; 8 | import { MediaType } from '@volcengine/rtc'; 9 | import { Switch, Select } from '@arco-design/web-react'; 10 | import DrawerRowItem from '@/components/DrawerRowItem'; 11 | import { RootState } from '@/store'; 12 | import RtcClient from '@/lib/RtcClient'; 13 | import { useDeviceState } from '@/lib/useCommon'; 14 | import { updateSelectedDevice } from '@/store/slices/device'; 15 | import utils from '@/utils/utils'; 16 | import styles from './index.module.less'; 17 | 18 | interface IDeviceDrawerButtonProps { 19 | type?: MediaType.AUDIO | MediaType.VIDEO; 20 | } 21 | 22 | const DEVICE_NAME = { 23 | [MediaType.AUDIO]: '麦克风', 24 | [MediaType.VIDEO]: '摄像头', 25 | }; 26 | 27 | function DeviceDrawerButton(props: IDeviceDrawerButtonProps) { 28 | const { type = MediaType.AUDIO } = props; 29 | const device = useDeviceState(); 30 | const isEnable = type === MediaType.AUDIO ? device.isAudioPublished : device.isVideoPublished; 31 | const switcher = type === MediaType.AUDIO ? device.switchMic : device.switchCamera; 32 | const devicePermissions = useSelector((state: RootState) => state.device.devicePermissions); 33 | const devices = useSelector((state: RootState) => state.device); 34 | const selectedDevice = 35 | type === MediaType.AUDIO ? devices.selectedMicrophone : devices.selectedCamera; 36 | const permission = devicePermissions?.[type === MediaType.AUDIO ? 'audio' : 'video']; 37 | 38 | const dispatch = useDispatch(); 39 | const deviceList = useMemo( 40 | () => (type === MediaType.AUDIO ? devices.audioInputs : devices.videoInputs), 41 | [devices] 42 | ); 43 | 44 | const handleDeviceChange = (value: string) => { 45 | RtcClient.switchDevice(type, value); 46 | if (type === MediaType.AUDIO) { 47 | dispatch( 48 | updateSelectedDevice({ 49 | selectedMicrophone: value, 50 | }) 51 | ); 52 | } 53 | if (type === MediaType.VIDEO) { 54 | dispatch( 55 | updateSelectedDevice({ 56 | selectedCamera: value, 57 | }) 58 | ); 59 | } 60 | }; 61 | 62 | return ( 63 | 71 |
    {DEVICE_NAME[type]}
    72 |
    73 | switcher(enable)} 77 | disabled={!permission} 78 | /> 79 | 86 |
    87 |
    88 | ), 89 | }} 90 | /> 91 | ); 92 | } 93 | 94 | export default DeviceDrawerButton; 95 | -------------------------------------------------------------------------------- /src/pages/MainPage/Menu/components/Interrupt/index.module.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | .interrupt { 7 | position: relative; 8 | display: flex; 9 | flex-direction: row; 10 | align-items: center; 11 | justify-content: space-between; 12 | 13 | .label { 14 | font-size: 13px; 15 | font-weight: 400; 16 | line-height: 22px; 17 | color: var(--text-color-text-1, rgba(12, 13, 14, 1)); 18 | 19 | .icon { 20 | margin-left: 4px; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/pages/MainPage/Menu/components/Interrupt/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | import { Popover, Switch } from '@arco-design/web-react'; 7 | import { IconQuestionCircle } from '@arco-design/web-react/icon'; 8 | import { useState } from 'react'; 9 | import { useDispatch } from 'react-redux'; 10 | import Config from '@/config'; 11 | import styles from './index.module.less'; 12 | import RtcClient from '@/lib/RtcClient'; 13 | import { clearHistoryMsg } from '@/store/slices/room'; 14 | 15 | function Interrupt() { 16 | const dispatch = useDispatch(); 17 | const [switchAble, setSwitchAble] = useState(true); 18 | const [enable, setEnable] = useState(Config.InterruptMode); 19 | const handleChange = () => { 20 | setSwitchAble(false); 21 | setEnable(!enable); 22 | Config.InterruptMode = !enable; 23 | if (RtcClient.getAudioBotEnabled()) { 24 | dispatch(clearHistoryMsg()); 25 | } 26 | RtcClient.updateAudioBot(); 27 | setTimeout(() => { 28 | setSwitchAble(true); 29 | }, 3000); 30 | }; 31 | return ( 32 |
    33 |
    34 | 语音打断 35 | 36 | 37 | 38 |
    39 |
    40 | 41 |
    42 |
    43 | ); 44 | } 45 | 46 | export default Interrupt; 47 | -------------------------------------------------------------------------------- /src/pages/MainPage/Menu/components/Operation/index.module.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | .device { 7 | display: flex; 8 | flex-direction: column; 9 | gap: 16px; 10 | } 11 | 12 | .box { 13 | position: relative; 14 | width: 100%; 15 | border-radius: 16px; 16 | background-color: white; 17 | border: 1px solid var(--line-color-border-2, rgba(234, 237, 241, 1)); 18 | padding: 16px 24px 16px 24px; 19 | box-sizing: border-box; 20 | margin-bottom: 16px; 21 | } -------------------------------------------------------------------------------- /src/pages/MainPage/Menu/components/Operation/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | import { MediaType } from '@volcengine/rtc'; 7 | import DeviceDrawerButton from '../DeviceDrawerButton'; 8 | import { useVisionMode } from '@/lib/useCommon'; 9 | import AISettingAnchor from '../AISettingAnchor'; 10 | import Interrupt from '../Interrupt'; 11 | import styles from './index.module.less'; 12 | 13 | function Operation() { 14 | const { isVisionMode, isScreenMode } = useVisionMode(); 15 | return ( 16 |
    17 | 18 | 19 | 20 | {isVisionMode && !isScreenMode ? : ''} 21 |
    22 | ); 23 | } 24 | 25 | export default Operation; 26 | -------------------------------------------------------------------------------- /src/pages/MainPage/Menu/index.module.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | .wrapper { 7 | width: 200px; 8 | height: 100%; 9 | border-radius: 16px; 10 | display: flex; 11 | flex-direction: column; 12 | align-items: center; 13 | 14 | .info { 15 | .bold { 16 | font-size: 13px; 17 | font-weight: 500; 18 | line-height: 22px; 19 | color: var(--text-color-text-1, rgba(12, 13, 14, 1)); 20 | } 21 | 22 | .gray { 23 | display: flex; 24 | flex-direction: row; 25 | align-items: center; 26 | font-size: 13px; 27 | font-weight: 400; 28 | line-height: 22px; 29 | color: var(--text-color-text-3, rgba(115, 122, 135, 1)); 30 | 31 | .value { 32 | width: 65%; 33 | font-size: 12px; 34 | font-weight: 500; 35 | margin-left: 5px; 36 | } 37 | 38 | :global { 39 | .arco-typography { 40 | margin-bottom: 0px; 41 | } 42 | } 43 | } 44 | 45 | .buttonArea { 46 | width: 100%; 47 | display: flex; 48 | flex-direction: row; 49 | align-items: center; 50 | justify-content: space-between; 51 | margin-top: 8px; 52 | 53 | .getMore { 54 | width: 100%; 55 | color: #fff; 56 | height: 32px; 57 | text-shadow: none; 58 | box-shadow: none; 59 | border: none; 60 | padding: 0px 24px; 61 | background: linear-gradient(56.59deg, #3C73FF 15.53%, #6E41EE 62.28%, #D641EE 90.32%), 62 | radial-gradient(203.56% 121.74% at 27.12% -21.74%, rgba(82, 182, 255, 0.2) 0%, rgba(143, 65, 238, 0) 100%), 63 | radial-gradient(134.75% 51.95% at 26.69% 5.8%, rgba(157, 214, 255, 0.1) 0%, rgba(143, 65, 238, 0) 100%), 64 | radial-gradient(82.39% 83.92% at 147.46% 76.45%, rgba(82, 99, 255, 0.8) 0%, rgba(143, 65, 238, 0) 100%); 65 | border-radius: 4px; 66 | display: flex; 67 | flex-direction: row; 68 | justify-content: center; 69 | align-items: center; 70 | 71 | color: var(--Primary-Neutral-0, #FFF); 72 | text-align: center; 73 | 74 | /* Body/body-2 medium */ 75 | font-family: "PingFang SC"; 76 | font-size: 13px; 77 | font-style: normal; 78 | font-weight: 500; 79 | cursor: pointer; 80 | } 81 | 82 | .getMore:hover { 83 | opacity: 0.9; 84 | } 85 | 86 | .getMore:active { 87 | opacity: 1; 88 | } 89 | 90 | .getMore[disabled], 91 | .getMore[disabled]:hover { 92 | color: #fff; 93 | background: linear-gradient(95.87deg, #1664FF 0%, #8040FF 97.7%); 94 | opacity: 0.8; 95 | } 96 | } 97 | } 98 | 99 | .questions { 100 | display: flex; 101 | flex-direction: column; 102 | gap: 8px; 103 | 104 | .title { 105 | font-size: 13px; 106 | font-weight: 500; 107 | line-height: 22px; 108 | } 109 | 110 | .line { 111 | font-size: 12px; 112 | font-weight: 400; 113 | line-height: 20px; 114 | color: rgba(66, 70, 78, 1); 115 | cursor: pointer; 116 | } 117 | } 118 | 119 | .device { 120 | display: flex; 121 | flex-direction: column; 122 | gap: 16px; 123 | } 124 | 125 | .box { 126 | position: relative; 127 | width: 100%; 128 | border-radius: 16px; 129 | background-color: white; 130 | border: 1px solid var(--line-color-border-2, rgba(234, 237, 241, 1)); 131 | padding: 16px 24px 16px 24px; 132 | box-sizing: border-box; 133 | margin-bottom: 16px; 134 | } 135 | 136 | .resetTime { 137 | position: relative; 138 | width: 100%; 139 | border-radius: 16px; 140 | padding: 0px 24px 8px 24px; 141 | box-sizing: border-box; 142 | display: flex; 143 | flex-direction: row; 144 | justify-content: center; 145 | align-items: center; 146 | 147 | user-select: none; 148 | -webkit-user-select: none; 149 | -moz-user-select: none; 150 | -ms-user-select: none; 151 | 152 | .normalLine { 153 | color: #42464E; 154 | /* Body/body-1 regular */ 155 | font-family: "PingFang SC"; 156 | font-size: 12px; 157 | font-style: normal; 158 | font-weight: 400; 159 | line-height: 20px; /* 166.667% */ 160 | letter-spacing: 0.036px; 161 | opacity: 0.8; 162 | } 163 | } 164 | } 165 | 166 | .mobile-camera-wrapper { 167 | position: relative; 168 | width: 100%; 169 | height: 100%; 170 | border-radius: 16px; 171 | display: flex; 172 | flex-direction: column; 173 | justify-content: center; 174 | align-items: center; 175 | margin-bottom: 16px; 176 | 177 | .mobile-camera { 178 | position: relative !important; 179 | width: 100% !important; 180 | height: 100% !important; 181 | top: auto !important; 182 | right: auto !important; 183 | } 184 | } -------------------------------------------------------------------------------- /src/pages/MainPage/Menu/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | import VERTC from '@volcengine/rtc'; 7 | import { useEffect, useState } from 'react'; 8 | import { Tooltip, Typography } from '@arco-design/web-react'; 9 | import { useDispatch, useSelector } from 'react-redux'; 10 | import { useVisionMode } from '@/lib/useCommon'; 11 | import { RootState } from '@/store'; 12 | import RtcClient from '@/lib/RtcClient'; 13 | import Operation from './components/Operation'; 14 | import { Questions } from '@/config'; 15 | import { COMMAND, INTERRUPT_PRIORITY } from '@/utils/handler'; 16 | import CameraArea from '../MainArea/Room/CameraArea'; 17 | import { setHistoryMsg, setInterruptMsg } from '@/store/slices/room'; 18 | import utils from '@/utils/utils'; 19 | import packageJson from '../../../../package.json'; 20 | import styles from './index.module.less'; 21 | 22 | function Menu() { 23 | const dispatch = useDispatch(); 24 | const [question, setQuestion] = useState(''); 25 | const room = useSelector((state: RootState) => state.room); 26 | const scene = room.scene; 27 | const isJoined = room?.isJoined; 28 | const isVisionMode = useVisionMode(); 29 | 30 | const handleQuestion = (que: string) => { 31 | RtcClient.commandAudioBot(COMMAND.EXTERNAL_TEXT_TO_LLM, INTERRUPT_PRIORITY.HIGH, que); 32 | setQuestion(que); 33 | }; 34 | 35 | useEffect(() => { 36 | if (question && !room.isAITalking) { 37 | dispatch(setInterruptMsg()); 38 | dispatch( 39 | setHistoryMsg({ 40 | text: question, 41 | user: RtcClient.basicInfo.user_id, 42 | paragraph: true, 43 | definite: true, 44 | }) 45 | ); 46 | setQuestion(''); 47 | } 48 | }, [question, room.isAITalking]); 49 | 50 | return ( 51 |
    52 | {isJoined && utils.isMobile() && isVisionMode ? ( 53 |
    54 | 55 |
    56 | ) : null} 57 |
    58 |
    Demo Version {packageJson.version}
    59 |
    SDK Version {VERTC.getSdkVersion()}
    60 | {isJoined ? ( 61 |
    62 | 房间ID{' '} 63 | 64 | 71 | {room.roomId || '-'} 72 | 73 | 74 |
    75 | ) : ( 76 | '' 77 | )} 78 |
    79 | {isJoined ? ( 80 |
    81 |
    点击下述问题进行提问:
    82 | {Questions[scene].map((question) => ( 83 |
    handleQuestion(question)} className={styles.line} key={question}> 84 | {question} 85 |
    86 | ))} 87 |
    88 | ) : ( 89 | '' 90 | )} 91 | {isJoined ? : ''} 92 |
    93 | ); 94 | } 95 | 96 | export default Menu; 97 | -------------------------------------------------------------------------------- /src/pages/MainPage/index.module.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | .main { 7 | position: relative; 8 | width: 100%; 9 | height: calc(100% - 48px); 10 | display: flex; 11 | flex-direction: row; 12 | align-items: center; 13 | box-sizing: border-box; 14 | 15 | .mainArea { 16 | position: relative; 17 | width: calc(100% - 220px); 18 | height: 100%; 19 | margin-right: 2%; 20 | background-color: white; 21 | border-radius: 16px; 22 | overflow: hidden; 23 | } 24 | 25 | .isMobile { 26 | width: 100% !important; 27 | margin-right: 0% !important; 28 | border-radius: 0px !important; 29 | } 30 | 31 | .operationArea { 32 | position: relative; 33 | width: 200px; 34 | height: 100%; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/pages/MainPage/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | import Header from '@/components/Header'; 7 | import ResizeWrapper from '@/components/ResizeWrapper'; 8 | import Menu from './Menu'; 9 | import utils from '@/utils/utils'; 10 | import MainArea from './MainArea'; 11 | import styles from './index.module.less'; 12 | 13 | export default function () { 14 | return ( 15 | 16 |
    17 |
    23 |
    24 | 25 |
    26 | {utils.isMobile() ? null : ( 27 |
    28 | 29 |
    30 | )} 31 |
    32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | /// 7 | 8 | declare module '*.less' { 9 | const content: { [className: string]: string }; 10 | export default content; 11 | } 12 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | import { configureStore } from '@reduxjs/toolkit'; 7 | import roomSlice, { RoomState } from './slices/room'; 8 | import deviceSlice, { DeviceState } from './slices/device'; 9 | 10 | export interface RootState { 11 | room: RoomState; 12 | device: DeviceState; 13 | } 14 | 15 | const store = configureStore({ 16 | reducer: { 17 | room: roomSlice, 18 | device: deviceSlice, 19 | }, 20 | middleware: (getDefaultMiddleware) => 21 | getDefaultMiddleware({ 22 | serializableCheck: false, 23 | }), 24 | }); 25 | 26 | export default store; 27 | -------------------------------------------------------------------------------- /src/store/slices/device.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 7 | import { DeviceType } from '@/interface'; 8 | 9 | export const medias = [DeviceType.Microphone]; 10 | 11 | export const MediaName = { 12 | [DeviceType.Microphone]: 'microphone', 13 | [DeviceType.Camera]: 'camera', 14 | }; 15 | 16 | export interface DeviceState { 17 | audioInputs: MediaDeviceInfo[]; 18 | videoInputs: MediaDeviceInfo[]; 19 | selectedCamera?: string; 20 | selectedMicrophone?: string; 21 | devicePermissions: { 22 | audio: boolean; 23 | video: boolean; 24 | }; 25 | } 26 | const initialState: DeviceState = { 27 | audioInputs: [], 28 | videoInputs: [], 29 | devicePermissions: { 30 | audio: true, 31 | video: true, 32 | }, 33 | }; 34 | 35 | export const DeviceSlice = createSlice({ 36 | name: 'deivce', 37 | initialState, 38 | reducers: { 39 | updateMediaInputs: (state, { payload }) => { 40 | if (payload.audioInputs) { 41 | state.audioInputs = payload.audioInputs; 42 | } 43 | if (payload.videoInputs) { 44 | state.videoInputs = payload.videoInputs; 45 | } 46 | }, 47 | updateSelectedDevice: (state, { payload }) => { 48 | if (payload.selectedCamera) { 49 | state.selectedCamera = payload.selectedCamera; 50 | } 51 | if (payload.selectedMicrophone) { 52 | state.selectedMicrophone = payload.selectedMicrophone; 53 | } 54 | }, 55 | 56 | setMicrophoneList: (state, action: PayloadAction) => { 57 | state.audioInputs = action.payload; 58 | }, 59 | 60 | setDevicePermissions: ( 61 | state, 62 | action: PayloadAction<{ 63 | audio: boolean; 64 | video: boolean; 65 | }> 66 | ) => { 67 | state.devicePermissions = action.payload; 68 | }, 69 | }, 70 | }); 71 | export const { updateMediaInputs, updateSelectedDevice, setMicrophoneList, setDevicePermissions } = 72 | DeviceSlice.actions; 73 | 74 | export default DeviceSlice.reducer; 75 | -------------------------------------------------------------------------------- /src/theme.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | @primary-color: #1664ff; 7 | -------------------------------------------------------------------------------- /src/utils/handler.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | import { useDispatch } from 'react-redux'; 7 | import logger from './logger'; 8 | import { 9 | setHistoryMsg, 10 | setInterruptMsg, 11 | updateAITalkState, 12 | updateAIThinkState, 13 | } from '@/store/slices/room'; 14 | import RtcClient from '@/lib/RtcClient'; 15 | import Utils from '@/utils/utils'; 16 | 17 | export type AnyRecord = Record; 18 | 19 | export enum MESSAGE_TYPE { 20 | BRIEF = 'conv', 21 | SUBTITLE = 'subv', 22 | FUNCTION_CALL = 'tool', 23 | } 24 | 25 | export enum AGENT_BRIEF { 26 | UNKNOWN, 27 | LISTENING, 28 | THINKING, 29 | SPEAKING, 30 | INTERRUPTED, 31 | FINISHED, 32 | } 33 | 34 | /** 35 | * @brief 指令类型 36 | */ 37 | export enum COMMAND { 38 | /** 39 | * @brief 打断指令 40 | */ 41 | INTERRUPT = 'interrupt', 42 | /** 43 | * @brief 发送外部文本驱动 TTS 44 | */ 45 | EXTERNAL_TEXT_TO_SPEECH = 'ExternalTextToSpeech', 46 | /** 47 | * @brief 发送外部文本驱动 LLM 48 | */ 49 | EXTERNAL_TEXT_TO_LLM = 'ExternalTextToLLM', 50 | } 51 | /** 52 | * @brief 打断的类型 53 | */ 54 | export enum INTERRUPT_PRIORITY { 55 | /** 56 | * @brief 占位 57 | */ 58 | NONE, 59 | /** 60 | * @brief 高优先级。传入信息直接打断交互,进行处理。 61 | */ 62 | HIGH, 63 | /** 64 | * @brief 中优先级。等待当前交互结束后,进行处理。 65 | */ 66 | MEDIUM, 67 | /** 68 | * @brief 低优先级。如当前正在发生交互,直接丢弃 Message 传入的信息。 69 | */ 70 | LOW, 71 | } 72 | 73 | export const MessageTypeCode = { 74 | [MESSAGE_TYPE.SUBTITLE]: 1, 75 | [MESSAGE_TYPE.FUNCTION_CALL]: 2, 76 | [MESSAGE_TYPE.BRIEF]: 3, 77 | }; 78 | 79 | export const useMessageHandler = () => { 80 | const dispatch = useDispatch(); 81 | 82 | const maps = { 83 | /** 84 | * @brief 接收状态变化信息 85 | * @note https://www.volcengine.com/docs/6348/1415216?s=g 86 | */ 87 | [MESSAGE_TYPE.BRIEF]: (parsed: AnyRecord) => { 88 | const { Stage } = parsed || {}; 89 | const { Code, Description } = Stage || {}; 90 | logger.debug(Code, Description); 91 | switch (Code) { 92 | case AGENT_BRIEF.THINKING: 93 | dispatch(updateAIThinkState({ isAIThinking: true })); 94 | break; 95 | case AGENT_BRIEF.SPEAKING: 96 | dispatch(updateAITalkState({ isAITalking: true })); 97 | break; 98 | case AGENT_BRIEF.FINISHED: 99 | dispatch(updateAITalkState({ isAITalking: false })); 100 | break; 101 | case AGENT_BRIEF.INTERRUPTED: 102 | dispatch(setInterruptMsg()); 103 | break; 104 | default: 105 | break; 106 | } 107 | }, 108 | /** 109 | * @brief 字幕 110 | * @note https://www.volcengine.com/docs/6348/1337284?s=g 111 | */ 112 | [MESSAGE_TYPE.SUBTITLE]: (parsed: AnyRecord) => { 113 | const data = parsed.data?.[0] || {}; 114 | /** debounce 记录用户输入文字 */ 115 | if (data) { 116 | const { text: msg, definite, userId: user, paragraph } = data; 117 | logger.debug('handleRoomBinaryMessageReceived', data); 118 | if ((window as any)._debug_mode) { 119 | dispatch(setHistoryMsg({ msg, user, paragraph, definite })); 120 | } else { 121 | const isAudioEnable = RtcClient.getAudioBotEnabled(); 122 | if (isAudioEnable) { 123 | dispatch(setHistoryMsg({ text: msg, user, paragraph, definite })); 124 | } 125 | } 126 | } 127 | }, 128 | /** 129 | * @brief Function calling 130 | * @note https://www.volcengine.com/docs/6348/1359441?s=g 131 | */ 132 | [MESSAGE_TYPE.FUNCTION_CALL]: (parsed: AnyRecord) => { 133 | const name: string = parsed?.tool_calls?.[0]?.function?.name; 134 | console.log('[Function Call] - Called by sendUserBinaryMessage'); 135 | const map: Record = { 136 | getcurrentweather: '今天下雪, 最低气温零下10度', 137 | }; 138 | 139 | RtcClient.engine.sendUserBinaryMessage( 140 | 'RobotMan_', 141 | Utils.string2tlv( 142 | JSON.stringify({ 143 | ToolCallID: parsed?.tool_calls?.[0]?.id, 144 | Content: map[name.toLocaleLowerCase().replaceAll('_', '')], 145 | }), 146 | 'func' 147 | ) 148 | ); 149 | }, 150 | }; 151 | 152 | return { 153 | parser: (buffer: ArrayBuffer) => { 154 | try { 155 | const { type, value } = Utils.tlv2String(buffer); 156 | maps[type as MESSAGE_TYPE]?.(JSON.parse(value)); 157 | } catch (e) { 158 | logger.debug('parse error', e); 159 | } 160 | }, 161 | }; 162 | }; 163 | -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | class Logger { 7 | public debug(...args: any[]) { 8 | console.debug(...args); 9 | } 10 | 11 | public log(...args: any[]) { 12 | console.log(...args); 13 | } 14 | 15 | public error(...args: any[]) { 16 | console.error(...args); 17 | } 18 | 19 | public warn(...args: any[]) { 20 | console.warn(...args); 21 | } 22 | } 23 | 24 | export default new Logger(); 25 | -------------------------------------------------------------------------------- /src/utils/utils.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | @meetingBackgroundColor: #1e2128; 7 | 8 | .flex-box { 9 | display: flex; 10 | flex-direction: row; 11 | justify-content: space-between; 12 | align-items: center; 13 | } 14 | 15 | .split-line(@h, @m) { 16 | display: inline-block; 17 | height: @h; 18 | width: 0; 19 | border-right: 1px solid #4e5969; 20 | margin: 0 @m; 21 | } 22 | 23 | .place-holder { 24 | font-family: "PingFang SC"; 25 | font-style: normal; 26 | font-weight: normal; 27 | font-size: 14px; 28 | line-height: 22px; 29 | color: #86909c; 30 | position: relative; 31 | left: 5px; 32 | } 33 | -------------------------------------------------------------------------------- /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved. 3 | * SPDX-license-identifier: BSD-3-Clause 4 | */ 5 | 6 | class Utils { 7 | formatTime = (time: number): string => { 8 | if (time < 0) { 9 | return '00:00'; 10 | } 11 | let minutes: number | string = Math.floor(time / 60); 12 | let seconds: number | string = time % 60; 13 | minutes = minutes > 9 ? `${minutes}` : `0${minutes}`; 14 | seconds = seconds > 9 ? `${seconds}` : `0${seconds}`; 15 | 16 | return `${minutes}:${seconds}`; 17 | }; 18 | 19 | formatDate = (date: Date): string => { 20 | const hours = date.getHours(); 21 | const minutes = date.getMinutes(); 22 | const seconds = date.getSeconds(); 23 | 24 | const formattedHours = hours.toString().padStart(2, '0'); 25 | const formattedMinutes = minutes.toString().padStart(2, '0'); 26 | const formattedSeconds = seconds.toString().padStart(2, '0'); 27 | 28 | return `${formattedHours}:${formattedMinutes}:${formattedSeconds}`; 29 | }; 30 | 31 | setSessionInfo = (params: { [key: string]: any }) => { 32 | Object.keys(params).forEach((key) => { 33 | sessionStorage.setItem(key, params[key]); 34 | }); 35 | }; 36 | 37 | /** 38 | * @brief 获取 url 参数 39 | */ 40 | getUrlArgs = () => { 41 | const query = window.location.search.substring(1); 42 | const pairs = query.split('&'); 43 | return pairs.reduce<{ [key: string]: string }>((queries, pair) => { 44 | const [key, value] = pair.split('='); 45 | if (key && value) { 46 | queries[key] = decodeURIComponent(value); 47 | } 48 | return queries; 49 | }, {}); 50 | }; 51 | 52 | isPureObject = (target: any) => Object.prototype.toString.call(target).includes('Object'); 53 | 54 | isArray = Array.isArray; 55 | 56 | /** 57 | * @brief 将字符串包装成 TLV 58 | */ 59 | string2tlv(str: string, type: string) { 60 | const typeBuffer = new Uint8Array(4); 61 | 62 | for (let i = 0; i < type.length; i++) { 63 | typeBuffer[i] = type.charCodeAt(i); 64 | } 65 | 66 | const lengthBuffer = new Uint32Array(1); 67 | const valueBuffer = new TextEncoder().encode(str); 68 | 69 | lengthBuffer[0] = valueBuffer.length; 70 | 71 | const tlvBuffer = new Uint8Array(typeBuffer.length + 4 + valueBuffer.length); 72 | 73 | tlvBuffer.set(typeBuffer, 0); 74 | 75 | tlvBuffer[4] = (lengthBuffer[0] >> 24) & 0xff; 76 | tlvBuffer[5] = (lengthBuffer[0] >> 16) & 0xff; 77 | tlvBuffer[6] = (lengthBuffer[0] >> 8) & 0xff; 78 | tlvBuffer[7] = lengthBuffer[0] & 0xff; 79 | 80 | tlvBuffer.set(valueBuffer, 8); 81 | return tlvBuffer.buffer; 82 | } 83 | 84 | /** 85 | * @brief TLV 数据格式转换成字符串 86 | * @note TLV 数据格式 87 | * | magic number | length(big-endian) | value | 88 | * @param {ArrayBufferLike} tlvBuffer 89 | * @returns 90 | */ 91 | tlv2String(tlvBuffer: ArrayBufferLike) { 92 | const typeBuffer = new Uint8Array(tlvBuffer, 0, 4); 93 | const lengthBuffer = new Uint8Array(tlvBuffer, 4, 4); 94 | const valueBuffer = new Uint8Array(tlvBuffer, 8); 95 | 96 | let type = ''; 97 | for (let i = 0; i < typeBuffer.length; i++) { 98 | type += String.fromCharCode(typeBuffer[i]); 99 | } 100 | 101 | const length = 102 | (lengthBuffer[0] << 24) | (lengthBuffer[1] << 16) | (lengthBuffer[2] << 8) | lengthBuffer[3]; 103 | 104 | const value = new TextDecoder().decode(valueBuffer.subarray(0, length)); 105 | 106 | return { type, value }; 107 | } 108 | 109 | isMobile() { 110 | return /Mobi|Android|iPhone|iPad|Windows Phone/i.test(window.navigator.userAgent); 111 | } 112 | } 113 | 114 | export default new Utils(); 115 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | "baseUrl": ".", 19 | "paths": { 20 | "@/*": ["./src/*"] 21 | } 22 | }, 23 | "include": ["src"] 24 | } 25 | --------------------------------------------------------------------------------