[
  {
    "path": ".eslintrc",
    "content": "{\n  \"root\": true,\n  \"env\": {\n    \"browser\": true,\n    \"commonjs\": true,\n    \"es6\": true,\n    \"node\": true,\n    \"jest\": true\n  },\n  \"parser\": \"@typescript-eslint/parser\",\n  \"extends\": [\n    \"airbnb\",\n    \"plugin:react/recommended\",\n    \"plugin:prettier/recommended\",\n    \"plugin:react-hooks/recommended\"\n  ],\n  \"parserOptions\": {\n    \"ecmaFeatures\": {\n      \"experimentalObjectRestSpread\": true\n    },\n    \"sourceType\": \"module\"\n  },\n  \"plugins\": [\"react\", \"babel\", \"@typescript-eslint/eslint-plugin\"],\n  \"globals\": {\n    \"ActiveXObject\": false,\n    \"describe\": false,\n    \"it\": false,\n    \"expect\": false,\n    \"jest\": false,\n    \"$\": false,\n    \"afterEach\": false,\n    \"beforeEach\": false\n  },\n  \"overrides\": [\n    {\n      \"files\": [\"*.ts\", \"*.tsx\"],\n      \"rules\": {\n        \"@typescript-eslint/no-unused-vars\": [2, { \"args\": \"none\" }],\n        \"@typescript-eslint/no-use-before-define\": [2, { \"functions\": false, \"classes\": false }]\n      }\n    }\n  ],\n  \"rules\": {\n    \"prettier/prettier\": [\"warn\", { \"trailingComma\": \"es5\", \"printWidth\": 100 }],\n    \"linebreak-style\": \"off\",\n    \"no-console\": [\"warn\", { \"allow\": [\"warn\", \"error\", \"log\"] }],\n    \"no-case-declarations\": 0,\n    \"no-param-reassign\": 0,\n    \"no-underscore-dangle\": 0,\n    \"no-useless-constructor\": 0,\n    \"no-unused-vars\": [2, { \"vars\": \"all\", \"args\": \"none\" }],\n    \"no-restricted-syntax\": 0,\n    \"no-unused-expressions\": [\"error\", { \"allowShortCircuit\": true, \"allowTernary\": true }],\n    \"no-plusplus\": 0,\n    \"no-return-assign\": 0,\n    \"no-script-url\": 0,\n    \"no-extend-native\": 0,\n    \"no-restricted-globals\": 0,\n    \"no-nested-ternary\": 0,\n    \"no-empty\": 0,\n    \"no-void\": 0,\n    \"no-useless-escape\": 0,\n    \"no-bitwise\": 0,\n    \"no-mixed-operators\": 0,\n    \"consistent-return\": 0,\n    \"one-var\": 0,\n    \"prefer-promise-reject-errors\": 0,\n    \"prefer-destructuring\": 0,\n    \"global-require\": 0,\n    \"guard-for-in\": 0,\n    \"func-names\": 0,\n    \"strict\": 0,\n    \"radix\": 0,\n    \"no-prototype-builtins\": 0,\n    \"class-methods-use-this\": 0,\n    \"import/no-dynamic-require\": 0,\n    \"import/no-unresolved\": 0,\n    \"import/extensions\": 0,\n    \"import/no-extraneous-dependencies\": 0,\n    \"import/prefer-default-export\": 0,\n    \"import/no-absolute-path\": 0,\n    \"react/no-danger\": 0,\n    \"react/forbid-prop-types\": 0,\n    \"react/prop-types\": 0,\n    \"react/jsx-filename-extension\": [1, { \"extensions\": [\".js\", \".jsx\", \"ts\", \"tsx\"] }],\n    \"react/sort-comp\": 0,\n    \"react/no-did-update-set-state\": 0,\n    \"react/prefer-stateless-function\": 0,\n    \"react/jsx-closing-tag-location\": 0,\n    \"react/jsx-no-bind\": 0,\n    \"react/no-array-index-key\": 0,\n    \"react/no-children-prop\": 0,\n    \"react/no-did-mount-set-state\": 0,\n    \"react/no-find-dom-node\": 0,\n    \"react/default-props-match-prop-types\": 0,\n    \"react/jsx-one-expression-per-line\": 0,\n    \"react/react-in-jsx-scope\": 0,\n    \"react/jsx-props-no-spreading\": 0,\n    \"jsx-a11y/anchor-is-valid\": 0,\n    \"jsx-a11y/no-static-element-interactions\": 0,\n    \"jsx-a11y/click-events-have-key-events\": 0,\n    \"jsx-a11y/no-noninteractive-element-interactions\": 0,\n    \"jsx-a11y/alt-text\": 0,\n    \"jsx-a11y/label-has-for\": 0,\n    \"jsx-a11y/label-has-associated-control\": 0,\n    \"jsx-a11y/no-noninteractive-tabindex\": 0,\n    \"jsx-a11y/tabindex-no-positive\": 0,\n    \"react/jsx-indent\": 0,\n    \"react/display-name\": 0,\n    \"react/no-multi-comp\": 0,\n    \"react/destructuring-assignment\": 0,\n    \"react/no-access-state-in-setstate\": 0,\n    \"react/button-has-type\": 0,\n    \"react/require-default-props\": 0,\n    \"react/jsx-wrap-multilines\": 0,\n    \"react/no-render-return-value\": 0,\n    \"array-callback-return\": 0,\n    \"no-cond-assign\": 0,\n    \"@typescript-eslint/explicit-function-return-type\": 0,\n    \"no-use-before-define\": 0,\n    \"@typescript-eslint/no-use-before-define\": 2,\n    \"@typescript-eslint/no-var-requires\": 0,\n    \"@typescript-eslint/no-empty-function\": 0,\n    \"no-shadow\": 0,\n    \"no-continue\": 0,\n    \"no-loop-func\": 0,\n    \"prefer-spread\": 0,\n    \"react-hooks/rules-of-hooks\": \"error\",\n    \"react-hooks/exhaustive-deps\": \"warn\",\n    \"no-undef\": 0\n  }\n}\n"
  },
  {
    "path": ".gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n.vscode\n# testing\n/coverage\n\n# production\n/build\n\n# misc\n.DS_Store\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.eslintcache\n\n/Server/node_modules\n/Server/log\n/log\nyarn.lock\n.eslintcache\npnpm-lock.yaml\ndist/\nlog/"
  },
  {
    "path": ".npmrc",
    "content": "registry = 'https://registry.npmjs.org/'\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"arrowParens\": \"always\",\n  \"semi\": true,\n  \"singleQuote\": true,\n  \"jsxSingleQuote\": false,\n  \"printWidth\": 100,\n  \"useTabs\": false,\n  \"tabWidth\": 2,\n  \"trailingComma\": \"es5\"\n}\n"
  },
  {
    "path": ".stylelintrc",
    "content": "{\n  \"extends\": [\"stylelint-config-standard\", \"stylelint-config-prettier\"],\n  \"customSyntax\": \"postcss-less\",\n  \"rules\": {\n    \"no-descending-specificity\": null,\n    \"no-duplicate-selectors\": null,\n    \"font-family-no-missing-generic-family-keyword\": null,\n    \"block-opening-brace-space-before\": \"always\",\n    \"declaration-block-trailing-semicolon\": null,\n    \"declaration-colon-newline-after\": null,\n    \"indentation\": null,\n    \"selector-descendant-combinator-no-non-space\": null,\n    \"selector-class-pattern\": null,\n    \"keyframes-name-pattern\": null,\n    \"no-invalid-position-at-import-rule\": null,\n    \"number-max-precision\": 6,\n    \"color-function-notation\": null,\n    \"selector-pseudo-class-no-unknown\": [\n      true,\n      {\n        \"ignorePseudoClasses\": [\"global\"]\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\n1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\n2. 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.\n3. 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.\nTHIS 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.\n======\n======\nTHE FOLLOWING SETS FORTH ATTRIBUTION NOTICES FOR THIRD PARTY SOFTWARE THAT MAY BE CONTAINED IN PORTIONS OF RTC AIGC DEMO.  \n======\nWebRTC\nCopyright (c) 2011, The WebRTC project authors. All rights reserved.\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\n1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\n2. 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.\n3. 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.\nTHIS 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.\n======"
  },
  {
    "path": "README.md",
    "content": "# 交互式AIGC场景 AIGC Demo\n\n此 Demo 为简化版本, 如您有 1.5.x 版本 UI 的诉求, 可切换至 1.5.1 分支。\n跑通阶段时, 无须关心代码实现，仅需按需完成 `Server/scenes/*.json` 的场景信息填充即可。\n\n## 简介\n- 在 AIGC 对话场景下，火山引擎 AIGC-RTC Server 云端服务，通过整合 RTC 音视频流处理，ASR 语音识别，大模型接口调用集成，以及 TTS 语音生成等能力，提供基于流式语音的端到端AIGC能力链路。\n- 用户只需调用基于标准的 OpenAPI 接口即可配置所需的 ASR、LLM、TTS 类型和参数。火山引擎云端计算服务负责边缘用户接入、云端资源调度、音视频流压缩、文本与语音转换处理以及数据订阅传输等环节。简化开发流程，让开发者更专注在对大模型核心能力的训练及调试，从而快速推进AIGC产品应用创新。     \n- 同时火山引擎 RTC拥有成熟的音频 3A 处理、视频处理等技术以及大规模音视频聊天能力，可支持 AIGC 产品更便捷的支持多模态交互、多人互动等场景能力，保持交互的自然性和高效性。 \n\n## 【必看】环境准备\n**Node 版本: 16.0+**\n\n### 1. 运行环境\n需要准备两个 Terminal，分别启动服务端和前端页面。\n\n### 2. 服务开通\n开通 ASR、TTS、LLM、RTC 等服务，可参考 [开通服务](https://www.volcengine.com/docs/6348/1315561?s=g) 进行相关服务的授权与开通。\n\n### 3. 场景配置\n`Server/scenes/*.json`\n\n您可以自定义具体场景, 并按需根据模版填充 `SceneConfig`、`AccountConfig`、`RTCConfig`、`VoiceChat` 中需要的参数。\n\nDemo 中以 `Custom` 场景为例，您可以自行新增场景。\n\n注意：\n- `SceneConfig`：场景的信息，例如名称、头像等。\n- `AccountConfig`：场景下的账号信息，https://console.volcengine.com/iam/keymanage/ 获取 AK/SK。\n- `RTCConfig`：场景下的 RTC 配置。\n    - AppId、AppKey 可从 https://console.volcengine.com/rtc/aigc/listRTC 中获取。\n    - RoomId、UserId 可自定义也可不填，交由服务端生成。\n- `VoiceChat`: 场景下的 AIGC 配置。\n    - 可参考 https://www.volcengine.com/docs/6348/1558163 中参数描述，完整填写参数内容。\n    - 可通过 [快速跑通 Demo](https://console.volcengine.com/rtc/aigc/run?s=g) 快速获取参数, 跑通后点击右上角 `接入 API` 按钮复制相关代码贴到 JSON 配置文件中即可。\n## 快速开始\n请注意，服务端和 Web 端都需要启动, 启动步骤如下:\n### 服务端\n进到项目根目录\n#### 安装依赖\n```shell\ncd Server\nyarn\n```\n#### 运行项目\n```shell\nyarn dev\n```\n\n### 前端页面\n进到项目根目录\n#### 安装依赖\n```shell\nyarn\n```\n#### 运行项目\n```shell\nyarn dev\n```\n\n### 常见问题\n| 问题 | 解决方案 |\n| :-- | :-- |\n| 如何使用第三方模型、Coze Bot | 模型相关配置代码对应目录 `src/config/scenes/` 下json 文件，填写对应官方模型/ Coze/ 第三方模型的参数后，可点击页面上的 \"修改 AI 人设\" 进行切换。 |\n| **启动智能体之后, 对话无反馈，或者一直停留在 \"AI 准备中, 请稍侯\"；在启用数字人的情况下，一直停留在“数字人准备中，请稍候”** | <li>可能因为控制台中相关权限没有正常授予，请参考[流程](https://www.volcengine.com/docs/6348/1315561?s=g)再次确认下是否完成相关操作。此问题的可能性较大，建议仔细对照是否已经将相应的权限开通。</li><li>参数传递可能有问题, 例如参数大小写、类型等问题，请再次确认下这类型问题是否存在。</li><li>相关资源可能未开通或者用量不足/欠费，请再次确认。</li><li>**请检查当前使用的模型 ID / 数字人 AppId / Token 等内容都是正确且可用的。**</li><li>数字人服务有并发限制，当达到并发限制时，同样会表现为一直停留在“数字人准备中”状态</li> |\n| **浏览器报了 `Uncaught (in promise) r: token_error` 错误** | 请检查您填在项目中的 RTC Token 是否合法，检测用于生成 Token 的 UserId、RoomId 以及 Token 本身是否与项目中填写的一致；或者 Token 可能过期, 可尝试重新生成下。 |\n| **[StartVoiceChat]Failed(Reason: The task has been started. Please do not call the startup task interface repeatedly.)** 报错 | 如果设置的 RoomId、UserId 为固定值，重复调用 startAgent 会导致出错，只需先调用 stopAgent 后再重新 startAgent 即可。 |\n| 为什么麦克风、摄像头开启失败？浏览器报了`TypeError: Cannot read properties of undefined (reading 'getUserMedia')` | 检查当前页面是否为[安全上下文](https://developer.mozilla.org/zh-CN/docs/Web/Security/Secure_Contexts)（简单来说，检查当前页面是否为 `localhost` 或者 是否为 https 协议）。浏览器[限制](https://developer.mozilla.org/zh-CN/docs/Web/Security/Secure_Contexts/features_restricted_to_secure_contexts) `getUserMedia` 只能在安全上下文中使用。 |\n| 为什么我的麦克风正常、摄像头也正常，但是设备没有正常工作? | 可能是设备权限未授予，详情可参考 [Web 排查设备权限获取失败问题](https://www.volcengine.com/docs/6348/1356355?s=g)。 |\n| 接口调用时, 返回 \"Invalid 'Authorization' header, Pls check your authorization header\" 错误 | `Server/app.js` 中的 AK/SK 不正确 |\n| 什么是 RTC | **R**eal **T**ime **C**ommunication, RTC 的概念可参考[官网文档](https://www.volcengine.com/docs/6348/66812?s=g)。 |\n| 不清楚什么是主账号，什么是子账号 | 可以参考[官方概念](https://www.volcengine.com/docs/6257/64963?hyperlink_open_type=lark.open_in_browser&s=g) 。|\n| 我有自己的服务端了, 我应该怎么让前端调用我的服务端呢 | 修改 `src/config/index.ts` 中的 `AIGC_PROXY_HOST` 请求域名和接口并在 `src/app/api.ts` 中修改接口参数配置 `APIS_CONFIG` |\n\n如果有上述以外的问题，欢迎联系我们反馈。\n\n### 相关文档\n- [场景介绍](https://www.volcengine.com/docs/6348/1310537?s=g)\n- [Demo 体验](https://www.volcengine.com/docs/6348/1310559?s=g)\n- [场景搭建方案](https://www.volcengine.com/docs/6348/1310560?s=g)\n\n## 更新日志\n\n### OpenAPI 更新\n参考 [OpenAPI 更新](https://www.volcengine.com/docs/6348/1544162) 中与 实时对话式 AI 相关的更新内容。\n\n### Demo 更新\n\n#### [1.6.0]\n- 2025-09-30\n    - 更新数字人场景相关配置\n- 2025-07-08\n    - 更新 RTC Web SDK 版本至 4.66.20\n- 2025-06-26\n    - 修复进房有问题的 BUG\n- 2025-06-23\n    - 简化 Demo 使用, 配置归一化。\n    - 删除无用组件。\n    - 追加服务端 README。\n- 2025-06-18\n    - 更新 RTC Web SDK 版本至 4.66.16\n    - 更新 UI 和参数配置方式\n    - 更新 Readme 文档\n    - 追加 Node 服务的参数检测能力\n    - 追加 Node 服务的 Token 生成能力"
  },
  {
    "path": "Server/.npmrc",
    "content": "registry = 'https://registry.npmjs.org/'"
  },
  {
    "path": "Server/README.md",
    "content": "# Node Server\n\n## 启动命令\n```\nyarn\n\nyarn dev\n```\n\n## 使用须知\nNode 服务启动时会自动读取 `Server/scenes` 下的所有文件作为可用的场景, 并通过接口 API 返回相关信息。\n\n因此，您需要：\n1. 在 `Server/scenes` 目录下参考其它 JSON 的格式, 自定义创建一个 `xxxx.json` 文件，用于描述您的场景，其中 xxxx 为场景名称。\n2. 确保您的 `.json` 文件符合模版定义(可参考 Custom.json), 大小写敏感。\n3. 新增场景 JSON 后须重启 Node 服务，保证场景信息被正常读取。\n4. JSON 文件中, 若 `RTCConfig.RoomId`、`RTCConfig.UserId`、`RTCConfig.Token` 其中之一未填写, Node 服务将自动生成对应的值以保证对话可以正常启动。\n\n\n## 相关参数获取\n- AccountConfig\n    - 可在 https://console.volcengine.com/iam/keymanage/ 获取 AK/SK。\n- RTCConfig\n    - AppId、AppKey 可从 https://console.volcengine.com/rtc/aigc/listRTC 中获取。\n    - RoomId、UserId 可自定义也可不填，交由服务端生成。\n- VoiceChat\n    - 可参考 https://www.volcengine.com/docs/6348/1558163 中参数描述\n    - 可通过 [快速跑通 Demo](https://console.volcengine.com/rtc/aigc/run?s=g) 快速获取参数, 跑通后点击右上角 `接入 API` 按钮复制相关代码贴到 JSON 配置文件中即可。\n\n\n## 注意\n- 相关错误会通过服务端接口返回。\n- Node 服务会根据您配置的 `VoiceChat` 中是否存在视觉模型相关的配置返回相关信息给前端页面, 从而控制相关 UI 是否展示。\n- 使用时请留意相关服务已开通。"
  },
  {
    "path": "Server/app.js",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nconst Koa = require('koa');\nconst uuid = require('uuid');\nconst bodyParser = require('koa-bodyparser');\nconst cors = require('koa2-cors');\nconst { Signer } = require('@volcengine/openapi');\nconst fetch = require('node-fetch');\nconst { wrapper, assert, readFiles } = require('./util');\nconst TokenManager = require('./token');\nconst Privileges = require('./token').privileges;\n\nconst Scenes = readFiles('./scenes', '.json');\n\nconst app = new Koa();\n\napp.use(cors({\n  origin: '*'\n}));\n\napp.use(bodyParser());\n\napp.use(async ctx => {\n  /**\n   * @brief 代理 AIGC 的 OpenAPI 请求\n   */\n  await wrapper({\n    ctx,\n    apiName: 'proxy',\n    containResponseMetadata: false,\n    logic: async () => {\n      const { Action, Version = '2024-12-01' } = ctx.query || {};\n      assert(Action, 'Action 不能为空');\n      assert(Version, 'Version 不能为空');\n\n      const { SceneID } = ctx.request.body;\n\n      assert(SceneID, 'SceneID 不能为空, SceneID 用于指定场景的 JSON');\n\n      const JSONData = Scenes[SceneID];\n      assert(JSONData, `${SceneID} 不存在, 请先在 Server/scenes 下定义该场景的 JSON.`);\n\n      const { VoiceChat = {}, AccountConfig = {} } = JSONData;\n      assert(AccountConfig.accessKeyId, 'AccountConfig.accessKeyId 不能为空');\n      assert(AccountConfig.secretKey, 'AccountConfig.secretKey 不能为空');\n\n      let body = {};\n      switch(Action) {\n        case 'StartVoiceChat':\n          body = VoiceChat;\n          break;\n        case 'StopVoiceChat':\n          const { AppId, RoomId, TaskId } = VoiceChat;\n          assert(AppId, 'VoiceChat.AppId 不能为空');\n          assert(RoomId, 'VoiceChat.RoomId 不能为空');\n          assert(TaskId, 'VoiceChat.TaskId 不能为空');\n          body = {\n            AppId, RoomId, TaskId\n          };\n          break;\n        default:\n          break;\n      }\n\n      /** 参考 https://github.com/volcengine/volc-sdk-nodejs 可获取更多 火山 TOP 网关 SDK 的使用方式 */\n      const openApiRequestData = {\n        region: 'cn-north-1',\n        method: 'POST',\n        params: {\n          Action,\n          Version,\n        },\n        headers: {\n          Host: 'rtc.volcengineapi.com',\n          'Content-type': 'application/json',\n        },\n        body,\n      };\n      const signer = new Signer(openApiRequestData, \"rtc\");\n      signer.addAuthorization(AccountConfig);\n      \n      /** 参考 https://www.volcengine.com/docs/6348/69828 可获取更多 OpenAPI 的信息 */\n      const result = await fetch(`https://rtc.volcengineapi.com?Action=${Action}&Version=${Version}`, {\n        method: 'POST',\n        headers: openApiRequestData.headers,\n        body: JSON.stringify(body),\n      });\n      return result.json();\n    }\n  });\n\n  wrapper({\n    ctx,\n    apiName: 'getScenes',\n    logic: () => {\n      const scenes = Object.keys(Scenes).map((scene) => {\n        const { SceneConfig, RTCConfig = {}, VoiceChat } = Scenes[scene];\n        const { AppId, RoomId, UserId, AppKey, Token } = RTCConfig;\n        assert(AppId, `${scene} 场景的 RTCConfig.AppId 不能为空`);\n        if (AppId && (!Token || !UserId || !RoomId)) {\n          RTCConfig.RoomId = VoiceChat.RoomId = RoomId || uuid.v4();\n          RTCConfig.UserId = VoiceChat.AgentConfig.TargetUserId[0] = UserId || uuid.v4();\n\n          assert(AppKey, `自动生成 Token 时, ${scene} 场景的 AppKey 不可为空`);\n          const key = new TokenManager.AccessToken(AppId, AppKey, RTCConfig.RoomId, RTCConfig.UserId);\n          key.addPrivilege(Privileges.PrivSubscribeStream, 0);\n          key.addPrivilege(Privileges.PrivPublishStream, 0);\n          key.expireTime(Math.floor(new Date() / 1000) + (24 * 3600));\n          RTCConfig.Token = key.serialize();\n        }\n        SceneConfig.id = scene;\n        SceneConfig.botName = VoiceChat?.AgentConfig?.UserId;\n        SceneConfig.isInterruptMode = VoiceChat?.Config?.InterruptMode === 0;\n        SceneConfig.isVision = VoiceChat?.Config?.LLMConfig?.VisionConfig?.Enable;\n        SceneConfig.isScreenMode = VoiceChat?.Config?.LLMConfig?.VisionConfig?.SnapshotConfig?.StreamType === 1;\n        SceneConfig.isAvatarScene = VoiceChat?.Config?.AvatarConfig?.Enabled;\n        SceneConfig.avatarBgUrl = VoiceChat?.Config?.AvatarConfig?.BackgroundUrl;\n        delete RTCConfig.AppKey;\n        return {\n          scene: SceneConfig || {},\n          rtc: RTCConfig,\n        };\n      });\n      return {\n        scenes,\n      };\n    }\n  });\n});\n\napp.listen(3001, () => {\n  console.log('AIGC Server is running at http://localhost:3001');\n});\n\n"
  },
  {
    "path": "Server/nodemon.json",
    "content": "{\n  \"watch\": [\".\"],\n  \"ext\": \"js,json\",\n  \"ignore\": [\"node_modules/*\"]\n}"
  },
  {
    "path": "Server/package.json",
    "content": "{\n  \"name\": \"AIGCServer\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Server for demo to call open api\",\n  \"main\": \"app.js\",\n  \"license\": \"BSD-3-Clause\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@volcengine/openapi\": \"^1.22.0\",\n    \"koa\": \"^2.15.3\",\n    \"koa-bodyparser\": \"^4.4.1\",\n    \"koa2-cors\": \"^2.0.6\",\n    \"lodash\": \"^4.17.21\",\n    \"node-fetch\": \"^2.3.2\",\n    \"uuid\": \"^11.1.0\"\n  },\n  \"devDependencies\": {\n    \"nodemon\": \"^3.1.10\"\n  },\n  \"scripts\": {\n    \"dev\": \"nodemon app.js\",\n    \"start\": \"nodemon app.js\"\n  }\n}\n"
  },
  {
    "path": "Server/scenes/Custom.json",
    "content": "{\n  \"SceneConfig\": {\n    \"icon\": \"https://lf3-rtc-demo.volccdn.com/obj/rtc-aigc-assets/DoubaoAvatar.png\",\n    \"name\": \"自定义助手\"\n  },\n  \"AccountConfig\": {\n    \"accessKeyId\": \"\",\n    \"secretKey\": \"\"\n  },\n  \"RTCConfig\": {\n    \"AppId\": \"\",\n    \"AppKey\": \"\",\n    \"RoomId\": \"\",\n    \"UserId\": \"\",\n    \"Token\": \"\"\n  },\n  \"VoiceChat\": {\n    \"AppId\": \"\",\n    \"RoomId\": \"\",\n    \"TaskId\": \"\",\n    \"AgentConfig\": {\n      \"TargetUserId\": [\n        \"\"\n      ],\n      \"WelcomeMessage\": \"你好，我是小宁，有什么需要帮忙的吗？\",\n      \"UserId\": \"\",\n      \"EnableConversationStateCallback\": true\n    },\n    \"Config\": {\n      \"ASRConfig\": {\n        \"Provider\": \"volcano\",\n        \"ProviderParams\": {\n          \"Mode\": \"smallmodel\",\n          \"AppId\": \"\",\n          \"Cluster\": \"volcengine_streaming_common\"\n        }\n      },\n      \"TTSConfig\": {\n        \"Provider\": \"volcano\",\n        \"ProviderParams\": {\n          \"app\": {\n            \"appid\": \"\",\n            \"cluster\": \"volcano_tts\"\n          },\n          \"audio\": {\n            \"voice_type\": \"BV001_streaming\",\n            \"speed_ratio\": 1,\n            \"pitch_ratio\": 1,\n            \"volume_ratio\": 1\n          }\n        }\n      },\n      \"LLMConfig\": {\n        \"Mode\": \"ArkV3\",\n        \"EndPointId\": \"\",\n        \"SystemMessages\": [\n          \"你是小宁，性格幽默又善解人意。你在表达时需简明扼要，有自己的观点。\"\n        ],\n        \"VisionConfig\": {\n          \"Enable\": false\n        }\n      },\n      \"AvatarConfig\": {\n        \"Enabled\": false,\n        \"AvatarType\": \"3min\",\n        \"AvatarRole\": \"250623-zhibo-linyunzhi\",\n        \"BackgroundUrl\": \"\",\n        \"VideoBitrate\": 2000,\n        \"AvatarAppID\": \"\",\n        \"AvatarToken\": \"\"\n      },\n      \"InterruptMode\": 0\n    }\n  }\n}"
  },
  {
    "path": "Server/token.js",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nvar crypto = require('crypto');\n\nvar randomInt = Math.floor(Math.random() * 0xFFFFFFFF);\n\nconst VERSION = \"001\";\nconst VERSION_LENGTH = 3;\n\nconst APP_ID_LENGTH = 24;\n\nprivileges = {\n    PrivPublishStream: 0,\n\n    // not exported, do not use directly\n    privPublishAudioStream: 1,\n    privPublishVideoStream: 2,\n    privPublishDataStream: 3,\n\n    PrivSubscribeStream: 4,\n};\n\n\nmodule.exports.privileges = privileges;\n\n// Initializes token struct by required parameters.\nvar AccessToken = function (appID, appKey, roomID, userID) {\n    let token = this;\n    this.appID = appID;\n    this.appKey = appKey;\n    this.roomID = roomID;\n    this.userID = userID;\n    this.issuedAt = Math.floor(new Date() / 1000);\n    this.nonce = randomInt;\n    this.expireAt = 0;\n    this.privileges = {};\n\n    // AddPrivilege adds permission for token with an expiration.\n    this.addPrivilege = function (privilege, expireTimestamp) {\n        if (token.privileges === undefined) {\n            token.privileges = {}\n        }\n        token.privileges[privilege] = expireTimestamp;\n\n        if (privilege === privileges.PrivPublishStream) {\n            token.privileges[privileges.privPublishVideoStream] = expireTimestamp;\n            token.privileges[privileges.privPublishAudioStream] = expireTimestamp;\n            token.privileges[privileges.privPublishDataStream] = expireTimestamp;\n        }\n    };\n\n    // ExpireTime sets token expire time, won't expire by default.\n    // The token will be invalid after expireTime no matter what privilege's expireTime is.\n    this.expireTime = function (expireTimestamp) {\n        token.expireAt = expireTimestamp;\n    };\n\n    this.packMsg = function () {\n        var bufM = new ByteBuf();\n        bufM.putUint32(token.nonce);\n        bufM.putUint32(token.issuedAt);\n        bufM.putUint32(token.expireAt);\n        bufM.putString(token.roomID);\n        bufM.putString(token.userID);\n        bufM.putTreeMapUInt32(token.privileges);\n        return bufM.pack()\n    };\n\n    // Serialize generates the token string\n    this.serialize = function () {\n        var bytesM = this.packMsg();\n\n        var signature = encodeHMac(token.appKey, bytesM);\n        var content = new ByteBuf().putBytes(bytesM).putBytes(signature).pack();\n\n        return (VERSION + token.appID + content.toString('base64'));\n    };\n\n    // Verify checks if this token valid, called by server side.\n    this.verify = function (key) {\n        if (token.expireAt > 0 && Math.floor(new Date() / 1000) > token.expireAt) {\n            return false\n        }\n\n        token.appKey = key;\n        return encodeHMac(token.appKey, this.packMsg()).toString() === token.signature;\n    }\n\n};\n\n// Parse retrieves token information from raw string\nvar Parse = function (raw) {\n    try {\n        if (raw.length <= VERSION_LENGTH + APP_ID_LENGTH) {\n            return\n        }\n        if (raw.substr(0, VERSION_LENGTH) !== VERSION) {\n            return\n        }\n        var token = new AccessToken(\"\", \"\", \"\", \"\");\n        token.appID = raw.substr(VERSION_LENGTH, APP_ID_LENGTH);\n\n        var contentBuf = Buffer.from(raw.substr(VERSION_LENGTH + APP_ID_LENGTH), 'base64');\n        var readbuf = new ReadByteBuf(contentBuf);\n\n        var msg = readbuf.getString();\n        token.signature = readbuf.getString().toString();\n\n        // parse msg\n        var msgBuf = new ReadByteBuf(msg);\n        token.nonce = msgBuf.getUint32();\n        token.issuedAt = msgBuf.getUint32();\n        token.expireAt = msgBuf.getUint32();\n        token.roomID = msgBuf.getString().toString();\n        token.userID = msgBuf.getString().toString();\n        token.privileges = msgBuf.getTreeMapUInt32();\n\n        return token\n    } catch (err) {\n        console.log(err);\n    }\n};\n\n\nmodule.exports.version = VERSION;\nmodule.exports.AccessToken = AccessToken;\nmodule.exports.Parse = Parse;\n\nvar encodeHMac = function (key, message) {\n    return crypto.createHmac('sha256', key).update(message).digest();\n};\n\nvar ByteBuf = function () {\n    var that = {\n        buffer: Buffer.alloc(1024)\n        , position: 0\n    };\n\n\n    that.pack = function () {\n        var out = Buffer.alloc(that.position);\n        that.buffer.copy(out, 0, 0, out.length);\n        return out;\n    };\n\n    that.putUint16 = function (v) {\n        that.buffer.writeUInt16LE(v, that.position);\n        that.position += 2;\n        return that;\n    };\n\n    that.putUint32 = function (v) {\n        that.buffer.writeUInt32LE(v, that.position);\n        that.position += 4;\n        return that;\n    };\n\n    that.putBytes = function (bytes) {\n        that.putUint16(bytes.length);\n        bytes.copy(that.buffer, that.position);\n        that.position += bytes.length;\n        return that;\n    };\n\n    that.putString = function (str) {\n        return that.putBytes(Buffer.from(str));\n    };\n\n    that.putTreeMap = function (map) {\n        if (!map) {\n            that.putUint16(0);\n            return that;\n        }\n\n        that.putUint16(Object.keys(map).length);\n        for (var key in map) {\n            that.putUint16(key);\n            that.putString(map[key]);\n        }\n\n        return that;\n    };\n\n    that.putTreeMapUInt32 = function (map) {\n        if (!map) {\n            that.putUint16(0);\n            return that;\n        }\n\n        that.putUint16(Object.keys(map).length);\n        for (var key in map) {\n            that.putUint16(key);\n            that.putUint32(map[key]);\n        }\n\n        return that;\n    };\n\n    return that;\n};\n\nvar ReadByteBuf = function (bytes) {\n    var that = {\n        buffer: bytes\n        , position: 0\n    };\n\n    that.getUint16 = function () {\n        var ret = that.buffer.readUInt16LE(that.position);\n        that.position += 2;\n        return ret;\n    };\n\n    that.getUint32 = function () {\n        var ret = that.buffer.readUInt32LE(that.position);\n        that.position += 4;\n        return ret;\n    };\n\n    that.getString = function () {\n        var len = that.getUint16();\n\n        var out = Buffer.alloc(len);\n        that.buffer.copy(out, 0, that.position, (that.position + len));\n        that.position += len;\n        return out;\n    };\n\n    that.getTreeMapUInt32 = function () {\n        var map = {};\n        var len = that.getUint16();\n        for (var i = 0; i < len; i++) {\n            var key = that.getUint16();\n            var value = that.getUint32();\n            map[key] = value;\n        }\n        return map;\n    };\n\n    return that;\n};\n"
  },
  {
    "path": "Server/util.js",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\nconst fs = require('fs');\nconst path = require('path');\n\nconst judgeMethodPath = (method) => {\n    return (ctx, pathname) => ctx.method.toLowerCase() === method && ctx.url.startsWith(`/${pathname}`);\n}\n\nconst readFiles = (dir, suffix) => {\n    const scenes = {};\n    fs.readdirSync(path.join(__dirname, dir)).map((p) => {\n        const data = JSON.parse(fs.readFileSync(path.join(__dirname, dir, p)));\n        scenes[p.replace(suffix, '')] = data;\n    });\n    return scenes;\n}\n\nconst assert = (expression, msg) => {\n    if (!!!expression || expression?.includes?.(' ')) {\n        console.log(`\\x1b[31m校验失败: ${msg}\\x1b[0m`)\n      throw new Error(msg);\n    }\n}\n\nconst wrapper = async ({\n    ctx,\n    method = 'post',\n    apiName,\n    logic,\n    containResponseMetadata = true,\n}) => {\n    if (judgeMethodPath(method)(ctx, apiName)) {\n        const ResponseMetadata = { Action: apiName };\n        try {\n            const res = await logic();\n            ctx.body = containResponseMetadata ? {\n                ResponseMetadata,\n                Result: res,\n            } : res;\n        } catch (e) {\n            ResponseMetadata.Error = {\n                Code: -1,\n                Message: e?.toString(),\n            };\n            ctx.body = {\n                ResponseMetadata,\n            }\n        }\n    }\n}\n\nconst deepAssert = (params = {}, prefix = '') => {\n    if (typeof params === 'object') {\n        Object.keys(params).forEach(key => {\n            assert(params[key], `${prefix}: ${key} 不能为空, 请修改 /Server/sensitive.js`);\n            deepAssert(params[key], `${prefix}: ${key}.`);\n        })\n    }\n}\n\nmodule.exports = {\n    wrapper,\n    assert,\n    readFiles,\n}"
  },
  {
    "path": "craco.config.js",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nconst CracoLessPlugin = require('craco-less');\nconst path = require('path');\n\nmodule.exports = {\n  webpack: {\n    alias: {\n      '@': path.resolve(__dirname, 'src'),\n    },\n  },\n  plugins: [\n    {\n      plugin: CracoLessPlugin,\n      options: {\n        lessLoaderOptions: {\n          lessOptions: {\n            javascriptEnabled: true,\n          },\n        },\n      },\n    },\n  ],\n};\n"
  },
  {
    "path": "message.js",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nconst reset = '\\x1b[0m';\nconst bright = '\\x1b[1m';\nconst green = '\\x1b[32m';\n\nconsole.log(`${bright}${bright}===================================================`);\nconsole.log(`${bright}${green}| 请查看目录下的 README.md 内容, 否则启动可能失败 |`);\nconsole.log(`${bright}${reset}===================================================${reset}`);"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"aigc\",\n  \"version\": \"1.6.0\",\n  \"license\": \"BSD-3-Clause\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@reduxjs/toolkit\": \"^1.8.3\",\n    \"@volcengine/rtc\": \"~4.66.20\",\n    \"@arco-design/web-react\": \"^2.65.0\",\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-redux\": \"^8.0.2\",\n    \"react-router\": \"^6.3.0\",\n    \"react-router-dom\": \"^6.3.0\",\n    \"redux\": \"^4.2.0\",\n    \"uuid\": \"^8.3.2\"\n  },\n  \"scripts\": {\n    \"dev\": \"npm run echo && npm run start\",\n    \"start\": \"cross-env REACT_APP_LOCAL=cn  craco start\",\n    \"server:start\": \"node Server/app.js\",\n    \"build\": \"craco build\",\n    \"test\": \"craco test\",\n    \"eject\": \"react-scripts eject\",\n    \"prettier\": \"prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'\",\n    \"eslint\": \"eslint  src/ --fix --cache --quiet --ext .js,.jsx,.ts,.tsx\",\n    \"stylelint\": \"stylelint 'src/**/*.less' --fix\",\n    \"pre-commit\": \"npm run eslint && npm run stylelint\",\n    \"echo\": \"node message.js\"\n  },\n  \"eslintConfig\": {\n    \"extends\": [\n      \"react-app\",\n      \"react-app/jest\"\n    ]\n  },\n  \"browserslist\": {\n    \"production\": [\n      \">0.2%\",\n      \"not dead\",\n      \"not op_mini all\"\n    ],\n    \"development\": [\n      \"last 1 chrome version\",\n      \"last 1 firefox version\",\n      \"last 1 safari version\"\n    ]\n  },\n  \"devDependencies\": {\n    \"@craco/craco\": \"^6.4.5\",\n    \"@types/lodash\": \"^4.17.4\",\n    \"@types/node\": \"^16.11.45\",\n    \"@types/react\": \"^18.0.15\",\n    \"@types/react-dom\": \"^18.0.6\",\n    \"@types/react-helmet\": \"^6.1.11\",\n    \"@types/uuid\": \"^8.3.4\",\n    \"craco-less\": \"^2.0.0\",\n    \"cross-env\": \"^7.0.3\",\n    \"eslint-config-airbnb\": \"^19.0.4\",\n    \"eslint-config-prettier\": \"^8.5.0\",\n    \"eslint-plugin-babel\": \"^5.3.1\",\n    \"eslint-plugin-prettier\": \"^4.2.1\",\n    \"postcss-less\": \"^6.0.0\",\n    \"prettier\": \"^2.7.1\",\n    \"react-scripts\": \"5.0.1\",\n    \"stylelint\": \"^14.9.1\",\n    \"stylelint-config-prettier\": \"^9.0.3\",\n    \"stylelint-config-standard\": \"^26.0.0\",\n    \"typescript\": \"^4.7.4\"\n  }\n}\n"
  },
  {
    "path": "public/index.html",
    "content": "<!--\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n -->\n<!DOCTYPE html>\n<html lang=\"zh\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <meta name=\"theme-color\" content=\"#000000\" />\n    <meta name=\"description\" content=\"volc video demo\" />\n    <link rel=\"icon\" href=\"/favicon.png\" />\n    <!--\n      Notice the use of %PUBLIC_URL% in the tags above.\n      It will be replaced with the URL of the `public` folder during the build.\n      Only files inside the `public` folder can be referenced from the HTML.\n\n      Unlike \"/favicon.ico\" or \"favicon.ico\", \"%PUBLIC_URL%/favicon.ico\" will\n      work correctly both with client-side routing and a non-root public URL.\n      Learn how to configure a non-root public URL by running `npm run build`.\n    -->\n    <title>火山引擎 RTC 实时对话式 AI 体验 Demo ——— 支持 DeepSeek 和 豆包视觉理解模型</title>\n  </head>\n  <body>\n    <noscript>You need to enable JavaScript to run this app.</noscript>\n    <div id=\"root\"></div>\n    <!--\n      This HTML file is a template.\n      If you open it directly in the browser, you will see an empty page.\n\n      You can add webfonts, meta tags, or analytics to this file.\n      The build step will place the bundled scripts into the <body> tag.\n\n      To begin the development, run `npm start` or `yarn start`.\n      To create a production bundle, use `npm run build` or `yarn build`.\n    -->\n  </body>\n</html>\n"
  },
  {
    "path": "public/robots.txt",
    "content": "# https://www.robotstxt.org/robotstxt.html\nUser-agent: *\nDisallow:\n"
  },
  {
    "path": "src/App.tsx",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport { BrowserRouter, Routes, Route } from 'react-router-dom';\nimport MainPage from './pages/MainPage';\nimport '@arco-design/web-react/dist/css/arco.css';\n\nfunction App() {\n  console.warn('运行问题可参考 README 内容进行排查');\n  return (\n    <BrowserRouter>\n      <Routes>\n        <Route path=\"/\">\n          <Route index element={<MainPage />} />\n          <Route path=\"/*\" element={<MainPage />} />\n        </Route>\n      </Routes>\n    </BrowserRouter>\n  );\n}\n\nexport default App;\n"
  },
  {
    "path": "src/app/api.ts",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\n/**\n * @brief Basic APIs\n */\nexport const BasicAPIs = [\n  {\n    action: 'getScenes',\n    apiPath: '/getScenes',\n    method: 'post',\n  },\n] as const;\n\n/**\n * @brief Basic APIs\n */\nexport const AigcAPIs = [\n  {\n    action: 'StartVoiceChat',\n    apiPath: '/proxy',\n    method: 'post',\n  },\n  {\n    action: 'StopVoiceChat',\n    apiPath: '/proxy',\n    method: 'post',\n  },\n] as const;\n"
  },
  {
    "path": "src/app/base.ts",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport { Message } from '@arco-design/web-react';\nimport { AIGC_PROXY_HOST } from '@/config';\nimport type { RequestResponse, ApiConfig, ApiNames, Apis } from './type';\n\ntype Headers = Record<string, string>;\n\nexport type DeepPartial<T> = {\n  [P in keyof T]?: T[P] extends Array<infer U>\n    ? Array<DeepPartial<U>>\n    : T[P] extends object\n    ? DeepPartial<T[P]>\n    : T[P];\n};\n\n/**\n * @brief Get\n * @param apiName\n * @param headers\n */\nexport const requestGetMethod = ({\n  action,\n  headers = {},\n}: {\n  action: string;\n  headers?: Record<string, string>;\n}) => {\n  return async (params: Record<string, any> = {}) => {\n    const url = `${AIGC_PROXY_HOST}?Action=${action}&${Object.keys(params)\n      .map((key) => `${key}=${params[key]}`)\n      .join('&')}`;\n    const res = await fetch(url, {\n      headers: {\n        ...headers,\n      },\n    });\n    return res;\n  };\n};\n\n/**\n * @brief Post\n */\nexport const requestPostMethod = ({\n  action,\n  apiPath,\n  isJson = true,\n  headers = {},\n}: {\n  action: string;\n  apiPath: string;\n  isJson?: boolean;\n  headers?: Headers;\n}) => {\n  return async <T>(params: T) => {\n    const res = await fetch(`${AIGC_PROXY_HOST}${apiPath}?Action=${action}`, {\n      method: 'post',\n      headers: {\n        'content-type': 'application/json',\n        ...headers,\n      },\n      body: (isJson ? JSON.stringify(params) : params) as BodyInit,\n    });\n    return res;\n  };\n};\n\n/**\n * @brief Return handler\n * @param res\n */\nexport const resultHandler = (res: RequestResponse) => {\n  const { Result, ResponseMetadata } = res || {};\n  // Record request id for debug.\n  if (ResponseMetadata.Action === 'StartVoiceChat') {\n    const requestId = ResponseMetadata.RequestId;\n    requestId && sessionStorage.setItem('RequestID', requestId);\n  }\n  if (ResponseMetadata.Error) {\n    Message.error(\n      `[${ResponseMetadata?.Action}]call failed(reason: ${ResponseMetadata.Error?.Message})`\n    );\n    throw new Error(\n      `[${ResponseMetadata?.Action}]call failed(${JSON.stringify(ResponseMetadata, null, 2)})`\n    );\n  }\n  return Result;\n};\n\n/**\n * @brief Generate APIs by apiConfigs\n * @param apiConfigs\n */\nexport const generateAPIs = <T extends readonly ApiConfig[]>(apiConfigs: T) =>\n  apiConfigs.reduce<Apis<T>>((store, cur) => {\n    const { action, apiPath = '', method = 'get' } = cur;\n\n    const actionKey = action as ApiNames<T>;\n    store[actionKey] = async (params) => {\n      const queryData =\n        method === 'get'\n          ? await requestGetMethod({ action })(params)\n          : await requestPostMethod({ action, apiPath })(params);\n      const res = await queryData?.json();\n      return resultHandler(res);\n    };\n    return store;\n  }, {} as Apis<T>);\n"
  },
  {
    "path": "src/app/index.ts",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport { AigcAPIs, BasicAPIs } from './api';\nimport { generateAPIs } from './base';\n\nconst VoiceChat = generateAPIs(AigcAPIs);\nconst Basic = generateAPIs(BasicAPIs);\n\nexport default {\n  VoiceChat,\n  Basic,\n};\n"
  },
  {
    "path": "src/app/type.ts",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nexport type RequestParams = Record<string, any>;\n\nexport interface RequestResponse {\n  ResponseMetadata: Partial<{\n    Action: string;\n    Version: string;\n    Service: string;\n    Region: string;\n    RequestId: string;\n    Error: {\n      Code: string;\n      Message: string;\n    };\n  }>;\n  Result: any;\n}\n\ntype TupleToUnion<T extends readonly unknown[]> = T[number];\ntype RequestFn = <T extends keyof RequestResponse>(params?: RequestParams[T]) => RequestResponse[T];\ntype PromiseRequestFn = <T extends keyof RequestResponse>(\n  params?: RequestParams[T]\n) => Promise<RequestResponse[T]>;\n\nexport type ApiConfig = { action: string; method: string; apiPath?: string };\nexport type ApiNames<T extends readonly ApiConfig[]> = TupleToUnion<T>['action'];\nexport type Apis<T extends readonly ApiConfig[]> = Record<\n  ApiNames<T>,\n  RequestFn | PromiseRequestFn\n>;\n"
  },
  {
    "path": "src/components/AIAvatarLoading/index.module.less",
    "content": ".container {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  width: 100%;\n  height: 100%;\n}\n\n.avatarContainer {\n  position: relative;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n}\n\n.avatarSvg {\n  max-width: 80%;\n  height: auto;\n}\n\n/* 加载文字样式 */\n.loadingText {\n  margin-top: 30px;\n  font-size: 18px;\n  text-align: center;\n}\n"
  },
  {
    "path": "src/components/AIAvatarLoading/index.tsx",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport styles from './index.module.less';\n\nfunction AIAvatarReadying() {\n  return (\n    <div className={styles.container}>\n      <div className={styles.avatarContainer}>\n        {/* SVG 包含人型轮廓和流光效果 */}\n        <svg\n          className={styles.avatarSvg}\n          width=\"35vh\"\n          height=\"42vh\"\n          viewBox=\"0 0 457 549\"\n          fill=\"none\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n        >\n          {/* 原始人型轮廓 */}\n          <path\n            d=\"M175.137 244.821C175.12 240.915 174.986 232.095 174.729 228.127L174.727 228.106L174.726 228.087L174.668 227.474C174.045 221.385 171.924 216.347 168.181 212.481L167.801 212.098C164.091 208.429 159.982 204.706 155.477 200.929C153.336 198.887 151.625 196.437 150.444 193.724L150.433 193.697L150.42 193.671L149.841 192.422C148.509 189.52 147.278 186.572 146.149 183.585C144.846 179.572 143.541 175.295 142.214 170.751L141.912 169.719L140.886 169.401L140.555 169.295C138.934 168.753 137.41 167.955 136.041 166.931C134.262 165.342 132.653 163.572 131.238 161.651C129.191 158.679 127.692 155.364 126.813 151.863L126.785 151.752L126.745 151.645L126.492 150.944C125.279 147.431 124.799 143.704 125.085 139.994C125.478 136.364 126.156 133.326 127.121 130.858L127.127 130.842L127.133 130.826C128.206 127.935 129.823 125.278 131.897 122.997L132.438 122.403L132.418 121.6C132.157 111.445 132.679 101.284 133.98 91.2086C135.184 81.8895 137.078 72.6727 139.647 63.6344L139.651 63.6207L139.654 63.608C142.166 54.2848 146.286 45.4712 151.827 37.5641L151.84 37.5446L151.854 37.525C156.768 30.1535 162.226 24.1949 168.194 19.6344C174.287 15.0104 180.474 11.3978 186.775 8.77893L186.779 8.77698C192.676 6.31057 198.595 4.59753 204.487 3.63049L205.665 3.4469C212.051 2.52917 218.207 2.07191 224.173 2.0719H226.075C232.924 2.20416 239.709 3.04996 246.463 4.62952L246.472 4.63147C253.613 6.26715 260.58 8.59178 267.272 11.5719V11.5709C273.733 14.4735 279.477 17.7844 284.513 21.4576C289.542 25.1405 293.399 28.7986 296.171 32.3785L296.179 32.3893L296.188 32.4C302.893 40.8262 307.772 50.094 310.875 60.2135L310.878 60.2203C313.858 69.819 316.08 79.6371 317.523 89.5836C318.827 100.26 319.489 111.077 319.489 122.036V123.048L320.306 123.648C321.902 124.82 323.183 126.369 324.034 128.157L324.062 128.216L324.095 128.273C325.021 129.93 325.815 132.135 326.456 134.989V134.99C327.041 137.657 327.136 141.06 326.627 145.227L326.624 145.255L326.621 145.285C326.108 150.808 325.015 155.025 323.453 158.076L323.449 158.082L323.446 158.089C321.83 161.302 320.045 163.696 318.165 165.395L318.148 165.41L318.133 165.425C316.207 167.244 313.9 168.612 311.381 169.429L310.377 169.755L310.079 170.768C308.749 175.302 307.465 179.574 306.145 183.578C304.848 187.009 303.364 190.366 301.697 193.633L301.694 193.639C300.082 196.825 298.307 199.225 296.446 200.897C292.063 204.433 288.379 207.515 285.397 210.127L284.158 211.221C280.486 214.496 278.291 219.798 277.187 226.703C276.335 231 276.102 240.098 276.349 244.339L276.35 244.348V244.357C276.652 248.822 277.859 253.232 279.914 257.618L279.917 257.625L279.921 257.631C282.032 262.048 285.236 266.234 289.477 270.139C293.797 274.118 299.755 277.607 307.276 280.617V280.618C313.953 283.342 321.212 285.795 329.068 287.978L329.071 287.979C331.164 288.557 334.747 289.129 338.918 289.705C343.13 290.286 348.095 290.889 353.013 291.508C357.944 292.129 362.834 292.768 366.946 293.424C371.118 294.091 374.29 294.746 375.915 295.364V295.365C377.947 296.146 381.43 296.926 385.408 297.711C389.43 298.504 394.194 299.346 398.827 300.216C403.491 301.092 408.052 302.002 411.788 302.947C413.656 303.419 415.286 303.893 416.601 304.365C417.952 304.85 418.829 305.283 419.296 305.629L419.3 305.631C424.589 309.523 428.393 314.851 430.644 321.74C445.731 382.021 453.785 439.764 454.411 481.881C454.725 502.978 453.172 520 449.79 531.429C448.096 537.155 445.996 541.291 443.602 543.85C441.281 546.33 438.717 547.314 435.77 546.913L435.755 546.911L435.741 546.91L433.611 546.654C388.296 541.315 305.942 536.993 226.451 532.245C145.556 527.413 67.7489 522.144 34.3936 514.951H34.3926C31.4052 514.293 29.0212 513.642 27.2168 513.005C25.3611 512.349 24.3001 511.77 23.8018 511.342L23.7822 511.325L23.7617 511.309L23.4365 511.037C20.0453 508.119 15.7049 502.035 11.8477 492.491C7.88642 482.689 4.49264 469.376 3.15625 452.444C0.490903 418.673 6.01322 370.536 31.4814 307.264C34.9856 304.532 39.8152 302.214 45.5293 300.212C51.4403 298.14 58.1707 296.444 65.1162 294.954C72.0624 293.463 79.164 292.19 85.8398 290.95C92.4891 289.714 98.7347 288.507 103.871 287.155C108.374 285.97 118.591 284.526 128.896 282.753C133.986 281.877 139.052 280.926 143.308 279.9C147.458 278.899 151.135 277.76 153.242 276.407L153.243 276.408C158.848 272.856 163.246 269.382 166.311 265.953C169.34 262.586 171.586 259.177 173.007 255.68C174.418 252.207 175.137 248.593 175.137 244.83V244.821Z\"\n            stroke=\"url(#paint0_linear)\"\n            strokeWidth=\"4\"\n          />\n\n          {/* 渐变定义 */}\n          <defs>\n            {/* 原始渐变 */}\n            <linearGradient\n              id=\"paint0_linear\"\n              x1=\"142.5\"\n              y1=\"83.5\"\n              x2=\"299.5\"\n              y2=\"401.5\"\n              gradientUnits=\"userSpaceOnUse\"\n            >\n              <stop stopColor=\"#6792FF\" />\n              <stop offset=\"0.138788\" stopColor=\"#D093FF\" />\n              <stop offset=\"0.282833\" stopColor=\"#9DFFE3\" stopOpacity=\"0.318618\" />\n              <stop offset=\"0.519953\" stopColor=\"white\" stopOpacity=\"0\" />\n              <stop offset=\"1\" stopColor=\"white\" stopOpacity=\"0\" />\n\n              {/* 添加动画效果，使渐变沿着路径运动 */}\n              <animate attributeName=\"x1\" values=\"0; 457; 0\" dur=\"4s\" repeatCount=\"indefinite\" />\n              <animate\n                attributeName=\"y1\"\n                values=\"549; 157; 549\"\n                dur=\"4s\"\n                repeatCount=\"indefinite\"\n              />\n              <animate\n                attributeName=\"x2\"\n                values=\"157; 614; 614\"\n                dur=\"4s\"\n                repeatCount=\"indefinite\"\n              />\n              <animate\n                attributeName=\"y2\"\n                values=\"157; 706; 157\"\n                dur=\"4s\"\n                repeatCount=\"indefinite\"\n              />\n            </linearGradient>\n          </defs>\n        </svg>\n\n        {/* 加载文字 */}\n        <div className={styles.loadingText}>数字人准备中，请稍候...</div>\n      </div>\n    </div>\n  );\n}\n\nexport default AIAvatarReadying;\n"
  },
  {
    "path": "src/components/AiAvatarCard/index.module.less",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\n.card {\n  position: absolute;\n  top: 0;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  text-align: center;\n  text-align: center;\n  width: 100%;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  gap: 24px;\n\n  .avatar {\n    position: relative;\n    border-radius: 50%;\n    width: 167.5px;\n    height: 167.5px;\n    img {\n      width: 100%;\n      height: 100%;\n    }\n  }\n\n  .aiStatus {\n    position: absolute;\n    border: 1px solid;\n    border-image-source: linear-gradient(77.86deg, #e5f2ff -3.23%, #d9e5ff 51.11%, #f6e2ff 98.65%);\n    box-shadow: 0px 2px 22px 0px #0000001a;\n    width: 93px;\n    height: 73px;\n    border-radius: 24px;\n    top: -28px;\n    left: -56px;\n    color: #635bff;\n    font-weight: 500;\n    font-size: 16px;\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    flex-direction: column;\n    gap: 8px;\n    background: #ffffff;\n  }\n\n  .barContainer {\n    display: flex;\n    gap: 4px;\n  }\n\n  .bar {\n    width: 11px;\n    height: 16px;\n    border-radius: 6px;\n    animation: shake 1s ease infinite;\n    background-color: #4f4fff;\n  }\n\n  .bar:nth-child(1) {\n    animation-delay: -0.4s;\n  }\n\n  .bar:nth-child(2) {\n    animation-delay: -0.2s;\n  }\n\n  @keyframes shake {\n    0% {\n      transform: scaleY(1);\n    }\n    50% {\n      transform: scaleY(0.5);\n    }\n    100% {\n      transform: scaleY(1);\n    }\n  }\n}\n\n.fullScreen {\n  .avatar {\n    width: 115px;\n    height: 115px;\n  }\n\n  .aiStatus {\n    width: 72px;\n    height: 56px;\n    top: -24px;\n    left: 86px;\n    font-size: 12px;\n  }\n}\n"
  },
  {
    "path": "src/components/AiAvatarCard/index.tsx",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport { useSelector } from 'react-redux';\nimport { RootState } from '@/store';\nimport UserTag from '../UserTag';\nimport { useDeviceState, useScene } from '@/lib/useCommon';\nimport style from './index.module.less';\n\ninterface IAiAvatarCardProps {\n  showStatus: boolean;\n  showUserTag: boolean;\n  className?: string;\n}\n\nconst THRESHOLD_VOLUME = 18;\n\nfunction AiAvatarCard(props: IAiAvatarCardProps) {\n  const { showStatus, showUserTag, className } = props;\n  const room = useSelector((state: RootState) => state.room);\n  const { icon } = useScene();\n  const { scene, isAITalking, isFullScreen } = room;\n  const volume = room.localUser.audioPropertiesInfo?.linearVolume || 0;\n  const { isAudioPublished } = useDeviceState();\n  const isLoading = volume >= THRESHOLD_VOLUME && isAudioPublished;\n\n  return (\n    <div className={`${style.card} ${className} ${isFullScreen ? style.fullScreen : ''}`}>\n      <div className={style.avatar}>\n        <img id=\"avatar-card\" src={icon} alt=\"Avatar\" />\n        {showStatus ? (\n          isAITalking ? (\n            <div className={style.aiStatus}>\n              <div className={style.barContainer}>\n                <div className={style.bar} />\n                <div className={style.bar} />\n                <div className={style.bar} />\n              </div>\n            </div>\n          ) : isLoading ? (\n            <div className={style.aiStatus}>正在听...</div>\n          ) : null\n        ) : null}\n      </div>\n      {showUserTag ? <UserTag name={scene} /> : null}\n    </div>\n  );\n}\n\nexport default AiAvatarCard;\n"
  },
  {
    "path": "src/components/AiChangeCard/CheckScene/index.module.less",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\n.wrapper {\n  position: relative;\n  width: max-content;\n  height: 50px;\n  border-radius: 100px;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  cursor: pointer;\n  color: #737a87;\n  font-size: 14px;\n  line-height: 22px;\n  border: 1px solid#DDE2E9;\n  margin-bottom: 16px;\n\n  .content {\n    width: 100%;\n    height: 100%;\n    padding: 12px;\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    z-index: 1;\n    gap: 4px;\n\n    .icon {\n      border-radius: 50%;\n      width: 26px;\n      height: 26px;\n    }\n\n    .checked-text {\n      width: max-content;\n      font-size: 13px;\n      line-height: 22px;\n    }\n  }\n}\n\n.wrapper:hover {\n  box-shadow: 0px 5px 6px 0px rgba(82, 102, 133, 0.15);\n}\n\n.active {\n  border: 1px solid transparent;\n  background: linear-gradient(77.86deg, #f1f9ff -3.23%, #edf3ff 51.11%, #faf4ff 98.65%) padding-box,\n    linear-gradient(77.86deg, #3b91ff -3.23%, #0d5eff 51.11%, #c069ff 98.65%) border-box;\n\n  .content {\n    .checked-text {\n      background: linear-gradient(90deg, #004fff 38.86%, #9865ff 100%);\n      background-clip: text;\n      -webkit-background-clip: text;\n      -webkit-text-fill-color: transparent;\n      font-size: 13px;\n      font-weight: 500;\n      line-height: 22px;\n    }\n  }\n}\n\n.active:hover {\n  box-shadow: 0px 5px 6px 0px rgba(82, 102, 133, 0.15);\n}\n\n.tag {\n  position: absolute;\n  top: 0;\n  right: 0;\n  z-index: 3;\n  font-size: 10px;\n  font-weight: 500;\n  line-height: 18px;\n  transform: translate(20%, -50%);\n  background: rgba(134, 123, 227, 1);\n  padding: 0px 6px 0px 6px;\n  border-radius: 20px 20px 20px 0px;\n  color: white;\n}\n"
  },
  {
    "path": "src/components/AiChangeCard/CheckScene/index.tsx",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport styles from './index.module.less';\n\ninterface IProps {\n  checked: boolean;\n  title?: string;\n  onClick?: () => void;\n  icon?: string;\n  tag?: string;\n}\n\nfunction CheckScene(props: IProps) {\n  const { tag, icon, title, checked, onClick } = props;\n  return (\n    <div className={`${styles.wrapper} ${checked ? styles.active : ''}`} onClick={onClick}>\n      {tag ? <div className={styles.tag}>{tag}</div> : ''}\n      <div className={styles.content}>\n        {icon ? <img className={styles.icon} src={icon} alt=\"icon\" /> : ''}\n        <div className={styles['checked-text']}>{title}</div>\n      </div>\n    </div>\n  );\n}\n\nexport default CheckScene;\n"
  },
  {
    "path": "src/components/AiChangeCard/index.module.less",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\n.card {\n  position: relative;\n  text-align: center;\n  text-align: center;\n  width: 100%;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  gap: 24px;\n\n  .avatar {\n    img {\n      width: 128px;\n      height: 128px;\n    }\n    border-radius: 50%;\n    width: 128px;\n    height: 128px;\n    background: linear-gradient(180deg, #c3e4ff 0%, #98d6fe 100%);\n  }\n\n  .title {\n    font-weight: 500;\n    font-size: 24px;\n    line-height: 32px;\n    color: #1d2129;\n  }\n\n  .desc {\n    margin-top: 8px;\n    font-weight: 400;\n    font-size: 14px;\n    line-height: 22px;\n    color: #737a87;\n  }\n\n  .exceededTitle {\n    font-weight: 500;\n    font-size: 24px;\n    line-height: 32px;\n    background: linear-gradient(77.86deg, #3b91ff -3.23%, #0d5eff 51.11%, #c069ff 98.65%);\n    -webkit-background-clip: text;\n    background-clip: text;\n    color: transparent;\n    cursor: pointer;\n    img {\n      margin-left: 4px;\n    }\n  }\n\n  .sceneContainer {\n    display: flex;\n    flex-wrap: wrap;\n    gap: 8px;\n    justify-content: center;\n    width: max-content;\n  }\n}\n"
  },
  {
    "path": "src/components/AiChangeCard/index.tsx",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport { useDispatch, useSelector } from 'react-redux';\nimport { RootState } from '@/store';\nimport CheckScene from './CheckScene';\nimport { SceneConfig, updateScene } from '@/store/slices/room';\nimport { useScene } from '@/lib/useCommon';\nimport style from './index.module.less';\n\nfunction AIChangeCard() {\n  const { scene, sceneConfigMap } = useSelector((state: RootState) => state.room);\n  const dispatch = useDispatch();\n  const { icon, isVision } = useScene();\n  const Scenes = Object.keys(sceneConfigMap).map(key => sceneConfigMap[key]);\n\n  const handleChecked = (checkedScene: string) => {\n    dispatch(updateScene(checkedScene));\n  };\n\n  return (\n    <div className={style.card}>\n      <div className={style.avatar}>\n        <img id=\"avatar-card\" src={icon} alt=\"Avatar\" />\n      </div>\n      <div className={style.title}>\n        <div>Hi，欢迎体验实时对话式 AI</div>\n        <div className={style.desc}>\n          {isVision ? <>支持豆包 Vision 模型和 深度思考模型，</> : ''}\n          超多对话场景等你开启\n        </div>\n      </div>\n      <div className={style.sceneContainer}>\n        {Scenes.map((key: SceneConfig) =>\n          <CheckScene\n            key={key.name}\n            icon={key.icon}\n            title={key.name}\n            checked={key.id === scene}\n            onClick={() => handleChecked(key.id)}\n          />\n        )}\n      </div>\n    </div>\n  );\n}\n\nexport default AIChangeCard;\n"
  },
  {
    "path": "src/components/DrawerRowItem/index.module.less",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\n.row {\n    width: 100%;\n    display: flex;\n    flex-direction: row;\n    align-items: center;\n    cursor: pointer;\n\n    .firstPart {\n      display: flex;\n      flex-direction: row;\n      align-items: center;\n      width: 90%;\n      color: var(--text-color-text-2, var(--text-color-text-2, #42464E));\n      text-align: center;\n\n      /* Body/body-2 medium */\n      font-family: \"PingFang SC\";\n      font-size: 13px;\n      font-style: normal;\n      font-weight: 500;\n      line-height: 22px; /* 169.231% */\n      letter-spacing: 0.039px;\n    }\n\n    .finalPart {\n      display: flex;\n      flex-direction: row;\n      align-items: center;\n      width: 10%;\n      justify-content: flex-end;\n\n      .rightOutlined {\n        font-size: 12px;\n      }\n    }\n\n    .icon {\n      margin-right: 4px;\n    }\n}\n\n\n\n.footer {\n  width: calc(100% - 12px);\n  display: flex;\n  flex-direction: row;\n  justify-content: flex-end;\n  align-items: center;\n\n  .cancel {\n    width: 88px;\n    height: 32px;\n    border-radius: 6px;\n    border: 1px solid var(--line-color-border-3, rgba(221, 226, 233, 1));\n    margin-right: 12px;\n  }\n\n  .confirm {\n    width: 88px;\n    height: 32px;\n    border-radius: 6px;\n    background: linear-gradient(95.87deg, #1664FF 0%, #8040FF 97.7%);\n    color: white;\n  }\n}\n\n.children {\n  width: 100%;\n  height: 100%;\n  position: relative;\n  overflow: hidden;\n}\n\n:global {\n  .ant-drawer-body {\n    padding: 12px 24px 0px 24px;\n  }\n}"
  },
  {
    "path": "src/components/DrawerRowItem/index.tsx",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport React, { useState } from 'react';\nimport { Drawer, DrawerProps } from '@arco-design/web-react';\nimport { IconRight } from '@arco-design/web-react/icon';\nimport styles from './index.module.less';\n\ntype IDrawerRowItemProps = {\n  btnSrc?: string;\n  btnText: string;\n  suffix?: React.ReactNode;\n  drawer?: {\n    title: string;\n    width?: string | number;\n    onOpen?: () => void;\n    onClose?: () => void;\n    onCancel?: () => void;\n    onConfirm?: (handleClose: () => void) => void;\n    children?: React.ReactNode;\n    footer?: React.ReactNode | boolean;\n  } & DrawerProps;\n} & React.HTMLAttributes<HTMLDivElement>;\n\nfunction DrawerRowItem(props: IDrawerRowItemProps) {\n  const { btnSrc, btnText, suffix, drawer, style, className = '' } = props;\n  const [open, setOpen] = useState(false);\n  const { onClose, onOpen } = drawer!;\n\n  const handleClose = () => {\n    drawer?.onCancel?.();\n    setOpen(false);\n    onClose?.();\n  };\n\n  const handleOpen = () => {\n    setOpen(true);\n    if (drawer) {\n      onOpen?.();\n    }\n  };\n\n  return (\n    <>\n      <div style={style || {}} className={`${styles.row} ${className}`} onClick={handleOpen}>\n        <div className={styles.firstPart}>\n          {btnSrc ? <img src={btnSrc} className={styles.icon} alt=\"svg\" /> : ''}\n          {btnText}\n          {suffix}\n        </div>\n        <div className={styles.finalPart}>\n          <IconRight className={styles.rightOutlined} />\n        </div>\n      </div>\n      <Drawer\n        closable\n        title={drawer?.title || ''}\n        width={drawer?.width || 400}\n        className={styles.drawer}\n        visible={open}\n        onCancel={handleClose}\n        footer={null}\n      >\n        <div className={styles.children}>{drawer?.children}</div>\n      </Drawer>\n    </>\n  );\n}\n\nexport default DrawerRowItem;\n"
  },
  {
    "path": "src/components/FullScreenCard/index.module.less",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\n.card {\n  position: absolute;\n  top: 0;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  text-align: center;\n  width: 100%;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  background: #fff;\n\n  .tag {\n    position: absolute;\n    left: 16px;\n    top: 16px;\n  }\n  \n  &.hidden {\n    display: none;\n  }\n}\n\n.blur-bg {\n  background-position: center;\n  background-size: cover;\n  background-repeat: no-repeat;\n  filter: blur(20px);\n  transform: scale(1.1);\n}\n"
  },
  {
    "path": "src/components/FullScreenCard/index.tsx",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport { useSelector } from 'react-redux';\nimport UserTag from '../UserTag';\nimport { RootState } from '@/store';\nimport style from './index.module.less';\nimport { useScene } from '@/lib/useCommon';\nimport { isMobile } from '@/utils/utils';\n\nexport const LocalFullID = 'local-full-player';\nexport const RemoteFullID = 'remote-full-player';\n\nfunction FullScreenCard() {\n  const isFullScreen = useSelector((state: RootState) => state.room.isFullScreen);\n  const scene = useScene();\n  return (\n    <>\n      <div className={`${style.card} ${!isFullScreen ? style.hidden : ''}`} id={LocalFullID}>\n        <UserTag name=\"我\" className={style.tag} />\n      </div>\n      <div\n        className={`${style.card} ${isFullScreen ? style.hidden : ''} ${style['blur-bg']}`}\n        style={{ backgroundImage: `url(${scene.avatarBgUrl})` }}\n      />\n      <div\n        className={`${style.card} ${isFullScreen ? style.hidden : ''}`}\n        style={{ background: 'unset' }}\n      >\n        <div id={RemoteFullID} style={{ width: '60%', height: '100%' }} />\n        {!isMobile() ? <UserTag name=\"AI\" className={style.tag} /> : null}\n      </div>\n    </>\n  );\n}\n\nexport default FullScreenCard;\n"
  },
  {
    "path": "src/components/Header/index.module.less",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\n.header {\n  height: 48px;\n  background: white;\n  width: 100%;\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  position: relative;\n\n  :global {\n    .arco-popover-content-top {\n      padding: 0px;\n    }\n  }\n}\n\n.header-logo {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  margin-left: 24px;\n\n  :global {\n    img {\n      height: 24px;\n    }\n    .arco-popover-content {\n      padding: 0;\n    }\n  }\n}\n\n.menu-wrapper {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  row-gap: 8px;\n  justify-content: space-between;\n}\n\n.header-logo-text {\n  background: linear-gradient(90deg, #004FFF 38.86%, #9865FF 100%);\n  -webkit-background-clip: text;\n  background-clip: text;\n  color: transparent;\n  font-size: 16px;\n}\n\n.header-right {\n  z-index: 2;\n  color: #fff;\n  display: flex;\n  align-items: center;\n  :global {\n    span {\n      height: 24px;\n    }\n  }\n}\n\n.header-setting-btn {\n  background-color: transparent;\n  border: none;\n  margin-right: 24px;\n  color: #000000;\n  font-size: 16px;\n  cursor: pointer;\n}\n\n.header-pop {\n  :global {\n    .ant-popover-arrow {\n      left: 16px;\n      .ant-popover-arrow-content {\n        &:before {\n          background-color: white;\n        }\n      }\n    }\n    .ant-popover-content {\n      margin-left: 12px;\n    }\n    .ant-popover-inner {\n      margin-right: 12px;\n    }\n    .ant-popover-inner-content {\n      padding: 0;\n      background-color: white;\n      position: relative;\n      width: 100px;\n      height: 100px;\n      display: flex;\n      align-items: center;\n      flex-direction: column;\n      justify-content: space-between;\n      padding-bottom: 7px;\n      padding-top: 7px;\n      cursor: pointer;\n      color: black;\n\n      div {\n        font-size: 13px;\n        font-weight: 400;\n        line-height: 20px;\n        &:hover {\n          color: #1664ff;\n        }\n      }\n    }\n  }\n}\n\n.divider {\n  margin-top: 2px;\n  margin-bottom: 2px;\n  min-width: 70%;\n  width: 70%;\n}\n\n.header-right-text {\n  color: #000000;\n  margin-right: 24px;\n  cursor: pointer;\n}\n"
  },
  {
    "path": "src/components/Header/index.tsx",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport { Button, Divider, Popover } from '@arco-design/web-react';\nimport { IconMenu } from '@arco-design/web-react/icon';\nimport NetworkIndicator from '@/components/NetworkIndicator';\nimport { useIsMobile } from '@/utils/utils';\nimport Logo from '@/assets/img/Logo.svg';\nimport styles from './index.module.less';\n\nconst Disclaimer = 'https://www.volcengine.com/docs/6348/68916';\nconst ReversoContext = 'https://www.volcengine.com/docs/6348/68918';\nconst UserAgreement = 'https://www.volcengine.com/docs/6348/128955';\n\ninterface HeaderProps {\n  children?: React.ReactNode;\n  hide?: boolean;\n}\n\nfunction Header(props: HeaderProps) {\n  const { children, hide } = props;\n\n  const MenuProps = [\n    {\n      name: '免责声明',\n      url: Disclaimer,\n    },\n    {\n      name: '隐私政策',\n      url: ReversoContext,\n    },\n    {\n      name: '用户协议',\n      url: UserAgreement,\n    },\n  ];\n\n  return (\n    <div\n      className={styles.header}\n      style={{\n        display: hide ? 'none' : 'flex',\n      }}\n    >\n      <div className={styles['header-logo']}>\n        {useIsMobile() ? null : (\n          <Popover\n            content={\n              <div className={styles['menu-wrapper']}>\n                {MenuProps.map((menuItem) => (\n                  <Button\n                    type=\"text\"\n                    key={menuItem.name}\n                    onClick={() => {\n                      window.open(menuItem.url, '_blank');\n                    }}\n                  >\n                    {menuItem.name}\n                  </Button>\n                ))}\n              </div>\n            }\n          >\n            <IconMenu className={styles['header-setting-btn']} />\n          </Popover>\n        )}\n        <img src={Logo} alt=\"Logo\" />\n        <Divider type=\"vertical\" />\n        <span className={styles['header-logo-text']}>实时对话式 AI 体验馆</span>\n        <NetworkIndicator />\n      </div>\n      {children}\n      {useIsMobile() ? null : (\n        <div className={styles['header-right']}>\n          <div\n            className={styles['header-right-text']}\n            onClick={() =>\n              window.open('https://www.volcengine.com/product/veRTC/ConversationalAI', '_blank')\n            }\n          >\n            官网链接\n          </div>\n          <div\n            className={styles['header-right-text']}\n            onClick={() =>\n              window.open(\n                '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',\n                '_blank'\n              )\n            }\n          >\n            联系我们\n          </div>\n        </div>\n      )}\n    </div>\n  );\n}\n\nexport default Header;\n"
  },
  {
    "path": "src/components/Loading/AudioLoading/index.module.less",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\n.loader {\n    display: flex;\n    flex-direction: row;\n    justify-content: center;\n    align-items: center;\n    gap: 6px;\n    height: 36px;\n    margin-top: 4px;\n}\n\n.dot {\n    width: 20px;\n    height: 20px;\n    border-radius: 12px;\n    background-color: rgba(148, 116, 255, 1);\n}\n\n.dotter {\n    animation: glow 0.9s infinite;\n}\n\n@keyframes glow {\n    0% {\n        height: 20px;\n    }\n    50% {\n        height: 36px;\n    }\n    100% {\n        height: 20px;\n    }\n}"
  },
  {
    "path": "src/components/Loading/AudioLoading/index.tsx",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport { memo } from 'react';\nimport style from './index.module.less';\n\ninterface IAudioLoadingProps extends React.HTMLAttributes<HTMLDivElement> {\n  loading?: boolean;\n}\n\nfunction AudioLoading(props: IAudioLoadingProps) {\n  const { loading = false, className = '', color, ...rest } = props;\n  return (\n    <div className={`${style.loader} ${className}`} {...rest}>\n      {Array(3)\n        .fill(0)\n        .map((_, index) => (\n          <div\n            key={index}\n            className={`${style.dot} ${loading ? style.dotter : ''}`}\n            style={{\n              animationDelay: `${index * 0.3}s`,\n              backgroundColor: color || 'rgba(148, 116, 255, 1)',\n            }}\n          />\n        ))}\n    </div>\n  );\n}\n\nexport default memo(AudioLoading);\n"
  },
  {
    "path": "src/components/Loading/HorizonLoading/index.module.less",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\n.loader {\n    display: flex;\n}\n\n.dot {\n    width: 10px;\n    height: 10px;\n    border-radius: 50%;\n    background-color: white;\n    animation: glow 0.9s infinite;\n}"
  },
  {
    "path": "src/components/Loading/HorizonLoading/index.tsx",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport { memo } from 'react';\nimport style from './index.module.less';\n\ninterface ILoadingProps extends React.HTMLAttributes<HTMLDivElement> {\n  dotClassName?: string;\n  speed?: number;\n  gap?: number;\n}\n\nfunction Loading(props: ILoadingProps) {\n  const { dotClassName, gap = 5, speed = 0.9, className = '', ...rest } = props;\n  return (\n    <div\n      className={`${style.loader} ${className}`}\n      style={{\n        gap: `${gap}px`,\n      }}\n      {...rest}\n    >\n      {Array(3)\n        .fill(0)\n        .map((_, index) => (\n          <div\n            key={index}\n            className={`${style.dot} ${dotClassName}`}\n            style={{\n              animation: `glow linear ${speed.toFixed(1)}s infinite`,\n              animationDelay: `${(index * (speed / 3)).toFixed(1)}s`,\n            }}\n          />\n        ))}\n    </div>\n  );\n}\n\nexport default memo(Loading);\n"
  },
  {
    "path": "src/components/Loading/VerticalLoading/index.module.less",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\n.loader {\n  width: 40px;\n  height: 10px;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  margin: 6px 0px;\n}\n\n.bar {\n  width: 3px;\n  height: 12px;\n  margin: 1px;\n  display: inline-block;\n  animation: shake 0.6s ease infinite;\n}\n\n/* 为每个 bar 指定不同的延迟来实现抖动效果 */\n.bar:nth-child(1) {\n  animation-delay: -0.2s;\n}\n\n.bar:nth-child(2) {\n  animation-delay: -0.1s;\n}\n\n.bar:nth-child(3) {\n}\n\n@keyframes shake {\n  0% {\n    transform: scaleY(1);\n    background-color: var(--primary-color-primary-7, rgba(23, 89, 221, 1));\n  }\n  50% {\n    transform: scaleY(0.5);\n    background-color: var(--primary-color-primary-3, rgba(151, 188, 255, 1));\n  }\n  100% {\n    transform: scaleY(1);\n    background-color: var(--primary-color-primary-7, rgba(23, 89, 221, 1));\n  }\n}"
  },
  {
    "path": "src/components/Loading/VerticalLoading/index.tsx",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport { memo } from 'react';\nimport styles from './index.module.less';\n\nfunction Loading() {\n  return (\n    <span className={styles.loader}>\n      <span className={styles.bar} />\n      <span className={styles.bar} />\n      <span className={styles.bar} />\n    </span>\n  );\n}\n\nexport default memo(Loading);\n"
  },
  {
    "path": "src/components/LocalPlayerSet/index.module.less",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\n.container {\n  position: absolute;\n  right: 0;\n  top: 148px;\n  width: 36px;\n  height: 36px;\n  border-radius: 4px;\n  cursor: pointer;\n  z-index: 1;\n\n  img {\n    width: 100%;\n    height: 100%;\n  }\n}\n"
  },
  {
    "path": "src/components/LocalPlayerSet/index.tsx",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport { useState } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { Popover } from '@arco-design/web-react';\nimport { RootState } from '@/store';\nimport { updateFullScreen } from '@/store/slices/room';\nimport SET_LOCAL_PLAYER from '@/assets/img/setLocalPlayer.svg';\nimport styles from './index.module.less';\n\nfunction LocalPlayerSet() {\n  const dispatch = useDispatch();\n  const room = useSelector((state: RootState) => state.room);\n  const { isFullScreen } = room;\n  const [loading, setLoading] = useState(false);\n  const [isFull, setFull] = useState(isFullScreen);\n\n  const setLocalPlayer = () => {\n    setLoading(true);\n    setFull(!isFull);\n    dispatch(updateFullScreen({ isFullScreen: !isFull }));\n    setLoading(false);\n  };\n  return (\n    <div\n      onClick={setLocalPlayer}\n      className={styles.container}\n      style={{ cursor: loading ? 'not-allowed' : 'pointer' }}\n    >\n      <Popover content=\"切换屏幕\">\n        <img src={SET_LOCAL_PLAYER} alt=\"fullSize\" />\n      </Popover>\n    </div>\n  );\n}\n\nexport default LocalPlayerSet;\n"
  },
  {
    "path": "src/components/NetworkIndicator/index.module.less",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\n.panel {\n    display: flex;\n\n    .label {\n        width: 90px;\n        display: flex;\n        flex-direction: column;\n        gap: 4px;\n\n        .state {\n            font-weight: bold;\n        }\n    }\n\n    .value {\n        display: flex;\n        flex-direction: column;\n        gap: 4px;\n        width: max-content;\n\n        .state {\n            font-weight: bold;\n        }\n\n        .loss {\n            display: flex;\n            flex-direction: row;\n            justify-content: space-between;\n            gap: 12px;\n        }\n    }\n}\n\n.wrapper {\n    display: flex;\n    align-items: flex-end;\n    height: 14px;\n    width: 14px;\n    margin: 14px;\n    column-gap: 1.5px;\n    background-color: rgba(142, 142, 142, 0.05);\n    border-radius: 3px;\n    padding: 2px;\n\n    .indicator {\n        width: 30%;\n        border-color: rgba(127, 127, 127, 0.184);\n        border-width: 1px;\n        border-radius: 1px;\n        border-style: solid;\n        opacity: 0.8;\n        transition: height 0.3s;\n        box-sizing: border-box;\n    }\n}"
  },
  {
    "path": "src/components/NetworkIndicator/index.tsx",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport { useMemo } from 'react';\nimport { Popover } from '@arco-design/web-react';\nimport { useSelector } from 'react-redux';\nimport { IconArrowDown, IconArrowUp } from '@arco-design/web-react/icon';\nimport { NetworkQuality } from '@volcengine/rtc';\nimport { RootState } from '@/store';\nimport { useScene } from '@/lib/useCommon';\nimport style from './index.module.less';\n\nenum INDICATOR_COLORS {\n  GREAT = 'rgba(35, 195, 67, 1)',\n  FAIR = 'rgba(208, 141, 6, 1)',\n  BAD = 'rgba(245, 78, 78, 1)',\n  PLACE_HOLDER = 'transparent',\n}\n\nconst INDICATOR_TEXT = {\n  [NetworkQuality.UNKNOWN]: '正常',\n  [NetworkQuality.EXCELLENT]: '正常',\n  [NetworkQuality.GOOD]: '正常',\n  [NetworkQuality.POOR]: '一般',\n  [NetworkQuality.BAD]: '一般',\n  [NetworkQuality.VBAD]: '较差',\n  [NetworkQuality.DOWN]: '较差',\n};\n\nfunction NetworkIndicator() {\n  const room = useSelector((state: RootState) => state.room);\n  const { botName } = useScene();\n  const networkQuality = room.networkQuality;\n  const delay = room.localUser.audioStats?.rtt;\n  const audioLossRateUpper = room.localUser.audioStats?.audioLossRate || 0;\n  const audioLossRateLower =\n    room.remoteUsers.find((user) => user.userId === botName)?.audioStats\n      ?.audioLossRate || 0;\n\n  const indicators = useMemo(() => {\n    switch (networkQuality) {\n      case NetworkQuality.UNKNOWN:\n      case NetworkQuality.EXCELLENT:\n      case NetworkQuality.GOOD:\n        return Array(3).fill(INDICATOR_COLORS.GREAT);\n      case NetworkQuality.POOR:\n      case NetworkQuality.BAD:\n        return Array(2).fill(INDICATOR_COLORS.FAIR).concat(INDICATOR_COLORS.PLACE_HOLDER);\n      case NetworkQuality.VBAD:\n      case NetworkQuality.DOWN:\n      default:\n        return [INDICATOR_COLORS.BAD].concat(...Array(2).fill(INDICATOR_COLORS.PLACE_HOLDER));\n    }\n  }, [networkQuality]);\n\n  return (\n    <Popover\n      position=\"bl\"\n      content={\n        <div className={style.panel}>\n          <div className={style.label}>\n            <div className={style.state}>网络状态</div>\n            <div className={style.item}>延迟</div>\n            <div className={style.item}>丢包率</div>\n          </div>\n          <div className={style.value}>\n            <div\n              className={style.state}\n              style={{\n                color: indicators?.[0] || INDICATOR_COLORS.BAD,\n              }}\n            >\n              {INDICATOR_TEXT[networkQuality]}\n            </div>\n            <div className={style.item}>{delay ? delay.toFixed(0) : '- '}ms</div>\n            <div className={style.loss}>\n              <div>\n                <IconArrowUp style={{ color: 'rgba(22, 100, 255, 1)' }} />\n                <span>\n                  {`${audioLossRateUpper}` ? (audioLossRateUpper * 100)?.toFixed(0) : '- '}%\n                </span>\n              </div>\n              <div>\n                <IconArrowDown />\n                <span>\n                  {`${audioLossRateLower}` ? (audioLossRateLower * 100)?.toFixed(0) : '- '}%\n                </span>\n              </div>\n            </div>\n          </div>\n        </div>\n      }\n    >\n      <div className={style.wrapper}>\n        {indicators.map((color, index) => (\n          <div\n            key={index}\n            className={style.indicator}\n            style={{\n              height: `${20 + (80 * (index + 1)) / 3}%`,\n              backgroundColor: color,\n            }}\n          />\n        ))}\n      </div>\n    </Popover>\n  );\n}\n\nexport default NetworkIndicator;\n"
  },
  {
    "path": "src/components/ResizeWrapper/index.module.less",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\n.container {\n    position: relative;\n}"
  },
  {
    "path": "src/components/ResizeWrapper/index.tsx",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport { useEffect, useRef } from 'react';\nimport styles from './index.module.less';\n\nexport type IWrapperProps = React.PropsWithChildren & {\n  className?: string;\n};\n\nexport default function (props: IWrapperProps) {\n  const { children, className = '' } = props;\n\n  const ref = useRef<HTMLDivElement>(null);\n\n  const resize = () => {\n    if (ref.current) {\n      ref.current.style.height = `${window.innerHeight}px`;\n    }\n  };\n\n  useEffect(() => {\n    resize();\n    window.addEventListener('resize', resize);\n    return () => {\n      window.removeEventListener('resize', resize);\n    };\n  }, []);\n\n  return (\n    <div className={`${styles.container} ${className}`} ref={ref}>\n      {children}\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/UserTag/index.module.less",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\n.userTagWrapper {\n  display: flex;\n  border-radius: 6px;\n  border: 0.4px solid #1f232926;\n  background-color: #fff;\n  width: max-content;\n  z-index: 1;\n  margin-bottom: 45px;\n}\n\n.iconContainer {\n  background-color: #5a6169;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  width: 20px;\n  height: 20px;\n  border-top-left-radius: 6px;\n  border-bottom-left-radius: 6px;\n}\n\n.nameContainer {\n  color: #0c0d0e;\n  padding: 0 4px;\n  height: 20px;\n  line-height: 20px;\n  font-size: 12px;\n  font-weight: 500;\n}\n"
  },
  {
    "path": "src/components/UserTag/index.tsx",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport { IconUser } from '@arco-design/web-react/icon';\nimport styles from './index.module.less';\n\ninterface IUserTagProps {\n  name: string;\n  className?: string;\n}\n\nfunction UserTag(props: IUserTagProps) {\n  const { name, className } = props;\n  return (\n    <div className={`${styles.userTagWrapper} ${className}`}>\n      <div className={styles.iconContainer}>\n        <IconUser style={{ fill: '#fff', strokeWidth: 0 }} />\n      </div>\n      <div className={styles.nameContainer}>{name}</div>\n    </div>\n  );\n}\n\nexport default UserTag;\n"
  },
  {
    "path": "src/config/index.ts",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nexport const Disclaimer = 'https://www.volcengine.com/docs/6348/68916';\nexport const ReversoContext = 'https://www.volcengine.com/docs/6348/68918';\nexport const UserAgreement = 'https://www.volcengine.com/docs/6348/128955';\n\n/**\n * @note 请求的 API Proxy Server(对应此 Demo 中包含的 Node server) 地址。\n *       您可按需改成自己需要访问的地址。\n */\nexport const AIGC_PROXY_HOST = 'http://localhost:3001';\n\nexport interface IScene {\n  icon: string;\n  name: string;\n  questions: string[];\n  agentConfig: Record<string, any>;\n  llmConfig: Record<string, any>;\n  asrConfig: Record<string, any>;\n  ttsConfig: Record<string, any>;\n}\n"
  },
  {
    "path": "src/index.less",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\n@import './theme.less';\n\nbody {\n  margin: 0;\n  overflow: hidden;\n  width: 100% !important;\n  background: linear-gradient(\n    109.22deg,\n    rgba(116, 37, 255, 0.05) 0.27%,\n    rgba(39, 88, 255, 0.05) 51.39%,\n    rgba(0, 102, 255, 0.05) 99.54%\n  );\n\n  img {\n    user-drag: none;\n    -webkit-user-drag: none;\n    user-select: none;\n    -webkit-user-select: none;\n    -moz-user-select: none;\n    -ms-user-select: none;\n  }\n\n  a {\n    text-decoration: none;\n  }\n}\n\n@keyframes glow {\n  0% {\n    opacity: 1;\n  }\n  40% {\n    opacity: 0.7;\n  }\n  100% {\n    opacity: 0.3;\n  }\n}\n"
  },
  {
    "path": "src/index.module.less",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\n#demo-for-xxx-provider {\n    flex: 1;\n  \n    // ------背景色------\n    // 页面可以配置背景色，但不建议，设计同学建议将背景色设置成透明，透出主应用的渐变背景色\n    background: transparent;\n    // -----------------\n  \n    // ------适配------\n    width: 100%;\n    height: 100%;\n    min-width: 730px; // 最小宽度，可根据情况自定义，页面显示区域不够最小高度时，会允许scroll。\n    // 建议pc端最小宽度小于等于730px（渲染区域的最小尺寸），这样可以避免页面滚动，用户体验更好。\n    min-height: 1000px; // 最小高度，可根据情况自定义，页面显示区域不够最小高度时，会允许scroll。\n  \n    // 官网规范，<768px时为移动端\n    @media (max-width: 767px) {\n      width: 100%; // 移动端渲染区域的宽度，等于设备屏幕的宽度\n    }\n    // -----------------\n  \n    // 写全局样式要防止与官网样式冲突\n    * {\n      -webkit-font-smoothing: antialiased;\n      -moz-osx-font-smoothing: grayscale;\n      box-sizing: border-box;\n    }\n  \n    .container-box {\n      display: flex;\n      flex-direction: column;\n      justify-content: center;\n      align-items: center;\n      height: 100%;\n    }\n  }\n  "
  },
  {
    "path": "src/index.tsx",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport ReactDOM from 'react-dom/client';\nimport { Provider } from 'react-redux';\nimport App from './App';\nimport store from './store';\nimport './index.less';\n\nconst root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);\nroot.render(\n  <Provider store={store}>\n    <App />\n  </Provider>\n);\n"
  },
  {
    "path": "src/interface.ts",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nexport enum DeviceType {\n  Camera = 'camera',\n  Microphone = 'microphone',\n}\n"
  },
  {
    "path": "src/lib/RtcClient.ts",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport VERTC, {\n  MirrorType,\n  StreamIndex,\n  IRTCEngine,\n  RoomProfileType,\n  onUserJoinedEvent,\n  onUserLeaveEvent,\n  MediaType,\n  LocalStreamStats,\n  RemoteStreamStats,\n  StreamRemoveReason,\n  LocalAudioPropertiesInfo,\n  RemoteAudioPropertiesInfo,\n  AudioProfileType,\n  DeviceInfo,\n  AutoPlayFailedEvent,\n  PlayerEvent,\n  NetworkQuality,\n  VideoRenderMode,\n  ScreenEncoderConfig,\n} from '@volcengine/rtc';\nimport RTCAIAnsExtension from '@volcengine/rtc/extension-ainr';\nimport { Message } from '@arco-design/web-react';\nimport Apis from '@/app/index';\nimport { string2tlv } from '@/utils/utils';\nimport { COMMAND, INTERRUPT_PRIORITY } from '@/utils/handler';\n\nexport interface IEventListener {\n  handleError: (e: { errorCode: any }) => void;\n  handleUserJoin: (e: onUserJoinedEvent) => void;\n  handleUserLeave: (e: onUserLeaveEvent) => void;\n  handleTrackEnded: (e: { kind: string; isScreen: boolean }) => void;\n  handleUserPublishStream: (e: { userId: string; mediaType: MediaType }) => void;\n  handleUserUnpublishStream: (e: {\n    userId: string;\n    mediaType: MediaType;\n    reason: StreamRemoveReason;\n  }) => void;\n  handleRemoteStreamStats: (e: RemoteStreamStats) => void;\n  handleLocalStreamStats: (e: LocalStreamStats) => void;\n  handleLocalAudioPropertiesReport: (e: LocalAudioPropertiesInfo[]) => void;\n  handleRemoteAudioPropertiesReport: (e: RemoteAudioPropertiesInfo[]) => void;\n  handleAudioDeviceStateChanged: (e: DeviceInfo) => void;\n  handleAutoPlayFail: (e: AutoPlayFailedEvent) => void;\n  handlePlayerEvent: (e: PlayerEvent) => void;\n  handleRoomBinaryMessageReceived: (e: { userId: string; message: ArrayBuffer }) => void;\n  handleNetworkQuality: (\n    uplinkNetworkQuality: NetworkQuality,\n    downlinkNetworkQuality: NetworkQuality\n  ) => void;\n}\n\nexport interface BasicBody {\n  app_id: string;\n  room_id: string;\n  user_id: string;\n  token?: string;\n}\n\n/**\n * @brief RTC Core Client\n * @notes Refer to official website documentation to get more information about the API.\n */\nexport class RTCClient {\n  engine!: IRTCEngine;\n\n  basicInfo!: BasicBody;\n\n  private _audioCaptureDevice?: string;\n\n  private _videoCaptureDevice?: string;\n\n  audioBotEnabled = false;\n\n  audioBotStartTime = 0;\n\n  createEngine = async () => {\n    this.engine = VERTC.createEngine(this.basicInfo.app_id);\n    try {\n      const AIAnsExtension = new RTCAIAnsExtension();\n      await this.engine.registerExtension(AIAnsExtension);\n      AIAnsExtension.enable();\n    } catch (error) {\n      console.warn(\n        `当前环境不支持 AI 降噪, 此错误可忽略, 不影响实际使用, e: ${(error as any).message}`\n      );\n    }\n  };\n\n  addEventListeners = ({\n    handleError,\n    handleUserJoin,\n    handleUserLeave,\n    handleTrackEnded,\n    handleUserPublishStream,\n    handleUserUnpublishStream,\n    handleRemoteStreamStats,\n    handleLocalStreamStats,\n    handleLocalAudioPropertiesReport,\n    handleRemoteAudioPropertiesReport,\n    handleAudioDeviceStateChanged,\n    handleAutoPlayFail,\n    handlePlayerEvent,\n    handleRoomBinaryMessageReceived,\n    handleNetworkQuality,\n  }: IEventListener) => {\n    this.engine.on(VERTC.events.onError, handleError);\n    this.engine.on(VERTC.events.onUserJoined, handleUserJoin);\n    this.engine.on(VERTC.events.onUserLeave, handleUserLeave);\n    this.engine.on(VERTC.events.onTrackEnded, handleTrackEnded);\n    this.engine.on(VERTC.events.onUserPublishStream, handleUserPublishStream);\n    this.engine.on(VERTC.events.onUserUnpublishStream, handleUserUnpublishStream);\n    this.engine.on(VERTC.events.onRemoteStreamStats, handleRemoteStreamStats);\n    this.engine.on(VERTC.events.onLocalStreamStats, handleLocalStreamStats);\n    this.engine.on(VERTC.events.onAudioDeviceStateChanged, handleAudioDeviceStateChanged);\n    this.engine.on(VERTC.events.onLocalAudioPropertiesReport, handleLocalAudioPropertiesReport);\n    this.engine.on(VERTC.events.onRemoteAudioPropertiesReport, handleRemoteAudioPropertiesReport);\n    this.engine.on(VERTC.events.onAutoplayFailed, handleAutoPlayFail);\n    this.engine.on(VERTC.events.onPlayerEvent, handlePlayerEvent);\n    this.engine.on(VERTC.events.onRoomBinaryMessageReceived, handleRoomBinaryMessageReceived);\n    this.engine.on(VERTC.events.onNetworkQuality, handleNetworkQuality);\n  };\n\n  joinRoom = () => {\n    console.log(' ------ userJoinRoom\\n', `roomId: ${this.basicInfo.room_id}\\n`, `uid: ${this.basicInfo.user_id}`);\n    return this.engine.joinRoom(\n      this.basicInfo.token!,\n      `${this.basicInfo.room_id!}`,\n      {\n        userId: this.basicInfo.user_id!,\n        extraInfo: JSON.stringify({\n          call_scene: 'RTC-AIGC',\n          user_name: this.basicInfo.user_id,\n          user_id: this.basicInfo.user_id,\n        }),\n      },\n      {\n        isAutoPublish: true,\n        isAutoSubscribeAudio: true,\n        roomProfileType: RoomProfileType.chat,\n      }\n    );\n  };\n\n  leaveRoom = () => {\n    this.audioBotEnabled = false;\n    this.engine.leaveRoom().catch();\n    VERTC.destroyEngine(this.engine);\n    this._audioCaptureDevice = undefined;\n  };\n\n  checkPermission(): Promise<{\n    video: boolean;\n    audio: boolean;\n  }> {\n    return VERTC.enableDevices({\n      video: false,\n      audio: true,\n    });\n  }\n\n  /**\n   * @brief get the devices\n   * @returns\n   */\n  async getDevices(props?: { video?: boolean; audio?: boolean }): Promise<{\n    audioInputs: MediaDeviceInfo[];\n    audioOutputs: MediaDeviceInfo[];\n    videoInputs: MediaDeviceInfo[];\n  }> {\n    const { video = false, audio = true } = props || {};\n    let audioInputs: MediaDeviceInfo[] = [];\n    let audioOutputs: MediaDeviceInfo[] = [];\n    let videoInputs: MediaDeviceInfo[] = [];\n    const { video: hasVideoPermission, audio: hasAudioPermission } = await VERTC.enableDevices({\n      video,\n      audio,\n    });\n    if (audio) {\n      const inputs = await VERTC.enumerateAudioCaptureDevices();\n      const outputs = await VERTC.enumerateAudioPlaybackDevices();\n      audioInputs = inputs.filter((i) => i.deviceId && i.kind === 'audioinput');\n      audioOutputs = outputs.filter((i) => i.deviceId && i.kind === 'audiooutput');\n      this._audioCaptureDevice = audioInputs.filter((i) => i.deviceId)?.[0]?.deviceId;\n      if (hasAudioPermission) {\n        if (!audioInputs?.length) {\n          Message.error('无麦克风设备, 请先确认设备情况。');\n        }\n        if (!audioOutputs?.length) {\n          Message.error('无扬声器设备, 请先确认设备情况。');\n        }\n      } else {\n        Message.error('暂无麦克风设备权限, 请先确认设备权限授予情况。');\n      }\n    }\n    if (video) {\n      videoInputs = await VERTC.enumerateVideoCaptureDevices();\n      videoInputs = videoInputs.filter((i) => i.deviceId && i.kind === 'videoinput');\n      this._videoCaptureDevice = videoInputs?.[0]?.deviceId;\n      if (hasVideoPermission) {\n        if (!videoInputs?.length) {\n          Message.error('无摄像头设备, 请先确认设备情况。');\n        }\n      } else {\n        Message.error('暂无摄像头设备权限, 请先确认设备权限授予情况。');\n      }\n    }\n\n    return {\n      audioInputs,\n      audioOutputs,\n      videoInputs,\n    };\n  }\n\n  startVideoCapture = async (camera?: string) => {\n    await this.engine.startVideoCapture(camera || this._videoCaptureDevice);\n  };\n\n  stopVideoCapture = async () => {\n    this.engine.setLocalVideoMirrorType(MirrorType.MIRROR_TYPE_RENDER);\n    await this.engine.stopVideoCapture();\n  };\n\n  startScreenCapture = async (enableAudio = false) => {\n    await this.engine.startScreenCapture({\n      enableAudio,\n    });\n  };\n\n  stopScreenCapture = async () => {\n    await this.engine.stopScreenCapture();\n  };\n\n  startAudioCapture = async (mic?: string) => {\n    await this.engine.startAudioCapture(mic || this._audioCaptureDevice);\n  };\n\n  stopAudioCapture = async () => {\n    await this.engine.stopAudioCapture();\n  };\n\n  publishStream = (mediaType: MediaType) => {\n    this.engine.publishStream(mediaType);\n  };\n\n  unpublishStream = (mediaType: MediaType) => {\n    this.engine.unpublishStream(mediaType);\n  };\n\n  publishScreenStream = async (mediaType: MediaType) => {\n    await this.engine.publishScreen(mediaType);\n  };\n\n  unpublishScreenStream = async (mediaType: MediaType) => {\n    await this.engine.unpublishScreen(mediaType);\n  };\n\n  setScreenEncoderConfig = async (description: ScreenEncoderConfig) => {\n    await this.engine.setScreenEncoderConfig(description);\n  };\n\n  /**\n   * @brief 设置业务标识参数\n   * @param businessId\n   */\n  setBusinessId = (businessId: string) => {\n    this.engine.setBusinessId(businessId);\n  };\n\n  setAudioVolume = (volume: number) => {\n    this.engine.setCaptureVolume(StreamIndex.STREAM_INDEX_MAIN, volume);\n    this.engine.setCaptureVolume(StreamIndex.STREAM_INDEX_SCREEN, volume);\n  };\n\n  /**\n   * @brief 设置音质档位\n   */\n  setAudioProfile = (profile: AudioProfileType) => {\n    this.engine.setAudioProfile(profile);\n  };\n\n  /**\n   * @brief 切换设备\n   */\n  switchDevice = (deviceType: MediaType, deviceId: string) => {\n    if (deviceType === MediaType.AUDIO) {\n      this._audioCaptureDevice = deviceId;\n      this.engine.setAudioCaptureDevice(deviceId);\n    }\n    if (deviceType === MediaType.VIDEO) {\n      this._videoCaptureDevice = deviceId;\n      this.engine.setVideoCaptureDevice(deviceId);\n    }\n    if (deviceType === MediaType.AUDIO_AND_VIDEO) {\n      this._audioCaptureDevice = deviceId;\n      this._videoCaptureDevice = deviceId;\n      this.engine.setVideoCaptureDevice(deviceId);\n      this.engine.setAudioCaptureDevice(deviceId);\n    }\n  };\n\n  setLocalVideoMirrorType = (type: MirrorType) => {\n    return this.engine.setLocalVideoMirrorType(type);\n  };\n\n  setLocalVideoPlayer = (\n    userId: string,\n    renderDom?: string | HTMLElement,\n    isScreenShare = false,\n    renderMode = VideoRenderMode.RENDER_MODE_FILL\n  ) => {\n    return this.engine.setLocalVideoPlayer(\n      isScreenShare ? StreamIndex.STREAM_INDEX_SCREEN : StreamIndex.STREAM_INDEX_MAIN,\n      {\n        renderDom,\n        userId,\n        renderMode,\n      }\n    );\n  };\n\n  setRemoteVideoPlayer = (userId: string, renderDom?: string | HTMLElement, renderMode = VideoRenderMode.RENDER_MODE_HIDDEN) => {\n    return this.engine.setRemoteVideoPlayer(\n      StreamIndex.STREAM_INDEX_MAIN,\n      {\n        renderDom,\n        userId,\n        renderMode,\n      }\n    );\n  }\n\n  /**\n   * @brief 移除播放器\n   */\n  removeLocalVideoPlayer = (userId: string, scope: StreamIndex | 'Both' = 'Both') => {\n    let removeScreen = scope === StreamIndex.STREAM_INDEX_SCREEN;\n    let removeCamera = scope === StreamIndex.STREAM_INDEX_MAIN;\n    if (scope === 'Both') {\n      removeCamera = true;\n      removeScreen = true;\n    }\n    if (removeScreen) {\n      this.engine.setLocalVideoPlayer(StreamIndex.STREAM_INDEX_SCREEN, { userId });\n    }\n    if (removeCamera) {\n      this.engine.setLocalVideoPlayer(StreamIndex.STREAM_INDEX_MAIN, { userId });\n    }\n  };\n\n  /**\n   * @brief 启用 AIGC\n   */\n  startAgent = async (scene: string) => {\n    if (this.audioBotEnabled) {\n      await this.stopAgent(scene);\n    }\n    await Apis.VoiceChat.StartVoiceChat({\n      SceneID: scene,\n    });\n    this.audioBotEnabled = true;\n    this.audioBotStartTime = Date.now();\n  };\n\n  /**\n   * @brief 关闭 AIGC\n   */\n  stopAgent = async (scene: string) => {\n    if (this.audioBotEnabled || sessionStorage.getItem('audioBotEnabled')) {\n      await Apis.VoiceChat.StopVoiceChat({\n        SceneID: scene,\n      });\n      this.audioBotStartTime = 0;\n      sessionStorage.removeItem('audioBotEnabled');\n    }\n    this.audioBotEnabled = false;\n  };\n\n  /**\n   * @brief 命令 AIGC\n   */\n  commandAgent = ({\n    command,\n    agentName,\n    interruptMode = INTERRUPT_PRIORITY.NONE,\n    message = '',\n  }: {\n    command: COMMAND;\n    agentName: string;\n    interruptMode?: INTERRUPT_PRIORITY;\n    message?: string;\n  }) => {\n    if (this.audioBotEnabled) {\n      this.engine.sendUserBinaryMessage(\n        agentName,\n        string2tlv(\n          JSON.stringify({\n            Command: command,\n            InterruptMode: interruptMode,\n            Message: message,\n          }),\n          'ctrl'\n        )\n      );\n      return;\n    }\n    console.warn('Interrupt failed, bot not enabled.');\n  };\n\n  /**\n   * @brief 更新 AIGC 配置\n   */\n  updateAgent = async (scene: string) => {\n    if (this.audioBotEnabled) {\n      await this.stopAgent(scene);\n      await this.startAgent(scene);\n    } else {\n      await this.startAgent(scene);\n    }\n  };\n\n  /**\n   * @brief 获取当前 AI 是否启用\n   */\n  getAgentEnabled = () => {\n    return this.audioBotEnabled;\n  };\n}\n\nexport default new RTCClient();\n"
  },
  {
    "path": "src/lib/listenerHooks.ts",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport VERTC, {\n  LocalAudioPropertiesInfo,\n  RemoteAudioPropertiesInfo,\n  LocalStreamStats,\n  MediaType,\n  onUserJoinedEvent,\n  onUserLeaveEvent,\n  RemoteStreamStats,\n  StreamRemoveReason,\n  StreamIndex,\n  DeviceInfo,\n  AutoPlayFailedEvent,\n  PlayerEvent,\n  NetworkQuality,\n} from '@volcengine/rtc';\nimport { useDispatch } from 'react-redux';\nimport { useRef } from 'react';\n\nimport {\n  IUser,\n  remoteUserJoin,\n  remoteUserLeave,\n  updateLocalUser,\n  updateRemoteUser,\n  addAutoPlayFail,\n  removeAutoPlayFail,\n  updateNetworkQuality,\n} from '@/store/slices/room';\nimport RtcClient, { IEventListener } from './RtcClient';\n\nimport { setMicrophoneList, updateSelectedDevice } from '@/store/slices/device';\nimport { useMessageHandler } from '@/utils/handler';\nimport store from '@/store';\n\nconst useRtcListeners = (): IEventListener => {\n  const dispatch = useDispatch();\n  const { parser } = useMessageHandler();\n  const playStatus = useRef<{ [key: string]: { audio: boolean; video: boolean } }>({});\n\n  const handleTrackEnded = async (event: { kind: string; isScreen: boolean }) => {\n    const { kind, isScreen } = event;\n    /** 浏览器自带的屏幕共享关闭触发方式，通过 onTrackEnd 事件去关闭 */\n    if (isScreen && kind === 'video') {\n      await RtcClient.stopScreenCapture();\n      await RtcClient.unpublishScreenStream(MediaType.VIDEO);\n      dispatch(\n        updateLocalUser({\n          publishScreen: false,\n        })\n      );\n    }\n  };\n\n  const handleUserJoin = (e: onUserJoinedEvent) => {\n    const extraInfo = JSON.parse(e.userInfo.extraInfo || '{}');\n    const userId = extraInfo.user_id || e.userInfo.userId;\n    const username = extraInfo.user_name || e.userInfo.userId;\n    dispatch(\n      remoteUserJoin({\n        userId,\n        username,\n      })\n    );\n  };\n\n  const handleError = (e: { errorCode: typeof VERTC.ErrorCode.DUPLICATE_LOGIN }) => {\n    const { errorCode } = e;\n    if (errorCode === VERTC.ErrorCode.DUPLICATE_LOGIN) {\n      console.log('踢人');\n    }\n  };\n\n  const handleUserLeave = (e: onUserLeaveEvent) => {\n    dispatch(remoteUserLeave(e.userInfo));\n    dispatch(removeAutoPlayFail(e.userInfo));\n  };\n\n  const handleUserPublishStream = (e: { userId: string; mediaType: MediaType }) => {\n    const { userId, mediaType } = e;\n    const payload: IUser = { userId };\n    if (mediaType === MediaType.AUDIO) {\n      payload.publishAudio = true;\n    } else if (mediaType === MediaType.VIDEO) {\n      payload.publishVideo = true;\n    } else if (mediaType === MediaType.AUDIO_AND_VIDEO) {\n      payload.publishAudio = true;\n      payload.publishVideo = true;\n    }\n    const isFullScreen = store.getState().room.isFullScreen;\n    RtcClient.setRemoteVideoPlayer(userId, isFullScreen ? 'remote-video-player' : 'remote-full-player');\n    dispatch(updateRemoteUser(payload));\n  };\n\n  const handleUserUnpublishStream = (e: {\n    userId: string;\n    mediaType: MediaType;\n    reason: StreamRemoveReason;\n  }) => {\n    const { userId, mediaType } = e;\n\n    const payload: IUser = { userId };\n    if (mediaType === MediaType.AUDIO) {\n      payload.publishAudio = false;\n    }\n\n    if (mediaType === MediaType.AUDIO_AND_VIDEO) {\n      payload.publishAudio = false;\n    }\n    RtcClient.setRemoteVideoPlayer(userId);\n    dispatch(updateRemoteUser(payload));\n  };\n\n  const handleRemoteStreamStats = (e: RemoteStreamStats) => {\n    dispatch(\n      updateRemoteUser({\n        userId: e.userId,\n        audioStats: e.audioStats,\n      })\n    );\n  };\n\n  const handleLocalStreamStats = (e: LocalStreamStats) => {\n    dispatch(\n      updateLocalUser({\n        audioStats: e.audioStats,\n      })\n    );\n  };\n\n  const handleLocalAudioPropertiesReport = (e: LocalAudioPropertiesInfo[]) => {\n    const localAudioInfo = e.find(\n      (audioInfo) => audioInfo.streamIndex === StreamIndex.STREAM_INDEX_MAIN\n    );\n    if (localAudioInfo) {\n      dispatch(\n        updateLocalUser({\n          audioPropertiesInfo: localAudioInfo.audioPropertiesInfo,\n        })\n      );\n    }\n  };\n\n  const handleRemoteAudioPropertiesReport = (e: RemoteAudioPropertiesInfo[]) => {\n    const remoteAudioInfo = e\n      .filter((audioInfo) => audioInfo.streamKey.streamIndex === StreamIndex.STREAM_INDEX_MAIN)\n      .map((audioInfo) => ({\n        userId: audioInfo.streamKey.userId,\n        audioPropertiesInfo: audioInfo.audioPropertiesInfo,\n      }));\n\n    if (remoteAudioInfo.length) {\n      dispatch(updateRemoteUser(remoteAudioInfo));\n    }\n  };\n\n  const handleAudioDeviceStateChanged = async (device: DeviceInfo) => {\n    const devices = await RtcClient.getDevices();\n\n    if (device.mediaDeviceInfo.kind === 'audioinput') {\n      let deviceId = device.mediaDeviceInfo.deviceId;\n      if (device.deviceState === 'inactive') {\n        deviceId = devices.audioInputs?.[0].deviceId || '';\n      }\n      RtcClient.switchDevice(MediaType.AUDIO, deviceId);\n      dispatch(setMicrophoneList(devices.audioInputs));\n\n      dispatch(\n        updateSelectedDevice({\n          selectedMicrophone: deviceId,\n        })\n      );\n    }\n  };\n\n  const handleAutoPlayFail = (event: AutoPlayFailedEvent) => {\n    const { userId, kind } = event;\n    let playUser = playStatus.current?.[userId] || {};\n    playUser = { ...playUser, [kind]: false };\n    playStatus.current[userId] = playUser;\n\n    dispatch(\n      addAutoPlayFail({\n        userId,\n      })\n    );\n  };\n\n  const addFailUser = (userId: string) => {\n    dispatch(addAutoPlayFail({ userId }));\n  };\n\n  const playerFail = (params: { type: 'audio' | 'video'; userId: string }) => {\n    const { type, userId } = params;\n    let playUser = playStatus.current?.[userId] || {};\n\n    playUser = { ...playUser, [type]: false };\n\n    const { audio, video } = playUser;\n\n    if (audio === false || video === false) {\n      addFailUser(userId);\n    }\n\n    return playUser;\n  };\n\n  const handlePlayerEvent = (event: PlayerEvent) => {\n    const { userId, rawEvent, type } = event;\n    let playUser = playStatus.current?.[userId] || {};\n\n    if (!playStatus.current) return;\n\n    if (rawEvent.type === 'playing') {\n      playUser = { ...playUser, [type]: true };\n      const { audio, video } = playUser;\n      if (audio !== false && video !== false) {\n        dispatch(removeAutoPlayFail({ userId }));\n      }\n    } else if (rawEvent.type === 'pause') {\n      playUser = playerFail({ type, userId });\n    }\n\n    playStatus.current[userId] = playUser;\n  };\n\n  const handleNetworkQuality = (\n    uplinkNetworkQuality: NetworkQuality,\n    downlinkNetworkQuality: NetworkQuality\n  ) => {\n    dispatch(\n      updateNetworkQuality({\n        networkQuality: Math.floor(\n          (uplinkNetworkQuality + downlinkNetworkQuality) / 2\n        ) as NetworkQuality,\n      })\n    );\n  };\n\n  const handleRoomBinaryMessageReceived = (event: { userId: string; message: ArrayBuffer }) => {\n    const { message } = event;\n    parser(message);\n  };\n\n  return {\n    handleError,\n    handleUserJoin,\n    handleUserLeave,\n    handleTrackEnded,\n    handleUserPublishStream,\n    handleUserUnpublishStream,\n    handleRemoteStreamStats,\n    handleLocalStreamStats,\n    handleLocalAudioPropertiesReport,\n    handleRemoteAudioPropertiesReport,\n    handleAudioDeviceStateChanged,\n    handleAutoPlayFail,\n    handlePlayerEvent,\n    handleRoomBinaryMessageReceived,\n    handleNetworkQuality,\n  };\n};\n\nexport default useRtcListeners;\n"
  },
  {
    "path": "src/lib/useCommon.ts",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport { useEffect, useState, useRef } from 'react';\nimport { useSelector, useDispatch } from 'react-redux';\nimport VERTC, { MediaType } from '@volcengine/rtc';\nimport { Modal } from '@arco-design/web-react';\nimport RtcClient from '@/lib/RtcClient';\nimport {\n  clearCurrentMsg,\n  clearHistoryMsg,\n  localJoinRoom,\n  localLeaveRoom,\n  updateAIGCState,\n  updateLocalUser,\n} from '@/store/slices/room';\n\nimport useRtcListeners from '@/lib/listenerHooks';\nimport { RootState } from '@/store';\n\nimport {\n  updateMediaInputs,\n  updateSelectedDevice,\n  setDevicePermissions,\n} from '@/store/slices/device';\nimport logger from '@/utils/logger';\n\nexport const ABORT_VISIBILITY_CHANGE = 'abortVisibilityChange';\nexport interface FormProps {\n  username: string;\n  roomId: string;\n  publishAudio: boolean;\n}\n\nexport const useScene = () => {\n  const { scene, sceneConfigMap } = useSelector((state: RootState) => state.room);\n  return sceneConfigMap[scene] || {};\n}\n\nexport const useRTC = () => {\n  const { scene, rtcConfigMap } = useSelector((state: RootState) => state.room);\n  return rtcConfigMap[scene] || {};\n}\n\nexport const useDeviceState = () => {\n  const dispatch = useDispatch();\n  const room = useSelector((state: RootState) => state.room);\n  const localUser = room.localUser;\n  const isAudioPublished = localUser.publishAudio;\n  const isVideoPublished = localUser.publishVideo;\n  const isScreenPublished = localUser.publishScreen;\n  const queryDevices = async (type: MediaType) => {\n    const mediaDevices = await RtcClient.getDevices({\n      audio: type === MediaType.AUDIO,\n      video: type === MediaType.VIDEO,\n    });\n    if (type === MediaType.AUDIO) {\n      dispatch(\n        updateMediaInputs({\n          audioInputs: mediaDevices.audioInputs,\n        })\n      );\n      dispatch(\n        updateSelectedDevice({\n          selectedMicrophone: mediaDevices.audioInputs[0]?.deviceId,\n        })\n      );\n    } else {\n      dispatch(\n        updateMediaInputs({\n          videoInputs: mediaDevices.videoInputs,\n        })\n      );\n      dispatch(\n        updateSelectedDevice({\n          selectedCamera: mediaDevices.videoInputs[0]?.deviceId,\n        })\n      );\n    }\n    return mediaDevices;\n  };\n\n  const switchMic = async (controlPublish = true) => {\n    if (controlPublish) {\n      await (!isAudioPublished\n        ? RtcClient.publishStream(MediaType.AUDIO)\n        : RtcClient.unpublishStream(MediaType.AUDIO));\n    }\n    queryDevices(MediaType.AUDIO);\n    await (!isAudioPublished ? RtcClient.startAudioCapture() : RtcClient.stopAudioCapture());\n    dispatch(\n      updateLocalUser({\n        publishAudio: !isAudioPublished,\n      })\n    );\n  };\n\n  const switchCamera = async (controlPublish = true) => {\n    if (controlPublish) {\n      await (!isVideoPublished\n        ? RtcClient.publishStream(MediaType.VIDEO)\n        : RtcClient.unpublishStream(MediaType.VIDEO));\n    }\n    queryDevices(MediaType.VIDEO);\n    await (!isVideoPublished ? RtcClient.startVideoCapture() : RtcClient.stopVideoCapture());\n    dispatch(\n      updateLocalUser({\n        publishVideo: !isVideoPublished,\n      })\n    );\n  };\n\n  const switchScreenCapture = async (controlPublish = true) => {\n    try {\n      !isScreenPublished\n        ? sessionStorage.setItem(ABORT_VISIBILITY_CHANGE, 'true')\n        : sessionStorage.removeItem(ABORT_VISIBILITY_CHANGE);\n      if (controlPublish) {\n        await (!isScreenPublished\n          ? RtcClient.publishScreenStream(MediaType.VIDEO)\n          : RtcClient.unpublishScreenStream(MediaType.VIDEO));\n      }\n      await (!isScreenPublished ? RtcClient.startScreenCapture() : RtcClient.stopScreenCapture());\n      dispatch(\n        updateLocalUser({\n          publishScreen: !isScreenPublished,\n        })\n      );\n    } catch {\n      console.warn('Not Authorized.');\n    }\n    sessionStorage.removeItem(ABORT_VISIBILITY_CHANGE);\n    return false;\n  };\n\n  return {\n    isAudioPublished,\n    isVideoPublished,\n    isScreenPublished,\n    switchMic,\n    switchCamera,\n    switchScreenCapture,\n  };\n};\n\nexport const useGetDevicePermission = () => {\n  const [permission, setPermission] = useState<{\n    audio: boolean;\n  }>();\n\n  const dispatch = useDispatch();\n\n  useEffect(() => {\n    (async () => {\n      const permission = await RtcClient.checkPermission();\n      dispatch(setDevicePermissions(permission));\n      setPermission(permission);\n    })();\n  }, [dispatch]);\n  return permission;\n};\n\nexport const useJoin = (): [\n  boolean,\n  () => Promise<void | boolean>\n] => {\n  const devicePermissions = useSelector((state: RootState) => state.device.devicePermissions);\n  const room = useSelector((state: RootState) => state.room);\n\n  const dispatch = useDispatch();\n\n  const { id } = useScene();\n  const { switchMic } = useDeviceState();\n  const [joining, setJoining] = useState(false);\n  const listeners = useRtcListeners();\n\n  const handleAIGCModeStart = async () => {\n    if (room.isAIGCEnable) {\n      await RtcClient.stopAgent(id);\n      dispatch(clearCurrentMsg());\n      await RtcClient.startAgent(id);\n    } else {\n      await RtcClient.startAgent(id);\n    }\n    dispatch(updateAIGCState({ isAIGCEnable: true }));\n  };\n\n  async function disPatchJoin(): Promise<boolean | undefined> {\n    if (joining) {\n      return;\n    }\n\n    const isSupported = await VERTC.isSupported();\n    if (!isSupported) {\n      Modal.error({\n        title: '不支持 RTC',\n        content: '您的浏览器可能不支持 RTC 功能，请尝试更换浏览器或升级浏览器后再重试。',\n      });\n      return;\n    }\n\n    setJoining(true);\n\n    /** 1. Create RTC Engine */\n    await RtcClient.createEngine();\n\n    /** 2.1 Set events callbacks */\n    RtcClient.addEventListeners(listeners);\n\n    /** 2.2 RTC starting to join room */\n    await RtcClient.joinRoom();\n    /** 3. Set users' devices info */\n    const mediaDevices = await RtcClient.getDevices({\n      audio: true,\n      video: false,\n    });\n\n    dispatch(\n      localJoinRoom({\n        roomId: RtcClient.basicInfo.room_id,\n        user: {\n          username: RtcClient.basicInfo.user_id,\n          userId: RtcClient.basicInfo.user_id,\n        },\n      })\n    );\n    dispatch(\n      updateSelectedDevice({\n        selectedMicrophone: mediaDevices.audioInputs[0]?.deviceId,\n        selectedCamera: mediaDevices.videoInputs[0]?.deviceId,\n      })\n    );\n    dispatch(updateMediaInputs(mediaDevices));\n\n    setJoining(false);\n\n    if (devicePermissions.audio) {\n      try {\n        await switchMic();\n      } catch (e) {\n        logger.debug('No permission for mic');\n      }\n    }\n\n    handleAIGCModeStart();\n  }\n\n  return [joining, disPatchJoin];\n};\n\nexport const useLeave = () => {\n  const dispatch = useDispatch();\n  const { id } = useScene();\n  const idRef = useRef(id);\n  idRef.current = id;\n\n  return async function () {\n    await Promise.all([\n      RtcClient.stopAudioCapture,\n      RtcClient.stopScreenCapture,\n      RtcClient.stopVideoCapture,\n    ]);\n    await RtcClient.stopAgent(idRef.current);\n    await RtcClient.leaveRoom();\n    dispatch(clearHistoryMsg());\n    dispatch(clearCurrentMsg());\n    dispatch(localLeaveRoom());\n    dispatch(updateAIGCState({ isAIGCEnable: false }));\n  };\n};"
  },
  {
    "path": "src/pages/MainPage/MainArea/Antechamber/InvokeButton/index.module.less",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\n.wrapper {\n    position: relative;\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n\n\n    .btn {\n        width: max-content;\n        height: max-content;\n        border-radius: 50%;\n        display: flex;\n        justify-content: center;\n        align-items: center;\n\n        .icon {\n            position: absolute;\n        }\n    }\n\n    .text {\n        margin-top: 8px;\n        color: rgba(115, 122, 135, 1);\n    }\n}\n\n.cursor {\n    cursor: pointer;\n}\n\n.cursor:hover {\n    opacity: 0.8;\n}\n\n.cursor:active {\n    opacity: 1;\n}\n\n.loader {\n    display: flex;\n    gap: 5px;\n}\n\n.dot {\n    width: 10px;\n    height: 10px;\n    border-radius: 50%;\n    background-color: white;\n    animation: glow 0.9s infinite;\n}\n\n@keyframes glow {\n    0% {\n        opacity: 1;\n    }\n    40% {\n        opacity: 0.7;\n    }\n    100% {\n        opacity: 0.3;\n    }\n}"
  },
  {
    "path": "src/pages/MainPage/MainArea/Antechamber/InvokeButton/index.tsx",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport Loading from './loading';\nimport style from './index.module.less';\nimport CallButtonSVG from '@/assets/img/CallWrapper.svg';\nimport PhoneSVG from '@/assets/img/Phone.svg';\n\ninterface IInvokeButtonProps extends React.HTMLAttributes<HTMLDivElement> {\n  loading?: boolean;\n}\n\nfunction InvokeButton(props: IInvokeButtonProps) {\n  const { loading, className, ...rest } = props;\n\n  return (\n    <div className={`${style.wrapper} ${loading ? '' : style.cursor} ${className}`} {...rest}>\n      <div className={style.btn}>\n        <img src={CallButtonSVG} alt=\"call\" />\n        {loading ? (\n          <Loading className={style.icon} />\n        ) : (\n          <img src={PhoneSVG} className={style.icon} alt=\"phone\" />\n        )}\n      </div>\n      <div className={style.text}>{loading ? '连接中' : '通话'}</div>\n    </div>\n  );\n}\n\nexport default InvokeButton;\n"
  },
  {
    "path": "src/pages/MainPage/MainArea/Antechamber/InvokeButton/loading.tsx",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport style from './index.module.less';\n\nfunction Loading(props: React.HTMLAttributes<HTMLDivElement>) {\n  const { className = '', ...rest } = props;\n  return (\n    <div className={`${style.loader} ${className}`} {...rest}>\n      {Array(3)\n        .fill(0)\n        .map((_, index) => (\n          <div\n            key={index}\n            className={style.dot}\n            style={{\n              animationDelay: `${index * 0.3}s`,\n            }}\n          />\n        ))}\n    </div>\n  );\n}\n\nexport default Loading;\n"
  },
  {
    "path": "src/pages/MainPage/MainArea/Antechamber/index.module.less",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\n .wrapper {\n    position: relative;\n    width: 100%;\n    height: 100%;\n    border-radius: 16px;\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n  \n    .avatar {\n      /**\n           * height = 128px in AvatarCard.avatar\n           *\n           */\n      margin-top: -128px;\n      /**\n           * width = 128px in AvatarCard.avatar \n           * 128px / 2 = 64px\n           *\n           */\n      transform: translateX(calc(50% - 64px));\n    }\n  \n    .mobile {\n      transform: none !important;\n    }\n  \n    .description {\n      font-family: PingFang SC;\n      font-weight: 400;\n      font-size: 10px;\n      line-height: 20px;\n      color: #c7ccd6;\n  \n      position: absolute;\n      bottom: 24px;\n      left: 24px;\n    }\n  \n    .invoke-btn {\n      margin-top: 32px;\n    }\n  \n    .mobileDesc {\n      font-weight: 400;\n      font-size: 14px;\n      line-height: 22px;\n      text-align: center;\n      color: #737a87;\n      position: absolute;\n      bottom: 12px;\n    }\n  }\n  \n  .mobile {\n    background: \n    /* 图层1 (最上层): 背景图片 */\n    /* url(...) [position] / [size] [repeat] */ url('../../../../assets/img/mobileBg.png')\n        center center / cover no-repeat,\n      /* 图层2 (下层): 渐变背景 */ linear-gradient(167.98deg, #f5f7ff 0%, #faf3ff 100%);\n    border-radius: 0;\n  }\n  "
  },
  {
    "path": "src/pages/MainPage/MainArea/Antechamber/index.tsx",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport { useDispatch } from 'react-redux';\nimport { isMobile } from '@/utils/utils';\nimport InvokeButton from '@/pages/MainPage/MainArea/Antechamber/InvokeButton';\nimport { useJoin, useScene } from '@/lib/useCommon';\nimport AIChangeCard from '@/components/AiChangeCard';\nimport { updateFullScreen, updateShowSubtitle } from '@/store/slices/room';\nimport style from './index.module.less';\n\nfunction Antechamber() {\n  const dispatch = useDispatch();\n  const [joining, dispatchJoin] = useJoin();\n  const { isScreenMode, isAvatarScene } = useScene();\n\n  const handleJoinRoom = () => {\n    dispatch(updateFullScreen({ isFullScreen: !isMobile() && !isScreenMode && !isAvatarScene })); // 初始化\n    dispatch(updateShowSubtitle({ isShowSubtitle: !isAvatarScene }));\n\n    if (!joining) {\n      dispatchJoin();\n    }\n  };\n\n  return (\n    <div className={`${style.wrapper} ${isMobile() ? style.mobile : ''}`}>\n      <AIChangeCard />\n      <InvokeButton onClick={handleJoinRoom} loading={joining} className={style['invoke-btn']} />\n      {isMobile() ? null : (\n        <div className={style.description}>Powered by 豆包大模型和火山引擎视频云 RTC</div>\n      )}\n    </div>\n  );\n}\n\nexport default Antechamber;\n"
  },
  {
    "path": "src/pages/MainPage/MainArea/Room/AudioController.tsx",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport { useDispatch, useSelector } from 'react-redux';\nimport AudioLoading from '@/components/Loading/AudioLoading';\nimport { RootState } from '@/store';\nimport RtcClient from '@/lib/RtcClient';\nimport { setInterruptMsg } from '@/store/slices/room';\nimport { useDeviceState, useScene } from '@/lib/useCommon';\nimport { COMMAND } from '@/utils/handler';\nimport style from './index.module.less';\n\nconst THRESHOLD_VOLUME = 18;\n\nfunction AudioController(props: React.HTMLAttributes<HTMLDivElement>) {\n  const { className, ...rest } = props;\n  const dispatch = useDispatch();\n  const { isInterruptMode, botName } = useScene();\n  const room = useSelector((state: RootState) => state.room);\n  const volume = room.localUser.audioPropertiesInfo?.linearVolume || 0;\n  const { isAudioPublished } = useDeviceState();\n  const { isAITalking } = room;\n  const isAIReady = room.msgHistory.length > 0;\n  const isLoading = volume >= THRESHOLD_VOLUME && isAudioPublished;\n\n  const handleInterrupt = () => {\n    RtcClient.commandAgent({\n      agentName: botName,\n      command: COMMAND.INTERRUPT,\n    });\n    dispatch(setInterruptMsg());\n  };\n  return (\n    <div className={`${className}`} {...rest}>\n      {isAudioPublished ? (\n        isAIReady && isAITalking ? (\n          <div className={style.interruptContainer}>\n            {isInterruptMode ? <div>语音打断 或 </div> : null}\n            <div onClick={handleInterrupt} className={style.interrupt}>\n              <div className={style.interruptIcon} />\n              <span>点此打断</span>\n            </div>\n          </div>\n        ) : isLoading ? null : (\n          <div className={style.closed}>请开始说话</div>\n        )\n      ) : (\n        <div className={style.closed}>你已关闭麦克风</div>\n      )}\n      <AudioLoading loading={isLoading} color={isAudioPublished ? undefined : '#EAEDF1'} />\n    </div>\n  );\n}\nexport default AudioController;\n"
  },
  {
    "path": "src/pages/MainPage/MainArea/Room/CameraArea.tsx",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport { useSelector } from 'react-redux';\nimport { VideoRenderMode } from '@volcengine/rtc';\nimport { useEffect } from 'react';\nimport { RootState } from '@/store';\nimport { useDeviceState, useScene } from '@/lib/useCommon';\nimport RtcClient from '@/lib/RtcClient';\n\nimport styles from './index.module.less';\nimport UserTag from '@/components/UserTag';\nimport LocalPlayerSet from '@/components/LocalPlayerSet';\nimport AiAvatarCard from '@/components/AiAvatarCard';\nimport UserAvatar from '@/assets/img/userAvatar.png';\nimport CameraCloseNoteSVG from '@/assets/img/CameraCloseNote.svg';\nimport ScreenCloseNoteSVG from '@/assets/img/ScreenCloseNote.svg';\nimport { LocalFullID, RemoteFullID } from '@/components/FullScreenCard';\n\nconst LocalVideoID = 'local-video-player';\nconst LocalScreenID = 'local-screen-player';\nconst RemoteVideoID = 'remote-video-player';\n\nfunction CameraArea(props: React.HTMLAttributes<HTMLDivElement>) {\n  const { className, ...rest } = props;\n  const room = useSelector((state: RootState) => state.room);\n  const { isFullScreen, scene } = room;\n  const { isVision, isScreenMode, botName } = useScene();\n  const { isVideoPublished, isScreenPublished, switchCamera, switchScreenCapture } =\n    useDeviceState();\n  const isRemoteVideoPublished = room.remoteUsers.find(user => user.username === botName)?.publishVideo ?? false\n\n  const setVideoPlayer = () => {\n    RtcClient.removeLocalVideoPlayer(room.localUser.username!);\n    if (isVideoPublished || isScreenPublished) {\n      RtcClient.setLocalVideoPlayer(\n        room.localUser.username!,\n        isFullScreen ? LocalFullID : isScreenMode ? LocalScreenID : LocalVideoID,\n        isScreenPublished,\n        isScreenMode ? VideoRenderMode.RENDER_MODE_FILL : VideoRenderMode.RENDER_MODE_HIDDEN\n      );\n      if(isRemoteVideoPublished) {\n        RtcClient.setRemoteVideoPlayer(\n          botName,\n          isFullScreen ? RemoteVideoID : RemoteFullID,\n        );\n      }\n    }\n  };\n\n  const handleOperateCamera = () => {\n    switchCamera();\n  };\n\n  const handleOperateScreenShare = () => {\n    switchScreenCapture();\n  };\n\n  useEffect(() => {\n    setVideoPlayer();\n  }, [isVideoPublished, isScreenPublished, isScreenMode, isFullScreen, isVision]);\n\n  return (\n    <div className={`${styles['camera-wrapper']} ${className}`} {...rest}>\n      <UserTag name={isFullScreen ? scene : '我'} className={styles.userTag} />\n      {isFullScreen ? (\n        <AiAvatarCard showUserTag={false} showStatus className={styles.fullScreenAiAvatar} />\n      ) : null}\n      {isVideoPublished || isScreenPublished ? <LocalPlayerSet /> : null}\n      <div\n        id={LocalVideoID}\n        className={`${styles['camera-player']} ${\n          isVideoPublished && !isScreenMode ? '' : styles['camera-player-hidden']\n        }`}\n      />\n      <div\n        id={LocalScreenID}\n        className={`${styles['camera-player']} ${\n          isScreenPublished && isScreenMode ? '' : styles['camera-player-hidden']\n        }`}\n      />\n      <div\n        id={RemoteVideoID}\n        className={`${styles['camera-player']} ${\n          isFullScreen && isRemoteVideoPublished ? '' : styles['camera-player-hidden']\n        }`}\n        style={{ position: 'absolute' }}\n      />\n      <div\n        className={`${styles['camera-placeholder']} ${\n          isVideoPublished || isScreenPublished ? styles['camera-player-hidden'] : ''\n        }`}\n      >\n        <img\n          src={isScreenMode ? ScreenCloseNoteSVG : isVision ? CameraCloseNoteSVG : UserAvatar}\n          alt=\"close\"\n          className={styles['camera-placeholder-close-note']}\n        />\n\n        {isFullScreen ? null : (\n          <div>\n            {isScreenMode ? (\n              <>\n                打开\n                <span onClick={handleOperateScreenShare} className={styles['camera-open-btn']}>\n                  屏幕共享\n                </span>\n                <div>体验豆包视觉理解模型</div>\n              </>\n            ) : isVision ? (\n              <>\n                打开\n                <span onClick={handleOperateCamera} className={styles['camera-open-btn']}>\n                  摄像头\n                </span>\n                <div>体验豆包视觉理解模型</div>\n              </>\n            ) : null}\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n\nexport default CameraArea;\n"
  },
  {
    "path": "src/pages/MainPage/MainArea/Room/Conversation.tsx",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport React, { useRef, useEffect } from 'react';\nimport { useSelector } from 'react-redux';\nimport { Tag, Spin } from '@arco-design/web-react';\nimport { RootState } from '@/store';\nimport Loading from '@/components/Loading/HorizonLoading';\nimport { isMobile } from '@/utils/utils';\nimport { useScene } from '@/lib/useCommon';\nimport USER_AVATAR from '@/assets/img/userAvatar.png';\nimport styles from './index.module.less';\nimport AIAvatarReadying from '@/components/AIAvatarLoading';\n\nconst lines: (string | React.ReactNode)[] = [];\n\nfunction Conversation(props: React.HTMLAttributes<HTMLDivElement> & { showSubtitle: boolean }) {\n  const { className, showSubtitle, ...rest } = props;\n  const room = useSelector((state: RootState) => state.room);\n  const { msgHistory, isFullScreen } = room;\n  const { userId } = useSelector((state: RootState) => state.room.localUser);\n  const { isAITalking, isUserTalking, scene } = useSelector((state: RootState) => state.room);\n  const isAIReady = msgHistory.length > 0;\n  const containerRef = useRef<HTMLDivElement>(null);\n  const { botName, icon, isAvatarScene } = useScene();\n\n  const isUserTextLoading = (owner: string) => {\n    return owner === userId && isUserTalking;\n  };\n\n  const isAITextLoading = (owner: string) => {\n    return (owner === botName || owner.includes('voiceChat_')) && isAITalking;\n  };\n\n  useEffect(() => {\n    const container = containerRef.current;\n    if (container) {\n      container.scrollTop = container.scrollHeight - container.clientHeight;\n    }\n  }, [msgHistory.length]);\n\n  return (\n    <div\n      ref={containerRef}\n      className={`${styles.conversation} ${className} ${isFullScreen ? styles.fullScreen : ''} ${\n        isMobile() ? styles.mobileConversation : ''\n      }`}\n      style={isAvatarScene && !isAIReady ? { justifyContent: 'center' } : {}}\n      {...rest}\n    >\n      {lines.map((line) => line)}\n      {!isAIReady ? (\n        <div className={styles.aiReadying}>\n          {isAvatarScene ? (\n            <AIAvatarReadying />\n          ) : (\n            <>\n              <Spin size={16} className={styles['aiReading-spin']} />\n              AI 准备中, 请稍侯\n            </>\n          )}\n        </div>\n      ) : (\n        ''\n      )}\n      {(showSubtitle ? msgHistory : [])?.map(({ value, user, isInterrupted }, index) => {\n        const isUserMsg = user === userId;\n        const isRobotMsg = user === botName || user.includes('voiceChat_');\n        if (!isUserMsg && !isRobotMsg) {\n          return '';\n        }\n        return (\n          <div\n            key={`msg-container-${index}`}\n            className={styles.mobileLine}\n            style={{ justifyContent: isUserMsg && isMobile() ? 'flex-end' : '' }}\n          >\n            {!isMobile() && (\n              <div className={styles.msgName}>\n                <div className={styles.avatar}>\n                  <img src={isUserMsg ? USER_AVATAR : icon} alt=\"Avatar\" />\n                </div>\n                {isUserMsg ? '我' : scene}\n              </div>\n            )}\n            <div\n              className={`${styles.sentence} ${isUserMsg ? styles.user : styles.robot}`}\n              key={`msg-${index}`}\n            >\n              <div className={styles.content}>\n                {value}\n                <div className={styles['loading-wrapper']}>\n                  {isAIReady &&\n                  (isUserTextLoading(user) || isAITextLoading(user)) &&\n                  index === msgHistory.length - 1 ? (\n                    <Loading gap={3} className={styles.loading} dotClassName={styles.dot} />\n                  ) : (\n                    ''\n                  )}\n                </div>\n              </div>\n              {!isUserMsg && isInterrupted ? <Tag className={styles.interruptTag}>已打断</Tag> : ''}\n            </div>\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n\nexport default Conversation;\n"
  },
  {
    "path": "src/pages/MainPage/MainArea/Room/ToolBar.tsx",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport { memo, useState } from 'react';\nimport { Drawer } from '@arco-design/web-react';\nimport { useDeviceState, useLeave, useScene } from '@/lib/useCommon';\nimport { isMobile } from '@/utils/utils';\nimport Menu from '../../Menu';\n\nimport style from './index.module.less';\nimport CameraOpenSVG from '@/assets/img/CameraOpen.svg';\nimport CameraCloseSVG from '@/assets/img/CameraClose.svg';\nimport MicOpenSVG from '@/assets/img/MicOpen.svg';\nimport MicCloseSVG from '@/assets/img/MicClose.svg';\nimport LeaveRoomSVG from '@/assets/img/LeaveRoom.svg';\nimport ScreenOnSVG from '@/assets/img/ScreenOn.svg';\nimport ScreenOffSVG from '@/assets/img/ScreenOff.svg';\n\nfunction ToolBar(props: React.HTMLAttributes<HTMLDivElement>) {\n  const { className, ...rest } = props;\n  const [open, setOpen] = useState(false);\n  const { isVision, isScreenMode } = useScene();\n  const leaveRoom = useLeave();\n  const {\n    isAudioPublished,\n    isVideoPublished,\n    isScreenPublished,\n    switchMic,\n    switchCamera,\n    switchScreenCapture,\n  } = useDeviceState();\n\n  return (\n    <div className={`${className} ${style.btns} ${isMobile() ? style.column : ''}`} {...rest}>\n      <img\n        src={isAudioPublished ? MicOpenSVG : MicCloseSVG}\n        onClick={() => switchMic(true)}\n        className={style.btn}\n        alt=\"mic\"\n      />\n      {!isVision ? null : isScreenMode && !isMobile() ? (\n        <img\n          src={isScreenPublished ? 'new-screen-off.svg' : 'new-screen-on.svg'}\n          onClick={() => switchScreenCapture()}\n          className={style.btn}\n          alt=\"screenShare\"\n        />\n      ) : (\n        <img\n          src={isVideoPublished ? CameraOpenSVG : CameraCloseSVG}\n          onClick={() => switchCamera(true)}\n          className={style.btn}\n          alt=\"camera\"\n        />\n      )}\n      {isScreenMode && (\n        <img\n          src={isScreenPublished ? ScreenOnSVG : ScreenOffSVG}\n          onClick={() => switchScreenCapture(true)}\n          className={style.btn}\n          alt=\"screenShare\"\n        />\n      )}\n      <img src={LeaveRoomSVG} onClick={leaveRoom} className={style.btn} alt=\"leave\" />\n      {isMobile() ? (\n        <Drawer\n          title=\"设置\"\n          visible={open}\n          onCancel={() => setOpen(false)}\n          style={{\n            width: 'max-content',\n          }}\n          footer={null}\n        >\n          <Menu />\n        </Drawer>\n      ) : null}\n    </div>\n  );\n}\nexport default memo(ToolBar);\n"
  },
  {
    "path": "src/pages/MainPage/MainArea/Room/index.module.less",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\n .wrapper {\n    position: relative;\n    width: 100%;\n    height: 100%;\n    border-radius: 16px;\n    padding: 32px;\n    box-sizing: border-box;\n  \n    .conversation,\n    .fullScreen,\n    .mobileConversation {\n      width: 100%;\n      position: relative;\n      height: 100%;\n      /**\n           * 100% 为容器高度\n           * 128px 为上层 DouBao Card Height\n           * 24px 为 margin top\n           * 36px * 2 为容器 padding\n           * 128 + 24 + 36 * 2 = 224px\n           */\n      max-height: calc(100% - 224px - 8px);\n      display: flex;\n      flex-direction: column;\n      padding-bottom: 12px;\n      overflow-x: hidden;\n      overflow-y: auto;\n      margin-top: 48px;\n  \n      .sentence {\n        position: relative;\n        display: flex;\n        flex-direction: row;\n        justify-content: flex-start;\n        flex-wrap: wrap;\n        align-items: center;\n        width: max-content;\n        white-space: normal;\n        max-width: 70%;\n        padding: 12px 16px;\n        margin-left: 32px;\n        gap: 8px;\n  \n        .content {\n          width: max-content;\n        }\n      }\n  \n      .user {\n        width: max-content;\n        border: 0px solid;\n        padding: 8px 12px 8px 12px;\n        border-radius: 12px;\n        background: #f1f3f5;\n        margin-bottom: 12px;\n      }\n      .robot {\n        font-family: PingFang SC;\n        color: #0c0d0e;\n        font-size: 14px;\n        font-weight: 500;\n        letter-spacing: 0.003em;\n        border: 1px solid transparent;\n        border-radius: 12px;\n        background: linear-gradient(77.86deg, #fff -3.23%, #fff 51.11%, #fff 98.65%) padding-box,\n          linear-gradient(77.86deg, #e5f2ff -3.23%, #d9e5ff 51.11%, #f6e2ff 98.65%) border-box;\n        margin-bottom: 12px;\n      }\n  \n      .loading-wrapper {\n        width: max-content;\n        display: inline-block;\n  \n        .loading {\n          margin-left: 8px;\n          width: max-content;\n        }\n  \n        .dot {\n          background-color: rgba(193, 163, 237, 1);\n          width: 8px;\n          height: 8px;\n        }\n      }\n  \n      .aiReadying {\n        font-family: PingFang SC;\n        font-size: 16px;\n        font-weight: 500;\n        color: rgba(27, 30, 61, 0.6);\n        text-align: center;\n        display: flex;\n        flex-direction: row;\n        justify-content: flex-start;\n        align-items: center;\n        line-height: 28px;\n      }\n  \n      .aiReading-spin {\n        margin-right: 12px;\n        line-height: 16px;\n      }\n  \n      .msgName {\n        display: flex;\n        gap: 8px;\n        align-items: center;\n        font-size: 12px;\n        line-height: 20px;\n        color: #737a87;\n        margin-bottom: 4px;\n  \n        .avatar {\n          border-radius: 50%;\n          width: 24px;\n          height: 24px;\n  \n          img {\n            width: 100%;\n            height: 100%;\n          }\n        }\n      }\n    }\n  \n    .fullScreen {\n      .msgName {\n        color: #fff;\n        text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3);\n      }\n  \n      .sentence {\n        color: #fff;\n      }\n  \n      .user {\n        background: rgba(0, 0, 0, 0.25);\n      }\n      .robot {\n        background: rgba(0, 12, 71, 0.5);\n      }\n    }\n  \n    .conversation::-webkit-scrollbar {\n      width: 0px;\n      height: 0px;\n    }\n  \n    .conversation::-webkit-scrollbar-thumb {\n      background: rgba(0, 0, 0, 0);\n      border-radius: 0px;\n    }\n  \n    .conversation::-webkit-scrollbar-track {\n      background: rgba(0, 0, 0, 0);\n      border-radius: 0px;\n    }\n  \n    .toolBar {\n      position: absolute;\n      right: 0px;\n      margin-right: 36px;\n      bottom: 36px;\n    }\n  \n    .controller {\n      position: absolute;\n      left: 0px;\n      bottom: 36px;\n      margin-left: 50%;\n      transform: translateX(-50%);\n    }\n  \n    .declare {\n      position: absolute;\n      bottom: 8px;\n      left: 12px;\n      color: var(--text-color-text-4, rgba(199, 204, 214, 1));\n      font-size: 10px;\n      font-weight: 400;\n      line-height: 20px;\n    }\n  }\n  \n  .text {\n    width: 100%;\n    text-align: center;\n    color: rgba(148, 116, 255, 1);\n    font-size: 14px;\n    font-weight: 500;\n    line-height: 22px;\n  }\n  \n  .closed {\n    width: 100%;\n    text-align: center;\n    color: #737a87;\n    font-size: 14px;\n    font-weight: 400;\n    line-height: 19.6px;\n  }\n  \n  .btns {\n    width: 100%;\n    display: flex;\n    flex-direction: row;\n    justify-content: flex-end;\n    align-items: center;\n    gap: 16px;\n  \n    .setting {\n      background-color: rgba(111, 111, 111, 0.497);\n      border-radius: 50%;\n      width: 48px;\n      height: 48px;\n      padding: 12px;\n      box-sizing: border-box;\n      cursor: pointer;\n    }\n  \n    .btn {\n      cursor: pointer;\n    }\n  \n    .btn:hover {\n      opacity: 0.8;\n    }\n  \n    .btn:active {\n      opacity: 1;\n    }\n  }\n  \n  .column {\n    margin-right: 0 !important;\n    justify-content: space-around;\n    align-items: center;\n    bottom: 64px !important;\n    gap: 0;\n  \n    img {\n      width: 84px;\n      height: 84px;\n    }\n  }\n  \n  .interruptContainer {\n    color: #635bff;\n    display: flex;\n    align-items: center;\n    gap: 8px;\n    font-size: 14px;\n    font-weight: 500;\n  }\n  \n  .interruptIcon {\n    display: inline-block;\n    width: 8px;\n    height: 8px;\n    background-color: #635bff;\n    border-radius: 2px;\n  }\n  \n  .interrupt {\n    display: flex;\n    flex-direction: row;\n    justify-content: center;\n    align-items: center;\n    background: rgba(99, 91, 255, 0.1);\n    border-radius: 4px;\n    width: max-content;\n    height: 26px;\n    padding: 0 8px;\n    gap: 4px;\n    cursor: pointer;\n    user-select: none;\n    -webkit-user-select: none; /* Safari */\n    -moz-user-select: none; /* Firefox */\n    -ms-user-select: none; /* Internet Explorer/Edge */\n  \n    &:hover {\n      opacity: 0.8;\n    }\n  \n    &:active {\n      opacity: 1;\n    }\n  }\n  \n  .camera-wrapper {\n    position: absolute;\n    top: 16px;\n    right: 16px;\n    width: 264px;\n    border-radius: 8px;\n    background: var(--line-color-border-2, rgba(234, 237, 241, 1));\n    display: flex;\n    flex-direction: column;\n    justify-content: center;\n    align-items: center;\n    border: 0.81px solid var(--line-color-border-3, rgba(221, 226, 233, 1));\n    z-index: 4;\n  \n    .camera-player {\n      width: 100%;\n      height: 184px;\n      border-radius: 8px;\n      overflow: hidden;\n    }\n  \n    .camera-player-hidden {\n      display: none !important;\n    }\n  \n    .camera-placeholder {\n      width: 100%;\n      height: 184px;\n      display: flex;\n      flex-direction: column;\n      justify-content: center;\n      align-items: center;\n      font-size: 12px;\n      color: #737a87;\n      border-bottom-left-radius: inherit;\n      border-bottom-right-radius: inherit;\n      text-align: center;\n  \n      .camera-placeholder-close-note {\n        margin-bottom: 8px;\n        width: 60px;\n        height: 60px;\n      }\n  \n      .camera-open-btn {\n        color: var(--primary-color-primary-6, rgba(22, 100, 255, 1));\n        cursor: pointer;\n        margin-left: 2px;\n      }\n    }\n  \n    .userTag {\n      position: absolute;\n      top: 4px;\n      left: 4px;\n    }\n  \n    .subTitleUserTag {\n      position: absolute;\n      top: -16px;\n      right: -16px;\n    }\n  }\n  \n  .visionDescriptionArea {\n    width: 100%;\n    background: linear-gradient(77.86deg, #f1f9ff -3.23%, #edf3ff 51.11%, #faf4ff 98.65%);\n    padding: 10px 0;\n    text-align: center;\n    border-bottom-left-radius: inherit;\n    border-bottom-right-radius: inherit;\n    box-sizing: border-box;\n    font-size: 12px;\n    line-height: 20px;\n    color: #737a87;\n  \n    .visionTitleText {\n      color: #42464e;\n      font-weight: 500;\n    }\n  }\n  \n  .subtitleAiAvatar {\n    opacity: 0.3;\n  }\n  \n  .fullScreenAiAvatar {\n    height: 184px;\n  }\n  \n  .mobile {\n    background: \n    /* 图层1 (最上层): 背景图片 */\n    /* url(...) [position] / [size] [repeat] */ url('../../../../assets/img/mobileBg.png')\n        center center / cover no-repeat,\n      /* 图层2 (下层): 渐变背景 */ linear-gradient(167.98deg, #f5f7ff 0%, #faf3ff 100%);\n  \n    .controller {\n      bottom: 156px;\n    }\n    border-radius: 0;\n  }\n  \n  .mobileConversation {\n    display: flex;\n    max-height: calc(100% - 324px) !important;\n    margin-top: 64px !important;\n  \n    .sentence {\n      margin-left: 0 !important;\n      max-width: 85% !important;\n    }\n\n    .mobileLine {\n      display: flex;\n    }\n  }\n  \n  .mobilePlayer {\n    width: 100%;\n    height: 100%;\n    position: absolute;\n    top: 0;\n    left: 0;\n  }\n  \n  @media (max-width: 767px) {\n    .mobileLine {\n      display: flex;\n      justify-content: flex-start;\n    }\n    .user {\n      align-self: flex-end;\n    }\n  }\n  "
  },
  {
    "path": "src/pages/MainPage/MainArea/Room/index.tsx",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport { useSelector } from 'react-redux';\nimport Conversation from './Conversation';\nimport ToolBar from './ToolBar';\nimport CameraArea from './CameraArea';\nimport AudioController from './AudioController';\nimport { isMobile } from '@/utils/utils';\nimport style from './index.module.less';\nimport AiAvatarCard from '@/components/AiAvatarCard';\nimport { RootState } from '@/store';\nimport UserTag from '@/components/UserTag';\nimport FullScreenCard from '@/components/FullScreenCard';\nimport MobileToolBar from '@/pages/Mobile/MobileToolBar';\nimport { useScene } from '@/lib/useCommon';\n\nfunction Room() {\n  const room = useSelector((state: RootState) => state.room);\n  const { isShowSubtitle, scene, isFullScreen } = room;\n  const { isAvatarScene } = useScene();\n  return (\n    <div className={`${style.wrapper} ${isMobile() ? style.mobile : ''}`}>\n      {isMobile() ? <div className={style.mobilePlayer} id=\"mobile-local-player\" /> : null}\n      {isMobile() ? <MobileToolBar /> : null}\n      {isShowSubtitle && !isMobile() ? (\n        <UserTag name={scene} className={style.subTitleUserTag} />\n      ) : null}\n      {isAvatarScene || (isFullScreen && !isMobile()) ? (\n        <FullScreenCard />\n      ) : isMobile() && isShowSubtitle ? null : (\n        <AiAvatarCard\n          showUserTag={!isShowSubtitle}\n          showStatus={!isShowSubtitle}\n          className={isShowSubtitle ? style.subtitleAiAvatar : ''}\n        />\n      )}\n      {isMobile() ? null : <CameraArea />}\n      <Conversation className={style.conversation} showSubtitle={isShowSubtitle} />\n      <ToolBar className={style.toolBar} />\n      <AudioController className={style.controller} />\n      <div className={style.declare}>AI生成内容由大模型生成，不能完全保障真实</div>\n    </div>\n  );\n}\n\nexport default Room;\n"
  },
  {
    "path": "src/pages/MainPage/MainArea/index.module.less",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\n.wrapper {\n  width: 100%;\n  height: 100%;\n  background-color: white;\n  border: 1px solid var(--line-color-border-2, rgba(234, 237, 241, 1));\n  border-radius: 16px;\n  padding: 20px 12.5%;\n\n  .space {\n    width: 100%;\n    min-height: 40px;\n  }\n\n  .doubaoIcon {\n    width: 111px;\n    height: 111px;\n    min-height: 111px;\n    overflow: hidden;\n  }\n\n  .interruptTag {\n    width: max-content;\n    height: 22px;\n    padding: 0px 6px 0px 6px;\n    border-radius: 4px;\n    margin-left: 4px;\n    font-family: PingFang SC;\n    font-size: 12px;\n    font-weight: 400;\n    line-height: 22px;\n    letter-spacing: 0.003em;\n    color: var(--text-color-text-3, rgba(115, 122, 135, 1));\n    background: var(--security-unknown-tag-unknown-1, rgba(241, 243, 245, 1));\n  }\n\n  .welcome {\n    font-family: PingFang SC;\n    font-size: 24px;\n    font-weight: 500;\n    line-height: 32px;\n    letter-spacing: 0.003em;\n    text-align: left;\n    margin-top: 8px;\n  }\n\n  .weight {\n    background: linear-gradient(90deg, #004FFF 38.86%, #9865FF 100%);\n    -webkit-background-clip: text;\n    background-clip: text;\n    color: transparent;\n  }\n\n  .tip {\n    font-family: PingFang SC;\n    font-size: 13px;\n    font-weight: 400;\n    line-height: 22px;\n    letter-spacing: 0.003em;\n    text-align: left;\n    color: rgba(27, 30, 61, 0.6);\n    margin-top: 18px;\n    margin-bottom: 18px;\n  }\n\n  .tagProblem {\n    width: max-content;\n    border-radius: 4px;\n    font-family: PingFang SC;\n    font-size: 12px;\n    font-weight: 500;\n    line-height: 20px;\n    letter-spacing: 0.003em;\n    text-align: center;\n    margin-bottom: 12px;\n    color: rgba(66, 70, 78, 1);\n  }\n\n  .conversation {\n    overflow-x: hidden;\n    overflow-y: auto;\n    width: 100%;\n    position: relative;\n    height: calc(75% - 12px);\n    display: flex;\n    flex-direction: column;\n    padding-bottom: 12px;\n\n    .aiReadying {\n      font-family: PingFang SC;\n      font-size: 16px;\n      font-weight: 500;\n      line-height: 18px;\n      letter-spacing: 0.003em;\n      color: rgba(27, 30, 61, 0.6);\n      margin-top: 12px;\n      text-align: center;\n      display: flex;\n      flex-direction: row;\n      justify-content: flex-start;\n      align-items: center;\n    }\n\n    .aiReading-spin {\n      margin-right: 12px;\n    }\n  }\n\n  .conversation::-webkit-scrollbar {\n    width: 0px;\n    height: 0px;\n  }\n  \n  .conversation::-webkit-scrollbar-thumb {\n    background: rgba(0,0,0,0);\n    border-radius: 0px;\n  }\n  \n  .conversation::-webkit-scrollbar-track {\n    background: rgba(0,0,0,0);\n    border-radius: 0px;\n  }\n\n  .sentence {\n    display: flex;\n    flex-direction: row;\n    justify-content: flex-start;\n    align-items: center;\n    width: 100%;\n  }\n  .user {\n    width: max-content;\n    border: 0px solid;\n    align-self: flex-end;\n    padding: 8px 12px 8px 12px;\n    border-radius: 12px 0px 12px 12px;\n    background: var(--background-color-bg-5, rgba(241, 243, 245, 1));\n    margin-top: 12px;\n  }\n  .robot {\n    font-family: PingFang SC;\n    font-size: 14px;\n    font-weight: 400;\n    letter-spacing: 0.003em;\n\n    border: 0px solid;\n    align-self: flex-start;\n    padding: 3px 12px 3px 0px;\n  }\n\n  .userTalkingWave {\n    height: 100px;\n  }\n\n  .userStopTalkingWave {\n    height: 100px;\n    transform: scaleY(.5);\n  }\n\n  .status {\n    overflow: hidden;\n    width: 100%;\n    height: 25%;\n    display: flex;\n    flex-direction: column;\n    justify-content: flex-end;\n    align-items: center;\n    gap: 8px;\n\n    .status-row {\n      display: flex;\n      flex-direction: row;\n      justify-content: center;\n      align-items: center;\n\n      .status-icon {\n        width: 24px;\n        height: 24px;\n        margin-right: 6px;\n      }\n\n      .status-text {\n        font-family: PingFang SC;\n        font-size: 14px;\n        font-weight: 500;\n        line-height: 22px;\n        letter-spacing: 0.003em;\n      }\n    }\n\n    .desc {\n      font-family: PingFang SC;\n      font-size: 10px;\n      font-weight: 400;\n      line-height: 18px;\n      letter-spacing: 0.003em;\n      text-align: center;\n      color: var(--text-color-text-4, rgba(199, 204, 214, 1));\n    }\n\n    .micNotify {\n      display: flex;\n      flex-direction: row;\n      justify-content: center;\n      align-items: center;\n    }\n\n    .micReopen {\n      position: relative;\n      width: 107px;\n      height: 40px;\n      padding: 5px 16px 5px 16px;\n      margin-left: 12px;\n      margin-right: 12px;\n      background-clip: padding-box; /* 确保背景不覆盖边框 */\n      border-radius: 12px;\n\n      &:hover,\n      &:active,\n      &:focus {\n        opacity: 1;\n        color: rgba(0, 0, 0, 0.85);\n        border-color: #d9d9d9;\n      }\n    }\n  }\n\n  .interrupt {\n    display: flex;\n    flex-direction: row;\n    justify-content: center;\n    align-items: center;\n    margin-top: 12px;\n    width: max-content;\n    line-height: 28px;\n    padding: 1px 6px 1px 6px;\n    border-radius: 4px;\n    margin-left: 4px;\n    font-family: PingFang SC;\n    font-size: 12px;\n    font-weight: 400;\n    letter-spacing: 0.003em;\n    text-align: left;\n    box-shadow: 0px 0px 0px 1px rgba(221, 226, 233, 1);\n    color: var(--text-color-text-3, rgba(115, 122, 135, 1));\n\n    &:hover,\n    &:active,\n    &:focus {\n      opacity: 1;\n      border-color: #d9d9d9;\n    }\n\n    img {\n      margin-right: 8px;\n    }\n  }\n}"
  },
  {
    "path": "src/pages/MainPage/MainArea/index.tsx",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport { useSelector } from 'react-redux';\nimport Antechamber from './Antechamber';\nimport Room from './Room';\n\nfunction MainArea() {\n  const room = useSelector((state: any) => state.room);\n  const isJoined = room.isJoined;\n  return isJoined ? <Room /> : <Antechamber />;\n}\n\nexport default MainArea;\n"
  },
  {
    "path": "src/pages/MainPage/Menu/components/DeviceDrawerButton/index.module.less",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\n.wrapper {\n  width: 100%;\n  display: flex;\n  flex-direction: row;\n  gap: 24px;\n  padding: 8px 16px;\n  \n  \n  .label {\n    display: flex;\n    flex-direction: column;\n    align-items: flex-start;\n    line-height: 16px;\n    gap: 12px;\n\n   .label-text {\n      font-family: PingFang SC;\n      font-size: 14px;\n      font-weight: 500;\n      line-height: 22px;\n      letter-spacing: 0.003em;\n      text-align: left;\n   }\n  }\n\n  .value {\n    display: flex;\n    flex-direction: column;\n    justify-content: center;\n    align-items: flex-start;\n    gap: 18px;\n  }\n}"
  },
  {
    "path": "src/pages/MainPage/Menu/components/DeviceDrawerButton/index.tsx",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport { useMemo } from 'react';\nimport { useSelector, useDispatch } from 'react-redux';\nimport { MediaType } from '@volcengine/rtc';\nimport { Switch, Select } from '@arco-design/web-react';\nimport DrawerRowItem from '@/components/DrawerRowItem';\nimport { RootState } from '@/store';\nimport RtcClient from '@/lib/RtcClient';\nimport { useDeviceState, useScene } from '@/lib/useCommon';\nimport { updateSelectedDevice } from '@/store/slices/device';\nimport { isMobile } from '@/utils/utils';\nimport styles from './index.module.less';\n\ninterface IDeviceDrawerButtonProps {\n  type?: MediaType.AUDIO | MediaType.VIDEO;\n}\n\nconst DEVICE_NAME = {\n  [MediaType.AUDIO]: '麦克风',\n  [MediaType.VIDEO]: '摄像头',\n};\n\nfunction DeviceDrawerButton(props: IDeviceDrawerButtonProps) {\n  const { type = MediaType.AUDIO } = props;\n  const device = useDeviceState();\n  const isEnable = type === MediaType.AUDIO ? device.isAudioPublished : device.isVideoPublished;\n  const switcher = type === MediaType.AUDIO ? device.switchMic : device.switchCamera;\n  const devicePermissions = useSelector((state: RootState) => state.device.devicePermissions);\n  const devices = useSelector((state: RootState) => state.device);\n  const selectedDevice =\n    type === MediaType.AUDIO ? devices.selectedMicrophone : devices.selectedCamera;\n  const permission = devicePermissions?.[type === MediaType.AUDIO ? 'audio' : 'video'];\n  const { isScreenMode } = useScene();\n  const isScreenEnable = device.isScreenPublished;\n  const changeScreenPublished = device.switchScreenCapture;\n\n  const SETTING_NAME = {\n    [MediaType.AUDIO]: '麦克风',\n    [MediaType.VIDEO]: isScreenMode ? '屏幕共享' : '视频',\n  };\n\n  const dispatch = useDispatch();\n  const deviceList = useMemo(\n    () => (type === MediaType.AUDIO ? devices.audioInputs : devices.videoInputs),\n    [devices]\n  );\n\n  const handleDeviceChange = (value: string) => {\n    RtcClient.switchDevice(type, value);\n    if (type === MediaType.AUDIO) {\n      dispatch(\n        updateSelectedDevice({\n          selectedMicrophone: value,\n        })\n      );\n    }\n    if (type === MediaType.VIDEO) {\n      dispatch(\n        updateSelectedDevice({\n          selectedCamera: value,\n        })\n      );\n    }\n  };\n\n  return (\n    <DrawerRowItem\n      btnText={`${SETTING_NAME[type]}设置`}\n      drawer={{\n        width: isMobile() ? '100%' : undefined,\n        title: `${SETTING_NAME[type]}设置`,\n        footer: null,\n        children: (\n          <>\n            {!isScreenMode && (\n              <div className={styles.wrapper}>\n                <div className={styles.label}>{DEVICE_NAME[type]}</div>\n                <div className={styles.value}>\n                  <Switch\n                    checked={isEnable}\n                    size=\"small\"\n                    onChange={(enable) => switcher(enable)}\n                    disabled={!permission}\n                  />\n                  <Select\n                    style={{ width: 250 }}\n                    value={selectedDevice}\n                    onChange={handleDeviceChange}\n                  >\n                    {deviceList.map((device) => (\n                      <Select.Option key={device.deviceId} value={device.deviceId}>\n                        {device.label}\n                      </Select.Option>\n                    ))}\n                  </Select>\n                </div>\n              </div>\n            )}\n            {type === MediaType.VIDEO && isScreenMode && (\n              <div className={styles.wrapper}>\n                <div className={styles.label}>屏幕共享</div>\n                <div className={styles.value}>\n                  <Switch checked={isScreenEnable} size=\"small\" onChange={changeScreenPublished} />\n                </div>\n              </div>\n            )}\n          </>\n        ),\n      }}\n    />\n  );\n}\n\nexport default DeviceDrawerButton;\n"
  },
  {
    "path": "src/pages/MainPage/Menu/components/Operation/index.module.less",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\n.device {\n  display: flex;\n  flex-direction: column;\n  gap: 16px;\n}\n\n.box {\n  position: relative;\n  width: 100%;\n  border-radius: 16px;\n  background-color: white;\n  border: 1px solid var(--line-color-border-2, rgba(234, 237, 241, 1));\n  padding: 16px 24px 16px 24px;\n  box-sizing: border-box;\n  margin-bottom: 16px;\n}"
  },
  {
    "path": "src/pages/MainPage/Menu/components/Operation/index.tsx",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport { MediaType } from '@volcengine/rtc';\nimport DeviceDrawerButton from '../DeviceDrawerButton';\nimport Subtitle from '../Subtitle';\nimport { useScene } from '@/lib/useCommon';\nimport styles from './index.module.less';\n\nfunction Operation() {\n  const { isVision } = useScene();\n  return (\n    <div className={`${styles.box} ${styles.device}`}>\n      <Subtitle />\n      {isVision && <DeviceDrawerButton type={MediaType.VIDEO} />}\n      <DeviceDrawerButton />\n    </div>\n  );\n}\n\nexport default Operation;\n"
  },
  {
    "path": "src/pages/MainPage/Menu/components/Subtitle/index.module.less",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\n.subtitle {\n  position: relative;\n  display: flex;\n  flex-direction: row;\n  align-items: center;\n  justify-content: space-between;\n\n  .label {\n    font-size: 13px;\n    font-weight: 400;\n    line-height: 22px;\n    color: var(--text-color-text-1, rgba(12, 13, 14, 1));\n\n    .icon {\n      margin-left: 4px;\n    }\n  }\n}\n"
  },
  {
    "path": "src/pages/MainPage/Menu/components/Subtitle/index.tsx",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport { useState } from 'react';\nimport { Switch } from '@arco-design/web-react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { RootState } from '@/store';\nimport { updateShowSubtitle } from '@/store/slices/room';\nimport styles from './index.module.less';\n\nfunction Subtitle() {\n  const dispatch = useDispatch();\n  const room = useSelector((state: RootState) => state.room);\n  const { isShowSubtitle } = room;\n  const [checked, setChecked] = useState(isShowSubtitle);\n  const [loading, setLoading] = useState(false);\n  const handleChange = () => {\n    setLoading(true);\n    setChecked(!checked);\n    dispatch(updateShowSubtitle({ isShowSubtitle: !checked }));\n    setLoading(false);\n  };\n  return (\n    <div className={styles.subtitle}>\n      <div className={styles.label}>字幕</div>\n      <div className={styles.value}>\n        <Switch size=\"small\" loading={loading} checked={checked} onChange={handleChange} />\n      </div>\n    </div>\n  );\n}\n\nexport default Subtitle;\n"
  },
  {
    "path": "src/pages/MainPage/Menu/index.module.less",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\n .wrapper {\n  width: 210px;\n  height: 100%;\n  border-radius: 16px;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n\n  .info {\n    display: flex;\n    flex-direction: column;\n    gap: 12px;\n\n    .title {\n      font-weight: 500;\n      font-size: 14px;\n      line-height: 22px;\n      color: #0c0d0e;\n    }\n\n    .desc {\n      display: flex;\n      flex-direction: row;\n      align-items: center;\n      gap: 6px;\n      font-size: 12px;\n      line-height: 20px;\n      color: #737a87;\n\n      :global {\n        div.arco-typography, p.arco-typography {\n          margin-bottom: 0px;\n        }\n      }\n    }\n    .bold {\n      font-size: 13px;\n      font-weight: 500;\n      line-height: 22px;\n      color: var(--text-color-text-1, rgba(12, 13, 14, 1));\n    }\n\n    .gray {\n      display: flex;\n      flex-direction: row;\n      align-items: center;\n      font-size: 13px;\n      font-weight: 400;\n      line-height: 22px;\n      color: var(--text-color-text-3, rgba(115, 122, 135, 1));\n\n      .value {\n        width: 65%;\n        font-size: 12px;\n        font-weight: 500;\n        margin-left: 5px;\n      }\n\n      :global {\n        .arco-typography {\n          margin-bottom: 0px;\n          display: inline-block;\n          color: #737a87;\n        }\n      }\n    }\n\n    .buttonArea {\n      width: 100%;\n      display: flex;\n      flex-direction: row;\n      align-items: center;\n      justify-content: space-between;\n      margin-top: 8px;\n\n      .getMore {\n        width: 100%;\n        color: #fff;\n        height: 36px;\n        text-shadow: none;\n        box-shadow: none;\n        border: none;\n        text-align: center;\n        background: linear-gradient(56.59deg, #3c73ff 15.53%, #6e41ee 62.28%, #d641ee 90.32%),\n          radial-gradient(\n            203.56% 121.74% at 27.12% -21.74%,\n            rgba(82, 182, 255, 0.2) 0%,\n            rgba(143, 65, 238, 0) 100%\n          ),\n          radial-gradient(\n            134.75% 51.95% at 26.69% 5.8%,\n            rgba(157, 214, 255, 0.1) 0%,\n            rgba(143, 65, 238, 0) 100%\n          ),\n          radial-gradient(\n            82.39% 83.92% at 147.46% 76.45%,\n            rgba(82, 99, 255, 0.8) 0%,\n            rgba(143, 65, 238, 0) 100%\n          );\n        border-radius: 6px;\n        display: flex;\n        flex-direction: row;\n        justify-content: center;\n        align-items: center;\n\n        color: var(--Primary-Neutral-0, #fff);\n        text-align: center;\n\n        /* Body/body-2 medium */\n        font-family: 'PingFang SC';\n        font-size: 13px;\n        font-style: normal;\n        font-weight: 500;\n        cursor: pointer;\n      }\n\n      .getMore:hover {\n        opacity: 0.9;\n      }\n\n      .getMore:active {\n        opacity: 1;\n      }\n\n      .getMore[disabled],\n      .getMore[disabled]:hover {\n        color: #fff;\n        background: linear-gradient(95.87deg, #1664ff 0%, #8040ff 97.7%);\n        opacity: 0.8;\n      }\n    }\n  }\n\n  .questions {\n    display: flex;\n    flex-direction: column;\n    gap: 8px;\n\n    .title {\n      font-size: 13px;\n      font-weight: 500;\n      line-height: 22px;\n    }\n\n    .line {\n      font-size: 12px;\n      font-weight: 400;\n      line-height: 20px;\n      color: rgba(66, 70, 78, 1);\n      cursor: pointer;\n    }\n  }\n\n  .device {\n    display: flex;\n    flex-direction: column;\n    gap: 16px;\n  }\n\n  .box {\n    position: relative;\n    width: 100%;\n    border-radius: 16px;\n    background-color: white;\n    border: 1px solid var(--line-color-border-2, rgba(234, 237, 241, 1));\n    padding: 16px 24px 16px 24px;\n    box-sizing: border-box;\n    margin-bottom: 16px;\n  }\n\n  .resetTime {\n    position: relative;\n    width: 100%;\n    border-radius: 16px;\n    padding: 0px 24px 8px 24px;\n    box-sizing: border-box;\n    display: flex;\n    flex-direction: row;\n    justify-content: center;\n    align-items: center;\n\n    user-select: none;\n    -webkit-user-select: none;\n    -moz-user-select: none;\n    -ms-user-select: none;\n\n    .normalLine {\n      color: #42464e;\n      /* Body/body-1 regular */\n      font-family: 'PingFang SC';\n      font-size: 12px;\n      font-style: normal;\n      font-weight: 400;\n      line-height: 20px; /* 166.667% */\n      letter-spacing: 0.036px;\n      opacity: 0.8;\n    }\n  }\n\n  .tagWrapper {\n    margin-top: 12px;\n    display: flex;\n    flex-wrap: wrap;\n    gap: 4px;\n  }\n}\n\n.mobile-camera-wrapper {\n  position: relative;\n  width: 100%;\n  height: 100%;\n  border-radius: 16px;\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  margin-bottom: 16px;\n\n  .mobile-camera {\n    position: relative !important;\n    width: 100% !important;\n    height: 100% !important;\n    top: auto !important;\n    right: auto !important;\n  }\n}\n"
  },
  {
    "path": "src/pages/MainPage/Menu/index.tsx",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport VERTC from '@volcengine/rtc';\nimport { Tooltip, Typography } from '@arco-design/web-react';\nimport { useSelector } from 'react-redux';\nimport { RootState } from '@/store';\nimport Operation from './components/Operation';\nimport CameraArea from '../MainArea/Room/CameraArea';\nimport { isMobile } from '@/utils/utils';\nimport { useScene } from '@/lib/useCommon';\nimport packageJson from '../../../../package.json';\nimport styles from './index.module.less';\n\nfunction Menu() {\n  const room = useSelector((state: RootState) => state.room);\n  const isJoined = room?.isJoined;\n  const { isVision, name } = useScene();\n  const requestId = sessionStorage.getItem('RequestID');\n\n  return (\n    <div className={styles.wrapper}>\n      {isJoined && isMobile() && isVision ? (\n        <div className={styles['mobile-camera-wrapper']}>\n          <CameraArea className={styles['mobile-camera']} />\n        </div>\n      ) : null}\n      <div className={`${styles.box} ${styles.info}`}>\n        <div className={styles.title}>AI 人设：{name}</div>\n      </div>\n      {isJoined ? <Operation /> : ''}\n      <div className={`${styles.box} ${styles.info}`}>\n        <div className={styles.title}>{isJoined ? '其他信息' : '版本信息'}</div>\n        <div className={styles.desc}>Demo Version {packageJson.version}</div>\n        <div className={styles.desc}>SDK Version {VERTC.getSdkVersion()}</div>\n        {isJoined ? (\n          <div className={styles.desc}>\n            房间ID{' '}\n            <Tooltip content={room.roomId || '-'}>\n              <Typography.Paragraph\n                ellipsis={{\n                  rows: 1,\n                  expandable: false,\n                }}\n                className={styles.value}\n              >\n                {room.roomId || '-'}\n              </Typography.Paragraph>\n            </Tooltip>\n          </div>\n        ) : (\n          ''\n        )}\n        {room.isAIGCEnable ? (\n          <div className={styles.desc}>\n            RequestID{' '}\n            <Tooltip content={requestId || '-'}>\n              <Typography.Paragraph\n                ellipsis={{\n                  rows: 1,\n                  expandable: false,\n                }}\n                className={styles.value}\n              >\n                {requestId || '-'}\n              </Typography.Paragraph>\n            </Tooltip>\n          </div>\n        ) : (\n          ''\n        )}\n      </div>\n    </div>\n  );\n}\n\nexport default Menu;\n"
  },
  {
    "path": "src/pages/MainPage/index.module.less",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\n.main {\n  position: relative;\n  width: 100%;\n  height: calc(100% - 48px);\n  display: flex;\n  flex-direction: row;\n  align-items: center;\n  box-sizing: border-box;\n\n  .mainArea {\n    position: relative;\n    width: calc(100% - 220px);\n    height: 100%;\n    margin-right: 2%;\n    background-color: white;\n    border-radius: 16px;\n    overflow: hidden;\n    border: 1px solid var(--line-color-border-2, #eaedf1);\n  }\n\n  .isMobile {\n    width: 100% !important;\n    margin-right: 0% !important;\n    border-radius: 0px !important;\n  }\n\n  .operationArea {\n    position: relative;\n    width: 200px;\n    height: 100%;\n  }\n}\n"
  },
  {
    "path": "src/pages/MainPage/index.tsx",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport { useEffect } from 'react';\nimport { useDispatch } from 'react-redux';\nimport Header from '@/components/Header';\nimport ResizeWrapper from '@/components/ResizeWrapper';\nimport Menu from './Menu';\nimport { useIsMobile } from '@/utils/utils';\nimport Apis from '@/app/index';\nimport MainArea from './MainArea';\nimport { ABORT_VISIBILITY_CHANGE, useLeave } from '@/lib/useCommon';\nimport { RTCConfig, SceneConfig, updateRTCConfig, updateScene, updateSceneConfig } from '@/store/slices/room';\nimport styles from './index.module.less';\n\nexport default function () {\n\n  const leaveRoom = useLeave();\n  const dispatch = useDispatch();\n\n  const getScenes = async () => {\n    const { scenes }: {\n      scenes: {\n        rtc: RTCConfig;\n        scene: SceneConfig;\n      }[];\n    } = await Apis.Basic.getScenes();\n    dispatch(updateScene(scenes[0].scene.id));\n    dispatch(updateSceneConfig(\n      scenes.reduce<Record<string, SceneConfig>>((prev, cur) => {\n        prev[cur.scene.id] = cur.scene;\n        return prev;\n      }, {})\n    ));\n    dispatch(updateRTCConfig(\n      scenes.reduce<Record<string, RTCConfig>>((prev, cur) => {\n        prev[cur.scene.id] = cur.rtc;\n        return prev;\n      }, {})\n    ));\n  }\n\n  useEffect(() => {\n    getScenes();\n    const isOriginalDemo = window.location.host.startsWith('localhost');\n    const handler = () => {\n      if (\n        document.visibilityState === 'hidden' &&\n        !sessionStorage.getItem(ABORT_VISIBILITY_CHANGE)\n      ) {\n        leaveRoom();\n      }\n    };\n    !isOriginalDemo && document.addEventListener('visibilitychange', handler);\n    return () => {\n      !isOriginalDemo && document.removeEventListener('visibilitychange', handler);\n    };\n  }, []);\n\n  return (\n    <ResizeWrapper className={styles.container}>\n      <Header />\n      <div\n        className={styles.main}\n        style={{\n          padding: useIsMobile() ? '' : '24px',\n        }}\n      >\n        <div className={`${styles.mainArea} ${useIsMobile() ? styles.isMobile : ''}`}>\n          <MainArea />\n        </div>\n        {useIsMobile() ? null : (\n          <div className={styles.operationArea}>\n            <Menu />\n          </div>\n        )}\n      </div>\n    </ResizeWrapper>\n  );\n}\n"
  },
  {
    "path": "src/pages/Mobile/MobileToolBar/index.module.less",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\n.wrapper {\n  position: absolute;\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  padding: 16px;\n  z-index: 2;\n  top: 0;\n  left: 0;\n  right: 0;\n  color: #42464e;\n  font-size: 16px;\n  font-weight: 500;\n\n  > div {\n    display: flex;\n    align-items: center;\n    gap: 8px;\n  }\n\n  .setting {\n    background-color: #fff;\n    width: 36px;\n    height: 36px;\n    line-height: 28px;\n    border-radius: 50%;\n    text-align: center;\n    border: 0.5px solid #ffffff80;\n    cursor: pointer;\n    font-weight: 500;\n  }\n\n  .aiSetting {\n    background-color: #fff;\n    height: 36px;\n    border-radius: 18px;\n    line-height: 36px;\n    padding: 0 8px;\n    cursor: pointer;\n  }\n\n  .screen,\n  .subtitle {\n    cursor: pointer;\n    background-color: #fff;\n    height: 36px;\n    border-radius: 18px;\n    line-height: 36px;\n    padding: 0 8px;\n  }\n\n  .showSubTitle {\n    background: #9474ff;\n    color: #fff;\n  }\n}\n"
  },
  {
    "path": "src/pages/Mobile/MobileToolBar/index.tsx",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport { useDispatch, useSelector } from 'react-redux';\nimport { memo, useEffect, useState } from 'react';\nimport { VideoRenderMode } from '@volcengine/rtc';\nimport { useDeviceState, useScene } from '@/lib/useCommon';\nimport { RootState } from '@/store';\nimport RtcClient from '@/lib/RtcClient';\n\nimport { updateShowSubtitle } from '@/store/slices/room';\nimport SettingsDrawer from '../SettingsDrawer';\nimport styles from './index.module.less';\n\nfunction MobileToolBar(props: React.HTMLAttributes<HTMLDivElement>) {\n  const dispatch = useDispatch();\n\n  const room = useSelector((state: RootState) => state.room);\n  const { isShowSubtitle } = room;\n  const [open, setOpen] = useState(false);\n  const [subTitleStatus, setSubTitleStatus] = useState(isShowSubtitle);\n\n  const { isScreenMode } = useScene();\n  const { isVideoPublished, isScreenPublished } = useDeviceState();\n\n  const switchSubtitle = () => {\n    setSubTitleStatus(!subTitleStatus);\n    dispatch(updateShowSubtitle({ isShowSubtitle: !subTitleStatus }));\n  };\n\n  const setVideoPlayer = () => {\n    if (isVideoPublished || isScreenPublished) {\n      RtcClient.setLocalVideoPlayer(\n        room.localUser.username!,\n        'mobile-local-player',\n        isScreenPublished,\n        isScreenMode ? VideoRenderMode.RENDER_MODE_FILL : VideoRenderMode.RENDER_MODE_HIDDEN\n      );\n    }\n  };\n\n  useEffect(() => {\n    setVideoPlayer();\n  }, [isVideoPublished, isScreenPublished, isScreenMode]);\n\n  return (\n    <div className={styles.wrapper}>\n      <div>\n        <div\n          className={`${styles.subtitle} ${subTitleStatus ? styles.showSubTitle : ''}`}\n          onClick={switchSubtitle}\n        >\n          字幕\n        </div>\n      </div>\n\n      <SettingsDrawer visible={open} onCancel={() => setOpen(false)} />\n    </div>\n  );\n}\nexport default memo(MobileToolBar);\n"
  },
  {
    "path": "src/pages/Mobile/SettingsDrawer/index.module.less",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\n.settingsPage {\n  background: linear-gradient(109.22deg, #7425ff0d 0.27%, #2758ff0d 51.39%, #0066ff0d 99.54%);\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n  box-sizing: border-box;\n  padding: 12px 0;\n}\n\n.settingsGroup {\n  background-color: #ffffff;\n  border-radius: 8px;\n  margin: 0 16px 12px 16px;\n  overflow: hidden;\n\n  &:last-of-type {\n    margin-bottom: 0;\n  }\n}\n\n.logoutButtonContainer {\n  margin: 20px 16px 0 16px;\n  width: calc(100% - 32px);\n}\n\n.logoutButton {\n  width: 100%;\n  padding: 15px;\n  background-color: #ffffff;\n  color: #ff706d;\n  border: none;\n  border-radius: 8px;\n  font-size: 15px;\n  font-weight: 500;\n  cursor: pointer;\n  text-align: center;\n\n  &:hover {\n    background-color: #f7f8fa;\n  }\n}\n\n.versionInfo {\n  display: flex;\n  flex-direction: column;\n  align-items: flex-end;\n  font-size: 14px;\n  line-height: 1.5;\n}\n\n.copyLinkText {\n  color: #165dff;\n}\n"
  },
  {
    "path": "src/pages/Mobile/SettingsDrawer/index.tsx",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport VERTC from '@volcengine/rtc';\nimport { Drawer, Message } from '@arco-design/web-react'; // Import Message if you plan to use it\nimport { useSelector } from 'react-redux';\nimport { RootState } from '@/store';\nimport { useLeave } from '@/lib/useCommon';\nimport { Disclaimer, ReversoContext, UserAgreement } from '@/config';\nimport { SettingsItem } from '../components/SettingsItem';\nimport packageJSON from '../../../../package.json';\nimport styles from './index.module.less';\n\ninterface SettingsDrawerProps {\n  visible: boolean;\n  onCancel: () => void;\n}\n\nfunction SettingsDrawer({ visible, onCancel }: SettingsDrawerProps) {\n  const room = useSelector((state: RootState) => state.room);\n  const { roomId } = room;\n  const leaveRoom = useLeave();\n\n  const handleLogout = () => {\n    leaveRoom();\n  };\n\n  const handleCopyLink = () => {\n    const pcLink = window.location.origin + window.location.pathname;\n    navigator.clipboard\n      .writeText(pcLink)\n      .then(() => {\n        Message.success('链接已复制');\n      })\n      .catch((err) => {\n        console.error('复制链接失败:', err);\n        Message.error('复制失败，请手动复制');\n      });\n  };\n\n  return (\n    <Drawer\n      title=\"设置\"\n      visible={visible}\n      onCancel={onCancel}\n      footer={null}\n      className={styles.settingsDrawer}\n      width=\"100%\"\n      bodyStyle={{ padding: 0 }}\n    >\n      <div className={styles.settingsPage}>\n        <div className={styles.settingsGroup}>\n          <SettingsItem label=\"房间ID\" value={roomId} showArrow={false} />\n          <SettingsItem label=\"隐私政策\" onClick={() => window.open(ReversoContext, '_blank')} />\n          <SettingsItem label=\"用户协议\" onClick={() => window.open(UserAgreement, '_blank')} />\n          <SettingsItem label=\"免责声明\" onClick={() => window.open(Disclaimer, '_blank')} />\n          <SettingsItem\n            label=\"当前版本\"\n            value={\n              <div className={styles.versionInfo}>\n                <span>Demo version {packageJSON.version}</span>\n                <span>SDK version {VERTC.getSdkVersion()}</span>\n              </div>\n            }\n            showArrow={false}\n          />\n        </div>\n\n        <div className={styles.settingsGroup}>\n          <SettingsItem\n            label=\"复制链接到 PC 体验\"\n            value=\"复制链接\"\n            onClick={handleCopyLink}\n            showArrow={false}\n            valueClassName={styles.copyLinkText}\n          />\n        </div>\n\n        <div className={styles.logoutButtonContainer}>\n          <button className={styles.logoutButton} onClick={handleLogout}>\n            退出房间\n          </button>\n        </div>\n      </div>\n    </Drawer>\n  );\n}\n\nexport default SettingsDrawer;\n"
  },
  {
    "path": "src/pages/Mobile/components/SettingsItem/index.module.less",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\n.settingsItem {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  padding: 16px;\n  background-color: #ffffff;\n  cursor: pointer;\n  min-height: 50px;\n  box-sizing: border-box;\n\n  &:not(:last-child) {\n    border-bottom: 0.5px solid #f0f0f0; // 更细的分割线\n  }\n\n  .label {\n    font-size: 16px;\n    color: #0c0d0e;\n    font-weight: 500;\n    min-width: 64px;\n  }\n\n  .valueContainer {\n    color: #737a87;\n    font-size: 14px;\n    display: flex;\n    align-items: center;\n    justify-content: flex-end;\n    gap: 6px;\n    text-align: right;\n  }\n}\n"
  },
  {
    "path": "src/pages/Mobile/components/SettingsItem/index.tsx",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport { IconRight } from '@arco-design/web-react/icon';\nimport styles from './index.module.less';\n\ninterface SettingsItemProps {\n  label: string;\n  value?: string | React.ReactNode;\n  onClick?: () => void;\n  showArrow?: boolean;\n  valueClassName?: string;\n}\n\nexport function SettingsItem({\n  label,\n  value,\n  onClick,\n  showArrow = true,\n  valueClassName,\n}: SettingsItemProps) {\n  return (\n    <div className={styles.settingsItem} onClick={onClick}>\n      <span className={styles.label}>{label}</span>\n      <div className={styles.valueContainer}>\n        {value && <span className={`${styles.value} ${valueClassName || ''}`}>{value}</span>}\n        {showArrow && <IconRight className={styles.arrowIcon} />}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/react-app-env.d.ts",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\n/// <reference types=\"react-scripts\" />\n\ndeclare module '*.less' {\n  const content: { [className: string]: string };\n  export default content;\n}\n"
  },
  {
    "path": "src/store/index.ts",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport { configureStore } from '@reduxjs/toolkit';\nimport roomSlice, { RoomState } from './slices/room';\nimport deviceSlice, { DeviceState } from './slices/device';\n\nexport interface RootState {\n  room: RoomState;\n  device: DeviceState;\n}\n\nconst store = configureStore({\n  reducer: {\n    room: roomSlice,\n    device: deviceSlice,\n  },\n  middleware: (getDefaultMiddleware) =>\n    getDefaultMiddleware({\n      serializableCheck: false,\n    }),\n});\n\nexport default store;\n"
  },
  {
    "path": "src/store/slices/device.ts",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport { createSlice, PayloadAction } from '@reduxjs/toolkit';\nimport { DeviceType } from '@/interface';\n\nexport const medias = [DeviceType.Microphone];\n\nexport const MediaName = {\n  [DeviceType.Microphone]: 'microphone',\n  [DeviceType.Camera]: 'camera',\n};\n\nexport interface DeviceState {\n  audioInputs: MediaDeviceInfo[];\n  videoInputs: MediaDeviceInfo[];\n  selectedCamera?: string;\n  selectedMicrophone?: string;\n  devicePermissions: {\n    audio: boolean;\n    video: boolean;\n  };\n}\nconst initialState: DeviceState = {\n  audioInputs: [],\n  videoInputs: [],\n  devicePermissions: {\n    audio: true,\n    video: true,\n  },\n};\n\nexport const DeviceSlice = createSlice({\n  name: 'deivce',\n  initialState,\n  reducers: {\n    updateMediaInputs: (state, { payload }) => {\n      if (payload.audioInputs) {\n        state.audioInputs = payload.audioInputs;\n      }\n      if (payload.videoInputs) {\n        state.videoInputs = payload.videoInputs;\n      }\n    },\n    updateSelectedDevice: (state, { payload }) => {\n      if (payload.selectedCamera) {\n        state.selectedCamera = payload.selectedCamera;\n      }\n      if (payload.selectedMicrophone) {\n        state.selectedMicrophone = payload.selectedMicrophone;\n      }\n    },\n\n    setMicrophoneList: (state, action: PayloadAction<MediaDeviceInfo[]>) => {\n      state.audioInputs = action.payload;\n    },\n\n    setDevicePermissions: (\n      state,\n      action: PayloadAction<{\n        audio: boolean;\n        video: boolean;\n      }>\n    ) => {\n      state.devicePermissions = action.payload;\n    },\n  },\n});\nexport const { updateMediaInputs, updateSelectedDevice, setMicrophoneList, setDevicePermissions } =\n  DeviceSlice.actions;\n\nexport default DeviceSlice.reducer;\n"
  },
  {
    "path": "src/store/slices/room.ts",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport { createSlice } from '@reduxjs/toolkit';\nimport {\n  AudioPropertiesInfo,\n  LocalAudioStats,\n  NetworkQuality,\n  RemoteAudioStats,\n} from '@volcengine/rtc';\nimport RtcClient from '@/lib/RtcClient';\n\nexport interface IUser {\n  username?: string;\n  userId?: string;\n  publishAudio?: boolean;\n  publishVideo?: boolean;\n  publishScreen?: boolean;\n  audioStats?: RemoteAudioStats;\n  audioPropertiesInfo?: AudioPropertiesInfo;\n}\n\nexport type LocalUser = Omit<IUser, 'audioStats'> & {\n  loginToken?: string;\n  audioStats?: LocalAudioStats;\n};\n\nexport interface Msg {\n  value: string;\n  time: string;\n  user: string;\n  paragraph?: boolean;\n  definite?: boolean;\n  isInterrupted?: boolean;\n}\n\nexport interface SceneConfig {\n  id: string;\n  icon?: string;\n  name?: string;\n  questions?: string[];\n  botName: string;\n  isVision: boolean;\n  isScreenMode: boolean;\n  isInterruptMode: boolean;\n  isAvatarScene: boolean;\n  avatarBgUrl: string;\n}\n\nexport interface RTCConfig {\n  AppId: string;\n  RoomId: string;\n  UserId: string;\n  Token: string;\n}\n\nexport interface RoomState {\n  time: number;\n  roomId?: string;\n  localUser: LocalUser;\n  remoteUsers: IUser[];\n  autoPlayFailUser: string[];\n  /**\n   * @brief 是否已加房\n   */\n  isJoined: boolean;\n  /**\n   * @brief 选择的场景\n   */\n  scene: string;\n  /**\n   * @brief 场景下的配置\n   */\n  sceneConfigMap: Record<string, SceneConfig>;\n  /**\n   * @brief RTC 相关的配置\n   */\n  rtcConfigMap: Record<string, RTCConfig>;\n\n  /**\n   * @brief AI 通话是否启用\n   */\n  isAIGCEnable: boolean;\n  /**\n   * @brief AI 是否正在说话\n   */\n  isAITalking: boolean;\n  /**\n   * @brief AI 思考中\n   */\n  isAIThinking: boolean;\n  /**\n   * @brief 用户是否正在说话\n   */\n  isUserTalking: boolean;\n  /**\n   * @brief 网络质量\n   */\n  networkQuality: NetworkQuality;\n\n  /**\n   * @brief 对话记录\n   */\n  msgHistory: Msg[];\n\n  /**\n   * @brief 当前的对话\n   */\n  currentConversation: {\n    [user: string]: {\n      /**\n       * @brief 实时对话内容\n       */\n      msg: string;\n      /**\n       * @brief 当前实时对话内容是否能被定义为 \"问题\"\n       */\n      definite: boolean;\n    };\n  };\n\n  /**\n   * @brief 是否显示字幕\n   */\n  isShowSubtitle: boolean;\n\n  /**\n   * @brief 是否全屏\n   */\n  isFullScreen: boolean;\n\n  /**\n   * @brief 自定义人设名称\n   */\n  customSceneName: string;\n}\n\nconst initialState: RoomState = {\n  time: -1,\n  scene: '',\n  sceneConfigMap: {},\n  rtcConfigMap: {},\n  remoteUsers: [],\n  localUser: {\n    publishAudio: false,\n    publishVideo: false,\n    publishScreen: false,\n  },\n  autoPlayFailUser: [],\n  isJoined: false,\n  isAIGCEnable: false,\n  isAIThinking: false,\n  isAITalking: false,\n  isUserTalking: false,\n  networkQuality: NetworkQuality.UNKNOWN,\n\n  msgHistory: [],\n  currentConversation: {},\n  isShowSubtitle: true,\n  isFullScreen: false,\n  customSceneName: '',\n};\n\nexport const roomSlice = createSlice({\n  name: 'room',\n  initialState,\n  reducers: {\n    localJoinRoom: (\n      state,\n      {\n        payload,\n      }: {\n        payload: {\n          roomId: string;\n          user: LocalUser;\n        };\n      }\n    ) => {\n      state.roomId = payload.roomId;\n      state.localUser = {\n        ...state.localUser,\n        ...payload.user,\n      };\n      state.isJoined = true;\n    },\n    localLeaveRoom: (state) => {\n      state.roomId = undefined;\n      state.time = -1;\n      state.localUser = {\n        publishAudio: false,\n        publishVideo: false,\n        publishScreen: false,\n      };\n      state.remoteUsers = [];\n      state.isJoined = false;\n    },\n    remoteUserJoin: (state, { payload }) => {\n      state.remoteUsers.push(payload);\n    },\n    remoteUserLeave: (state, { payload }) => {\n      const findIndex = state.remoteUsers.findIndex((user) => user.userId === payload.userId);\n      state.remoteUsers.splice(findIndex, 1);\n    },\n\n    updateScene: (state, { payload }) => {\n      state.scene = payload;\n    },\n\n    updateSceneConfig: (state, { payload }) => {\n      state.sceneConfigMap = payload;\n    },\n\n    updateRTCConfig: (state, { payload }) => {\n      state.rtcConfigMap = payload;\n      RtcClient.basicInfo = {\n        app_id: payload[state.scene].AppId,\n        room_id: payload[state.scene].RoomId,\n        user_id: payload[state.scene].UserId,\n        token: payload[state.scene].Token,\n      };\n    },\n\n    updateLocalUser: (state, { payload }: { payload: Partial<LocalUser> }) => {\n      state.localUser = {\n        ...state.localUser,\n        ...(payload || {}),\n      };\n    },\n\n    updateNetworkQuality: (state, { payload }) => {\n      state.networkQuality = payload.networkQuality;\n    },\n\n    updateRemoteUser: (state, { payload }: { payload: IUser | IUser[] }) => {\n      if (!Array.isArray(payload)) {\n        payload = [payload];\n      }\n\n      payload.forEach((user) => {\n        const findIndex = state.remoteUsers.findIndex((u) => u.userId === user.userId);\n        state.remoteUsers[findIndex] = {\n          ...state.remoteUsers[findIndex],\n          ...user,\n        };\n      });\n    },\n\n    updateRoomTime: (state, { payload }) => {\n      state.time = payload.time;\n    },\n\n    addAutoPlayFail: (state, { payload }) => {\n      const autoPlayFailUser = state.autoPlayFailUser;\n      const index = autoPlayFailUser.findIndex((item) => item === payload.userId);\n      if (index === -1) {\n        state.autoPlayFailUser.push(payload.userId);\n      }\n    },\n    removeAutoPlayFail: (state, { payload }) => {\n      const autoPlayFailUser = state.autoPlayFailUser;\n      const _autoPlayFailUser = autoPlayFailUser.filter((item) => item !== payload.userId);\n      state.autoPlayFailUser = _autoPlayFailUser;\n    },\n    clearAutoPlayFail: (state) => {\n      state.autoPlayFailUser = [];\n    },\n    updateAIGCState: (state, { payload }) => {\n      state.isAIGCEnable = payload.isAIGCEnable;\n    },\n    updateAITalkState: (state, { payload }) => {\n      state.isAIThinking = false;\n      state.isUserTalking = false;\n      state.isAITalking = payload.isAITalking;\n    },\n    updateAIThinkState: (state, { payload }) => {\n      state.isAIThinking = payload.isAIThinking;\n      state.isUserTalking = false;\n    },\n    clearHistoryMsg: (state) => {\n      state.msgHistory = [];\n    },\n    setHistoryMsg: (state, { payload }) => {\n      const { paragraph, definite } = payload;\n      const lastMsg = state.msgHistory.at(-1)! || {};\n      /** 是否需要再创建新句子 */\n      const fromBot =\n        payload.user === state.sceneConfigMap[state.scene].botName ||\n        payload.user.includes('voiceChat_');\n      /**\n       * Bot 的语句：\n       * 1. 在 SubtitleMode=0 时（未启用数字人时默认值），以 definite 判断是否需要追加新内容\n       * 2. 在 SubtitleMode=1 时（启用数字人时强制设定为 1），以 paragraph 判断是否需要追加新内容\n       * User 的语句以 paragraph 判断是否需要追加新内容\n       */\n      const currentSubtitleMode = state.sceneConfigMap[state.scene].isAvatarScene ? 1 : 0;\n      const lastMsgCompleted =\n        !fromBot || currentSubtitleMode ? lastMsg.paragraph : lastMsg.definite;\n\n      if (state.msgHistory.length) {\n        /** 如果上一句话是完整的则新增语句 */\n        if (lastMsgCompleted) {\n          state.msgHistory.push({\n            value: payload.text,\n            time: new Date().toString(),\n            user: payload.user,\n            definite,\n            paragraph,\n          });\n        } else {\n          /** 话未说完, 更新文字内容 */\n          if (fromBot && currentSubtitleMode) {\n            lastMsg.value += payload.text;\n          } else {\n            lastMsg.value = payload.text;\n          }\n          lastMsg.time = new Date().toString();\n          lastMsg.paragraph = paragraph;\n          lastMsg.definite = definite;\n          lastMsg.user = payload.user;\n        }\n      } else {\n        /** 首句话首字不会被打断 */\n        state.msgHistory.push({\n          value: payload.text,\n          time: new Date().toString(),\n          user: payload.user,\n          paragraph,\n        });\n      }\n    },\n    setInterruptMsg: (state) => {\n      state.isAITalking = false;\n      if (!state.msgHistory.length) {\n        return;\n      }\n      /** 找到最后一个末尾的字幕, 将其状态置换为打断 */\n      for (let id = state.msgHistory.length - 1; id >= 0; id--) {\n        const msg = state.msgHistory[id];\n        if (msg.value) {\n          if (!msg.definite) {\n            state.msgHistory[id].isInterrupted = true;\n          }\n          break;\n        }\n      }\n    },\n    clearCurrentMsg: (state) => {\n      state.currentConversation = {};\n      state.msgHistory = [];\n      state.isAITalking = false;\n      state.isUserTalking = false;\n    },\n    updateShowSubtitle: (state, { payload }) => {\n      state.isShowSubtitle = payload.isShowSubtitle;\n    },\n    updateFullScreen: (state, { payload }) => {\n      state.isFullScreen = payload.isFullScreen;\n    },\n    updatecustomSceneName: (state, { payload }) => {\n      state.customSceneName = payload.customSceneName;\n    },\n  },\n});\n\nexport const {\n  localJoinRoom,\n  localLeaveRoom,\n  remoteUserJoin,\n  remoteUserLeave,\n  updateRemoteUser,\n  updateLocalUser,\n  updateRoomTime,\n  addAutoPlayFail,\n  removeAutoPlayFail,\n  clearAutoPlayFail,\n  updateAIGCState,\n  updateAITalkState,\n  updateAIThinkState,\n  setHistoryMsg,\n  clearHistoryMsg,\n  clearCurrentMsg,\n  setInterruptMsg,\n  updateNetworkQuality,\n  updateScene,\n  updateSceneConfig,\n  updateRTCConfig,\n  updateShowSubtitle,\n  updateFullScreen,\n  updatecustomSceneName,\n} = roomSlice.actions;\n\nexport default roomSlice.reducer;\n"
  },
  {
    "path": "src/theme.less",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\n @primary-color: #1664ff;\n"
  },
  {
    "path": "src/utils/handler.ts",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport { useDispatch } from 'react-redux';\nimport logger from './logger';\nimport {\n  setHistoryMsg,\n  setInterruptMsg,\n  updateAITalkState,\n  updateAIThinkState,\n} from '@/store/slices/room';\nimport RtcClient from '@/lib/RtcClient';\nimport { string2tlv, tlv2String } from '@/utils/utils';\n\nexport type AnyRecord = Record<string, any>;\n\nexport enum MESSAGE_TYPE {\n  BRIEF = 'conv',\n  SUBTITLE = 'subv',\n  FUNCTION_CALL = 'tool',\n}\n\nexport enum AGENT_BRIEF {\n  UNKNOWN,\n  LISTENING,\n  THINKING,\n  SPEAKING,\n  INTERRUPTED,\n  FINISHED,\n}\n\n/**\n * @brief 指令类型\n */\nexport enum COMMAND {\n  /**\n   * @brief 打断指令\n   */\n  INTERRUPT = 'interrupt',\n  /**\n   * @brief 发送外部文本驱动 TTS\n   */\n  EXTERNAL_TEXT_TO_SPEECH = 'ExternalTextToSpeech',\n  /**\n   * @brief 发送外部文本驱动 LLM\n   */\n  EXTERNAL_TEXT_TO_LLM = 'ExternalTextToLLM',\n}\n/**\n * @brief 打断的类型\n */\nexport enum INTERRUPT_PRIORITY {\n  /**\n   * @brief 占位\n   */\n  NONE,\n  /**\n   * @brief 高优先级。传入信息直接打断交互，进行处理。\n   */\n  HIGH,\n  /**\n   * @brief 中优先级。等待当前交互结束后，进行处理。\n   */\n  MEDIUM,\n  /**\n   * @brief 低优先级。如当前正在发生交互，直接丢弃 Message 传入的信息。\n   */\n  LOW,\n}\n\nexport const MessageTypeCode = {\n  [MESSAGE_TYPE.SUBTITLE]: 1,\n  [MESSAGE_TYPE.FUNCTION_CALL]: 2,\n  [MESSAGE_TYPE.BRIEF]: 3,\n};\n\nexport const useMessageHandler = () => {\n  const dispatch = useDispatch();\n\n  const maps = {\n    /**\n     * @brief 接收状态变化信息\n     * @note https://www.volcengine.com/docs/6348/1415216?s=g\n     */\n    [MESSAGE_TYPE.BRIEF]: (parsed: AnyRecord) => {\n      const { Stage } = parsed || {};\n      const { Code, Description } = Stage || {};\n      logger.debug('[MESSAGE_TYPE.BRIEF]: ', Code, Description);\n      switch (Code) {\n        case AGENT_BRIEF.THINKING:\n          dispatch(updateAIThinkState({ isAIThinking: true }));\n          break;\n        case AGENT_BRIEF.SPEAKING:\n          dispatch(updateAITalkState({ isAITalking: true }));\n          break;\n        case AGENT_BRIEF.FINISHED:\n          dispatch(updateAITalkState({ isAITalking: false }));\n          break;\n        case AGENT_BRIEF.INTERRUPTED:\n          dispatch(setInterruptMsg());\n          break;\n        default:\n          break;\n      }\n    },\n    /**\n     * @brief 字幕\n     * @note https://www.volcengine.com/docs/6348/1337284?s=g\n     */\n    [MESSAGE_TYPE.SUBTITLE]: (parsed: AnyRecord) => {\n      const data = parsed.data?.[0] || {};\n      /** debounce 记录用户输入文字 */\n      if (data) {\n        const { text: msg, definite, userId: user, paragraph } = data;\n        const isAudioEnable = RtcClient.getAgentEnabled();\n        if ((window as any)._debug_mode) {\n          logger.debug('handleRoomBinaryMessageReceived', data);\n        }\n        if (isAudioEnable) {\n          dispatch(setHistoryMsg({ text: msg, user, paragraph, definite }));\n        }\n      }\n    },\n    /**\n     * @brief Function calling\n     * @note https://www.volcengine.com/docs/6348/1359441?s=g\n     */\n    [MESSAGE_TYPE.FUNCTION_CALL]: (parsed: AnyRecord) => {\n      const name: string = parsed?.tool_calls?.[0]?.function?.name;\n      console.log('[Function Call] - Called by sendUserBinaryMessage');\n      const map: Record<string, string> = {\n        getcurrentweather: '今天下雪， 最低气温零下10度',\n      };\n\n      RtcClient.engine.sendUserBinaryMessage(\n        'RobotMan_',\n        string2tlv(\n          JSON.stringify({\n            ToolCallID: parsed?.tool_calls?.[0]?.id,\n            Content: map[name.toLocaleLowerCase().replaceAll('_', '')],\n          }),\n          'func'\n        )\n      );\n    },\n  };\n\n  return {\n    parser: (buffer: ArrayBuffer) => {\n      try {\n        const { type, value } = tlv2String(buffer);\n        maps[type as MESSAGE_TYPE]?.(JSON.parse(value));\n      } catch (e) {\n        logger.debug('parse error', e);\n      }\n    },\n  };\n};\n"
  },
  {
    "path": "src/utils/logger.ts",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nclass Logger {\n  public debug(...args: any[]) {\n    console.debug(...args);\n  }\n\n  public log(...args: any[]) {\n    console.log(...args);\n  }\n\n  public error(...args: any[]) {\n    console.error(...args);\n  }\n\n  public warn(...args: any[]) {\n    console.warn(...args);\n  }\n}\n\nexport default new Logger();\n"
  },
  {
    "path": "src/utils/utils.less",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\n@meetingBackgroundColor: #1e2128;\n\n.flex-box {\n  display: flex;\n  flex-direction: row;\n  justify-content: space-between;\n  align-items: center;\n}\n\n.split-line(@h, @m) {\n  display: inline-block;\n  height: @h;\n  width: 0;\n  border-right: 1px solid #4e5969;\n  margin: 0 @m;\n}\n\n.place-holder {\n  font-family: \"PingFang SC\";\n  font-style: normal;\n  font-weight: normal;\n  font-size: 14px;\n  line-height: 22px;\n  color: #86909c;\n  position: relative;\n  left: 5px;\n}\n"
  },
  {
    "path": "src/utils/utils.ts",
    "content": "/**\n * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.\n * SPDX-license-identifier: BSD-3-Clause\n */\n\nimport { useEffect, useState } from 'react';\n\n/**\n * @brief 将字符串包装成 TLV\n */\nexport const string2tlv = (str: string, type: string) => {\n  const typeBuffer = new Uint8Array(4);\n\n  for (let i = 0; i < type.length; i++) {\n    typeBuffer[i] = type.charCodeAt(i);\n  }\n\n  const lengthBuffer = new Uint32Array(1);\n  const valueBuffer = new TextEncoder().encode(str);\n\n  lengthBuffer[0] = valueBuffer.length;\n\n  const tlvBuffer = new Uint8Array(typeBuffer.length + 4 + valueBuffer.length);\n\n  tlvBuffer.set(typeBuffer, 0);\n\n  tlvBuffer[4] = (lengthBuffer[0] >> 24) & 0xff;\n  tlvBuffer[5] = (lengthBuffer[0] >> 16) & 0xff;\n  tlvBuffer[6] = (lengthBuffer[0] >> 8) & 0xff;\n  tlvBuffer[7] = lengthBuffer[0] & 0xff;\n\n  tlvBuffer.set(valueBuffer, 8);\n  return tlvBuffer.buffer;\n};\n\n/**\n * @brief TLV 数据格式转换成字符串\n * @note TLV 数据格式\n * | magic number | length(big-endian) | value |\n * @param {ArrayBufferLike} tlvBuffer\n * @returns\n */\nexport const tlv2String = (tlvBuffer: ArrayBufferLike) => {\n  const typeBuffer = new Uint8Array(tlvBuffer, 0, 4);\n  const lengthBuffer = new Uint8Array(tlvBuffer, 4, 4);\n  const valueBuffer = new Uint8Array(tlvBuffer, 8);\n\n  let type = '';\n  for (let i = 0; i < typeBuffer.length; i++) {\n    type += String.fromCharCode(typeBuffer[i]);\n  }\n\n  const length =\n    (lengthBuffer[0] << 24) | (lengthBuffer[1] << 16) | (lengthBuffer[2] << 8) | lengthBuffer[3];\n\n  const value = new TextDecoder().decode(valueBuffer.subarray(0, length));\n\n  return { type, value };\n};\n\nexport const isMobile = () =>\n  /Mobi|Android|iPhone|iPad|Windows Phone/i.test(window.navigator.userAgent) ||\n  window?.innerWidth < 767;\n\nexport function useIsMobile() {\n  const getIsMobile = () =>\n    /Mobi|Android|iPhone|iPad|Windows Phone/i.test(window.navigator.userAgent) ||\n    window.innerWidth < 767;\n\n  const [isMobile, setIsMobile] = useState(getIsMobile());\n\n  useEffect(() => {\n    const handleResize = () => {\n      const value = getIsMobile();\n      setIsMobile(value);\n    };\n    window.addEventListener('resize', handleResize);\n    return () => window.removeEventListener('resize', handleResize);\n  }, []);\n\n  return isMobile;\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES6\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    }\n  },\n  \"include\": [\"src\"]\n}\n"
  }
]