[
  {
    "path": ".commitlintrc.json",
    "content": "{\n  \"extends\": [\"@commitlint/config-conventional\"]\n}\n"
  },
  {
    "path": ".dockerignore",
    "content": "node_modules\nDockerfile\n.*\n*/.*\n"
  },
  {
    "path": ".editorconfig",
    "content": "# Editor configuration, see http://editorconfig.org\n\nroot = true\n\n[*]\ncharset = utf-8\nindent_style = tab\nindent_size = 2\nend_of_line = lf\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n"
  },
  {
    "path": ".eslintrc.cjs",
    "content": "module.exports = {\n  root: true,\n  extends: ['@antfu'],\n}\n"
  },
  {
    "path": ".gitattributes",
    "content": "\"*.vue\"    eol=lf\n\"*.js\"     eol=lf\n\"*.ts\"     eol=lf\n\"*.jsx\"    eol=lf\n\"*.tsx\"    eol=lf\n\"*.cjs\"    eol=lf\n\"*.cts\"    eol=lf\n\"*.mjs\"    eol=lf\n\"*.mts\"    eol=lf\n\"*.json\"   eol=lf\n\"*.html\"   eol=lf\n\"*.css\"    eol=lf\n\"*.less\"   eol=lf\n\"*.scss\"   eol=lf\n\"*.sass\"   eol=lf\n\"*.styl\"   eol=lf\n\"*.md\"     eol=lf\n"
  },
  {
    "path": ".github/workflows/build_docker.yml",
    "content": "name: build_docker\n\non:\n  push:\n    branches: [main]\n  release:\n    types: [created] # 表示在创建新的 Release 时触发\n\njobs:\n  build_docker:\n    name: Build docker\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v3\n\n      - run: |\n          echo \"本次构建的版本为：${GITHUB_REF_NAME} (但是这个变量目前上下文中无法获取到)\"\n          echo 本次构建的版本为：${{ github.ref_name }}\n          env\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v2\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v2\n      - name: Login to DockerHub\n        uses: docker/login-action@v2\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n      - name: Build and push\n        id: docker_build\n        uses: docker/build-push-action@v4\n        with:\n          context: .\n          push: true\n          labels: ${{ steps.meta.outputs.labels }}\n          platforms: linux/amd64,linux/arm64\n          tags: |\n            ${{ secrets.DOCKERHUB_USERNAME }}/chatgpt-web:${{ github.ref_name }}\n            ${{ secrets.DOCKERHUB_USERNAME }}/chatgpt-web:latest\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  push:\n    branches:\n      - main\n\n  pull_request:\n    branches:\n      - main\n\njobs:\n  lint:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - name: Set node\n        uses: actions/setup-node@v3\n        with:\n          node-version: 18.x\n\n      - name: Setup\n        run: npm i -g @antfu/ni\n\n      - name: Install\n        run: nci\n\n      - name: Lint\n        run: nr lint:fix\n\n  typecheck:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - name: Set node\n        uses: actions/setup-node@v3\n        with:\n          node-version: 18.x\n\n      - name: Setup\n        run: npm i -g @antfu/ni\n\n      - name: Install\n        run: nci\n\n      - name: Typecheck\n        run: nr type-check\n"
  },
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nservice/log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\n#backend\nservice/venv\nservice/message_store.json\n\ndocker-compose/nginx/html\n\nnode_modules\n.DS_Store\ndist\ndist-ssr\ncoverage\n*.local\n\n/cypress/videos/\n/cypress/screenshots/\n\n# Editor directories and files\n.vscode/*\n!.vscode/settings.json\n!.vscode/extensions.json\n.idea\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"
  },
  {
    "path": ".husky/commit-msg",
    "content": "#!/usr/bin/env sh\n. \"$(dirname -- \"$0\")/_/husky.sh\"\n\nnpx --no -- commitlint --edit \n"
  },
  {
    "path": ".husky/pre-commit",
    "content": "#!/usr/bin/env sh\n. \"$(dirname -- \"$0\")/_/husky.sh\"\n\nnpx lint-staged\n"
  },
  {
    "path": ".npmrc",
    "content": "strict-peer-dependencies=false\n"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n  \"recommendations\": [\"Vue.volar\", \"dbaeumer.vscode-eslint\"]\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"prettier.enable\": false,\n  \"editor.formatOnSave\": false,\n  \"editor.codeActionsOnSave\": {\n    \"source.fixAll.eslint\": true\n  },\n  \"eslint.validate\": [\n    \"javascript\",\n    \"javascriptreact\",\n    \"typescript\",\n    \"typescriptreact\",\n    \"vue\",\n    \"html\",\n    \"json\",\n    \"jsonc\",\n    \"json5\",\n    \"yaml\",\n    \"yml\",\n    \"markdown\"\n  ],\n  \"cSpell.words\": [\n    \"antfu\",\n    \"axios\",\n    \"bumpp\",\n    \"chatgpt\",\n    \"chenzhaoyu\",\n    \"commitlint\",\n    \"davinci\",\n    \"dockerhub\",\n    \"esno\",\n    \"GPTAPI\",\n    \"highlightjs\",\n    \"hljs\",\n    \"iconify\",\n    \"katex\",\n    \"katexmath\",\n    \"linkify\",\n    \"logprobs\",\n    \"mdhljs\",\n\t\t\"mila\",\n    \"nodata\",\n    \"OPENAI\",\n    \"pinia\",\n    \"Popconfirm\",\n    \"rushstack\",\n    \"Sider\",\n    \"tailwindcss\",\n    \"traptitech\",\n    \"tsup\",\n    \"Typecheck\",\n    \"unplugin\",\n    \"VITE\",\n    \"vueuse\",\n    \"Zhao\"\n  ],\n  \"i18n-ally.enabledParsers\": [\n    \"ts\"\n  ],\n  \"i18n-ally.sortKeys\": true,\n  \"i18n-ally.keepFulfilled\": true,\n  \"i18n-ally.localesPaths\": [\n    \"src/locales\"\n  ],\n  \"i18n-ally.keystyle\": \"nested\"\n}\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# 贡献指南\n感谢你的宝贵时间。你的贡献将使这个项目变得更好！在提交贡献之前，请务必花点时间阅读下面的入门指南。\n\n## 语义化版本\n该项目遵循语义化版本。我们对重要的漏洞修复发布修订号，对新特性或不重要的变更发布次版本号，对重大且不兼容的变更发布主版本号。\n\n每个重大更改都将记录在 `changelog` 中。\n\n## 提交 Pull Request\n1. Fork [此仓库](https://github.com/Chanzhaoyu/chatgpt-web)，从 `main` 创建分支。新功能实现请发 pull request 到 `feature` 分支。其他更改发到 `main` 分支。\n2. 使用 `npm install pnpm -g` 安装 `pnpm` 工具。\n3. `vscode` 安装了 `Eslint` 插件，其它编辑器如 `webStorm` 打开了 `eslint` 功能。\n4. 根目录下执行 `pnpm bootstrap`。\n5. `/service/` 目录下执行 `pnpm install`。\n6. 对代码库进行更改。如果适用的话，请确保进行了相应的测试。\n7. 请在根目录下执行 `pnpm lint:fix` 进行代码格式检查。\n8. 请在根目录下执行 `pnpm type-check` 进行类型检查。\n9. 提交 git commit, 请同时遵守 [Commit 规范](#commit-指南)\n10. 提交 `pull request`， 如果有对应的 `issue`，请进行[关联](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword)。\n\n## Commit 指南\n\nCommit messages 请遵循[conventional-changelog 标准](https://www.conventionalcommits.org/en/v1.0.0/)：\n\n```bash\n<类型>[可选 范围]: <描述>\n\n[可选 正文]\n\n[可选 脚注]\n```\n\n### Commit 类型\n\n以下是 commit 类型列表:\n\n- feat: 新特性或功能\n- fix: 缺陷修复\n- docs: 文档更新\n- style: 代码风格或者组件样式更新\n- refactor: 代码重构，不引入新功能和缺陷修复\n- perf: 性能优化\n- test: 单元测试\n- chore: 其他不修改 src 或测试文件的提交\n\n\n## License\n\n[MIT](./license)\n"
  },
  {
    "path": "Dockerfile",
    "content": "# build front-end\nFROM node:lts-alpine AS builder\n\nCOPY ./ /app\nWORKDIR /app\n\nRUN apk add --no-cache git \\\n    && npm install pnpm -g \\\n    && pnpm install \\\n    && pnpm run build \\\n    && rm -rf /root/.npm /root/.pnpm-store /usr/local/share/.cache /tmp/*\n\n"
  },
  {
    "path": "README.md",
    "content": "# ChatGPT Web\n\n### 使用界面\n![cover3](./docs/c3.png)\n![cover](./docs/c1.png)\n![cover2](./docs/c2.png)\n\n### 性格微调功能\n![cover4](./docs/c4.png)\n\n### 文本审查功能(使用OpenAI官方接口)\n![cover5](./docs/c5.png)\n\n- [ChatGPT Web](#chatgpt-web)\n\t- [介绍](#介绍)\n\t- [快速部署](#快速部署)\n\t- [开发环境搭建](#开发环境搭建)\n\t\t- [Node](#Node)\n\t\t- [Python](#Python)\n\t- [开发环境启动项目](#开发环境启动项目)\n\t\t- [启动前端服务](#启动前端服务)\n\t\t- [启动后端服务](#启动后端服务)\n\t- [打包和部署](#打包和部署)\n\t\t- [前端打包](#前端资源打包(需要安装node))\n\t\t- [后端打包](#后端服务打包为docker容器(需要安装docker和docker-compose))\n\t- [使用DockerCompose启动](#使用DockerCompose启动)\n\t- [常见问题](#常见问题)\n\t- [参与贡献](#参与贡献)\n\t- [赞助原作者](#赞助)\n\t- [License](#license)\n\n## 介绍\n\n这是一个可以私有化部署的`ChatGpt`网页，使用`OpenAI`的官方API接入`gpt-3.5`或`gpt-4`模型来实现接近`ChatGPT Plus`的对话效果。\n源代码Fork和修改于[Chanzhaoyu/chatgpt-web](https://github.com/Chanzhaoyu/chatgpt-web/)\n\n与OpenAI官方提供`ChatGPT Plus`对比，`ChatGPT Web`有以下优势：\n\n1. **省钱(仅限`gpt-3.5`)**。\n  - 按照日常用量，你可以用1折左右的价格，体验与`ChatGPT Plus`几乎相同的对话服务。\n  - 语音识别可以在本地离线完成，完全免费。\n2. **0门槛使用**。你可以将自建的`ChatGPT Web`\n\t 站点分享给家人和朋友，他们不再需要解决网络问题，就可以轻松享受到`ChatGPT Plus`带来的生产力提升。\n3. **可以缓解网络封锁的影响**。`ChatGPT Web`只需要一个`OpenAI API Key`即可使用，如果你所在的地区无法访问`OpenAI`\n\t ，你可以将`ChatGPT Web`部署在海外服务器上，或在当地服务器上配置`socks_proxy`参数来转发请求给代理软件，即可正常使用。\n\n和[Chanzhaoyu的原版](https://github.com/Chanzhaoyu/chatgpt-web/)的主要区别：\n\n1. 专注于易用、易部署、不操心，我将尽我所能做到对小白用户友好，因此本项目也会舍弃一些专业功能，例如：\n\n\t - 不支持accessToken这类非官方使用方式。我认为你只是希望有个稳定能用的AI助手，并不想折腾这些\n\t - 不支持反向代理。第三方的反代地址安全性存疑、封号也超级快！\n   - 不做导入和管理Prompt模板的功能。使用者没有“这个链接干什么用的”、“什么是json文件”、“为什么要我自己审核json文件的安全性，怎么审核？”此类烦恼\n\n2. 可以识别语音消息：通过OpenAI官方`whisper-1`接口，或免费的`whisper.cpp`实现。懒得打字的时候很好用\n3. 可以调整`ChatGpt`的性格\n4. 可以调整记住的上下文数量\n\n其它区别：\n1. 增加日语界面\n2. 优化了移动端体验\n\n## 快速部署\n\n如果你不需要自己开发，只需要部署使用，可以直接跳到 [使用最新版本docker镜像启动](#使用最新版本docker镜像启动)\n\n## 开发环境搭建\n\n### Node\n\n`node` 需要 `^16 || ^18` 版本（`node >= 14`\n需要安装 [fetch polyfill](https://github.com/developit/unfetch#usage-as-a-polyfill)\n），使用 [nvm](https://github.com/nvm-sh/nvm) 可管理本地多个 `node` 版本\n\n```shell\nnode -v\n```\n\n如果你没有安装过 `pnpm`\n\n```shell\nnpm install pnpm -g\n```\n\n### Python\n\n`python` 需要 `3.10` 以上版本，进入文件夹 `/service` 运行以下命令\n\n```shell\npip install --no-cache-dir -r requirements.txt\n```\n\n### whisper.cpp编译\n这一步的目的是设置本地语音识别功能，这个模块来自[ggerganov/whisper](https://github.com/ggerganov/whisper.cpp)\n\n如果你的系统是windows，可以跳过这一步，因为whisper的二进制文件，我已经为你下载好，放在项目里了；\n\n如果你的系统是linux，则需要你自己编译whisper：\n\n```shell\ncd ./tools/local-whisper/linux/\nchmod +x init.sh\n./init.sh\n```\n\n`init.sh`脚本执行完成之后，你将看到`./tools/local-whisper/linux/whisper.cpp-master/`目录，目录中有个叫`main`的二进制文件，这就是你需要的`whisper`。\n\n## 开发环境启动项目\n\n### 启动前端服务\n\n根目录下运行以下命令\n\n```shell\n# 前端网页的默认端口号是1002，对接的后端服务的默认端口号是3002，可以在 .env 和 .vite.config.ts 文件中修改\npnpm bootstrap\npnpm dev\n```\n\n### 启动后端服务\n\n只有`--openai_api_key`是必填的启动参数，需要先去[OpenAI](https://platform.openai.com/)\n注册账号，然后在[这里](https://platform.openai.com/account/api-keys)获取`OPENAI_API_KEY`。\n\n```shell\n# 进入文件夹 `/service` 运行以下命令\npython main.py --openai_api_key=\"$OPENAI_API_KEY\"\n```\n\n除了`openai_api_key`这个必填的参数之外，还有以下可选参数可用：\n\n- `openai_timeout_ms` 访问OpenAI的超时时间(毫秒)，默认值为 '100000'\n- `api_model` 默认值为 gpt-3.5-turbo 也可以使用很贵的 gpt-4\n- `socks_proxy` 代理，默认值为空字符串，格式示例: `http://127.0.0.1:10808`\n- `use_local_whisper` 设置为`true`可以使用离线模型来完成语音识别，如果设置为`false`就会使用OpenAI的API进行语音识别，默认值为: `true`\n- `host` HOST，默认值为 0.0.0.0\n- `port` PORT，默认值为 3002\n\n也就是说你也可以这样启动\n```shell\npython main.py --openai_api_key=\"$OPENAI_API_KEY\" --openai_timeout_ms=\"$OPENAI_TIMEOUT_MS\" --api_model=\"$API_MODEL\" --socks_proxy=\"$SOCKS_PROXY\" --use_local_whisper=\"$USE_LOCAL_WHISPER\" --host=\"$HOST\" --port=\"$PORT\"\n```\n\n\n## 打包和部署\n\n### 前端资源打包(需要安装node)\n\n1. 根目录下运行以下命令\n\t ```shell\n\t pnpm run build\n\t ```\n2. 将打包好的文件夹`dist`文件夹复制到`/docker-compose/nginx`目录下，并改名为`html`\n\t```shell\n\tcp dist/ docker-compose/nginx/html -r\n\t```\n3. 配置访问权限\n\t```shell\n\t  # 进入文件夹 `/docker-compose/nginx`\n    cd docker-compose/nginx\n    # 运行add_user.sh脚本，根据提示创建用户名和密码\n    # (密码文件将被保存在 /docker-compose/nginx/auth/.htpasswd)\n    bash add_user.sh\n    # 如果你想删除一个用户，可以使用remove_user.sh脚本\n    bash remove_user.sh\n\t```\n\n### 后端服务打包为docker容器(需要安装docker和docker-compose)\n\n1. 进入文件夹 `/service` 运行以下命令\n\t ```shell\n\t docker build -t chatgpt-web-backend .\n\t ```\n\n### 使用DockerCompose启动\n\n- 进入文件夹 `/docker-compose` 修改 `docker-compose.yml` 文件\n\n\t```\n  version: '3'\n\n  services:\n    app:\n      image: chatgpt-web-backend # 这里填你打包的后端服务的镜像名字\n      ports:\n        - 3002:3002\n      environment:\n        OPENAI_API_KEY: your_openai_api_key\n        # 访问OpenAI的超时时间(毫秒)，可选，默认值为 '100000'\n        OPENAI_TIMEOUT_MS: '100000'\n        # 可选，默认值为 gpt-3.5-turbo\n        API_MODEL: gpt-3.5-turbo\n        # Socks代理，可选，格式为 http://127.0.0.1:10808\n        SOCKS_PROXY: ''\n        # 可选，将USE_LOCAL_WHISPER设置为`true`可以使用离线模型来完成语音识别，如果设置为`false`就会使用OpenAI的API进行语音识别，默认值为: `true`\n \t\t\t\t# 使用离线模型就不需要向OpenAI付费，但会额外消耗一些服务器的cpu和内存资源，这里使用的是tiny模型，工作时占用的内存大概是125MB左右\n \t\t\t\t# 具体性能消耗参考whisper.cpp的官方文档： https://github.com/ggerganov/whisper.cpp#memory-usage\n        USE_LOCAL_WHISPER: 'true'\n        # HOST，可选，默认值为 0.0.0.0\n        HOST: 0.0.0.0\n        # PORT，可选，默认值为 3002\n        PORT: 3002\n    nginx:\n      depends_on:\n        - app\n      image: nginx:alpine\n      ports:\n        - '80:80'\n      expose:\n        - '80'\n      volumes:\n        - ./nginx/html/:/etc/nginx/html/\n        - ./nginx/auth/:/etc/nginx/auth/\n        - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf\n      links:\n        - app\n\t```\n\n- 进入文件夹 `/docker-compose` 运行以下命令\n\n\t```shell\n\t# 前台运行\n\tdocker-compose up\n\t# 或后台运行\n\tdocker-compose up -d\n\t```\n\t**建议先在前台运行试用一下，看看有没有报错，如果启动和使用都没有问题，再改成后台运行。**\n\n  启动成功之后，访问`http://你的机器ip`即可看到网页内容。\n\n## 使用最新版本docker镜像启动\n\n- 如果你只是想自己使用docker部署，可以直接使用我已经打包好的镜像和前端资源\n\n- 首先将前端资源的压缩包解压\n\n\t前端资源的压缩包在`/docker-compose/nginx/html.zip`，使用`unzip html.zip`将其解压缩。你应该可以看到`/docker-compose/nginx/html`下面有`index.html`和其他前端资源。\n\n- 然后给你的网站添加用户名和密码\n\n\t```shell\n  # 进入文件夹 `/docker-compose/nginx`\n  cd docker-compose/nginx\n  # 运行add_user.sh脚本，根据提示创建用户名和密码\n  # (密码文件将被保存在 /docker-compose/nginx/auth/.htpasswd)\n  bash add_user.sh\n  # 如果你想删除一个用户，可以使用remove_user.sh脚本\n  bash remove_user.sh\n\t```\n\n\n- 最后修改`docker-compose/docker-compose.yml`文件。\n\n\t除了填写你自己的`OPENAI_API_KEY`之外，还要根据自己的系统环境修改`image`的标签，如果你部署用的是x86架构的机器，就填写`wenjing95/chatgpt-web-backend:x86_64`，如果你用的是arm架构的机器，就填写`wenjing95/chatgpt-web-backend:aarch64`。\n\n\t```\n\tversion: '3'\n\n\tservices:\n\t  app:\n      # 根据自己的系统选择x86_64还是aarch64\n      image: wenjing95/chatgpt-web-backend:x86_64\n      # image: wenjing95/chatgpt-web-backend:aarch64\n      ports:\n        - 3002:3002\n      environment:\n        # 记得填写你的OPENAI_API_KEY\n        OPENAI_API_KEY: your_openai_api_key\n        # 访问OpenAI的超时时间(毫秒)，可选，默认值为 '100000'\n        OPENAI_TIMEOUT_MS: '100000'\n        # 可选，默认值为 gpt-3.5-turbo\n        API_MODEL: gpt-3.5-turbo\n        # Socks代理，可选，格式为 http://127.0.0.1:10808\n        SOCKS_PROXY: ''\n        # 可选，将USE_LOCAL_WHISPER设置为`true`可以使用离线模型来完成语音识别，如果设置为`false`就会使用OpenAI的API进行语音识别，默认值为: `true`\n \t\t\t\t# 使用离线模型就不需要向OpenAI付费，但会额外消耗一些服务器的cpu和内存资源，这里使用的是tiny模型，工作时占用的内存大概是125MB左右\n \t\t\t\t# 具体性能消耗参考whisper.cpp的官方文档： https://github.com/ggerganov/whisper.cpp#memory-usage\n\t\t\t\tUSE_LOCAL_WHISPER: 'true'\n        # HOST，可选，默认值为 0.0.0.0\n        HOST: 0.0.0.0\n        # PORT，可选，默认值为 3002\n        PORT: 3002\n    nginx:\n      depends_on:\n        - app\n      image: nginx:alpine\n      ports:\n        - '80:80'\n      expose:\n        - '80'\n      volumes:\n        - ./nginx/html/:/etc/nginx/html/\n        - ./nginx/auth/:/etc/nginx/auth/\n        - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf\n      links:\n        - app\n\t```\n\n- 最后进入文件夹 `/docker-compose` 运行以下命令\n\n\t```shell\n\t# 前台运行\n\tdocker-compose up\n\t# 或后台运行\n\tdocker-compose up -d\n\t```\n\t**建议先在前台运行试用一下，看看有没有报错，如果启动和使用都没有问题，再改成后台运行。**\n\n\t启动成功之后，访问`http://你的机器ip`即可看到网页内容。\n\n## 常见问题\n\nQ: 为什么 `Git` 提交总是报错？\n\nA: 因为有提交信息验证，请遵循 [Commit 指南](./CONTRIBUTING.md)\n\nQ: 如果只使用前端页面，在哪里改请求接口？\n\nA: 根目录下 `.env` 文件中的 `VITE_GLOB_API_URL` 字段。\n\nQ: 文件保存时全部爆红?\n\nA: `vscode` 请安装项目推荐插件，或手动安装 `Eslint` 插件。\n\nQ: 前端没有打字机效果？\n\nA: 一种可能原因是经过 Nginx 反向代理，开启了 buffer，则 Nginx\n会尝试从后端缓冲一定大小的数据再发送给浏览器。请尝试在反代参数后添加 `proxy_buffering off;`，然后重载 Nginx。其他 web\nserver 配置同理。\n\nQ: 为什么录音功能不能用？\n\nA: 录音需要https环境，推荐使用cloudflare的免费https证书。\n\nQ: build docker容器的时候，显示`exec entrypoint.sh: no such file or directory`？\n\nA: 因为`entrypoint.sh`文件的换行符是`LF`，而不是`CRLF`，如果你用`CRLF`的IDE操作过这个文件，可能就会出错。可以使用`dos2unix`工具将`LF`换成`CRLF`。\n\n## 参与贡献\n\n贡献之前请先阅读 [贡献指南](./CONTRIBUTING.md)\n\n感谢原作者[Chanzhaoyu](https://github.com/Chanzhaoyu/chatgpt-web/)和所有做过贡献的人，还有生产力工具`ChatGpt`\n和`Github Copilot`!\n\n<a href=\"https://github.com/WenJing95/chatgpt-web/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=WenJing95/chatgpt-web\" />\n</a>\n\n## 赞助\n\n如果你觉得这个项目对你有帮助，请给我点个Star。\n\n如果情况允许，请支持原作者[Chanzhaoyu](https://github.com/Chanzhaoyu/chatgpt-web/)\n\n## License\n\nMIT © [WenJing95](./license)\n"
  },
  {
    "path": "config/index.ts",
    "content": "export * from './proxy'\n"
  },
  {
    "path": "config/proxy.ts",
    "content": "import type { ProxyOptions } from 'vite'\n\nexport function createViteProxy(isOpenProxy: boolean, viteEnv: ImportMetaEnv) {\n  if (!isOpenProxy)\n    return\n\n  const proxy: Record<string, string | ProxyOptions> = {\n    '/api': {\n      target: viteEnv.VITE_APP_API_BASE_URL,\n      changeOrigin: true,\n      rewrite: path => path.replace('/api/', '/'),\n    },\n  }\n\n  return proxy\n}\n"
  },
  {
    "path": "docker-compose/docker-compose.yml",
    "content": "version: '3'\n\nservices:\n  app:\n    # 根据自己的系统选择x86_64还是aarch64\n    image: wenjing95/chatgpt-web-backend:x86_64\n    # image: wenjing95/chatgpt-web-backend:aarch64\n    ports:\n      - 3002:3002\n    environment:\n      OPENAI_API_KEY: ''\n      # 访问OpenAI的超时时间，可选，默认值为 '100000'\n      OPENAI_TIMEOUT_MS: '100000'\n      # 可选，默认值为 gpt-3.5-turbo\n      API_MODEL: gpt-3.5-turbo\n      # Socks代理，可选，格式为 http://127.0.0.1:10808\n      SOCKS_PROXY: ''\n      # 可选，将USE_LOCAL_WHISPER设置为`true`可以使用离线模型来完成语音识别，如果设置为`false`就会使用OpenAI的API进行语音识别，默认值为: `true`\n      # 使用离线模型就不需要向OpenAI付费，但会额外消耗一些服务器的cpu和内存资源，这里使用的是tiny模型，工作时占用的内存大概是125MB左右\n      # 具体性能消耗参考whisper.cpp的官方文档： https://github.com/ggerganov/whisper.cpp#memory-usage\n      USE_LOCAL_WHISPER: 'true'\n      # HOST，可选，默认值为 0.0.0.0\n      HOST: 0.0.0.0\n      # PORT，可选，默认值为 3002\n      PORT: 3002\n  nginx:\n    depends_on:\n      - app\n    image: nginx:alpine\n    ports:\n      - '80:80'\n    expose:\n      - '80'\n    volumes:\n      - ./nginx/html/:/etc/nginx/html/\n      - ./nginx/auth/:/etc/nginx/auth/\n      - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf\n    links:\n      - app\n"
  },
  {
    "path": "docker-compose/nginx/add_user.sh",
    "content": "#!/bin/bash\n\nread -p \"Enter your desired username: \" username\nread -s -p \"Enter your desired password: \" password\nhtpasswd_file=\"./auth/.htpasswd\"\n\nif [ ! -f \"$htpasswd_file\" ]; then\n    sudo touch \"$htpasswd_file\"\nfi\n\nsudo sh -c \"echo -n '${username}:' >> $htpasswd_file\"\nsudo sh -c \"openssl passwd -apr1 ${password} >> $htpasswd_file\"\n\necho \"Username: ${username}\"\necho \"Password: ${password}\"\necho \".htpasswd file location: ${htpasswd_file}\"\n"
  },
  {
    "path": "docker-compose/nginx/auth/.htpasswd",
    "content": ""
  },
  {
    "path": "docker-compose/nginx/nginx.conf",
    "content": "\n\tserver {\n\t\t\tlisten 80;\n\t\t\tserver_name localhost;\n\t\t\tcharset utf-8;\n\t\t\terror_page   500 502 503 504  /50x.html;\n\n\t\t\tauth_basic \"Restricted Access\";\n\t\t\tauth_basic_user_file /etc/nginx/auth/.htpasswd;\n\n\t\t\tlocation / {\n\t\t\t\t\troot /etc/nginx/html/;\n\t\t\t\t\ttry_files $uri /index.html;\n\t\t\t}\n\n\t\t\tlocation /api/ {\n\t\t\t\t\tproxy_set_header   X-Real-IP $remote_addr;\n\t\t\t\t\tproxy_pass http://app:3002/;\n\t\t\t\t\tproxy_buffering off;\n\t\t\t\t\tclient_max_body_size 3M;\n\t\t\t}\n\n\t\t\tproxy_set_header Host $host;\n\t\t\tproxy_set_header X-Real-IP $remote_addr;\n\t\t\tproxy_set_header REMOTE-HOST $remote_addr;\n\t\t\tproxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n\t}\n\n"
  },
  {
    "path": "docker-compose/nginx/remove_user.sh",
    "content": "#!/bin/bash\n\nhtpasswd_file=\"./auth/.htpasswd\"\n\nif [ ! -f \"$htpasswd_file\" ]; then\n    echo \"No .htpasswd file found\"\n    exit 1\nfi\n\nread -p \"Enter the username you want to remove: \" username\n\n# Check if the user already exists in the file\nif ! grep -q \"${username}:\" \"$htpasswd_file\"; then\n    echo \"User not found in .htpasswd file\"\n    exit 1\nfi\n\n# Delete the user from the file\nsudo sed -i \"/${username}:/d\" \"$htpasswd_file\"\n\necho \"User ${username} has been removed from the .htpasswd file.\"\n"
  },
  {
    "path": "index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"zh-cmn-Hans\">\n<head>\n\t<meta charset=\"UTF-8\">\n\t<link rel=\"icon\" type=\"image/svg+xml\" href=\"/favicon.svg\">\n\t<meta content=\"yes\" name=\"apple-mobile-web-app-capable\"/>\n\t<link rel=\"apple-touch-icon\" href=\"/favicon.ico\">\n\t<meta name=\"viewport\"\n\t      content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=0, viewport-fit=cover\" />\n\t<meta content=\"telephone=no,email=no\" name=\"format-detection\" />\n\t<meta name=\"apple-mobile-web-app-title\" content=\"ChatGPT Web\"/>\n\t<title>ChatGPT Web</title>\n</head>\n\n<body class=\"dark:bg-black\">\n\t<div id=\"app\">\n\t\t<style>\n\t\t\t.loading-wrap {\n\t\t\t\tdisplay: flex;\n\t\t\t\tjustify-content: center;\n\t\t\t\talign-items: center;\n\t\t\t\theight: 100vh;\n\t\t\t}\n\n\t\t\t.balls {\n\t\t\t\twidth: 4em;\n\t\t\t\tdisplay: flex;\n\t\t\t\tflex-flow: row nowrap;\n\t\t\t\talign-items: center;\n\t\t\t\tjustify-content: space-between;\n\t\t\t}\n\n\t\t\t.balls div {\n\t\t\t\twidth: 0.8em;\n\t\t\t\theight: 0.8em;\n\t\t\t\tborder-radius: 50%;\n\t\t\t\tbackground-color: #4b9e5f;\n\t\t\t}\n\n\t\t\t.balls div:nth-of-type(1) {\n\t\t\t\ttransform: translateX(-100%);\n\t\t\t\tanimation: left-swing 0.5s ease-in alternate infinite;\n\t\t\t}\n\n\t\t\t.balls div:nth-of-type(3) {\n\t\t\t\ttransform: translateX(-95%);\n\t\t\t\tanimation: right-swing 0.5s ease-out alternate infinite;\n\t\t\t}\n\n\t\t\t@keyframes left-swing {\n\n\t\t\t\t50%,\n\t\t\t\t100% {\n\t\t\t\t\ttransform: translateX(95%);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t@keyframes right-swing {\n\t\t\t\t50% {\n\t\t\t\t\ttransform: translateX(-95%);\n\t\t\t\t}\n\n\t\t\t\t100% {\n\t\t\t\t\ttransform: translateX(100%);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t@media (prefers-color-scheme: dark) {\n\t\t\t\tbody {\n\t\t\t\t\tbackground: #121212;\n\t\t\t\t}\n\t\t\t}\n\t\t</style>\n\t\t<div class=\"loading-wrap\">\n\t\t\t<div class=\"balls\">\n\t\t\t\t<div></div>\n\t\t\t\t<div></div>\n\t\t\t\t<div></div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\t<script type=\"module\" src=\"/src/main.ts\"></script>\n</body>\n\n</html>\n"
  },
  {
    "path": "license",
    "content": "MIT License\n\nCopyright (c) 2023 WenJing\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"chatgpt-web\",\n  \"version\": \"2.10.9\",\n  \"private\": false,\n  \"description\": \"ChatGPT Web\",\n  \"author\": \"WenJing\",\n  \"keywords\": [\n    \"chatgpt-web\",\n    \"chatgpt\",\n    \"chatbot\",\n    \"vue\"\n  ],\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"run-p type-check build-only\",\n    \"preview\": \"vite preview\",\n    \"build-only\": \"vite build\",\n    \"type-check\": \"vue-tsc --noEmit\",\n    \"lint\": \"eslint .\",\n    \"lint:fix\": \"eslint . --fix\",\n    \"bootstrap\": \"pnpm install && pnpm run common:prepare\",\n    \"common:cleanup\": \"rimraf node_modules && rimraf pnpm-lock.yaml\",\n    \"common:prepare\": \"husky install\"\n  },\n  \"dependencies\": {\n    \"@traptitech/markdown-it-katex\": \"^3.6.0\",\n    \"@vueuse/core\": \"^9.13.0\",\n    \"highlight.js\": \"^11.7.0\",\n    \"katex\": \"^0.16.4\",\n    \"markdown-it\": \"^13.0.1\",\n    \"naive-ui\": \"^2.34.3\",\n    \"pinia\": \"^2.0.32\",\n    \"recorder-core\": \"^1.2.23020100\",\n    \"vite-plugin-pwa\": \"^0.14.4\",\n    \"vue\": \"^3.2.47\",\n    \"vue-i18n\": \"^9.2.2\",\n    \"vue-router\": \"^4.1.6\"\n  },\n  \"devDependencies\": {\n    \"@antfu/eslint-config\": \"^0.35.3\",\n    \"@commitlint/cli\": \"^17.4.4\",\n    \"@commitlint/config-conventional\": \"^17.4.4\",\n    \"@iconify/vue\": \"^4.1.0\",\n    \"@types/crypto-js\": \"^4.1.1\",\n    \"@types/katex\": \"^0.16.0\",\n    \"@types/markdown-it\": \"^12.2.3\",\n    \"@types/markdown-it-link-attributes\": \"^3.0.1\",\n    \"@types/node\": \"^18.14.6\",\n    \"@vitejs/plugin-vue\": \"^4.0.0\",\n    \"autoprefixer\": \"^10.4.13\",\n    \"axios\": \"^1.3.4\",\n    \"crypto-js\": \"^4.1.1\",\n    \"eslint\": \"^8.35.0\",\n    \"husky\": \"^8.0.3\",\n    \"less\": \"^4.1.3\",\n    \"lint-staged\": \"^13.1.2\",\n    \"markdown-it-link-attributes\": \"^4.0.1\",\n    \"npm-run-all\": \"^4.1.5\",\n    \"postcss\": \"^8.4.21\",\n    \"rimraf\": \"^4.2.0\",\n    \"tailwindcss\": \"^3.2.7\",\n    \"typescript\": \"~4.9.5\",\n    \"vite\": \"^4.1.4\",\n    \"vite-plugin-pwa\": \"^0.14.4\",\n    \"vue-tsc\": \"^1.2.0\"\n  },\n  \"lint-staged\": {\n    \"*.{ts,tsx,vue}\": [\n      \"pnpm lint:fix\"\n    ]\n  }\n}\n"
  },
  {
    "path": "postcss.config.js",
    "content": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n}\n"
  },
  {
    "path": "service/Dockerfile",
    "content": "# 使用 Python 3.10 作为基础镜像\nFROM python:3.10\n\n# 设置工作目录\nWORKDIR /app\n\n# 复制项目文件到容器中\nCOPY . .\n\n# ==rust是tiktoken依赖的环境==\nRUN curl https://sh.rustup.rs -sSf | sh -s -- -y\nENV PATH=\"/root/.cargo/bin:$PATH\"\n\n# 编译whisper\nWORKDIR ./tools/local-whisper/linux/\nRUN chmod +x init.sh\nRUN ./init.sh\nWORKDIR /app\n\n# 下载whisper使用的模型\n#wget -P ./tools/local-whisper/model https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-tiny.bin\n\n# 安装依赖\nRUN pip install --upgrade pip && pip install --upgrade --no-cache-dir -r requirements.txt\n\n# 设置环境变量\nENV OPENAI_API_KEY=\"\"\nENV OPENAI_TIMEOUT_MS=\"100000\"\nENV API_MODEL=\"gpt-3.5-turbo\"\nENV SOCKS_PROXY=\"\"\nENV USE_LOCAL_WHISPER: 'true'\nENV HOST=\"127.0.0.1\"\nENV PORT=3002\n\n# 复制 entrypoint.sh 脚本到容器中\nCOPY entrypoint.sh .\n\n# 修改 entrypoint.sh 的权限\nRUN chmod +x entrypoint.sh\n\n# 启动命令\nENTRYPOINT [\"./entrypoint.sh\"]\n"
  },
  {
    "path": "service/api_model.py",
    "content": "class ApiModel:\n    KNOWN_API_MODEL_NAMES = [\"gpt-3.5-turbo\",\n                             \"gpt-4\",\n                             \"gpt-3.5-turbo-16k\",\n                             # \"gpt-4-32k\"\n                             ]\n    KNOWN_API_MODEL_MAX_TOKENS = {\"gpt-3.5-turbo\": 4000,\n                                  \"gpt-4\": 8000,\n                                  \"gpt-3.5-turbo-16k\": 16000,\n                                  # \"gpt-4-32k\": 32000\n                                  }\n\n    @classmethod\n    def get_api_model_name(cls, api_model_name: str, default_api_model_name: str) -> str:\n        return api_model_name if api_model_name in cls.KNOWN_API_MODEL_NAMES else default_api_model_name\n\n    @classmethod\n    def get_max_token(cls, api_model_name: str, default_max_token: int) -> int:\n        return cls.KNOWN_API_MODEL_MAX_TOKENS.get(api_model_name, default_max_token)\n"
  },
  {
    "path": "service/chatgpt_wapper.py",
    "content": "import random\nimport time\n\nimport openai  # for OpenAI API calls\nimport traceback\nimport json\nimport asyncio\nfrom loguru import logger\nfrom backoff import on_exception, expo\nfrom tools.openai_token_control import discard_overlimit_messages\nimport concurrent.futures\nfrom errors import Errors\n\nbase = {\"role\": \"system\", \"content\": \"You are a helpful assistant.\"}\n\n\nasync def process(prompt, options, memory_count, top_p, message_store, timeout, max_token, model=\"gpt-3.5-turbo\"):\n    \"\"\"\n    发文字消息\n    \"\"\"\n    # 不能是空消息\n    if not prompt:\n        logger.error(\"Prompt is empty.\")\n        yield Errors.PROMPT_IS_EMPTY.value\n        return\n\n    # 内容审查\n    moderation_params = dict(\n        model='text-moderation-stable',\n        input=prompt,\n    )\n    moderation_res = await _moderation_create_async(moderation_params)\n    if moderation_res is None:\n        yield Errors.SOMETHING_WRONG_IN_OPENAI_MODERATION_API.value\n        return\n\n    try:\n        if moderation_res.results[0].flagged:\n            warning_text = \"[This content does not comply with OpenAI's usage policy. : {} ]\".format(\n                prompt\n            )\n            logger.warning(warning_text)\n            yield Errors.NOT_COMPLY_POLICY.value\n            return\n\n        chat = {\"role\": \"user\", \"content\": prompt}\n\n        # 组合历史消息\n        if options:\n            parent_message_id = options.get(\"parentMessageId\")\n            messages = message_store.get_from_key(parent_message_id)\n            if messages:\n                messages.append(chat)\n            else:\n                messages = [base, chat]\n        else:\n            parent_message_id = None\n            messages = [base, chat]\n\n        # 记忆\n        messages = messages[-memory_count:]\n\n        # 消息不能超过token限制\n        # todo 压缩过去消息\n        messages = discard_overlimit_messages(messages, model, max_token)\n\n        # send to OpenAI\n        params = dict(\n            stream=True, messages=messages, model=model, top_p=top_p, timeout=timeout\n        )\n        if parent_message_id:\n            params[\"request_id\"] = parent_message_id\n\n        res = await _chat_completions_create_async(params)\n        if res is None:\n            yield Errors.SOMETHING_WRONG_IN_OPENAI_GPT_API.value\n            return\n\n        # 处理结果\n        text = \"\"\n        role = \"\"\n        prev_message_dict = {}\n        for openai_object in res:\n            openai_object_dict = openai_object.to_dict_recursive()\n\n            prev_message_dict = openai_object_dict\n\n            if not role:\n                role = openai_object_dict[\"choices\"][0][\"delta\"].get(\"role\", \"\")\n\n            text_delta = openai_object_dict[\"choices\"][0][\"delta\"].get(\"content\", \"\")\n            text += text_delta\n\n            message = json.dumps(dict(\n                role=role,\n                id=openai_object_dict[\"id\"],\n                parentMessageId=parent_message_id,\n                text=text,\n                # delta=text_delta,\n                # detail=dict(\n                #     id=openai_object_dict[\"id\"],\n                #     object=openai_object_dict[\"object\"],\n                #     # created=openai_object_dict[\"created\"],\n                #     # model=openai_object_dict[\"model\"],\n                #     # choices=openai_object_dict[\"choices\"]\n                # )\n            ))\n            yield \"data: \" + message\n\n    except:\n        err = traceback.format_exc()\n        logger.error(err)\n        yield Errors.SOMETHING_WRONG.value\n        return\n\n    try:\n        # save to cache\n        chat = {\"role\": role, \"content\": text}\n        messages.append(chat)\n\n        parent_message_id = prev_message_dict[\"id\"]\n        message_store.set(parent_message_id, messages)\n    except:\n        err = traceback.format_exc()\n        logger.error(err)\n\n\n@on_exception(expo, openai.error.RateLimitError, max_tries=5)\ndef _moderation_create(params):\n    return openai.Moderation.create(**params)\n\n\nasync def _moderation_create_async(params):\n    with concurrent.futures.ThreadPoolExecutor() as executor:\n        try:\n            result = await asyncio.get_event_loop().run_in_executor(\n                executor, _moderation_create, params\n            )\n        except:\n            err = traceback.format_exc()\n            logger.error(err)\n            # 这里处理 openai.error.RateLimitError 之外的错误\n            return None\n    return result\n\n\n@on_exception(expo, openai.error.RateLimitError, max_tries=5)\ndef _chat_completions_create(params):\n    return openai.ChatCompletion.create(**params)\n\n\nasync def _chat_completions_create_async(params):\n    with concurrent.futures.ThreadPoolExecutor() as executor:\n        try:\n            result = await asyncio.get_event_loop().run_in_executor(\n                executor, _chat_completions_create, params\n            )\n        except:\n            err = traceback.format_exc()\n            logger.error(err)\n            # 这里处理 openai.error.RateLimitError 之外的错误\n            return None\n    return result\n"
  },
  {
    "path": "service/entrypoint.sh",
    "content": "#!/bin/bash\n\n# 将环境变量传递给 Python 脚本\nexport OPENAI_API_KEY=\"$OPENAI_API_KEY\"\nexport OPENAI_TIMEOUT_MS=\"$OPENAI_TIMEOUT_MS\"\nexport API_MODEL=\"$API_MODEL\"\nexport SOCKS_PROXY=\"$SOCKS_PROXY\"\nexport USE_LOCAL_WHISPER=\"$USE_LOCAL_WHISPER\"\nexport HOST=\"$HOST\"\nexport PORT=\"$PORT\"\n\n# 启动 Python 脚本\npython main.py --openai_api_key=\"$OPENAI_API_KEY\" --openai_timeout_ms=\"$OPENAI_TIMEOUT_MS\" --api_model=\"$API_MODEL\" --socks_proxy=\"$SOCKS_PROXY\" --use_local_whisper=\"$USE_LOCAL_WHISPER\" --host=\"$HOST\" --port=\"$PORT\"\n"
  },
  {
    "path": "service/errors.py",
    "content": "from enum import Enum\n\n\nclass Errors(Enum):\n    SOMETHING_WRONG = \"ChatGptWebServerError:SomethingWrong\"\n    SOMETHING_WRONG_IN_OPENAI_GPT_API = \"ChatGptWebServerError:SomethingWrongInOpenaiGptApi\"\n    SOMETHING_WRONG_IN_OPENAI_MODERATION_API = \"ChatGptWebServerError:SomethingWrongInOpenaiModerationApi\"\n    SOMETHING_WRONG_IN_OPENAI_WHISPER_API = \"ChatGptWebServerError:SomethingWrongInOpenaiWhisperApi\"\n    SOMETHING_WRONG_IN_LOCAL_WHISPER = \"ChatGptWebServerError:SomethingWrongInLocalWhisper\"\n    UNKNOWN_WHISPER_SETTING = \"ChatGptWebServerError:UnknownWhisperSetting\"\n    NOT_COMPLY_POLICY = \"ChatGptWebServerError:NotComplyPolicy\"\n    PROMPT_IS_EMPTY = \"ChatGptWebServerError:PromptIsEmpty\"\n"
  },
  {
    "path": "service/main.py",
    "content": "import openai\nimport os\nfrom os.path import abspath, dirname\nfrom loguru import logger\nfrom chatgpt_wapper import process\nfrom fastapi import FastAPI, UploadFile, File\nfrom fastapi.responses import JSONResponse, StreamingResponse\nimport uvicorn\nfrom message_store import MessageStore\nfrom whisper_wapper import process_audio_local, process_audio_api\nimport argparse\nfrom api_model import ApiModel\nimport platform\nfrom pathlib import Path\n\nlog_folder = os.path.join(abspath(dirname(__file__)), \"log\")\nlogger.add(os.path.join(log_folder, \"{time}.log\"), level=\"INFO\")\n\nDEFAULT_TIMEOUT_MS_STRING = \"100000\"\nMIN_TIMEOUT_MS = 15000\nDEFAULT_DB_SIZE = 100000\nDEFAULT_API_MODEL = \"gpt-3.5-turbo\"\nDEFAULT_MAX_TOKEN = 4000\nCURRENT_OS = platform.system()\nUSE_LOCAL_WHISPER = True\n\ncurrent_file_path = Path(__file__).resolve()\nbase_directory = current_file_path.parent\n\nAUDIO_TMP_PATH = base_directory / \"audio_tmp\"\nLOCAL_WHISPER_MODEL_PATH = base_directory / \"tools\" / \"local-whisper\" / \"model\" / \"ggml-tiny.bin\"\nLOCAL_WHISPER_BIN_PATH_LINUX = base_directory / \"tools\" / \"local-whisper\" / \"linux\" / \"whisper.cpp-master\" / \"main\"\nLOCAL_WHISPER_BIN_PATH_WINDOWS = base_directory / \"tools\" / \"local-whisper\" / \"windows\" / \"main.exe\"\n\nmassage_store = MessageStore(db_path=\"message_store.json\", table_name=\"chatgpt\", max_size=DEFAULT_DB_SIZE)\n\napp = FastAPI()\n\nstream_response_headers = {\n    \"Content-Type\": \"application/octet-stream\",\n    \"Cache-Control\": \"no-cache\",\n}\n\n\n@app.post(\"/config\")\nasync def config():\n    return JSONResponse(content=dict(\n        message=None,\n        status=\"Success\",\n        data=dict(\n            apiModel=API_MODEL,\n            socksProxy=SOCKS_PROXY,\n            timeoutMs=OPENAI_TIMEOUT * 1000,\n        )\n    ))\n\n\n@app.post(\"/chat-process\")\nasync def chat_process(request_data: dict):\n    prompt = request_data[\"prompt\"]\n    options = request_data[\"options\"]\n\n    if 1 == request_data[\"memory\"]:\n        memory_count = 5\n    elif 50 == request_data[\"memory\"]:\n        memory_count = 20\n    else:\n        memory_count = 999\n\n    if 1 == request_data[\"top_p\"]:\n        top_p = 0.2\n    elif 50 == request_data[\"top_p\"]:\n        top_p = 0.6\n    else:\n        top_p = 1\n\n    answer_text = process(prompt, options, memory_count, top_p, MASSAGE_STORE, OPENAI_TIMEOUT, MAX_TOKEN,\n                          model=API_MODEL)\n    return StreamingResponse(content=answer_text, headers=stream_response_headers, media_type=\"text/event-stream\")\n\n\n@app.post(\"/audio-chat-process\")\nasync def audio_chat_process(audio: UploadFile = File(...)):\n    if USE_LOCAL_WHISPER:\n        local_whisper_bin_path = LOCAL_WHISPER_BIN_PATH_LINUX if \"Linux\" == CURRENT_OS else LOCAL_WHISPER_BIN_PATH_WINDOWS\n\n        prompt = process_audio_local(audio,\n                                     audio_tmp_path=AUDIO_TMP_PATH,\n                                     model_path=LOCAL_WHISPER_MODEL_PATH,\n                                     local_whisper_bin_path=local_whisper_bin_path)\n    else:\n        prompt = process_audio_api(audio,\n                                   timeout=OPENAI_TIMEOUT)\n\n    return StreamingResponse(content=prompt, headers=stream_response_headers, media_type=\"text/event-stream\")\n\n\ndef init_config():\n    # 读取配置\n    parser = argparse.ArgumentParser(description='')\n    parser.add_argument('--openai_api_key', type=str, help='API key for OpenAI')\n    parser.add_argument('--api_model', type=str, default=\"gpt-3.5-turbo\",\n                        help='OpenAI API model, default is gpt-3.5-turbo')\n    parser.add_argument('--socks_proxy', type=str, default=\"\",\n                        help='Socks proxy, default is \"\", e.g. http://127.0.0.1:10808')\n    parser.add_argument('--timeout_ms', type=str, default=DEFAULT_TIMEOUT_MS_STRING,\n                        help=\"(Deprecate) Timeout for OpenAI API, default is '100000'\")\n    parser.add_argument('--openai_timeout_ms', type=str, default=DEFAULT_TIMEOUT_MS_STRING,\n                        help=\"Timeout for OpenAI API, default is '100000'\")\n    parser.add_argument('--use_local_whisper', type=str, default='True',\n                        help=\"Use local whisper or api whisper. local whisper is free, api whisper is best,\"\n                             \" default is 'True'\")\n    parser.add_argument('--host', type=str, default=\"0.0.0.0\", help='Host for server, default is 0.0.0.0')\n    parser.add_argument('--port', type=str, default=\"3002\", help=\"Port for server, default is '3002'\")\n\n    args = parser.parse_args()\n\n    if not args.openai_api_key:\n        err = \"OpenAI API key is not found. use --openai_api_key to set it.\"\n        logger.error(err)\n        raise TypeError(err)\n    openai_api_key = args.openai_api_key\n    openai.api_key = args.openai_api_key\n\n    input_api_model = args.api_model\n    if not input_api_model:\n        err = \"API model is not found.\"\n        logger.error(err)\n        raise TypeError(err)\n\n    if input_api_model not in ApiModel.KNOWN_API_MODEL_NAMES:\n        warning = \"Unknown Api model '{}'. Legal settings are {}, The system has automatically set it to {}\".format(\n            input_api_model,\n            ApiModel.KNOWN_API_MODEL_NAMES,\n            DEFAULT_API_MODEL\n        )\n        logger.warning(warning)\n\n    api_model = ApiModel.get_api_model_name(input_api_model, DEFAULT_API_MODEL)\n    max_token = ApiModel.get_max_token(api_model, DEFAULT_MAX_TOKEN)\n\n    socks_proxy = args.socks_proxy\n    if socks_proxy:\n        logger.info(\"Socks proxy is enabled.\")\n        logger.info(\"Socks proxy is {}.\".format(socks_proxy))\n        openai.proxy = socks_proxy\n    else:\n        logger.info(\"Socks proxy is disabled.\")\n\n    if DEFAULT_TIMEOUT_MS_STRING != args.timeout_ms:\n        logger.warning(\"The parameter '--timeout_ms' is deprecated, please use '--openai_timeout_ms' instead.\")\n        args.openai_timeout_ms = args.timeout_ms\n\n    openai_timeout_ms = args.openai_timeout_ms or DEFAULT_TIMEOUT_MS_STRING\n\n    if isinstance(openai_timeout_ms, str):\n        try:\n            openai_timeout_ms = int(openai_timeout_ms)\n        except:\n            openai_timeout_ms = DEFAULT_TIMEOUT_MS_STRING\n\n    if openai_timeout_ms < MIN_TIMEOUT_MS:\n        openai_timeout_ms = MIN_TIMEOUT_MS\n        logger.warning(\n            \"OpenAI timeout is too short, the system has automatically set it to {openai_timeout_ms}(ms).\".format(\n                openai_timeout_ms=MIN_TIMEOUT_MS))\n\n    openai_timeout = openai_timeout_ms / 1000\n\n    # service_timeout_ms = args.service_timeout_ms or 100000\n    # if isinstance(service_timeout_ms, str):\n    #     try:\n    #         service_timeout_ms = int(service_timeout_ms)\n    #     except:\n    #         service_timeout_ms = 100000\n\n    # if service_timeout_ms < 15000:\n    #     service_timeout_ms = 15000\n    #     logger.warning(\"Service timeout is too short, the system has automatically set it to 15000(ms).\")\n\n    # service_timeout = service_timeout_ms / 1000\n\n    # if openai_timeout_ms > service_timeout_ms:\n    #     openai_timeout = service_timeout\n    #     logger.warning(\n    #         \"OpenAI timeout is longer than service timeout, the system has automatically set it to the same as service timeout.\")\n\n    host = args.host or \"0.0.0.0\"\n    port = args.port or 3002\n    if isinstance(port, str):\n        try:\n            port = int(port)\n        except:\n            err = \"Port must be a number.\"\n            logger.error(err)\n            raise TypeError(err)\n\n    use_local_whisper = True if args.use_local_whisper in ['True', 'true'] else False\n\n    AUDIO_TMP_PATH.mkdir(parents=True, exist_ok=True)\n\n    return massage_store, openai_api_key, host, port, api_model, \\\n           max_token, socks_proxy, openai_timeout, use_local_whisper\n\n\nif __name__ == \"__main__\":\n    MASSAGE_STORE, OPENAI_API_KEY, HOST, PORT, API_MODEL, MAX_TOKEN, SOCKS_PROXY, OPENAI_TIMEOUT, USE_LOCAL_WHISPER = init_config()\n    logger.info(\"OPENAI_API_KEY:{}\".format(OPENAI_API_KEY))\n    logger.info(\"HOST:{}\".format(HOST))\n    logger.info(\"PORT:{}\".format(PORT))\n    logger.info(\"API_MODEL:{}\".format(API_MODEL))\n    logger.info(\"MAX_TOKEN:{}\".format(MAX_TOKEN))\n    logger.info(\"SOCKS_PROXY:{}\".format(SOCKS_PROXY))\n    logger.info(\"OPENAI_TIMEOUT_MS:{}\".format(OPENAI_TIMEOUT * 1000))\n    logger.info(\"USE_LOCAL_WHISPER:{}\".format(USE_LOCAL_WHISPER))\n\n    logger.info(\"AUDIO_TMP_PATH:{}\".format(AUDIO_TMP_PATH))\n\n    uvicorn.run(app, host=HOST, port=PORT)\n"
  },
  {
    "path": "service/message_store.py",
    "content": "import time\nfrom tinydb import TinyDB, Query\n\nclass MessageStore:\n    def __init__(self, db_path, table_name, max_size=100000):\n        self.db = TinyDB(db_path)\n        self.table = self.db.table(table_name)\n        self.max_size = max_size\n\n    def set(self, key, value):\n        if len(self.table) >= self.max_size:\n            self._delete_oldest()\n        self.table.insert({'key': key, 'value': value, 'timestamp': time.time()})\n\n    def get_from_key(self, key):\n        query = Query()\n        result = self.table.get(query.key == key)\n        if result is None:\n            return None\n        return result['value']\n\n    def _delete_oldest(self):\n        records = self.table.all()\n        if len(records) >= self.max_size:\n            oldest_record = sorted(records, key=lambda r: r['timestamp'])[0]\n            self.table.remove(doc_ids=[oldest_record.doc_id])\n"
  },
  {
    "path": "service/tools/__init__.py",
    "content": ""
  },
  {
    "path": "service/tools/local-whisper/__init__.py",
    "content": ""
  },
  {
    "path": "service/tools/local-whisper/linux/init.sh",
    "content": "sudo apt update\nsudo apt install build-essential -y\n\nwget https://github.com/ggerganov/whisper.cpp/archive/refs/heads/master.zip\nunzip master.zip\nrm master.zip\ncd whisper.cpp-master || exit\n\nmake\n\n"
  },
  {
    "path": "service/tools/openai_token_control.py",
    "content": "import tiktoken\n\nMAX_RANGES = 10\n\n\ndef discard_overlimit_messages(messages: list, model: str, max_token: int) -> list:\n    \"\"\"\n    Discards messages that exceed the maximum number of tokens allowed by OpenAI.\n    \"\"\"\n    range_count = 0\n    while True:\n        # 如果消息太少，就不处理\n        if len(messages) <= 2:\n            return messages\n\n        # 防止意外(例如num_tokens_from_messages的实现没有及时跟随openai更新)出现死循环\n        if range_count > MAX_RANGES:\n            return messages\n\n        token_count = num_tokens_from_messages(messages, model=model)\n\n        range_count += 1\n\n        if token_count <= max_token:\n            return messages\n        else:\n            # 去掉过去的一半消息，给回答留下足够空间\n            # 通常来说问题比较短，回复比较长，如果只去掉最远的1、2条消息，可能会导致问题占了大部分token，比方说4090个\n            # 在最大token只能有4096个的情况下，回复只能有6个token，这样就会导致回复被截断\n            messages = messages[int(len(messages) / 2):]\n            continue\n\n\ndef num_tokens_from_string(string: str, encoding_name: str) -> int:\n    \"\"\"Returns the number of tokens in a text string.\"\"\"\n    encoding = tiktoken.get_encoding(encoding_name)\n    num_tokens = len(encoding.encode(string))\n    return num_tokens\n\n\ndef num_tokens_from_messages(messages, model=\"gpt-3.5-turbo\"):\n    \"\"\"Returns the number of tokens used by a list of messages.\"\"\"\n    try:\n        encoding = tiktoken.encoding_for_model(model)\n    except KeyError:\n        print(\"Warning: model not found. Using cl100k_base encoding.\")\n        encoding = tiktoken.get_encoding(\"cl100k_base\")\n\n    if model.startswith(\"gpt-3.5\"):\n        tokens_per_message = 4\n        tokens_per_name = -1\n    elif model.startswith(\"gpt-4\"):\n        tokens_per_message = 3\n        tokens_per_name = 1\n    else:\n        tokens_per_message = 4\n        tokens_per_name = -1\n        #raise NotImplementedError(\n        #    f\"\"\"num_tokens_from_messages() is not implemented for model {model}. See https://github.com/openai/openai-python/blob/main/chatml.md for information on how messages are converted to tokens.\"\"\")\n    num_tokens = 0\n    for message in messages:\n        num_tokens += tokens_per_message\n        for key, value in message.items():\n            num_tokens += len(encoding.encode(value))\n            if key == \"name\":\n                num_tokens += tokens_per_name\n    num_tokens += 3  # every reply is primed with <|start|>assistant<|message|>\n    return num_tokens\n\n\nif __name__ == '__main__':\n    print(num_tokens_from_string(\"test\", \"cl100k_base\"))\n    messages = [{\"role\": \"system\", \"content\": \"You are a helpful assistant.\"}, {\"role\": \"user\", \"content\": \"test\"}]\n    print(num_tokens_from_messages(messages, \"gpt-3.5-turbo-0301\"))\n\n    print(num_tokens_from_messages(messages, \"gpt-4\"))\n    print(num_tokens_from_messages(messages, \"gpt-3.5-turbo\"))\n    print(num_tokens_from_messages(messages, \"gpt-3.5-turbo-16k\"))\n"
  },
  {
    "path": "service/whisper_wapper.py",
    "content": "import openai\nimport subprocess\nfrom backoff import on_exception, expo\nfrom io import BytesIO\nfrom typing import Optional\nfrom fastapi import UploadFile\nimport concurrent.futures\nimport asyncio\nimport traceback\nfrom loguru import logger\nfrom errors import Errors\nimport shutil\nfrom pathlib import Path\nimport uuid\n\ndef upload_file_to_file_obj(upload_file: UploadFile, file_obj: Optional[BytesIO] = None):\n    if file_obj is None:\n        file_obj = BytesIO()\n    file_obj.write(upload_file.file.read())\n    file_obj.seek(0)\n    file_obj.name = upload_file.filename\n    return file_obj\n\n\nasync def process_audio_api(audio, timeout, model=\"whisper-1\"):\n    try:\n        file = upload_file_to_file_obj(audio)\n\n        params = dict(\n            model=model,\n            file=file,\n            request_timeout=timeout,\n        )\n        func = _create\n        transcript = await _create_async(params, func)\n        if transcript is None:\n            yield Errors.SOMETHING_WRONG_IN_OPENAI_WHISPER_API.value\n            return\n\n        prompt = transcript[\"text\"]\n        logger.debug(\"audio prompt: {}\".format(prompt))\n        del audio\n        del file\n    except:\n        err = traceback.format_exc()\n        logger.error(err)\n        yield Errors.SOMETHING_WRONG.value\n        return\n\n    if not prompt:\n        yield Errors.PROMPT_IS_EMPTY.value\n        return\n\n    yield \"data: \" + prompt\n\n\n@on_exception(expo, openai.error.RateLimitError, max_tries=5)\ndef _create(params):\n    return openai.Audio.transcribe(**params)\n\n\nasync def process_audio_local(audio, audio_tmp_path, model_path, local_whisper_bin_path):\n    async def save_audio_file_to_disk(audio_file: UploadFile, path: Path):\n        loop = asyncio.get_event_loop()\n        with path.open(\"wb\") as buffer:\n            await loop.run_in_executor(None, shutil.copyfileobj, audio_file.file, buffer)\n        audio_file.file.seek(0)\n\n    def delete_file_if_exists(file_path: Path):\n        if file_path.exists():\n            file_path.unlink()\n\n    filename = \"{}.wav\".format(str(uuid.uuid4()))\n    file_path = audio_tmp_path / filename\n\n    try:\n\n        await save_audio_file_to_disk(audio, file_path)\n\n        params = dict(\n            file_path=file_path.as_posix(),\n            model_path=model_path.as_posix(),\n            local_whisper_bin_path=local_whisper_bin_path.as_posix(),\n        )\n        func = _local_create\n        transcript = await _create_async(params, func)\n        if transcript is None:\n            yield Errors.SOMETHING_WRONG_IN_LOCAL_WHISPER.value\n            return\n\n        prompt = transcript[\"text\"]\n        logger.debug(\"audio prompt: {}\".format(prompt))\n\n        # remove leading newlines and spaces\n        prompt = prompt.lstrip(\"\\n \")\n\n        if \"[BLANK_AUDIO]\" in prompt:\n            prompt = \"\"\n    except:\n        err = traceback.format_exc()\n        logger.error(err)\n        yield Errors.SOMETHING_WRONG.value\n        return\n    finally:\n        del audio\n        delete_file_if_exists(file_path)\n\n    if not prompt:\n        yield Errors.PROMPT_IS_EMPTY.value\n        return\n\n    yield \"data: \" + prompt\n\n\ndef _local_create(params):\n    args = [\n        '--language', 'auto',\n        '--model', params[\"model_path\"],\n        '-f', params[\"file_path\"],\n        '-nt',\n    ]\n\n    command = [params[\"local_whisper_bin_path\"]] + args\n\n    result = subprocess.run(command, capture_output=True, text=True)\n\n    if result.returncode == 0:\n        logger.debug(\"Subprocess executed successfully.\")\n        logger.debug(\"Output:\", result.stdout)\n    else:\n        logger.debug(\"Subprocess execution failed.\")\n        logger.debug(\"Error:\", result.stderr)\n\n    return {\"text\": result.stdout}\n\n\nasync def _create_async(params, func):\n    with concurrent.futures.ThreadPoolExecutor() as executor:\n        try:\n            result = await asyncio.get_event_loop().run_in_executor(\n                executor, func, params\n            )\n        except:\n            err = traceback.format_exc()\n            logger.error(err)\n            return None\n    return result\n"
  },
  {
    "path": "src/App.vue",
    "content": "<script setup lang=\"ts\">\nimport { NConfigProvider } from 'naive-ui'\nimport { NaiveProvider } from '@/components/common'\nimport { useTheme } from '@/hooks/useTheme'\nimport { useLanguage } from '@/hooks/useLanguage'\n\nconst { theme, themeOverrides } = useTheme()\nconst { language } = useLanguage()\n</script>\n\n<template>\n  <NConfigProvider\n    class=\"h-full\"\n    :theme=\"theme\"\n    :theme-overrides=\"themeOverrides\"\n    :locale=\"language\"\n  >\n    <NaiveProvider>\n      <RouterView />\n    </NaiveProvider>\n  </NConfigProvider>\n</template>\n"
  },
  {
    "path": "src/api/index.ts",
    "content": "import type { AxiosProgressEvent, GenericAbortSignal } from 'axios'\nimport { post } from '@/utils/request'\n\nexport function fetchChatConfig<T = any>() {\n  return post<T>({\n    url: '/config',\n  })\n}\n\nexport function fetchChatAPIProcess<T = any>(\n  params: {\n    prompt: string\n    memory: number\n    top_p: number\n    options?: { conversationId?: string; parentMessageId?: string }\n    signal?: GenericAbortSignal\n    onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void\n  },\n) {\n  return post<T>({\n    url: '/chat-process',\n    data: {\n      prompt: params.prompt,\n      options: params.options,\n      memory: params.memory,\n      top_p: params.top_p,\n    },\n    signal: params.signal,\n    onDownloadProgress: params.onDownloadProgress,\n  })\n}\n\nexport function fetchAudioChatAPIProcess<T = any>(\n  params: {\n    formData: FormData\n    options?: { conversationId?: string; parentMessageId?: string }\n    onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void\n  },\n) {\n  return post<T>({\n    url: '/audio-chat-process',\n    data: params.formData,\n    onDownloadProgress: params.onDownloadProgress,\n  })\n}\n"
  },
  {
    "path": "src/components/common/HoverButton/Button.vue",
    "content": "<script setup lang='ts'>\ninterface Emit {\n  (e: 'click'): void\n}\n\nconst emit = defineEmits<Emit>()\n\nfunction handleClick() {\n  emit('click')\n}\n</script>\n\n<template>\n  <button\n    class=\"flex items-center justify-center w-10 h-10 transition rounded-full hover:bg-neutral-100 dark:hover:bg-[#414755]\"\n    @click=\"handleClick\"\n  >\n    <slot />\n  </button>\n</template>\n"
  },
  {
    "path": "src/components/common/HoverButton/index.vue",
    "content": "<script setup lang='ts'>\nimport { computed } from 'vue'\nimport type { PopoverPlacement } from 'naive-ui'\nimport { NTooltip } from 'naive-ui'\nimport Button from './Button.vue'\n\ninterface Props {\n  tooltip?: string\n  placement?: PopoverPlacement\n}\n\ninterface Emit {\n  (e: 'click'): void\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  tooltip: '',\n  placement: 'bottom',\n})\n\nconst emit = defineEmits<Emit>()\n\nconst showTooltip = computed(() => Boolean(props.tooltip))\n\nfunction handleClick() {\n  emit('click')\n}\n</script>\n\n<template>\n  <div v-if=\"showTooltip\">\n    <NTooltip :placement=\"placement\" trigger=\"hover\">\n      <template #trigger>\n        <Button @click=\"handleClick\">\n          <slot />\n        </Button>\n      </template>\n      {{ tooltip }}\n    </NTooltip>\n  </div>\n  <div v-else>\n    <Button @click=\"handleClick\">\n      <slot />\n    </Button>\n  </div>\n</template>\n"
  },
  {
    "path": "src/components/common/NaiveProvider/index.vue",
    "content": "<script setup lang=\"ts\">\nimport { defineComponent, h } from 'vue'\nimport {\n  NDialogProvider,\n  NLoadingBarProvider,\n  NMessageProvider,\n  NNotificationProvider,\n  useDialog,\n  useLoadingBar,\n  useMessage,\n  useNotification,\n} from 'naive-ui'\n\nfunction registerNaiveTools() {\n  window.$loadingBar = useLoadingBar()\n  window.$dialog = useDialog()\n  window.$message = useMessage()\n  window.$notification = useNotification()\n}\n\nconst NaiveProviderContent = defineComponent({\n  name: 'NaiveProviderContent',\n  setup() {\n    registerNaiveTools()\n  },\n  render() {\n    return h('div')\n  },\n})\n</script>\n\n<template>\n  <NLoadingBarProvider>\n    <NDialogProvider>\n      <NNotificationProvider>\n        <NMessageProvider>\n          <slot />\n          <NaiveProviderContent />\n        </NMessageProvider>\n      </NNotificationProvider>\n    </NDialogProvider>\n  </NLoadingBarProvider>\n</template>\n"
  },
  {
    "path": "src/components/common/Setting/About.vue",
    "content": "<script setup lang='ts'>\nimport { onMounted, ref } from 'vue'\nimport { NSpin } from 'naive-ui'\nimport { fetchChatConfig } from '@/api'\nimport pkg from '@/../package.json'\nimport { SvgIcon } from '@/components/common'\n\ninterface ConfigState {\n  timeoutMs?: number\n  apiModel?: string\n  socksProxy?: string\n}\n\nconst loading = ref(false)\n\nconst config = ref<ConfigState>()\n\nasync function fetchConfig() {\n  try {\n    loading.value = true\n    const { data } = await fetchChatConfig<ConfigState>()\n    config.value = data\n  }\n  catch (e) {\n\n  }\n  finally {\n    loading.value = false\n  }\n}\n\nonMounted(() => {\n  fetchConfig()\n})\n</script>\n\n<template>\n  <NSpin :show=\"loading\">\n    <div class=\"p-4 space-y-4\">\n      <h2 class=\"text-xl font-bold\">\n        ChatGpt Web - {{ pkg.version }}\n      </h2>\n      <a\n        href=\"https://github.com/WenJing95/chatgpt-web\"\n        target=\"_blank\"\n        class=\"text-[#4b9e5f] relative flex items-center\"\n      >\n        View Source Code\n        <SvgIcon class=\"text-lg text-[#4b9e5f] ml-1\" icon=\"carbon:logo-github\" />\n      </a>\n      <div class=\"p-2 space-y-2 rounded-md bg-neutral-100 dark:bg-neutral-700\">\n        <p v-text=\"$t(&quot;common.about_head&quot;)\" />\n        <p v-text=\"$t(&quot;common.about_body&quot;)\" />\n      </div>\n      <p>{{ $t(\"setting.api\") }}：{{ config?.apiModel ?? '-' }}</p>\n      <p>{{ $t(\"setting.timeout\") }}：{{ config?.timeoutMs ?? '-' }}</p>\n      <p>{{ $t(\"setting.socks\") }}：{{ config?.socksProxy ?? '-' }}</p>\n    </div>\n  </NSpin>\n</template>\n"
  },
  {
    "path": "src/components/common/Setting/Advance.vue",
    "content": "<script lang=\"ts\" setup>\nimport { computed, ref } from 'vue'\nimport { NRadioButton, NRadioGroup, NSlider, useMessage } from 'naive-ui'\nimport { useUserStore } from '@/store'\nimport type { UserInfo } from '@/store/modules/user/helper'\nimport { t } from '@/locales'\n\nconst userStore = useUserStore()\nconst userInfo = computed(() => userStore.userInfo)\n\nconst chatgpt_top_p = ref(userInfo.value.chatgpt_top_p ?? 100)\nconst chatgpt_memory = ref(userInfo.value.chatgpt_memory ?? 100)\n\nconst ms = useMessage()\n\nfunction updateChatgptParams(options: Partial<UserInfo>) {\n  userStore.updateUserInfo(options)\n  ms.success(t('common.success'))\n}\n</script>\n\n<template>\n  <div class=\"p-4 space-y-5 min-h-[200px]\">\n    <div class=\"space-y-6\">\n      <div class=\"flex flex-wrap items-center gap-4\">\n        <span class=\"flex-shrink-0 w-[100px]\">{{ $t('setting.chatgpt_memory_title') }}</span>\n        <div class=\"w-[300px]\">\n          <NSlider\n            v-model:value=\"chatgpt_memory\"\n            :marks=\"{\n              1: $t('setting.chatgpt_memory_choice_1'),\n              50: $t('setting.chatgpt_memory_choice_2'),\n              100: $t('setting.chatgpt_memory_choice_3'),\n            }\"\n            step=\"mark\"\n            @update:value=\"updateChatgptParams({ chatgpt_memory })\"\n          />\n        </div>\n      </div>\n      <div class=\"flex flex-wrap items-center gap-4\">\n        <span class=\"flex-shrink-0 w-[100px]\" />\n        <div class=\"w-[300px]  text-gray-500\">\n          {{ $t('setting.chatgpt_memory_memo') }}\n        </div>\n      </div>\n      <div class=\"flex flex-wrap items-center gap-4\">\n        <span class=\"flex-shrink-0 w-[100px]\">{{ $t('setting.chatgpt_top_p_title') }}</span>\n        <div class=\"w-[400px] text-gray-500\">\n          <NRadioGroup\n            v-model:value=\"chatgpt_top_p\"\n            name=\"radiobuttongroup2\"\n            size=\"medium\"\n            @update:value=\"updateChatgptParams({ chatgpt_top_p })\"\n          >\n            <NRadioButton\n              :key=\"0\"\n              :value=\"0\"\n            >\n              {{ $t('setting.chatgpt_top_p_choice_1') }}\n            </NRadioButton>\n            <NRadioButton\n              :key=\"50\"\n              :value=\"50\"\n            >\n              {{ $t('setting.chatgpt_top_p_choice_2') }}\n            </NRadioButton>\n            <NRadioButton\n              :key=\"100\"\n              :value=\"100\"\n            >\n              {{ $t('setting.chatgpt_top_p_choice_3') }}\n            </NRadioButton>\n          </NRadioGroup>\n        </div>\n      </div>\n      <div class=\"flex flex-wrap items-center gap-4\">\n        <span class=\"flex-shrink-0 w-[100px]\" />\n        <div class=\"w-[400px] text-gray-500\">\n          <span v-if=\"0 === chatgpt_top_p\">\n            {{ $t('setting.chatgpt_top_p_1_memo') }}\n          </span>\n          <span v-else-if=\"50 === chatgpt_top_p\">\n            {{ $t('setting.chatgpt_top_p_2_memo') }}\n          </span>\n          <span v-else>\n            {{ $t('setting.chatgpt_top_p_3_memo') }}\n          </span>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "src/components/common/Setting/General.vue",
    "content": "<script lang=\"ts\" setup>\nimport { computed, ref } from 'vue'\nimport { NButton, NImage, NInput, useMessage } from 'naive-ui'\nimport type { Language, Theme } from '@/store/modules/app/helper'\nimport { HoverButton, SvgIcon } from '@/components/common'\nimport { useAppStore, useUserStore } from '@/store'\nimport type { UserInfo } from '@/store/modules/user/helper'\nimport { t } from '@/locales'\n\ninterface Emit {\n  (event: 'update'): void\n}\n\nconst emit = defineEmits<Emit>()\n\nconst appStore = useAppStore()\nconst userStore = useUserStore()\n\nconst ms = useMessage()\n\nconst theme = computed(() => appStore.theme)\n\nconst userInfo = computed(() => userStore.userInfo)\n\nconst avatar = ref(userInfo.value.avatar ?? '')\n\nconst name = ref(userInfo.value.name ?? '')\n\nconst description = ref(userInfo.value.description ?? '')\n\nconst language = computed({\n  get() {\n    return appStore.language\n  },\n  set(value: Language) {\n    appStore.setLanguage(value)\n  },\n})\n\nconst themeOptions: { label: string; key: Theme; icon: string }[] = [\n  {\n    label: 'Auto',\n    key: 'auto',\n    icon: 'ri:contrast-line',\n  },\n  {\n    label: 'Light',\n    key: 'light',\n    icon: 'ri:sun-foggy-line',\n  },\n  {\n    label: 'Dark',\n    key: 'dark',\n    icon: 'ri:moon-foggy-line',\n  },\n]\n\nconst languageOptions: { label: string; key: Language; value: Language }[] = [\n  { label: '中文', key: 'zh-CN', value: 'zh-CN' },\n  { label: 'English', key: 'en-US', value: 'en-US' },\n  { label: '日本語', key: 'ja-JP', value: 'ja-JP' },\n]\n\nfunction updateUserInfo(options: Partial<UserInfo>) {\n  userStore.updateUserInfo(options)\n  ms.success(t('common.success'))\n}\n\nfunction randomAvatar() {\n  const avatar = `https://api.multiavatar.com/${Math.random()}.svg`\n  userStore.updateUserInfo({ avatar })\n  ms.success(t('common.success'))\n}\n\nfunction handleReset() {\n  userStore.resetUserInfo()\n  ms.success(t('common.success'))\n  emit('update')\n}\n</script>\n\n<template>\n  <div class=\"p-4 space-y-5 min-h-[200px]\">\n    <div class=\"space-y-6\">\n      <div class=\"flex items-center space-x-4\">\n        <span class=\"flex-shrink-0 w-[60px]\">{{ $t('setting.avatarLink') }}</span>\n        <div class=\"w-[200px]\">\n          <NInput v-model:value=\"avatar\" placeholder=\"\" />\n        </div>\n        <NButton size=\"small\" text type=\"primary\" @click=\"updateUserInfo({ avatar })\">\n          {{ $t('common.save') }}\n        </NButton>\n        <HoverButton :tooltip=\"$t('setting.randomAvatar')\" @click=\"randomAvatar()\">\n          <span class=\"text-xl text-[#4f555e] dark:text-white\">\n            <SvgIcon class=\"text-lg\" icon=\"mdi:dice-5-outline\" />\n          </span>\n        </HoverButton>\n      </div>\n      <div class=\"flex items-center space-x-4\">\n        <span class=\"flex-shrink-0 w-[60px]\" />\n        <div class=\"w-[200px]\">\n          <NImage\n            v-model:src=\"userInfo.avatar\"\n            width=\"100\"\n            class=\"rounded-full\"\n          />\n        </div>\n      </div>\n\n      <div class=\"flex items-center space-x-4\">\n        <span class=\"flex-shrink-0 w-[60px]\">{{ $t('setting.name') }}</span>\n        <div class=\"w-[200px]\">\n          <NInput v-model:value=\"name\" placeholder=\"\" />\n        </div>\n        <NButton size=\"small\" text type=\"primary\" @click=\"updateUserInfo({ name })\">\n          {{ $t('common.save') }}\n        </NButton>\n      </div>\n      <div class=\"flex items-center space-x-4\">\n        <span class=\"flex-shrink-0 w-[60px]\">{{ $t('setting.description') }}</span>\n        <div class=\"flex-1\">\n          <NInput v-model:value=\"description\" placeholder=\"\" />\n        </div>\n        <NButton size=\"small\" text type=\"primary\" @click=\"updateUserInfo({ description })\">\n          {{ $t('common.save') }}\n        </NButton>\n      </div>\n      <div class=\"flex items-center space-x-4\">\n        <span class=\"flex-shrink-0 w-[60px]\">{{ $t('setting.theme') }}</span>\n        <div class=\"flex flex-wrap items-center gap-4\">\n          <template v-for=\"item of themeOptions\" :key=\"item.key\">\n            <NButton\n              size=\"small\"\n              :type=\"item.key === theme ? 'primary' : undefined\"\n              @click=\"appStore.setTheme(item.key)\"\n            >\n              <template #icon>\n                <SvgIcon :icon=\"item.icon\" />\n              </template>\n            </NButton>\n          </template>\n        </div>\n      </div>\n      <div class=\"flex items-center space-x-4\">\n        <span class=\"flex-shrink-0 w-[60px]\">{{ $t('setting.language') }}</span>\n        <div class=\"flex flex-wrap items-center gap-4\">\n          <template v-for=\"item of languageOptions\" :key=\"item.key\">\n            <NButton\n              size=\"small\"\n              :type=\"item.key === language ? 'primary' : undefined\"\n              @click=\"appStore.setLanguage(item.key)\"\n            >\n              {{ item.label }}\n            </NButton>\n          </template>\n        </div>\n      </div>\n      <div class=\"flex items-center space-x-4\">\n        <span class=\"flex-shrink-0 w-[60px]\">{{ $t('setting.resetUserInfo') }}</span>\n        <NButton size=\"small\" text type=\"primary\" @click=\"handleReset\">\n          {{ $t('common.reset') }}\n        </NButton>\n      </div>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "src/components/common/Setting/index.vue",
    "content": "<script setup lang='ts'>\nimport { computed, ref } from 'vue'\nimport { NModal, NTabPane, NTabs } from 'naive-ui'\nimport General from './General.vue'\nimport Advance from './Advance.vue'\nimport About from './About.vue'\nimport { SvgIcon } from '@/components/common'\n\nconst props = defineProps<Props>()\n\nconst emit = defineEmits<Emit>()\n\ninterface Props {\n  visible: boolean\n}\n\ninterface Emit {\n  (e: 'update:visible', visible: boolean): void\n}\n\nconst active = ref('Advance')\n\nconst reload = ref(false)\n\nconst show = computed({\n  get() {\n    return props.visible\n  },\n  set(visible: boolean) {\n    emit('update:visible', visible)\n  },\n})\n\nfunction handleReload() {\n  reload.value = true\n  setTimeout(() => {\n    reload.value = false\n  }, 0)\n}\n</script>\n\n<template>\n  <NModal v-model:show=\"show\" :auto-focus=\"false\" preset=\"card\" style=\"width: 95%; max-width: 640px\">\n    <div>\n      <NTabs v-model:value=\"active\" type=\"line\" animated>\n        <NTabPane name=\"Advance\" tab=\"Advance\">\n          <template #tab>\n            <SvgIcon class=\"text-lg\" icon=\"ri:list-settings-line\" />\n            <span class=\"ml-2\">{{ $t('setting.advance') }}</span>\n          </template>\n          <Advance />\n        </NTabPane>\n        <NTabPane name=\"General\" tab=\"General\">\n          <template #tab>\n            <SvgIcon class=\"text-lg\" icon=\"ri:file-user-line\" />\n            <span class=\"ml-2\">{{ $t('setting.general') }}</span>\n          </template>\n          <div class=\"min-h-[100px]\">\n            <General v-if=\"!reload\" @update=\"handleReload\" />\n          </div>\n        </NTabPane>\n        <NTabPane name=\"Config\" tab=\"Config\">\n          <template #tab>\n            <SvgIcon class=\"text-lg\" icon=\"mdi:about-circle-outline\" />\n            <span class=\"ml-2\">{{ $t('setting.about') }}</span>\n          </template>\n          <About />\n        </NTabPane>\n      </NTabs>\n    </div>\n  </NModal>\n</template>\n"
  },
  {
    "path": "src/components/common/SvgIcon/index.vue",
    "content": "<script setup lang='ts'>\nimport { computed, useAttrs } from 'vue'\nimport { Icon } from '@iconify/vue'\n\ninterface Props {\n  icon?: string\n}\n\ndefineProps<Props>()\n\nconst attrs = useAttrs()\n\nconst bindAttrs = computed<{ class: string; style: string }>(() => ({\n  class: (attrs.class as string) || '',\n  style: (attrs.style as string) || '',\n}))\n</script>\n\n<template>\n  <Icon :icon=\"icon\" v-bind=\"bindAttrs\" />\n</template>\n"
  },
  {
    "path": "src/components/common/UserAvatar/index.vue",
    "content": "<script setup lang='ts'>\nimport { computed } from 'vue'\nimport { NAvatar } from 'naive-ui'\nimport { useUserStore } from '@/store'\nimport { isString } from '@/utils/is'\nimport { SvgIcon } from '@/components/common'\n\nconst userStore = useUserStore()\n\nconst userInfo = computed(() => userStore.userInfo)\n</script>\n\n<template>\n  <div class=\"flex items-center\">\n    <div class=\"w-10 h-10 overflow-hidden rounded-full\">\n      <template v-if=\"isString(userInfo.avatar) && userInfo.avatar.length > 0\">\n        <NAvatar\n          size=\"large\"\n          round\n          :src=\"userInfo.avatar\"\n        />\n      </template>\n      <template v-else>\n        <NAvatar size=\"large\" round />\n      </template>\n    </div>\n    <div class=\"ml-2\">\n      <h2 class=\"font-bold text-md flex \">\n        ChatGPT Web\n      </h2>\n      <p class=\"text-xs text-gray-500\">\n        <span\n          v-if=\"isString(userInfo.description) && userInfo.description !== ''\"\n          v-text=\"userInfo.description\"\n        />\n\n        <span>\n          <a\n            href=\"https://github.com/WenJing95/chatgpt-web\"\n            target=\"_blank\"\n            class=\"text-[#4b9e5f] relative flex items-center\"\n          >\n            View Source Code\n            <SvgIcon class=\"text-lg text-[#4b9e5f] ml-1\" icon=\"carbon:logo-github\" />\n          </a>\n        </span>\n      </p>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "src/components/common/index.ts",
    "content": "import HoverButton from './HoverButton/index.vue'\nimport NaiveProvider from './NaiveProvider/index.vue'\nimport SvgIcon from './SvgIcon/index.vue'\nimport UserAvatar from './UserAvatar/index.vue'\nimport Setting from './Setting/index.vue'\n\nexport { HoverButton, NaiveProvider, SvgIcon, UserAvatar, Setting }\n"
  },
  {
    "path": "src/components/custom/GithubSite.vue",
    "content": "<template>\n  <div class=\"text-neutral-400\">\n    <span>Forked and modified from </span>\n    <a href=\"https://github.com/Chanzhaoyu/chatgpt-bot\" target=\"_blank\" class=\"text-blue-500\">\n      Chanzhaoyu/chatgpt-web\n    </a>\n    <span>by</span>\n    <a href=\"https://github.com/WenJing95\" target=\"_blank\" class=\"text-blue-500\">\n      WenJing\n    </a>\n  </div>\n</template>\n"
  },
  {
    "path": "src/components/custom/index.ts",
    "content": "import GithubSite from './GithubSite.vue'\n\nexport { GithubSite }\n"
  },
  {
    "path": "src/hooks/useBasicLayout.ts",
    "content": "import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'\n\nexport function useBasicLayout() {\n  const breakpoints = useBreakpoints(breakpointsTailwind)\n  const isMobile = breakpoints.smaller('sm')\n\n  return { isMobile }\n}\n"
  },
  {
    "path": "src/hooks/useIconRender.ts",
    "content": "import { h } from 'vue'\nimport { SvgIcon } from '@/components/common'\n\nexport const useIconRender = () => {\n  interface IconConfig {\n    icon?: string\n    color?: string\n    fontSize?: number\n  }\n\n  interface IconStyle {\n    color?: string\n    fontSize?: string\n  }\n\n  const iconRender = (config: IconConfig) => {\n    const { color, fontSize, icon } = config\n\n    const style: IconStyle = {}\n\n    if (color)\n      style.color = color\n\n    if (fontSize)\n      style.fontSize = `${fontSize}px`\n\n    if (!icon)\n      window.console.warn('iconRender: icon is required')\n\n    return () => h(SvgIcon, { icon, style })\n  }\n\n  return {\n    iconRender,\n  }\n}\n"
  },
  {
    "path": "src/hooks/useLanguage.ts",
    "content": "import { computed } from 'vue'\nimport { enUS, jaJP, zhCN } from 'naive-ui'\nimport { useAppStore } from '@/store'\nimport { setLocale } from '@/locales'\n\nexport function useLanguage() {\n  const appStore = useAppStore()\n\n  const language = computed(() => {\n    switch (appStore.language) {\n      case 'zh-CN':\n        setLocale('zh-CN')\n        return zhCN\n      case 'en-US':\n        setLocale('en-US')\n        return enUS\n      case 'ja-JP':\n        setLocale('ja-JP')\n        return jaJP\n      default:\n        setLocale('zh-CN')\n        return zhCN\n    }\n  })\n\n  return { language }\n}\n"
  },
  {
    "path": "src/hooks/useTheme.ts",
    "content": "import type { GlobalThemeOverrides } from 'naive-ui'\nimport { computed, watch } from 'vue'\nimport { darkTheme, useOsTheme } from 'naive-ui'\nimport { useAppStore } from '@/store'\n\nexport function useTheme() {\n  const appStore = useAppStore()\n\n  const OsTheme = useOsTheme()\n\n  const isDark = computed(() => {\n    if (appStore.theme === 'auto')\n      return OsTheme.value === 'dark'\n    else\n      return appStore.theme === 'dark'\n  })\n\n  const theme = computed(() => {\n    return isDark.value ? darkTheme : undefined\n  })\n\n  const themeOverrides = computed<GlobalThemeOverrides>(() => {\n    if (isDark.value) {\n      return {\n        common: {},\n      }\n    }\n    return {}\n  })\n\n  watch(\n    () => isDark.value,\n    (dark) => {\n      if (dark)\n        document.documentElement.classList.add('dark')\n      else\n        document.documentElement.classList.remove('dark')\n    },\n    { immediate: true },\n  )\n\n  return { theme, themeOverrides }\n}\n"
  },
  {
    "path": "src/locales/en-US.ts",
    "content": "export default {\n  common: {\n    delete: 'Delete',\n    save: 'Save',\n    reset: 'Reset',\n    yes: 'Yes',\n    no: 'No',\n    noData: 'No Data',\n    wrong: 'Something went wrong, please try again later.',\n    success: 'Success',\n    failed: 'Failed',\n    about_head: 'This project was created by Chanzhaoyu, and has been forked and modified by WenJing. Its released under the MIT License.',\n    about_body: 'If you find this helpful, please give me a star on GitHub. If you would like to make a donate, please donate to the original author Chanzhaoyu. Thank you!',\n  },\n  chat: {\n    newChat: 'New Chat',\n    placeholder: 'Ask me anything...(Shift + Enter = line break)',\n    placeholderMobile: 'Ask me anything...',\n    copy: 'Copy',\n    copied: 'Copied',\n    copyCode: 'Copy Code',\n    clearChat: 'Clear Chat',\n    clearChatConfirm: 'Are you sure to clear this chat?',\n    deleteMessage: 'Delete Message',\n    deleteMessageConfirm: 'Are you sure to delete this message?',\n    deleteHistoryConfirm: 'Are you sure to clear this history?',\n    clickToTalk: 'Click to talk',\n    clickToSend: 'Click to send',\n    recordingInProgress: '[Recording in progress...]',\n    openMicrophoneFailedTitle: 'Microphone Opening Failed',\n    openMicrophoneFailedText: 'HTTPS environment and permission are required',\n    stopResponding: 'Stop Responding',\n    preview: 'Preview',\n    showRawText: 'Show as raw text',\n  },\n  setting: {\n    setting: 'Setting',\n    randomAvatar: 'Generate a random avatar',\n    general: 'General',\n    advance: 'Advance',\n    about: 'About',\n    avatarLink: 'Avatar Link',\n    name: 'Name',\n    description: 'Description',\n    resetUserInfo: 'Reset UserInfo',\n    theme: 'Theme',\n    language: 'Language',\n\n    chatgpt_memory_title: 'ChatGpt\\'s memory capacity',\n    chatgpt_memory_memo: 'The stronger the memory, the more context ChatGPT can remember during conversations, but it may consume more costs.',\n    chatgpt_memory_choice_1: 'Normal Memory (5 logs)',\n    chatgpt_memory_choice_2: 'Medium Memory (20 logs)',\n    chatgpt_memory_choice_3: 'Strong Memory (all logs)',\n\n    chatgpt_top_p_title: 'The personality of ChatGpt',\n    chatgpt_top_p_1_memo: 'Tends to precise analysis, reducing the possibility of ChatGPT\\'s nonsense.',\n    chatgpt_top_p_2_memo: 'Balancing accuracy and creativity in responses.',\n    chatgpt_top_p_3_memo: 'Brainstorming mode, tends to provide richer information.',\n    chatgpt_top_p_choice_1: 'Accurate',\n    chatgpt_top_p_choice_2: 'Balanced personality',\n    chatgpt_top_p_choice_3: 'Exploratory',\n\n    api: 'API',\n    timeout: 'Timeout',\n    socks: 'Socks',\n  },\n  server: {\n    PromptIsEmpty: 'Hello! How can I assist you today?',\n    NotComplyPolicy: 'Sorry, the content you have sent does not comply with our usage policy. Please note that our platform prohibits the publication of content that involves harassment, discrimination, violence, pornography, and other violations of laws, regulations, and social ethics. If you have any questions, please contact the developer for further assistance. Thank you.',\n    SomethingWrong: 'Oops, something went wrong. Please try again later.',\n    SomethingWrongInOpenaiGptApi: 'Oops, something went wrong in OpenAI GPT API. Please try again later.',\n    SomethingWrongInOpenaiModerationApi: 'Oops, something went wrong in OpenAI Moderation API. Please try again later.',\n    SomethingWrongInOpenaiWhisperApi: 'Oops, something went wrong in OpenAI Whisper API. Please try again later.',\n    SomethingWrongInLocalWhisper: 'Oops, something went wrong in local Whisper API. Please contact the developer for further assistance.',\n  },\n\n}\n"
  },
  {
    "path": "src/locales/index.ts",
    "content": "import type { App } from 'vue'\nimport { createI18n } from 'vue-i18n'\nimport en from './en-US'\nimport cn from './zh-CN'\nimport jp from './ja-JP'\nimport { useAppStoreWithOut } from '@/store/modules/app'\nimport type { Language } from '@/store/modules/app/helper'\n\nconst appStore = useAppStoreWithOut()\n\nconst defaultLocale = appStore.language || 'zh-CN'\n\nconst i18n = createI18n({\n  locale: defaultLocale,\n  fallbackLocale: 'en-US',\n  allowComposition: true,\n  messages: {\n    'en-US': en,\n    'zh-CN': cn,\n    'ja-JP': jp,\n  },\n})\n\nexport function t(key: string) {\n  return i18n.global.t(key)\n}\n\nexport function setLocale(locale: Language) {\n  i18n.global.locale = locale\n}\n\nexport function setupI18n(app: App) {\n  app.use(i18n)\n}\n\nexport default i18n\n"
  },
  {
    "path": "src/locales/ja-JP.ts",
    "content": "export default {\n  common: {\n    delete: '削除',\n    save: '保存する',\n    reset: 'リセット',\n    yes: 'はい',\n    no: 'いいえ',\n    noData: 'データなし',\n    wrong: '問題が発生しました。後でもう一度試してください。',\n    success: '成功しました',\n    failed: '失敗しました',\n    about_head: '作成者はChanzhaoyuで、編集者はWenJingです。ライセンスはMITです。',\n    about_body: 'もしプロジェクトが役に立った場合は、Githubでスターをつけていただくか、元の作者に寄付をご検討いただけると幸いです。',\n  },\n  chat: {\n    newChat: '新しい会話',\n    placeholder: '何でも聞いてください...（Shift + Enter = 改行）',\n    placeholderMobile: '何でも聞いてください...',\n    copy: 'コピー',\n    copied: 'コピー済み',\n    copyCode: 'コードをコピー',\n    clearChat: 'チャットをクリア',\n    clearChatConfirm: 'このチャットをクリアしてもよろしいですか？',\n    deleteMessage: 'メッセージを削除',\n    deleteMessageConfirm: 'このメッセージを削除してもよろしいですか？',\n    deleteHistoryConfirm: 'この履歴をクリアしてもよろしいですか？',\n    clickToTalk: 'クリックして録音開始',\n    clickToSend: '送信',\n    recordingInProgress: '[録音中...]',\n    openMicrophoneFailedTitle: 'マイクのオープンに失敗しました',\n    openMicrophoneFailedText: 'HTTPS環境下で、設定でマイクの使用が許可されていることを確認してください',\n    stopResponding: '応答を停止する',\n    preview: 'プレビュー',\n    showRawText: '生のテキストを表示する',\n  },\n  setting: {\n    setting: '設定',\n    randomAvatar: 'アバターをランダムに生成する',\n    general: '一般',\n    advance: '高度な設定',\n    about: 'このアプリについて',\n    avatarLink: 'アバターリンク',\n    name: '名前',\n    description: '説明',\n    resetUserInfo: 'ユーザー情報をリセット',\n    theme: 'テーマ',\n    language: '言語',\n\n    chatgpt_memory_title: '記憶力',\n    chatgpt_memory_memo: '記憶力が強いほど、ChatGptは会話中に覚えている文脈が多くなりますが、より多くのコストがかかる可能性があります。',\n    chatgpt_memory_choice_1: '記憶力が弱い(5件)',\n    chatgpt_memory_choice_2: '記憶力が普通(20件)',\n    chatgpt_memory_choice_3: '記憶力が強い(すべて)',\n\n    chatgpt_top_p_title: '性格',\n    chatgpt_top_p_1_memo: '正確な分析に傾くことで、ChatGptの無意味な発言の可能性を減らします。',\n    chatgpt_top_p_2_memo: '回答の正確さと創造性のバランスを兼ね備える。',\n    chatgpt_top_p_3_memo: 'ブレインストーミングモードで、より豊富な情報を提供する傾向があります。',\n    chatgpt_top_p_choice_1: '正確性重視',\n    chatgpt_top_p_choice_2: '一石二鳥',\n    chatgpt_top_p_choice_3: 'アイデア出し重視',\n\n    api: 'API',\n    timeout: 'タイムアウト',\n    socks: 'ソックス',\n  },\n  server: {\n    PromptIsEmpty: 'こんにちは！今日は何かお手伝いできますか？',\n    NotComplyPolicy: '申し訳ありませんが、送信されたコンテンツが私たちの使用ポリシーに準拠していません。当社のプラットフォームでは、ハラスメント、差別、暴力、ポルノグラフィ、その他の法律、規制、社会倫理に違反するコンテンツの投稿を禁止しています。ご質問がある場合は、開発者にお問い合わせいただくか、サポートをご利用ください。ありがとうございます。',\n    SomethingWrong: '申し訳ありませんが、問題が発生しました。後でもう一度お試しください。',\n    SomethingWrongInOpenaiGptApi: '申し訳ありませんが、OpenAI GPT APIへのアクセス中に問題が発生しました。後でもう一度お試しください。',\n    SomethingWrongInOpenaiModerationApi: '申し訳ありませんが、OpenAI Moderation APIへのアクセス中に問題が発生しました。後でもう一度お試しください。',\n    SomethingWrongInOpenaiWhisperApi: '申し訳ありませんが、OpenAI Whisper APIへのアクセス中に問題が発生しました。後でもう一度お試しください。',\n    SomethingWrongInLocalWhisper: '申し訳ありませんが、Local Whisper APIへのアクセス中に問題が発生しました。開発者に連絡してさらなるサポートを求めてください。',\n  },\n}\n"
  },
  {
    "path": "src/locales/zh-CN.ts",
    "content": "export default {\n  common: {\n    delete: '删除',\n    save: '保存',\n    reset: '重置',\n    yes: '是',\n    no: '否',\n    noData: '暂无数据',\n    wrong: '出错了，请稍后再试',\n    success: '操作成功',\n    failed: '操作失败',\n    about_head: '本项目原作者为Chanzhaoyu, 经WenJing分叉和修改，基于 MIT 协议开源。',\n    about_body: '如果你觉得此项目对你有帮助，请在Github给我点个Star；如果你希望捐助，请捐助原作者，谢谢你！',\n  },\n  chat: {\n    newChat: '新的对话',\n    placeholder: '有问题尽管问我...(Shift + Enter = 换行)',\n    placeholderMobile: '有问题尽管问我...',\n    copy: '复制',\n    copied: '复制成功',\n    copyCode: '复制代码',\n    clearChat: '清空会话',\n    clearChatConfirm: '是否清空会话?',\n    deleteMessage: '删除消息',\n    deleteMessageConfirm: '是否删除此消息?',\n    deleteHistoryConfirm: '确定删除此记录?',\n    clickToTalk: '点我开始录音',\n    clickToSend: '正在录音，点击发送',\n    recordingInProgress: '[正在录音...]',\n    openMicrophoneFailedTitle: '打开麦克风失败',\n    openMicrophoneFailedText: '需要https环境并且在设置中开启权限',\n    stopResponding: '停止回复',\n    preview: '预览',\n    showRawText: '显示原文',\n  },\n  setting: {\n    setting: '设置',\n    randomAvatar: '随机生成一个头像',\n    general: '一般',\n    advance: '高级',\n    about: '关于',\n    avatarLink: '头像链接',\n    name: '名称',\n    description: '描述',\n    resetUserInfo: '重置用户信息',\n    theme: '主题',\n    language: '语言',\n\n    chatgpt_memory_title: '记忆力',\n    chatgpt_memory_memo: '记忆力越强，ChatGPT 在对话过程中能记住的上下文越多，但可能会消耗更多的费用',\n    chatgpt_memory_choice_1: '普通记忆(5条)',\n    chatgpt_memory_choice_2: '中等记忆(20条)',\n    chatgpt_memory_choice_3: '最强记忆(全部)',\n\n    chatgpt_top_p_title: '性格',\n    chatgpt_top_p_1_memo: '倾向于提供精确的分析，减少ChatGpt胡说八道的可能性',\n    chatgpt_top_p_2_memo: '兼顾回答的准确性和想象力',\n    chatgpt_top_p_3_memo: '倾向于提供更丰富的信息',\n    chatgpt_top_p_choice_1: '准确可信',\n    chatgpt_top_p_choice_2: '平衡性格',\n    chatgpt_top_p_choice_3: '发散思维',\n\n    api: 'API',\n    timeout: '超时',\n    socks: 'Socks',\n  },\n  server: {\n    PromptIsEmpty: '你好！今天我能为您提供什么帮助？',\n    NotComplyPolicy: '对不起，您发送的内容不符合我们的使用政策。请注意，我们的平台禁止发布涉及骚扰、歧视、暴力、色情等违反法律法规和社会道德的内容。如有疑问，请联系开发者获取更多帮助。谢谢。',\n    SomethingWrong: '出错了，请稍后再试',\n    SomethingWrongInOpenaiGptApi: '访问OpenAI GPT API出错，请稍后再试',\n    SomethingWrongInOpenaiModerationApi: '访问OpenAI Moderation API出错，请稍后再试',\n    SomethingWrongInOpenaiWhisperApi: '访问OpenAI Whisper API出错，请稍后再试',\n    SomethingWrongInLocalWhisper: '访问本地Whisper API出错，请联系开发者获取更多帮助',\n  },\n\n}\n"
  },
  {
    "path": "src/main.ts",
    "content": "import { createApp } from 'vue'\nimport App from './App.vue'\nimport { setupI18n } from './locales'\nimport { setupAssets, setupScrollbarStyle } from './plugins'\nimport { setupStore } from './store'\nimport { setupRouter } from './router'\n\nasync function bootstrap() {\n  const app = createApp(App)\n  setupAssets()\n\n  setupScrollbarStyle()\n\n  setupStore(app)\n\n  setupI18n(app)\n\n  await setupRouter(app)\n\n  app.mount('#app')\n}\n\nbootstrap()\n"
  },
  {
    "path": "src/plugins/assets.ts",
    "content": "import 'katex/dist/katex.min.css'\nimport '@/styles/lib/tailwind.css'\nimport '@/styles/lib/highlight.less'\nimport '@/styles/lib/github-markdown.less'\nimport '@/styles/global.less'\n\n/** Tailwind's Preflight Style Override */\nfunction naiveStyleOverride() {\n  const meta = document.createElement('meta')\n  meta.name = 'naive-ui-style'\n  document.head.appendChild(meta)\n}\n\nfunction setupAssets() {\n  naiveStyleOverride()\n}\n\nexport default setupAssets\n"
  },
  {
    "path": "src/plugins/index.ts",
    "content": "import setupAssets from './assets'\nimport setupScrollbarStyle from './scrollbarStyle'\n\nexport { setupAssets, setupScrollbarStyle }\n"
  },
  {
    "path": "src/plugins/scrollbarStyle.ts",
    "content": "import { darkTheme, lightTheme } from 'naive-ui'\n\nconst setupScrollbarStyle = () => {\n  const style = document.createElement('style')\n  const styleContent = `\n    ::-webkit-scrollbar {\n      background-color: transparent;\n      width: ${lightTheme.Scrollbar.common?.scrollbarWidth};\n    }\n    ::-webkit-scrollbar-thumb {\n      background-color: ${lightTheme.Scrollbar.common?.scrollbarColor};\n      border-radius: ${lightTheme.Scrollbar.common?.scrollbarBorderRadius};\n    }\n    html.dark ::-webkit-scrollbar {\n      background-color: transparent;\n      width: ${darkTheme.Scrollbar.common?.scrollbarWidth};\n    }\n    html.dark ::-webkit-scrollbar-thumb {\n      background-color: ${darkTheme.Scrollbar.common?.scrollbarColor};\n      border-radius: ${darkTheme.Scrollbar.common?.scrollbarBorderRadius};\n    }\n  `\n\n  style.innerHTML = styleContent\n  document.head.appendChild(style)\n}\n\nexport default setupScrollbarStyle\n"
  },
  {
    "path": "src/router/index.ts",
    "content": "import type { App } from 'vue'\nimport type { RouteRecordRaw } from 'vue-router'\nimport { createRouter, createWebHashHistory } from 'vue-router'\nimport { ChatLayout } from '@/views/chat/layout'\n\nconst routes: RouteRecordRaw[] = [\n  {\n    path: '/',\n    name: 'Root',\n    component: ChatLayout,\n    redirect: '/chat',\n    children: [\n      {\n        path: '/chat/:uuid?',\n        name: 'Chat',\n        component: () => import('@/views/chat/index.vue'),\n      },\n    ],\n  },\n\n  {\n    path: '/403',\n    name: '403',\n    component: () => import('@/views/exception/403/index.vue'),\n  },\n\n  {\n    path: '/404',\n    name: '404',\n    component: () => import('@/views/exception/404/index.vue'),\n  },\n\n  {\n    path: '/:pathMatch(.*)*',\n    name: 'notFound',\n    redirect: '/404',\n  },\n]\n\nexport const router = createRouter({\n  history: createWebHashHistory(),\n  routes,\n  scrollBehavior: () => ({ left: 0, top: 0 }),\n})\n\nexport async function setupRouter(app: App) {\n  app.use(router)\n  await router.isReady()\n}\n"
  },
  {
    "path": "src/store/index.ts",
    "content": "import type { App } from 'vue'\nimport { createPinia } from 'pinia'\n\nexport const store = createPinia()\n\nexport function setupStore(app: App) {\n  app.use(store)\n}\n\nexport * from './modules'\n"
  },
  {
    "path": "src/store/modules/app/helper.ts",
    "content": "import { ss } from '@/utils/storage'\n\nconst LOCAL_NAME = 'appSetting'\nconst ZH_CN = 'zh-CN'\nconst EN_US = 'en-US'\nconst JA_JP = 'ja-JP'\n\nexport type Theme = 'light' | 'dark' | 'auto'\n\nexport type Language = typeof ZH_CN | typeof EN_US | typeof JA_JP\n\nexport type focusTextarea = true\n\nexport interface AppState {\n  siderCollapsed: boolean\n  theme: Theme\n  language: Language\n  focusTextarea: focusTextarea\n}\n\nexport function defaultSetting(): AppState {\n  const browserLanguage = window.navigator.language || ZH_CN\n  let defaultLanguage: string\n  if (browserLanguage.includes('zh'))\n    defaultLanguage = ZH_CN\n\n  else if (browserLanguage.includes('ja'))\n    defaultLanguage = JA_JP\n\n  else if (browserLanguage.includes('en'))\n    defaultLanguage = EN_US\n\n  else\n    defaultLanguage = ZH_CN\n\n  return { siderCollapsed: false, theme: 'dark', language: defaultLanguage as Language, focusTextarea: true }\n}\n\nexport function getLocalSetting(): AppState {\n  const localSetting: AppState | undefined = ss.get(LOCAL_NAME)\n  return { ...defaultSetting(), ...localSetting }\n}\n\nexport function setLocalSetting(setting: AppState): void {\n  ss.set(LOCAL_NAME, setting)\n}\n"
  },
  {
    "path": "src/store/modules/app/index.ts",
    "content": "import { defineStore } from 'pinia'\nimport type { AppState, Language, Theme } from './helper'\nimport { getLocalSetting, setLocalSetting } from './helper'\nimport { store } from '@/store'\n\nexport const useAppStore = defineStore('app-store', {\n  state: (): AppState => getLocalSetting(),\n  actions: {\n    setSiderCollapsed(collapsed: boolean) {\n      this.siderCollapsed = collapsed\n      this.recordState()\n    },\n\n    setTheme(theme: Theme) {\n      this.theme = theme\n      this.recordState()\n    },\n\n    setLanguage(language: Language) {\n      if (this.language !== language) {\n        this.language = language\n        this.recordState()\n      }\n    },\n\n    setFocusTextarea() {\n      this.focusTextarea = true\n      this.recordState()\n    },\n\n    recordState() {\n      setLocalSetting(this.$state)\n    },\n  },\n})\n\nexport function useAppStoreWithOut() {\n  return useAppStore(store)\n}\n"
  },
  {
    "path": "src/store/modules/chat/helper.ts",
    "content": "import { ss } from '@/utils/storage'\n\nconst LOCAL_NAME = 'chatStorage'\n\nexport function defaultState(): Chat.ChatState {\n  const uuid = 1002\n  return { active: uuid, history: [{ uuid, title: 'New Chat', isEdit: false }], chat: [{ uuid, data: [] }] }\n}\n\nexport function getLocalState(): Chat.ChatState {\n  const localState = ss.get(LOCAL_NAME)\n  return localState ?? defaultState()\n}\n\nexport function setLocalState(state: Chat.ChatState) {\n  ss.set(LOCAL_NAME, state)\n}\n"
  },
  {
    "path": "src/store/modules/chat/index.ts",
    "content": "import { defineStore } from 'pinia'\nimport { getLocalState, setLocalState } from './helper'\nimport { router } from '@/router'\n\nexport const useChatStore = defineStore('chat-store', {\n  state: (): Chat.ChatState => getLocalState(),\n\n  getters: {\n    getChatHistoryByCurrentActive(state: Chat.ChatState) {\n      const index = state.history.findIndex(item => item.uuid === state.active)\n      if (index !== -1)\n        return state.history[index]\n      return null\n    },\n\n    getChatByUuid(state: Chat.ChatState) {\n      return (uuid?: number) => {\n        if (uuid)\n          return state.chat.find(item => item.uuid === uuid)?.data ?? []\n        return state.chat.find(item => item.uuid === state.active)?.data ?? []\n      }\n    },\n  },\n\n  actions: {\n    addHistory(history: Chat.History, chatData: Chat.Chat[] = []) {\n      this.history.unshift(history)\n      this.chat.unshift({ uuid: history.uuid, data: chatData })\n      this.active = history.uuid\n      this.reloadRoute(history.uuid)\n    },\n\n    updateHistory(uuid: number, edit: Partial<Chat.History>) {\n      const index = this.history.findIndex(item => item.uuid === uuid)\n      if (index !== -1) {\n        this.history[index] = { ...this.history[index], ...edit }\n        this.recordState()\n      }\n    },\n\n    async deleteHistory(index: number) {\n      this.history.splice(index, 1)\n      this.chat.splice(index, 1)\n\n      if (this.history.length === 0) {\n        this.active = null\n        this.reloadRoute()\n        return\n      }\n\n      if (index > 0 && index <= this.history.length) {\n        const uuid = this.history[index - 1].uuid\n        this.active = uuid\n        this.reloadRoute(uuid)\n        return\n      }\n\n      if (index === 0) {\n        if (this.history.length > 0) {\n          const uuid = this.history[0].uuid\n          this.active = uuid\n          this.reloadRoute(uuid)\n        }\n      }\n\n      if (index > this.history.length) {\n        const uuid = this.history[this.history.length - 1].uuid\n        this.active = uuid\n        this.reloadRoute(uuid)\n      }\n    },\n\n    async setActive(uuid: number) {\n      this.active = uuid\n      return await this.reloadRoute(uuid)\n    },\n\n    getChatByUuidAndIndex(uuid: number, index: number) {\n      if (!uuid || uuid === 0) {\n        if (this.chat.length)\n          return this.chat[0].data[index]\n        return null\n      }\n      const chatIndex = this.chat.findIndex(item => item.uuid === uuid)\n      if (chatIndex !== -1)\n        return this.chat[chatIndex].data[index]\n      return null\n    },\n\n    addChatByUuid(uuid: number, chat: Chat.Chat, newChatText: string) {\n      if (!uuid || uuid === 0) {\n        if (this.history.length === 0) {\n          const uuid = Date.now()\n          this.history.push({ uuid, title: chat.text, isEdit: false })\n          this.chat.push({ uuid, data: [chat] })\n          this.active = uuid\n          this.recordState()\n        }\n        else {\n          this.chat[0].data.push(chat)\n          if (this.history[0].title === newChatText)\n            this.history[0].title = chat.text\n          this.recordState()\n        }\n      }\n\n      const index = this.chat.findIndex(item => item.uuid === uuid)\n      if (index !== -1) {\n        this.chat[index].data.push(chat)\n        if (this.history[index].title === newChatText)\n          this.history[index].title = chat.text\n        this.recordState()\n      }\n    },\n\n    updateChatByUuid(uuid: number, index: number, chat: Chat.Chat) {\n      if (!uuid || uuid === 0) {\n        if (this.chat.length) {\n          this.chat[0].data[index] = chat\n          this.recordState()\n        }\n        return\n      }\n\n      const chatIndex = this.chat.findIndex(item => item.uuid === uuid)\n      if (chatIndex !== -1) {\n        this.chat[chatIndex].data[index] = chat\n        this.recordState()\n      }\n    },\n\n    updateChatSomeByUuid(uuid: number, index: number, chat: Partial<Chat.Chat>) {\n      if (!uuid || uuid === 0) {\n        if (this.chat.length) {\n          this.chat[0].data[index] = { ...this.chat[0].data[index], ...chat }\n          this.recordState()\n        }\n        return\n      }\n\n      const chatIndex = this.chat.findIndex(item => item.uuid === uuid)\n      if (chatIndex !== -1) {\n        this.chat[chatIndex].data[index] = { ...this.chat[chatIndex].data[index], ...chat }\n        this.recordState()\n      }\n    },\n\n    deleteChatByUuid(uuid: number, index: number) {\n      if (!uuid || uuid === 0) {\n        if (this.chat.length) {\n          this.chat[0].data.splice(index, 1)\n          this.recordState()\n        }\n        return\n      }\n\n      const chatIndex = this.chat.findIndex(item => item.uuid === uuid)\n      if (chatIndex !== -1) {\n        this.chat[chatIndex].data.splice(index, 1)\n        this.recordState()\n      }\n    },\n\n    clearChatByUuid(uuid: number, chat_title: string) {\n      if (!uuid || uuid === 0) {\n        if (this.chat.length) {\n          this.chat[0].data = []\n          this.history[0].title = chat_title\n          this.recordState()\n        }\n        return\n      }\n\n      const index = this.chat.findIndex(item => item.uuid === uuid)\n      if (index !== -1) {\n        this.chat[index].data = []\n        this.recordState()\n      }\n    },\n\n    async reloadRoute(uuid?: number) {\n      this.recordState()\n      await router.push({ name: 'Chat', params: { uuid } })\n    },\n\n    recordState() {\n      setLocalState(this.$state)\n    },\n  },\n})\n"
  },
  {
    "path": "src/store/modules/index.ts",
    "content": "export * from './app'\nexport * from './chat'\nexport * from './user'\n"
  },
  {
    "path": "src/store/modules/user/helper.ts",
    "content": "import { ss } from '@/utils/storage'\n\nconst LOCAL_NAME = 'userStorage'\n\nexport interface UserInfo {\n  avatar: string\n  name: string\n  description: string\n  chatgpt_top_p: number\n  chatgpt_memory: number\n}\n\nexport interface UserState {\n  userInfo: UserInfo\n}\n\nexport function defaultSetting(): UserState {\n  return {\n    userInfo: {\n      avatar: 'https://api.multiavatar.com/0.8481955987976837.svg',\n      name: 'ChatGPT Web',\n\n      description: '',\n      chatgpt_top_p: 100,\n      chatgpt_memory: 50,\n    },\n  }\n}\n\nexport function getLocalState(): UserState {\n  const localSetting: UserState | undefined = ss.get(LOCAL_NAME)\n  return { ...defaultSetting(), ...localSetting }\n}\n\nexport function setLocalState(setting: UserState): void {\n  ss.set(LOCAL_NAME, setting)\n}\n"
  },
  {
    "path": "src/store/modules/user/index.ts",
    "content": "import { defineStore } from 'pinia'\nimport type { UserInfo, UserState } from './helper'\nimport { defaultSetting, getLocalState, setLocalState } from './helper'\n\nexport const useUserStore = defineStore('user-store', {\n  state: (): UserState => getLocalState(),\n  actions: {\n    updateUserInfo(userInfo: Partial<UserInfo>) {\n      this.userInfo = { ...this.userInfo, ...userInfo }\n      this.recordState()\n    },\n\n    resetUserInfo() {\n      this.userInfo = { ...defaultSetting().userInfo }\n      this.recordState()\n    },\n\n    recordState() {\n      setLocalState(this.$state)\n    },\n  },\n})\n"
  },
  {
    "path": "src/styles/global.less",
    "content": "html,\nbody,\n#app {\n\theight: 100%;\n}\n\nbody {\n\tpadding-bottom: env(safe-area-inset-bottom);\n}\n\n.clickable-element {\n\tcursor: pointer;\n}\n"
  },
  {
    "path": "src/styles/lib/github-markdown.less",
    "content": "html.dark {\n  .markdown-body {\n    color-scheme: dark;\n    --color-prettylights-syntax-comment: #8b949e;\n    --color-prettylights-syntax-constant: #79c0ff;\n    --color-prettylights-syntax-entity: #d2a8ff;\n    --color-prettylights-syntax-storage-modifier-import: #c9d1d9;\n    --color-prettylights-syntax-entity-tag: #7ee787;\n    --color-prettylights-syntax-keyword: #ff7b72;\n    --color-prettylights-syntax-string: #a5d6ff;\n    --color-prettylights-syntax-variable: #ffa657;\n    --color-prettylights-syntax-brackethighlighter-unmatched: #f85149;\n    --color-prettylights-syntax-invalid-illegal-text: #f0f6fc;\n    --color-prettylights-syntax-invalid-illegal-bg: #8e1519;\n    --color-prettylights-syntax-carriage-return-text: #f0f6fc;\n    --color-prettylights-syntax-carriage-return-bg: #b62324;\n    --color-prettylights-syntax-string-regexp: #7ee787;\n    --color-prettylights-syntax-markup-list: #f2cc60;\n    --color-prettylights-syntax-markup-heading: #1f6feb;\n    --color-prettylights-syntax-markup-italic: #c9d1d9;\n    --color-prettylights-syntax-markup-bold: #c9d1d9;\n    --color-prettylights-syntax-markup-deleted-text: #ffdcd7;\n    --color-prettylights-syntax-markup-deleted-bg: #67060c;\n    --color-prettylights-syntax-markup-inserted-text: #aff5b4;\n    --color-prettylights-syntax-markup-inserted-bg: #033a16;\n    --color-prettylights-syntax-markup-changed-text: #ffdfb6;\n    --color-prettylights-syntax-markup-changed-bg: #5a1e02;\n    --color-prettylights-syntax-markup-ignored-text: #c9d1d9;\n    --color-prettylights-syntax-markup-ignored-bg: #1158c7;\n    --color-prettylights-syntax-meta-diff-range: #d2a8ff;\n    --color-prettylights-syntax-brackethighlighter-angle: #8b949e;\n    --color-prettylights-syntax-sublimelinter-gutter-mark: #484f58;\n    --color-prettylights-syntax-constant-other-reference-link: #a5d6ff;\n    --color-fg-default: #c9d1d9;\n    --color-fg-muted: #8b949e;\n    --color-fg-subtle: #6e7681;\n    --color-canvas-default: #0d1117;\n    --color-canvas-subtle: #161b22;\n    --color-border-default: #30363d;\n    --color-border-muted: #21262d;\n    --color-neutral-muted: rgba(110,118,129,0.4);\n    --color-accent-fg: #58a6ff;\n    --color-accent-emphasis: #1f6feb;\n    --color-attention-subtle: rgba(187,128,9,0.15);\n    --color-danger-fg: #f85149;\n  }\n}\n\nhtml {\n  .markdown-body {\n    color-scheme: light;\n    --color-prettylights-syntax-comment: #6e7781;\n    --color-prettylights-syntax-constant: #0550ae;\n    --color-prettylights-syntax-entity: #8250df;\n    --color-prettylights-syntax-storage-modifier-import: #24292f;\n    --color-prettylights-syntax-entity-tag: #116329;\n    --color-prettylights-syntax-keyword: #cf222e;\n    --color-prettylights-syntax-string: #0a3069;\n    --color-prettylights-syntax-variable: #953800;\n    --color-prettylights-syntax-brackethighlighter-unmatched: #82071e;\n    --color-prettylights-syntax-invalid-illegal-text: #f6f8fa;\n    --color-prettylights-syntax-invalid-illegal-bg: #82071e;\n    --color-prettylights-syntax-carriage-return-text: #f6f8fa;\n    --color-prettylights-syntax-carriage-return-bg: #cf222e;\n    --color-prettylights-syntax-string-regexp: #116329;\n    --color-prettylights-syntax-markup-list: #3b2300;\n    --color-prettylights-syntax-markup-heading: #0550ae;\n    --color-prettylights-syntax-markup-italic: #24292f;\n    --color-prettylights-syntax-markup-bold: #24292f;\n    --color-prettylights-syntax-markup-deleted-text: #82071e;\n    --color-prettylights-syntax-markup-deleted-bg: #ffebe9;\n    --color-prettylights-syntax-markup-inserted-text: #116329;\n    --color-prettylights-syntax-markup-inserted-bg: #dafbe1;\n    --color-prettylights-syntax-markup-changed-text: #953800;\n    --color-prettylights-syntax-markup-changed-bg: #ffd8b5;\n    --color-prettylights-syntax-markup-ignored-text: #eaeef2;\n    --color-prettylights-syntax-markup-ignored-bg: #0550ae;\n    --color-prettylights-syntax-meta-diff-range: #8250df;\n    --color-prettylights-syntax-brackethighlighter-angle: #57606a;\n    --color-prettylights-syntax-sublimelinter-gutter-mark: #8c959f;\n    --color-prettylights-syntax-constant-other-reference-link: #0a3069;\n    --color-fg-default: #24292f;\n    --color-fg-muted: #57606a;\n    --color-fg-subtle: #6e7781;\n    --color-canvas-default: #ffffff;\n    --color-canvas-subtle: #f6f8fa;\n    --color-border-default: #d0d7de;\n    --color-border-muted: hsla(210,18%,87%,1);\n    --color-neutral-muted: rgba(175,184,193,0.2);\n    --color-accent-fg: #0969da;\n    --color-accent-emphasis: #0969da;\n    --color-attention-subtle: #fff8c5;\n    --color-danger-fg: #cf222e;\n  }\n}\n\n.markdown-body {\n  -ms-text-size-adjust: 100%;\n  -webkit-text-size-adjust: 100%;\n  margin: 0;\n  color: var(--color-fg-default);\n  background-color: var(--color-canvas-default);\n  font-family: -apple-system,BlinkMacSystemFont,\"Segoe UI\",\"Noto Sans\",Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\";\n  font-size: 16px;\n  line-height: 1.5;\n  word-wrap: break-word;\n}\n\n.markdown-body .octicon {\n  display: inline-block;\n  fill: currentColor;\n  vertical-align: text-bottom;\n}\n\n.markdown-body h1:hover .anchor .octicon-link:before,\n.markdown-body h2:hover .anchor .octicon-link:before,\n.markdown-body h3:hover .anchor .octicon-link:before,\n.markdown-body h4:hover .anchor .octicon-link:before,\n.markdown-body h5:hover .anchor .octicon-link:before,\n.markdown-body h6:hover .anchor .octicon-link:before {\n  width: 16px;\n  height: 16px;\n  content: ' ';\n  display: inline-block;\n  background-color: currentColor;\n  -webkit-mask-image: url(\"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' aria-hidden='true'><path fill-rule='evenodd' d='M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z'></path></svg>\");\n  mask-image: url(\"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' aria-hidden='true'><path fill-rule='evenodd' d='M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z'></path></svg>\");\n}\n\n.markdown-body details,\n.markdown-body figcaption,\n.markdown-body figure {\n  display: block;\n}\n\n.markdown-body summary {\n  display: list-item;\n}\n\n.markdown-body [hidden] {\n  display: none !important;\n}\n\n.markdown-body a {\n  background-color: transparent;\n  color: var(--color-accent-fg);\n  text-decoration: none;\n}\n\n.markdown-body abbr[title] {\n  border-bottom: none;\n  text-decoration: underline dotted;\n}\n\n.markdown-body b,\n.markdown-body strong {\n  font-weight: var(--base-text-weight-semibold, 600);\n}\n\n.markdown-body dfn {\n  font-style: italic;\n}\n\n.markdown-body h1 {\n  margin: .67em 0;\n  font-weight: var(--base-text-weight-semibold, 600);\n  padding-bottom: .3em;\n  font-size: 2em;\n  border-bottom: 1px solid var(--color-border-muted);\n}\n\n.markdown-body mark {\n  background-color: var(--color-attention-subtle);\n  color: var(--color-fg-default);\n}\n\n.markdown-body small {\n  font-size: 90%;\n}\n\n.markdown-body sub,\n.markdown-body sup {\n  font-size: 75%;\n  line-height: 0;\n  position: relative;\n  vertical-align: baseline;\n}\n\n.markdown-body sub {\n  bottom: -0.25em;\n}\n\n.markdown-body sup {\n  top: -0.5em;\n}\n\n.markdown-body img {\n  border-style: none;\n  max-width: 100%;\n  box-sizing: content-box;\n  background-color: var(--color-canvas-default);\n}\n\n.markdown-body code,\n.markdown-body kbd,\n.markdown-body pre,\n.markdown-body samp {\n  font-family: monospace;\n  font-size: 1em;\n}\n\n.markdown-body figure {\n  margin: 1em 40px;\n}\n\n.markdown-body hr {\n  box-sizing: content-box;\n  overflow: hidden;\n  background: transparent;\n  border-bottom: 1px solid var(--color-border-muted);\n  height: .25em;\n  padding: 0;\n  margin: 24px 0;\n  background-color: var(--color-border-default);\n  border: 0;\n}\n\n.markdown-body input {\n  font: inherit;\n  margin: 0;\n  overflow: visible;\n  font-family: inherit;\n  font-size: inherit;\n  line-height: inherit;\n}\n\n.markdown-body [type=button],\n.markdown-body [type=reset],\n.markdown-body [type=submit] {\n  -webkit-appearance: button;\n}\n\n.markdown-body [type=checkbox],\n.markdown-body [type=radio] {\n  box-sizing: border-box;\n  padding: 0;\n}\n\n.markdown-body [type=number]::-webkit-inner-spin-button,\n.markdown-body [type=number]::-webkit-outer-spin-button {\n  height: auto;\n}\n\n.markdown-body [type=search]::-webkit-search-cancel-button,\n.markdown-body [type=search]::-webkit-search-decoration {\n  -webkit-appearance: none;\n}\n\n.markdown-body ::-webkit-input-placeholder {\n  color: inherit;\n  opacity: .54;\n}\n\n.markdown-body ::-webkit-file-upload-button {\n  -webkit-appearance: button;\n  font: inherit;\n}\n\n.markdown-body a:hover {\n  text-decoration: underline;\n}\n\n.markdown-body ::placeholder {\n  color: var(--color-fg-subtle);\n  opacity: 1;\n}\n\n.markdown-body hr::before {\n  display: table;\n  content: \"\";\n}\n\n.markdown-body hr::after {\n  display: table;\n  clear: both;\n  content: \"\";\n}\n\n.markdown-body table {\n  border-spacing: 0;\n  border-collapse: collapse;\n  display: block;\n  width: max-content;\n  max-width: 100%;\n  overflow: auto;\n}\n\n.markdown-body td,\n.markdown-body th {\n  padding: 0;\n}\n\n.markdown-body details summary {\n  cursor: pointer;\n}\n\n.markdown-body details:not([open])>*:not(summary) {\n  display: none !important;\n}\n\n.markdown-body a:focus,\n.markdown-body [role=button]:focus,\n.markdown-body input[type=radio]:focus,\n.markdown-body input[type=checkbox]:focus {\n  outline: 2px solid var(--color-accent-fg);\n  outline-offset: -2px;\n  box-shadow: none;\n}\n\n.markdown-body a:focus:not(:focus-visible),\n.markdown-body [role=button]:focus:not(:focus-visible),\n.markdown-body input[type=radio]:focus:not(:focus-visible),\n.markdown-body input[type=checkbox]:focus:not(:focus-visible) {\n  outline: solid 1px transparent;\n}\n\n.markdown-body a:focus-visible,\n.markdown-body [role=button]:focus-visible,\n.markdown-body input[type=radio]:focus-visible,\n.markdown-body input[type=checkbox]:focus-visible {\n  outline: 2px solid var(--color-accent-fg);\n  outline-offset: -2px;\n  box-shadow: none;\n}\n\n.markdown-body a:not([class]):focus,\n.markdown-body a:not([class]):focus-visible,\n.markdown-body input[type=radio]:focus,\n.markdown-body input[type=radio]:focus-visible,\n.markdown-body input[type=checkbox]:focus,\n.markdown-body input[type=checkbox]:focus-visible {\n  outline-offset: 0;\n}\n\n.markdown-body kbd {\n  display: inline-block;\n  padding: 3px 5px;\n  font: 11px ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;\n  line-height: 10px;\n  color: var(--color-fg-default);\n  vertical-align: middle;\n  background-color: var(--color-canvas-subtle);\n  border: solid 1px var(--color-neutral-muted);\n  border-bottom-color: var(--color-neutral-muted);\n  border-radius: 6px;\n  box-shadow: inset 0 -1px 0 var(--color-neutral-muted);\n}\n\n.markdown-body h1,\n.markdown-body h2,\n.markdown-body h3,\n.markdown-body h4,\n.markdown-body h5,\n.markdown-body h6 {\n  margin-top: 24px;\n  margin-bottom: 16px;\n  font-weight: var(--base-text-weight-semibold, 600);\n  line-height: 1.25;\n}\n\n.markdown-body h2 {\n  font-weight: var(--base-text-weight-semibold, 600);\n  padding-bottom: .3em;\n  font-size: 1.5em;\n  border-bottom: 1px solid var(--color-border-muted);\n}\n\n.markdown-body h3 {\n  font-weight: var(--base-text-weight-semibold, 600);\n  font-size: 1.25em;\n}\n\n.markdown-body h4 {\n  font-weight: var(--base-text-weight-semibold, 600);\n  font-size: 1em;\n}\n\n.markdown-body h5 {\n  font-weight: var(--base-text-weight-semibold, 600);\n  font-size: .875em;\n}\n\n.markdown-body h6 {\n  font-weight: var(--base-text-weight-semibold, 600);\n  font-size: .85em;\n  color: var(--color-fg-muted);\n}\n\n.markdown-body p {\n  margin-top: 0;\n  margin-bottom: 10px;\n}\n\n.markdown-body blockquote {\n  margin: 0;\n  padding: 0 1em;\n  color: var(--color-fg-muted);\n  border-left: .25em solid var(--color-border-default);\n}\n\n.markdown-body ul,\n.markdown-body ol {\n  margin-top: 0;\n  margin-bottom: 0;\n  padding-left: 2em;\n}\n\n.markdown-body ol ol,\n.markdown-body ul ol {\n  list-style-type: lower-roman;\n}\n\n.markdown-body ul ul ol,\n.markdown-body ul ol ol,\n.markdown-body ol ul ol,\n.markdown-body ol ol ol {\n  list-style-type: lower-alpha;\n}\n\n.markdown-body dd {\n  margin-left: 0;\n}\n\n.markdown-body tt,\n.markdown-body code,\n.markdown-body samp {\n  font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;\n  font-size: 12px;\n}\n\n.markdown-body pre {\n  margin-top: 0;\n  margin-bottom: 0;\n  font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;\n  font-size: 12px;\n  word-wrap: normal;\n}\n\n.markdown-body .octicon {\n  display: inline-block;\n  overflow: visible !important;\n  vertical-align: text-bottom;\n  fill: currentColor;\n}\n\n.markdown-body input::-webkit-outer-spin-button,\n.markdown-body input::-webkit-inner-spin-button {\n  margin: 0;\n  -webkit-appearance: none;\n  appearance: none;\n}\n\n.markdown-body::before {\n  display: table;\n  content: \"\";\n}\n\n.markdown-body::after {\n  display: table;\n  clear: both;\n  content: \"\";\n}\n\n.markdown-body>*:first-child {\n  margin-top: 0 !important;\n}\n\n.markdown-body>*:last-child {\n  margin-bottom: 0 !important;\n}\n\n.markdown-body a:not([href]) {\n  color: inherit;\n  text-decoration: none;\n}\n\n.markdown-body .absent {\n  color: var(--color-danger-fg);\n}\n\n.markdown-body .anchor {\n  float: left;\n  padding-right: 4px;\n  margin-left: -20px;\n  line-height: 1;\n}\n\n.markdown-body .anchor:focus {\n  outline: none;\n}\n\n.markdown-body p,\n.markdown-body blockquote,\n.markdown-body ul,\n.markdown-body ol,\n.markdown-body dl,\n.markdown-body table,\n.markdown-body pre,\n.markdown-body details {\n  margin-top: 0;\n  margin-bottom: 16px;\n}\n\n.markdown-body blockquote>:first-child {\n  margin-top: 0;\n}\n\n.markdown-body blockquote>:last-child {\n  margin-bottom: 0;\n}\n\n.markdown-body h1 .octicon-link,\n.markdown-body h2 .octicon-link,\n.markdown-body h3 .octicon-link,\n.markdown-body h4 .octicon-link,\n.markdown-body h5 .octicon-link,\n.markdown-body h6 .octicon-link {\n  color: var(--color-fg-default);\n  vertical-align: middle;\n  visibility: hidden;\n}\n\n.markdown-body h1:hover .anchor,\n.markdown-body h2:hover .anchor,\n.markdown-body h3:hover .anchor,\n.markdown-body h4:hover .anchor,\n.markdown-body h5:hover .anchor,\n.markdown-body h6:hover .anchor {\n  text-decoration: none;\n}\n\n.markdown-body h1:hover .anchor .octicon-link,\n.markdown-body h2:hover .anchor .octicon-link,\n.markdown-body h3:hover .anchor .octicon-link,\n.markdown-body h4:hover .anchor .octicon-link,\n.markdown-body h5:hover .anchor .octicon-link,\n.markdown-body h6:hover .anchor .octicon-link {\n  visibility: visible;\n}\n\n.markdown-body h1 tt,\n.markdown-body h1 code,\n.markdown-body h2 tt,\n.markdown-body h2 code,\n.markdown-body h3 tt,\n.markdown-body h3 code,\n.markdown-body h4 tt,\n.markdown-body h4 code,\n.markdown-body h5 tt,\n.markdown-body h5 code,\n.markdown-body h6 tt,\n.markdown-body h6 code {\n  padding: 0 .2em;\n  font-size: inherit;\n}\n\n.markdown-body summary h1,\n.markdown-body summary h2,\n.markdown-body summary h3,\n.markdown-body summary h4,\n.markdown-body summary h5,\n.markdown-body summary h6 {\n  display: inline-block;\n}\n\n.markdown-body summary h1 .anchor,\n.markdown-body summary h2 .anchor,\n.markdown-body summary h3 .anchor,\n.markdown-body summary h4 .anchor,\n.markdown-body summary h5 .anchor,\n.markdown-body summary h6 .anchor {\n  margin-left: -40px;\n}\n\n.markdown-body summary h1,\n.markdown-body summary h2 {\n  padding-bottom: 0;\n  border-bottom: 0;\n}\n\n.markdown-body ul.no-list,\n.markdown-body ol.no-list {\n  padding: 0;\n  list-style-type: none;\n}\n\n.markdown-body ol[type=a] {\n  list-style-type: lower-alpha;\n}\n\n.markdown-body ol[type=A] {\n  list-style-type: upper-alpha;\n}\n\n.markdown-body ol[type=i] {\n  list-style-type: lower-roman;\n}\n\n.markdown-body ol[type=I] {\n  list-style-type: upper-roman;\n}\n\n.markdown-body ol[type=\"1\"] {\n  list-style-type: decimal;\n}\n\n.markdown-body div>ol:not([type]) {\n  list-style-type: decimal;\n}\n\n.markdown-body ul ul,\n.markdown-body ul ol,\n.markdown-body ol ol,\n.markdown-body ol ul {\n  margin-top: 0;\n  margin-bottom: 0;\n}\n\n.markdown-body li>p {\n  margin-top: 16px;\n}\n\n.markdown-body li+li {\n  margin-top: .25em;\n}\n\n.markdown-body dl {\n  padding: 0;\n}\n\n.markdown-body dl dt {\n  padding: 0;\n  margin-top: 16px;\n  font-size: 1em;\n  font-style: italic;\n  font-weight: var(--base-text-weight-semibold, 600);\n}\n\n.markdown-body dl dd {\n  padding: 0 16px;\n  margin-bottom: 16px;\n}\n\n.markdown-body table th {\n  font-weight: var(--base-text-weight-semibold, 600);\n}\n\n.markdown-body table th,\n.markdown-body table td {\n  padding: 6px 13px;\n  border: 1px solid var(--color-border-default);\n}\n\n.markdown-body table tr {\n  background-color: var(--color-canvas-default);\n  border-top: 1px solid var(--color-border-muted);\n}\n\n.markdown-body table tr:nth-child(2n) {\n  background-color: var(--color-canvas-subtle);\n}\n\n.markdown-body table img {\n  background-color: transparent;\n}\n\n.markdown-body img[align=right] {\n  padding-left: 20px;\n}\n\n.markdown-body img[align=left] {\n  padding-right: 20px;\n}\n\n.markdown-body .emoji {\n  max-width: none;\n  vertical-align: text-top;\n  background-color: transparent;\n}\n\n.markdown-body span.frame {\n  display: block;\n  overflow: hidden;\n}\n\n.markdown-body span.frame>span {\n  display: block;\n  float: left;\n  width: auto;\n  padding: 7px;\n  margin: 13px 0 0;\n  overflow: hidden;\n  border: 1px solid var(--color-border-default);\n}\n\n.markdown-body span.frame span img {\n  display: block;\n  float: left;\n}\n\n.markdown-body span.frame span span {\n  display: block;\n  padding: 5px 0 0;\n  clear: both;\n  color: var(--color-fg-default);\n}\n\n.markdown-body span.align-center {\n  display: block;\n  overflow: hidden;\n  clear: both;\n}\n\n.markdown-body span.align-center>span {\n  display: block;\n  margin: 13px auto 0;\n  overflow: hidden;\n  text-align: center;\n}\n\n.markdown-body span.align-center span img {\n  margin: 0 auto;\n  text-align: center;\n}\n\n.markdown-body span.align-right {\n  display: block;\n  overflow: hidden;\n  clear: both;\n}\n\n.markdown-body span.align-right>span {\n  display: block;\n  margin: 13px 0 0;\n  overflow: hidden;\n  text-align: right;\n}\n\n.markdown-body span.align-right span img {\n  margin: 0;\n  text-align: right;\n}\n\n.markdown-body span.float-left {\n  display: block;\n  float: left;\n  margin-right: 13px;\n  overflow: hidden;\n}\n\n.markdown-body span.float-left span {\n  margin: 13px 0 0;\n}\n\n.markdown-body span.float-right {\n  display: block;\n  float: right;\n  margin-left: 13px;\n  overflow: hidden;\n}\n\n.markdown-body span.float-right>span {\n  display: block;\n  margin: 13px auto 0;\n  overflow: hidden;\n  text-align: right;\n}\n\n.markdown-body code,\n.markdown-body tt {\n  padding: .2em .4em;\n  margin: 0;\n  font-size: 85%;\n  white-space: break-spaces;\n  background-color: var(--color-neutral-muted);\n  border-radius: 6px;\n}\n\n.markdown-body code br,\n.markdown-body tt br {\n  display: none;\n}\n\n.markdown-body del code {\n  text-decoration: inherit;\n}\n\n.markdown-body samp {\n  font-size: 85%;\n}\n\n.markdown-body pre code {\n  font-size: 100%;\n}\n\n.markdown-body pre>code {\n  padding: 0;\n  margin: 0;\n  word-break: normal;\n  white-space: pre;\n  background: transparent;\n  border: 0;\n}\n\n.markdown-body .highlight {\n  margin-bottom: 16px;\n}\n\n.markdown-body .highlight pre {\n  margin-bottom: 0;\n  word-break: normal;\n}\n\n.markdown-body .highlight pre,\n.markdown-body pre {\n  padding: 16px;\n  overflow: auto;\n  font-size: 85%;\n  line-height: 1.45;\n  background-color: var(--color-canvas-subtle);\n  border-radius: 6px;\n}\n\n.markdown-body pre code,\n.markdown-body pre tt {\n  display: inline;\n  max-width: auto;\n  padding: 0;\n  margin: 0;\n  overflow: visible;\n  line-height: inherit;\n  word-wrap: normal;\n  background-color: transparent;\n  border: 0;\n}\n\n.markdown-body .csv-data td,\n.markdown-body .csv-data th {\n  padding: 5px;\n  overflow: hidden;\n  font-size: 12px;\n  line-height: 1;\n  text-align: left;\n  white-space: nowrap;\n}\n\n.markdown-body .csv-data .blob-num {\n  padding: 10px 8px 9px;\n  text-align: right;\n  background: var(--color-canvas-default);\n  border: 0;\n}\n\n.markdown-body .csv-data tr {\n  border-top: 0;\n}\n\n.markdown-body .csv-data th {\n  font-weight: var(--base-text-weight-semibold, 600);\n  background: var(--color-canvas-subtle);\n  border-top: 0;\n}\n\n.markdown-body [data-footnote-ref]::before {\n  content: \"[\";\n}\n\n.markdown-body [data-footnote-ref]::after {\n  content: \"]\";\n}\n\n.markdown-body .footnotes {\n  font-size: 12px;\n  color: var(--color-fg-muted);\n  border-top: 1px solid var(--color-border-default);\n}\n\n.markdown-body .footnotes ol {\n  padding-left: 16px;\n}\n\n.markdown-body .footnotes ol ul {\n  display: inline-block;\n  padding-left: 16px;\n  margin-top: 16px;\n}\n\n.markdown-body .footnotes li {\n  position: relative;\n}\n\n.markdown-body .footnotes li:target::before {\n  position: absolute;\n  top: -8px;\n  right: -8px;\n  bottom: -8px;\n  left: -24px;\n  pointer-events: none;\n  content: \"\";\n  border: 2px solid var(--color-accent-emphasis);\n  border-radius: 6px;\n}\n\n.markdown-body .footnotes li:target {\n  color: var(--color-fg-default);\n}\n\n.markdown-body .footnotes .data-footnote-backref g-emoji {\n  font-family: monospace;\n}\n\n.markdown-body .pl-c {\n  color: var(--color-prettylights-syntax-comment);\n}\n\n.markdown-body .pl-c1,\n.markdown-body .pl-s .pl-v {\n  color: var(--color-prettylights-syntax-constant);\n}\n\n.markdown-body .pl-e,\n.markdown-body .pl-en {\n  color: var(--color-prettylights-syntax-entity);\n}\n\n.markdown-body .pl-smi,\n.markdown-body .pl-s .pl-s1 {\n  color: var(--color-prettylights-syntax-storage-modifier-import);\n}\n\n.markdown-body .pl-ent {\n  color: var(--color-prettylights-syntax-entity-tag);\n}\n\n.markdown-body .pl-k {\n  color: var(--color-prettylights-syntax-keyword);\n}\n\n.markdown-body .pl-s,\n.markdown-body .pl-pds,\n.markdown-body .pl-s .pl-pse .pl-s1,\n.markdown-body .pl-sr,\n.markdown-body .pl-sr .pl-cce,\n.markdown-body .pl-sr .pl-sre,\n.markdown-body .pl-sr .pl-sra {\n  color: var(--color-prettylights-syntax-string);\n}\n\n.markdown-body .pl-v,\n.markdown-body .pl-smw {\n  color: var(--color-prettylights-syntax-variable);\n}\n\n.markdown-body .pl-bu {\n  color: var(--color-prettylights-syntax-brackethighlighter-unmatched);\n}\n\n.markdown-body .pl-ii {\n  color: var(--color-prettylights-syntax-invalid-illegal-text);\n  background-color: var(--color-prettylights-syntax-invalid-illegal-bg);\n}\n\n.markdown-body .pl-c2 {\n  color: var(--color-prettylights-syntax-carriage-return-text);\n  background-color: var(--color-prettylights-syntax-carriage-return-bg);\n}\n\n.markdown-body .pl-sr .pl-cce {\n  font-weight: bold;\n  color: var(--color-prettylights-syntax-string-regexp);\n}\n\n.markdown-body .pl-ml {\n  color: var(--color-prettylights-syntax-markup-list);\n}\n\n.markdown-body .pl-mh,\n.markdown-body .pl-mh .pl-en,\n.markdown-body .pl-ms {\n  font-weight: bold;\n  color: var(--color-prettylights-syntax-markup-heading);\n}\n\n.markdown-body .pl-mi {\n  font-style: italic;\n  color: var(--color-prettylights-syntax-markup-italic);\n}\n\n.markdown-body .pl-mb {\n  font-weight: bold;\n  color: var(--color-prettylights-syntax-markup-bold);\n}\n\n.markdown-body .pl-md {\n  color: var(--color-prettylights-syntax-markup-deleted-text);\n  background-color: var(--color-prettylights-syntax-markup-deleted-bg);\n}\n\n.markdown-body .pl-mi1 {\n  color: var(--color-prettylights-syntax-markup-inserted-text);\n  background-color: var(--color-prettylights-syntax-markup-inserted-bg);\n}\n\n.markdown-body .pl-mc {\n  color: var(--color-prettylights-syntax-markup-changed-text);\n  background-color: var(--color-prettylights-syntax-markup-changed-bg);\n}\n\n.markdown-body .pl-mi2 {\n  color: var(--color-prettylights-syntax-markup-ignored-text);\n  background-color: var(--color-prettylights-syntax-markup-ignored-bg);\n}\n\n.markdown-body .pl-mdr {\n  font-weight: bold;\n  color: var(--color-prettylights-syntax-meta-diff-range);\n}\n\n.markdown-body .pl-ba {\n  color: var(--color-prettylights-syntax-brackethighlighter-angle);\n}\n\n.markdown-body .pl-sg {\n  color: var(--color-prettylights-syntax-sublimelinter-gutter-mark);\n}\n\n.markdown-body .pl-corl {\n  text-decoration: underline;\n  color: var(--color-prettylights-syntax-constant-other-reference-link);\n}\n\n.markdown-body g-emoji {\n  display: inline-block;\n  min-width: 1ch;\n  font-family: \"Apple Color Emoji\",\"Segoe UI Emoji\",\"Segoe UI Symbol\";\n  font-size: 1em;\n  font-style: normal !important;\n  font-weight: var(--base-text-weight-normal, 400);\n  line-height: 1;\n  vertical-align: -0.075em;\n}\n\n.markdown-body g-emoji img {\n  width: 1em;\n  height: 1em;\n}\n\n.markdown-body .task-list-item {\n  list-style-type: none;\n}\n\n.markdown-body .task-list-item label {\n  font-weight: var(--base-text-weight-normal, 400);\n}\n\n.markdown-body .task-list-item.enabled label {\n  cursor: pointer;\n}\n\n.markdown-body .task-list-item+.task-list-item {\n  margin-top: 4px;\n}\n\n.markdown-body .task-list-item .handle {\n  display: none;\n}\n\n.markdown-body .task-list-item-checkbox {\n  margin: 0 .2em .25em -1.4em;\n  vertical-align: middle;\n}\n\n.markdown-body .contains-task-list:dir(rtl) .task-list-item-checkbox {\n  margin: 0 -1.6em .25em .2em;\n}\n\n.markdown-body .contains-task-list {\n  position: relative;\n}\n\n.markdown-body .contains-task-list:hover .task-list-item-convert-container,\n.markdown-body .contains-task-list:focus-within .task-list-item-convert-container {\n  display: block;\n  width: auto;\n  height: 24px;\n  overflow: visible;\n  clip: auto;\n}\n\n.markdown-body ::-webkit-calendar-picker-indicator {\n  filter: invert(50%);\n}\n"
  },
  {
    "path": "src/styles/lib/highlight.less",
    "content": "html.dark {\n\tpre code.hljs {\n\t\tdisplay: block;\n\t\toverflow-x: auto;\n\t\tpadding: 1em\n\t}\n\n\tcode.hljs {\n\t\tpadding: 3px 5px\n\t}\n\n\t.hljs {\n\t\tcolor: #abb2bf;\n\t\tbackground: #282c34\n\t}\n\n\t.hljs-keyword,\n\t.hljs-operator,\n\t.hljs-pattern-match {\n\t\tcolor: #f92672\n\t}\n\n\t.hljs-function,\n\t.hljs-pattern-match .hljs-constructor {\n\t\tcolor: #61aeee\n\t}\n\n\t.hljs-function .hljs-params {\n\t\tcolor: #a6e22e\n\t}\n\n\t.hljs-function .hljs-params .hljs-typing {\n\t\tcolor: #fd971f\n\t}\n\n\t.hljs-module-access .hljs-module {\n\t\tcolor: #7e57c2\n\t}\n\n\t.hljs-constructor {\n\t\tcolor: #e2b93d\n\t}\n\n\t.hljs-constructor .hljs-string {\n\t\tcolor: #9ccc65\n\t}\n\n\t.hljs-comment,\n\t.hljs-quote {\n\t\tcolor: #b18eb1;\n\t\tfont-style: italic\n\t}\n\n\t.hljs-doctag,\n\t.hljs-formula {\n\t\tcolor: #c678dd\n\t}\n\n\t.hljs-deletion,\n\t.hljs-name,\n\t.hljs-section,\n\t.hljs-selector-tag,\n\t.hljs-subst {\n\t\tcolor: #e06c75\n\t}\n\n\t.hljs-literal {\n\t\tcolor: #56b6c2\n\t}\n\n\t.hljs-addition,\n\t.hljs-attribute,\n\t.hljs-meta .hljs-string,\n\t.hljs-regexp,\n\t.hljs-string {\n\t\tcolor: #98c379\n\t}\n\n\t.hljs-built_in,\n\t.hljs-class .hljs-title,\n\t.hljs-title.class_ {\n\t\tcolor: #e6c07b\n\t}\n\n\t.hljs-attr,\n\t.hljs-number,\n\t.hljs-selector-attr,\n\t.hljs-selector-class,\n\t.hljs-selector-pseudo,\n\t.hljs-template-variable,\n\t.hljs-type,\n\t.hljs-variable {\n\t\tcolor: #d19a66\n\t}\n\n\t.hljs-bullet,\n\t.hljs-link,\n\t.hljs-meta,\n\t.hljs-selector-id,\n\t.hljs-symbol,\n\t.hljs-title {\n\t\tcolor: #61aeee\n\t}\n\n\t.hljs-emphasis {\n\t\tfont-style: italic\n\t}\n\n\t.hljs-strong {\n\t\tfont-weight: 700\n\t}\n\n\t.hljs-link {\n\t\ttext-decoration: underline\n\t}\n}\n\nhtml {\n\tpre code.hljs {\n\t\tdisplay: block;\n\t\toverflow-x: auto;\n\t\tpadding: 1em\n\t}\n\n\tcode.hljs {\n\t\tpadding: 3px 5px;\n\t\t&::-webkit-scrollbar {\n\t\t\theight: 4px;\n\t\t}\n\t}\n\n\t.hljs {\n\t\tcolor: #383a42;\n\t\tbackground: #fafafa\n\t}\n\n\t.hljs-comment,\n\t.hljs-quote {\n\t\tcolor: #a0a1a7;\n\t\tfont-style: italic\n\t}\n\n\t.hljs-doctag,\n\t.hljs-formula,\n\t.hljs-keyword {\n\t\tcolor: #a626a4\n\t}\n\n\t.hljs-deletion,\n\t.hljs-name,\n\t.hljs-section,\n\t.hljs-selector-tag,\n\t.hljs-subst {\n\t\tcolor: #e45649\n\t}\n\n\t.hljs-literal {\n\t\tcolor: #0184bb\n\t}\n\n\t.hljs-addition,\n\t.hljs-attribute,\n\t.hljs-meta .hljs-string,\n\t.hljs-regexp,\n\t.hljs-string {\n\t\tcolor: #50a14f\n\t}\n\n\t.hljs-attr,\n\t.hljs-number,\n\t.hljs-selector-attr,\n\t.hljs-selector-class,\n\t.hljs-selector-pseudo,\n\t.hljs-template-variable,\n\t.hljs-type,\n\t.hljs-variable {\n\t\tcolor: #986801\n\t}\n\n\t.hljs-bullet,\n\t.hljs-link,\n\t.hljs-meta,\n\t.hljs-selector-id,\n\t.hljs-symbol,\n\t.hljs-title {\n\t\tcolor: #4078f2\n\t}\n\n\t.hljs-built_in,\n\t.hljs-class .hljs-title,\n\t.hljs-title.class_ {\n\t\tcolor: #c18401\n\t}\n\n\t.hljs-emphasis {\n\t\tfont-style: italic\n\t}\n\n\t.hljs-strong {\n\t\tfont-weight: 700\n\t}\n\n\t.hljs-link {\n\t\ttext-decoration: underline\n\t}\n}\n"
  },
  {
    "path": "src/styles/lib/tailwind.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n"
  },
  {
    "path": "src/typings/chat.d.ts",
    "content": "declare namespace Chat {\n\n\tinterface Chat {\n\t\tdateTime: string\n\t\ttext: string\n\t\tinversion?: boolean\n\t\terror?: boolean\n\t\tloading?: boolean\n\t\tconversationOptions?: ConversationRequest | null\n\t\trequestOptions: { prompt: string; options?: ConversationRequest | null }\n\t}\n\n\tinterface History {\n\t\ttitle: string\n\t\tisEdit: boolean\n\t\tuuid: number\n\t}\n\n\tinterface ChatState {\n\t\tactive: number | null\n\t\thistory: History[]\n\t\tchat: { uuid: number; data: Chat[] }[]\n\t}\n\n\tinterface ConversationRequest {\n\t\tconversationId?: string\n\t\tparentMessageId?: string\n\t}\n\n\tinterface ConversationResponse {\n\t\tconversationId: string\n\t\tdetail: {\n\t\t\tchoices: { finish_reason: string; index: number; logprobs: any; text: string }[]\n\t\t\tcreated: number\n\t\t\tid: string\n\t\t\tmodel: string\n\t\t\tobject: string\n\t\t\tusage: { completion_tokens: number; prompt_tokens: number; total_tokens: number }\n\t\t}\n\t\tid: string\n\t\tparentMessageId: string\n\t\trole: string\n\t\ttext: string\n\t}\n}\n"
  },
  {
    "path": "src/typings/env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n\ninterface ImportMetaEnv {\n\treadonly VITE_GLOB_API_URL: string;\n\treadonly VITE_GLOB_API_TIMEOUT: string;\n\treadonly VITE_APP_API_BASE_URL: string;\n\treadonly VITE_GLOB_OPEN_LONG_REPLY: string;\n\treadonly VITE_GLOB_APP_PWA: string;\n}\n"
  },
  {
    "path": "src/typings/global.d.ts",
    "content": "interface Window {\n  $loadingBar?: import('naive-ui').LoadingBarProviderInst;\n  $dialog?: import('naive-ui').DialogProviderInst;\n  $message?: import('naive-ui').MessageProviderInst;\n  $notification?: import('naive-ui').NotificationProviderInst;\n}\n"
  },
  {
    "path": "src/utils/crypto/index.ts",
    "content": "import CryptoJS from 'crypto-js'\n\nconst CryptoSecret = '__CRYPTO_SECRET__'\n\nexport function enCrypto(data: any) {\n  const str = JSON.stringify(data)\n  return CryptoJS.AES.encrypt(str, CryptoSecret).toString()\n}\n\nexport function deCrypto(data: string) {\n  const bytes = CryptoJS.AES.decrypt(data, CryptoSecret)\n  const str = bytes.toString(CryptoJS.enc.Utf8)\n\n  if (str)\n    return JSON.parse(str)\n\n  return null\n}\n"
  },
  {
    "path": "src/utils/format/index.ts",
    "content": "/**\n * 转义 HTML 字符\n * @param source\n */\nexport function encodeHTML(source: string) {\n  return source\n    .replace(/&/g, '&amp;')\n    .replace(/</g, '&lt;')\n    .replace(/>/g, '&gt;')\n    .replace(/\"/g, '&quot;')\n    .replace(/'/g, '&#39;')\n}\n\n/**\n * 判断是否为代码块\n * @param text\n */\nexport function includeCode(text: string | null | undefined) {\n  const regexp = /^(?:\\s{4}|\\t).+/gm\n  return !!(text?.includes(' = ') || text?.match(regexp))\n}\n\n/**\n * 复制文本\n * @param options\n */\nexport function copyText(options: { text: string; origin?: boolean }) {\n  const props = { origin: true, ...options }\n\n  let input: HTMLInputElement | HTMLTextAreaElement\n\n  if (props.origin)\n    input = document.createElement('textarea')\n  else\n    input = document.createElement('input')\n\n  input.setAttribute('readonly', 'readonly')\n  input.value = props.text\n  document.body.appendChild(input)\n  input.select()\n  if (document.execCommand('copy'))\n    document.execCommand('copy')\n  document.body.removeChild(input)\n}\n"
  },
  {
    "path": "src/utils/is/index.ts",
    "content": "export function isNumber<T extends number>(value: T | unknown): value is number {\n  return Object.prototype.toString.call(value) === '[object Number]'\n}\n\nexport function isString<T extends string>(value: T | unknown): value is string {\n  return Object.prototype.toString.call(value) === '[object String]'\n}\n\nexport function isBoolean<T extends boolean>(value: T | unknown): value is boolean {\n  return Object.prototype.toString.call(value) === '[object Boolean]'\n}\n\nexport function isNull<T extends null>(value: T | unknown): value is null {\n  return Object.prototype.toString.call(value) === '[object Null]'\n}\n\nexport function isUndefined<T extends undefined>(value: T | unknown): value is undefined {\n  return Object.prototype.toString.call(value) === '[object Undefined]'\n}\n\nexport function isObject<T extends object>(value: T | unknown): value is object {\n  return Object.prototype.toString.call(value) === '[object Object]'\n}\n\nexport function isArray<T extends any[]>(value: T | unknown): value is T {\n  return Object.prototype.toString.call(value) === '[object Array]'\n}\n\nexport function isFunction<T extends (...args: any[]) => any | void | never>(value: T | unknown): value is T {\n  return Object.prototype.toString.call(value) === '[object Function]'\n}\n\nexport function isDate<T extends Date>(value: T | unknown): value is T {\n  return Object.prototype.toString.call(value) === '[object Date]'\n}\n\nexport function isRegExp<T extends RegExp>(value: T | unknown): value is T {\n  return Object.prototype.toString.call(value) === '[object RegExp]'\n}\n\nexport function isPromise<T extends Promise<any>>(value: T | unknown): value is T {\n  return Object.prototype.toString.call(value) === '[object Promise]'\n}\n\nexport function isSet<T extends Set<any>>(value: T | unknown): value is T {\n  return Object.prototype.toString.call(value) === '[object Set]'\n}\n\nexport function isMap<T extends Map<any, any>>(value: T | unknown): value is T {\n  return Object.prototype.toString.call(value) === '[object Map]'\n}\n\nexport function isFile<T extends File>(value: T | unknown): value is T {\n  return Object.prototype.toString.call(value) === '[object File]'\n}\n"
  },
  {
    "path": "src/utils/request/axios.ts",
    "content": "import axios, { type AxiosResponse } from 'axios'\n\nconst service = axios.create({\n  baseURL: import.meta.env.VITE_GLOB_API_URL,\n})\n\nservice.interceptors.request.use(\n  (config) => {\n    return config\n  },\n  (error) => {\n    return Promise.reject(error.response)\n  },\n)\n\nservice.interceptors.response.use(\n  (response: AxiosResponse): AxiosResponse => {\n    if (response.status === 200)\n      return response\n\n    throw new Error(response.status.toString())\n  },\n  (error) => {\n    return Promise.reject(error)\n  },\n)\n\nexport default service\n"
  },
  {
    "path": "src/utils/request/index.ts",
    "content": "import type { AxiosProgressEvent, AxiosResponse, GenericAbortSignal } from 'axios'\nimport request from './axios'\n\nexport interface HttpOption {\n  url: string\n  data?: any\n  method?: string\n  headers?: any\n  onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void\n  signal?: GenericAbortSignal\n  beforeRequest?: () => void\n  afterRequest?: () => void\n}\n\nexport interface Response<T = any> {\n  data: T\n  message: string | null\n  status: string\n}\n\nfunction http<T = any>(\n  { url, data, method, headers, onDownloadProgress, signal, beforeRequest, afterRequest }: HttpOption,\n) {\n  const successHandler = (res: AxiosResponse<Response<T>>) => {\n    if (res.data.status === 'Success' || typeof res.data === 'string')\n      return res.data\n\n    return Promise.reject(res.data)\n  }\n\n  const failHandler = (error: Response<Error>) => {\n    afterRequest?.()\n    throw new Error(error?.message || 'Error')\n  }\n\n  beforeRequest?.()\n\n  method = method || 'GET'\n\n  const params = Object.assign(typeof data === 'function' ? data() : data ?? {}, {})\n\n  return method === 'GET'\n    ? request.get(url, { params, signal, onDownloadProgress }).then(successHandler, failHandler)\n    : request.post(url, params, { headers, signal, onDownloadProgress }).then(successHandler, failHandler)\n}\n\nexport function get<T = any>(\n  { url, data, method = 'GET', onDownloadProgress, signal, beforeRequest, afterRequest }: HttpOption,\n): Promise<Response<T>> {\n  return http<T>({\n    url,\n    method,\n    data,\n    onDownloadProgress,\n    signal,\n    beforeRequest,\n    afterRequest,\n  })\n}\n\nexport function post<T = any>(\n  { url, data, method = 'POST', headers, onDownloadProgress, signal, beforeRequest, afterRequest }: HttpOption,\n): Promise<Response<T>> {\n  return http<T>({\n    url,\n    method,\n    data,\n    headers,\n    onDownloadProgress,\n    signal,\n    beforeRequest,\n    afterRequest,\n  })\n}\n\nexport default post\n"
  },
  {
    "path": "src/utils/storage/index.ts",
    "content": "export * from './local'\n"
  },
  {
    "path": "src/utils/storage/local.ts",
    "content": "import { deCrypto, enCrypto } from '../crypto'\n\ninterface StorageData<T = any> {\n  data: T\n  expire: number | null\n}\n\nexport function createLocalStorage(options?: { expire?: number | null; crypto?: boolean }) {\n  const DEFAULT_CACHE_TIME = 60 * 60 * 24 * 7\n\n  const { expire, crypto } = Object.assign(\n    {\n      expire: DEFAULT_CACHE_TIME,\n      crypto: true,\n    },\n    options,\n  )\n\n  function set<T = any>(key: string, data: T) {\n    const storageData: StorageData<T> = {\n      data,\n      expire: expire !== null ? new Date().getTime() + expire * 1000 : null,\n    }\n\n    const json = crypto ? enCrypto(storageData) : JSON.stringify(storageData)\n    window.localStorage.setItem(key, json)\n  }\n\n  function get(key: string) {\n    const json = window.localStorage.getItem(key)\n    if (json) {\n      let storageData: StorageData | null = null\n\n      try {\n        storageData = crypto ? deCrypto(json) : JSON.parse(json)\n      }\n      catch {\n        // Prevent failure\n      }\n\n      if (storageData) {\n        const { data, expire } = storageData\n        if (expire === null || expire >= Date.now())\n          return data\n      }\n\n      remove(key)\n      return null\n    }\n  }\n\n  function remove(key: string) {\n    window.localStorage.removeItem(key)\n  }\n\n  function clear() {\n    window.localStorage.clear()\n  }\n\n  return {\n    set,\n    get,\n    remove,\n    clear,\n  }\n}\n\nexport const ls = createLocalStorage()\n\nexport const ss = createLocalStorage({ expire: null, crypto: false })\n"
  },
  {
    "path": "src/views/chat/components/Message/Avatar.vue",
    "content": "<script lang=\"ts\" setup>\nimport { computed } from 'vue'\nimport { NAvatar } from 'naive-ui'\nimport { useUserStore } from '@/store'\nimport { isString } from '@/utils/is'\n\ninterface Props {\n  image?: boolean\n}\ndefineProps<Props>()\n\nconst userStore = useUserStore()\n\nconst avatar = computed(() => userStore.userInfo.avatar)\n</script>\n\n<template>\n  <template v-if=\"image\">\n    <NAvatar v-if=\"isString(avatar) && avatar.length > 0\" :src=\"avatar\" />\n    <NAvatar v-else round />\n  </template>\n  <span v-else class=\"text-[28px] dark:text-white\">\n    <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 32 32\" aria-hidden=\"true\" width=\"1em\" height=\"1em\">\n      <path d=\"M29.71,13.09A8.09,8.09,0,0,0,20.34,2.68a8.08,8.08,0,0,0-13.7,2.9A8.08,8.08,0,0,0,2.3,18.9,8,8,0,0,0,3,25.45a8.08,8.08,0,0,0,8.69,3.87,8,8,0,0,0,6,2.68,8.09,8.09,0,0,0,7.7-5.61,8,8,0,0,0,5.33-3.86A8.09,8.09,0,0,0,29.71,13.09Zm-12,16.82a6,6,0,0,1-3.84-1.39l.19-.11,6.37-3.68a1,1,0,0,0,.53-.91v-9l2.69,1.56a.08.08,0,0,1,.05.07v7.44A6,6,0,0,1,17.68,29.91ZM4.8,24.41a6,6,0,0,1-.71-4l.19.11,6.37,3.68a1,1,0,0,0,1,0l7.79-4.49V22.8a.09.09,0,0,1,0,.08L13,26.6A6,6,0,0,1,4.8,24.41ZM3.12,10.53A6,6,0,0,1,6.28,7.9v7.57a1,1,0,0,0,.51.9l7.75,4.47L11.85,22.4a.14.14,0,0,1-.09,0L5.32,18.68a6,6,0,0,1-2.2-8.18Zm22.13,5.14-7.78-4.52L20.16,9.6a.08.08,0,0,1,.09,0l6.44,3.72a6,6,0,0,1-.9,10.81V16.56A1.06,1.06,0,0,0,25.25,15.67Zm2.68-4-.19-.12-6.36-3.7a1,1,0,0,0-1.05,0l-7.78,4.49V9.2a.09.09,0,0,1,0-.09L19,5.4a6,6,0,0,1,8.91,6.21ZM11.08,17.15,8.38,15.6a.14.14,0,0,1-.05-.08V8.1a6,6,0,0,1,9.84-4.61L18,3.6,11.61,7.28a1,1,0,0,0-.53.91ZM12.54,14,16,12l3.47,2v4L16,20l-3.47-2Z\" fill=\"currentColor\" />\n    </svg>\n  </span>\n</template>\n"
  },
  {
    "path": "src/views/chat/components/Message/Text.vue",
    "content": "<script lang=\"ts\" setup>\nimport { computed, ref } from 'vue'\nimport MarkdownIt from 'markdown-it'\nimport mdKatex from '@traptitech/markdown-it-katex'\nimport mila from 'markdown-it-link-attributes'\nimport hljs from 'highlight.js'\nimport { useBasicLayout } from '@/hooks/useBasicLayout'\nimport { t } from '@/locales'\ninterface Props {\n  inversion?: boolean\n  error?: boolean\n  text?: string\n  loading?: boolean\n  asRawText?: boolean\n}\nconst props = defineProps<Props>()\nconst { isMobile } = useBasicLayout()\nconst textRef = ref<HTMLElement>()\nconst mdi = new MarkdownIt({\n  linkify: true,\n  highlight(code, language) {\n    const validLang = !!(language && hljs.getLanguage(language))\n    if (validLang) {\n      const lang = language ?? ''\n      return highlightBlock(hljs.highlight(code, { language: lang }).value, lang)\n    }\n    return highlightBlock(hljs.highlightAuto(code).value, '')\n  },\n})\nmdi.use(mila, { attrs: { target: '_blank', rel: 'noopener' } })\nmdi.use(mdKatex, { blockClass: 'katexmath-block rounded-md p-[10px]', errorColor: ' #cc0000' })\nconst wrapClass = computed(() => {\n  return [\n    'text-wrap',\n    'min-w-[20px]',\n    'rounded-md',\n    isMobile.value ? 'p-2' : 'px-3 py-2',\n    props.inversion ? 'bg-[#d2f9d1]' : 'bg-[#f4f6f8]',\n    props.inversion ? 'dark:bg-[#a1dc95]' : 'dark:bg-[#1e1e20]',\n    props.inversion ? 'message-request' : 'message-reply',\n    { 'text-red-500': props.error },\n  ]\n})\nconst text = computed(() => {\n  const value = props.text ?? ''\n  if (!props.asRawText)\n    return mdi.render(value)\n  return value\n})\nfunction highlightBlock(str: string, lang?: string) {\n  return `<pre class=\"code-block-wrapper\"><div class=\"code-block-header\"><span class=\"code-block-header__lang\">${lang}</span><span class=\"code-block-header__copy\">${t('chat.copyCode')}</span></div><code class=\"hljs code-block-body ${lang}\">${str}</code></pre>`\n}\ndefineExpose({ textRef })\n</script>\n\n<template>\n  <div class=\"text-black\" :class=\"wrapClass\">\n    <div ref=\"textRef\" class=\"leading-relaxed break-words\">\n      <div v-if=\"!inversion\" class=\"flex items-end\">\n        <div v-if=\"!asRawText\" class=\"w-full markdown-body\" v-html=\"text\" />\n        <div v-else class=\"w-full whitespace-pre-wrap\" v-text=\"text\" />\n        <span v-if=\"loading\" class=\"dark:text-white w-[4px] h-[20px] block animate-blink\" />\n      </div>\n      <div v-else class=\"whitespace-pre-wrap\" v-text=\"text\" />\n    </div>\n  </div>\n</template>\n\n<style lang=\"less\">\n@import url(./style.less);\n</style>\n"
  },
  {
    "path": "src/views/chat/components/Message/index.vue",
    "content": "<script setup lang='ts'>\nimport { computed, ref } from 'vue'\nimport { NDropdown } from 'naive-ui'\nimport AvatarComponent from './Avatar.vue'\nimport TextComponent from './Text.vue'\nimport { SvgIcon } from '@/components/common'\nimport { copyText } from '@/utils/format'\nimport { useIconRender } from '@/hooks/useIconRender'\nimport { t } from '@/locales'\nimport { useBasicLayout } from '@/hooks/useBasicLayout'\n\ninterface Props {\n  dateTime?: string\n  text?: string\n  inversion?: boolean\n  error?: boolean\n  loading?: boolean\n}\n\ninterface Emit {\n  (ev: 'regenerate'): void\n  (ev: 'delete'): void\n}\n\nconst props = defineProps<Props>()\n\nconst emit = defineEmits<Emit>()\n\nconst { isMobile } = useBasicLayout()\n\nconst { iconRender } = useIconRender()\n\nconst textRef = ref<HTMLElement>()\n\nconst asRawText = ref(props.inversion)\n\nconst messageRef = ref<HTMLElement>()\n\nconst options = computed(() => {\n  const common = [\n    {\n      label: t('chat.copy'),\n      key: 'copyText',\n      icon: iconRender({ icon: 'ri:file-copy-2-line' }),\n    },\n    {\n      label: t('common.delete'),\n      key: 'delete',\n      icon: iconRender({ icon: 'ri:delete-bin-line' }),\n    },\n  ]\n  if (!props.inversion) {\n    common.unshift({\n      label: asRawText.value ? t('chat.preview') : t('chat.showRawText'),\n      key: 'toggleRenderType',\n      icon: iconRender({ icon: asRawText.value ? 'ic:outline-code-off' : 'ic:outline-code' }),\n    })\n  }\n  return common\n})\n\nfunction handleSelect(key: 'copyText' | 'delete' | 'toggleRenderType') {\n  switch (key) {\n    case 'copyText':\n      copyText({ text: props.text ?? '' })\n      return\n    case 'toggleRenderType':\n      asRawText.value = !asRawText.value\n      return\n    case 'delete':\n      emit('delete')\n  }\n}\n\nfunction handleRegenerate() {\n  messageRef.value?.scrollIntoView()\n  emit('regenerate')\n}\n</script>\n\n<template>\n  <div\n    ref=\"messageRef\"\n    class=\"flex w-full mb-6 overflow-hidden\"\n    :class=\"[{ 'flex-row-reverse': inversion }]\"\n  >\n    <div\n      class=\"flex items-center justify-center flex-shrink-0 h-8 overflow-hidden rounded-full basis-8\"\n      :class=\"[inversion ? 'ml-2' : 'mr-2']\"\n    >\n      <AvatarComponent :image=\"inversion\" />\n    </div>\n    <div class=\"overflow-hidden text-md \" :class=\"[inversion ? 'items-end' : 'items-start']\">\n      <p class=\"text-sm text-[#b4bbc4]\" :class=\"[inversion ? 'text-right' : 'text-left']\">\n        {{ dateTime }}\n      </p>\n      <div\n        class=\"flex items-end gap-1 mt-2\"\n        :class=\"[inversion ? 'flex-row-reverse' : 'flex-row']\"\n      >\n        <TextComponent\n          ref=\"textRef\"\n          :inversion=\"inversion\"\n          :error=\"error\"\n          :text=\"text\"\n          :loading=\"loading\"\n          :as-raw-text=\"asRawText\"\n        />\n        <div class=\"flex flex-col\">\n          <button\n            v-if=\"!inversion\"\n            class=\"mb-2 transition text-neutral-300 hover:text-neutral-800 dark:hover:text-neutral-300\"\n            @click=\"handleRegenerate\"\n          >\n            <SvgIcon icon=\"ri:restart-line\" />\n          </button>\n          <NDropdown\n            :trigger=\"isMobile ? 'click' : 'hover'\"\n            :placement=\"!inversion ? 'right' : 'left'\"\n            :options=\"options\"\n            @select=\"handleSelect\"\n          >\n            <button class=\"transition text-neutral-300 hover:text-neutral-800 dark:hover:text-neutral-200\">\n              <SvgIcon icon=\"ri:more-2-fill\" />\n            </button>\n          </NDropdown>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "src/views/chat/components/Message/style.less",
    "content": ".markdown-body {\n\tbackground-color: transparent;\n\tfont-size: 14px;\n\n\tp {\n\t\twhite-space: pre-wrap;\n\t}\n\n\tol {\n\t\tlist-style-type: decimal;\n\t}\n\n\tul {\n\t\tlist-style-type: disc;\n\t}\n\n\tpre code,\n\tpre tt {\n\t\tline-height: 1.65;\n\t}\n\n\t.highlight pre,\n\tpre {\n\t\tbackground-color: #fff;\n\t}\n\n\tcode.hljs {\n\t\tpadding: 0;\n\t}\n\n\t.code-block {\n\t\t&-wrapper {\n\t\t\tposition: relative;\n\t\t\tpadding-top: 24px;\n\t\t}\n\n\t\t&-header {\n\t\t\tposition: absolute;\n\t\t\ttop: 5px;\n\t\t\tright: 0;\n\t\t\twidth: 100%;\n\t\t\tpadding: 0 1rem;\n\t\t\tdisplay: flex;\n\t\t\tjustify-content: flex-end;\n\t\t\talign-items: center;\n\t\t\tcolor: #b3b3b3;\n\n\t\t\t&__copy{\n\t\t\t\tcursor: pointer;\n\t\t\t\tmargin-left: 0.5rem;\n\t\t\t\tuser-select: none;\n\t\t\t\t&:hover {\n\t\t\t\t\tcolor: #65a665;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nhtml.dark {\n\t.message-reply {\n\t\t.raw-text {\n\t\t\twhite-space: pre-wrap;\n\t\t\tcolor: var(--n-text-color);\n\t\t}\n\t}\n\n\t.highlight pre,\n\tpre {\n\t\tbackground-color: #282c34;\n\t}\n}\n"
  },
  {
    "path": "src/views/chat/components/index.ts",
    "content": "import Message from './Message/index.vue'\n\nexport { Message }\n"
  },
  {
    "path": "src/views/chat/hooks/useChat.ts",
    "content": "import { useChatStore } from '@/store'\n\nexport function useChat() {\n  const chatStore = useChatStore()\n\n  const getChatByUuidAndIndex = (uuid: number, index: number) => {\n    return chatStore.getChatByUuidAndIndex(uuid, index)\n  }\n\n  const addChat = (uuid: number, chat: Chat.Chat, newChatText: string) => {\n    chatStore.addChatByUuid(uuid, chat, newChatText)\n  }\n\n  const updateChat = (uuid: number, index: number, chat: Chat.Chat) => {\n    chatStore.updateChatByUuid(uuid, index, chat)\n  }\n\n  const updateChatSome = (uuid: number, index: number, chat: Partial<Chat.Chat>) => {\n    chatStore.updateChatSomeByUuid(uuid, index, chat)\n  }\n\n  return {\n    addChat,\n    updateChat,\n    updateChatSome,\n    getChatByUuidAndIndex,\n  }\n}\n"
  },
  {
    "path": "src/views/chat/hooks/useCopyCode.ts",
    "content": "import { onMounted, onUpdated } from 'vue'\nimport { copyText } from '@/utils/format'\n\nexport function useCopyCode() {\n  function copyCodeBlock() {\n    const codeBlockWrapper = document.querySelectorAll('.code-block-wrapper')\n    codeBlockWrapper.forEach((wrapper) => {\n      const copyBtn = wrapper.querySelector('.code-block-header__copy')\n      const codeBlock = wrapper.querySelector('.code-block-body')\n      if (copyBtn && codeBlock) {\n        copyBtn.addEventListener('click', () => {\n          if (navigator.clipboard?.writeText)\n            navigator.clipboard.writeText(codeBlock.textContent ?? '')\n          else\n            copyText({ text: codeBlock.textContent ?? '', origin: true })\n        })\n      }\n    })\n  }\n\n  onMounted(() => copyCodeBlock())\n\n  onUpdated(() => copyCodeBlock())\n}\n"
  },
  {
    "path": "src/views/chat/hooks/useScroll.ts",
    "content": "import type { Ref } from 'vue'\nimport { nextTick, ref } from 'vue'\n\ntype ScrollElement = HTMLDivElement | null\n\ninterface ScrollReturn {\n  scrollRef: Ref<ScrollElement>\n  scrollToBottom: () => Promise<void>\n  scrollToTop: () => Promise<void>\n  scrollToBottomIfAtBottom: () => Promise<void>\n}\n\nexport function useScroll(): ScrollReturn {\n  const scrollRef = ref<ScrollElement>(null)\n\n  const scrollToBottom = async () => {\n    await nextTick()\n    if (scrollRef.value)\n      scrollRef.value.scrollTop = scrollRef.value.scrollHeight\n  }\n\n  const scrollToTop = async () => {\n    await nextTick()\n    if (scrollRef.value)\n      scrollRef.value.scrollTop = 0\n  }\n\n  const scrollToBottomIfAtBottom = async () => {\n    await nextTick()\n    if (scrollRef.value) {\n      const threshold = 50 // 阈值，表示滚动条到底部的距离阈值\n      const distanceToBottom = scrollRef.value.scrollHeight - scrollRef.value.scrollTop - scrollRef.value.clientHeight\n      if (distanceToBottom <= threshold)\n        scrollRef.value.scrollTop = scrollRef.value.scrollHeight\n    }\n  }\n\n  return {\n    scrollRef,\n    scrollToBottom,\n    scrollToTop,\n    scrollToBottomIfAtBottom,\n  }\n}\n"
  },
  {
    "path": "src/views/chat/index.vue",
    "content": "<script setup lang='ts'>\nimport type { Ref } from 'vue'\nimport { computed, onMounted, onUnmounted, ref, watch } from 'vue'\nimport { useRoute } from 'vue-router'\nimport { NButton, NInput, useDialog } from 'naive-ui'\nimport Recorder from 'recorder-core/recorder.wav.min'\nimport { Message } from './components'\nimport { useScroll } from './hooks/useScroll'\nimport { useChat } from './hooks/useChat'\nimport { useCopyCode } from './hooks/useCopyCode'\nimport { HoverButton, SvgIcon } from '@/components/common'\nimport { useBasicLayout } from '@/hooks/useBasicLayout'\nimport { useAppStore, useChatStore, useUserStore } from '@/store'\nimport { fetchAudioChatAPIProcess, fetchChatAPIProcess } from '@/api'\nimport { t } from '@/locales'\n\nlet controller = new AbortController()\n\nconst route = useRoute()\nconst dialog = useDialog()\n\nconst chatStore = useChatStore()\nconst userStore = useUserStore()\nconst appStore = useAppStore()\n\nconst userInfo = computed(() => userStore.userInfo)\nconst focusTextarea = computed(() => appStore.focusTextarea)\n\nuseCopyCode()\nconst { isMobile } = useBasicLayout()\nconst { addChat, updateChat, updateChatSome, getChatByUuidAndIndex } = useChat()\nconst { scrollRef, scrollToBottom } = useScroll()\n\nconst { uuid } = route.params as { uuid: string }\n\nconst dataSources = computed(() => chatStore.getChatByUuid(+uuid))\nconst conversationList = computed(() => dataSources.value.filter(item => (!item.inversion && !!item.conversationOptions)))\n\nconst prompt = ref<string>('')\nconst isAudioPrompt = ref<boolean>(false)\nconst loading = ref<boolean>(false)\nconst recProtectionPeriod = ref<boolean>(false)\nconst sendingRecord = ref<boolean>(false)\nconst inputRef = ref<Ref | null>(null)\nconst recording = ref<boolean>(false)\nconst audioMode = ref<boolean>(false)\nconst actionVisible = ref<boolean>(true)\n\n// 未知原因刷新页面，loading 状态不会重置，手动重置\ndataSources.value.forEach((item, index) => {\n  if (item.loading)\n    updateChatSome(+uuid, index, { loading: false })\n})\n\nfunction isServerError(responseText: string) {\n  return responseText.startsWith('ChatGptWebServerError:')\n}\nfunction getServerErrorType(responseText: string) {\n  if (responseText.startsWith('ChatGptWebServerError:'))\n    return responseText.split(':')[1]\n\n  return ''\n}\n\nfunction handleSubmit() {\n  onConversation()\n}\n\nlet rec = new Recorder()\nconst recOpen = function (success: Function) {\n  rec = Recorder({\n    type: 'wav',\n    sampleRate: 16000,\n    bitRate: 16,\n    onProcess() {\n    },\n  })\n\n  rec.open(() => {\n    success && success()\n    // },function(msg:string,isUserNotAllow:boolean){\n  }, () => {\n    dialog.warning({\n      title: t('chat.openMicrophoneFailedTitle'),\n      content: t('chat.openMicrophoneFailedText'),\n    })\n  })\n}\n\nfunction _recStart() { // 打开了录音后才能进行start、stop调用\n  rec.start()\n  recording.value = true\n  isAudioPrompt.value = true\n\n  // temporarily disable rec button\n  recProtectionPeriod.value = true\n  setTimeout(() => {\n    recProtectionPeriod.value = false\n  }, 2000)\n\n  addChat(\n    +uuid,\n    {\n      dateTime: new Date().toLocaleString(),\n      text: t('chat.recordingInProgress'),\n      loading: true,\n      inversion: true,\n      error: false,\n      conversationOptions: null,\n      requestOptions: { prompt: '', options: null },\n    },\n    t('chat.newChat'),\n  )\n  scrollToBottom()\n}\n\nasync function recStart() {\n  if (recording.value || loading.value)\n    return\n\n  recOpen(() => {\n    _recStart()\n  })\n}\n\nasync function recStop() {\n  if (!rec) {\n    recording.value = false\n    return\n  }\n\n  sendingRecord.value = true\n\n  // rec.stop(async function(blob:Blob,duration:any){\n  rec.stop(async (blob: Blob) => {\n    rec.close()\n    rec = null\n\n    recording.value = false\n    if (!blob)\n      return\n\n    controller = new AbortController()\n\n    let options: Chat.ConversationRequest = {}\n    const lastContext = conversationList.value[conversationList.value.length - 1]?.conversationOptions\n\n    if (lastContext)\n      options = { ...lastContext }\n\n    const formData = new FormData()\n    formData.append('audio', blob, 'recording.wav')\n\n    try {\n      await fetchAudioChatAPIProcess<Chat.ConversationResponse>({\n        formData,\n        options,\n        onDownloadProgress: ({ event }) => {\n          const xhr = event.target\n          let { responseText } = xhr\n          responseText = removeDataPrefix(responseText)\n          prompt.value = responseText\n          isAudioPrompt.value = true\n\n          // 如果解析音频消息出错，就显示something wrong\n          const isError = isServerError(responseText)\n          if (isError) {\n            updateChat(\n              +uuid,\n              dataSources.value.length - 1,\n              {\n                dateTime: new Date().toLocaleString(),\n                text: t(`server.${getServerErrorType(responseText)}`),\n                inversion: false,\n                error: true,\n                loading: false,\n                requestOptions: { prompt: '', ...options },\n              },\n            )\n            scrollToBottom()\n\n            loading.value = false\n            sendingRecord.value = false\n\n            return\n          }\n\n          onConversation()\n        },\n      })\n    }\n    catch (error: any) {\n      const errorMessage = error?.message ?? t('common.wrong')\n\n      if (error.message === 'canceled') {\n        updateChatSome(\n          +uuid,\n          dataSources.value.length - 1,\n          {\n            loading: false,\n          },\n        )\n        scrollToBottom()\n        return\n      }\n\n      const currentChat = getChatByUuidAndIndex(+uuid, dataSources.value.length - 1)\n\n      if (currentChat?.text && currentChat.text !== '') {\n        updateChatSome(\n          +uuid,\n          dataSources.value.length - 1,\n          {\n            text: `${currentChat.text}\\n[${errorMessage}]`,\n            error: false,\n            loading: false,\n          },\n        )\n        return\n      }\n\n      updateChat(\n        +uuid,\n        dataSources.value.length - 1,\n        {\n          dateTime: new Date().toLocaleString(),\n          text: errorMessage,\n          inversion: false,\n          error: true,\n          loading: false,\n          conversationOptions: null,\n          requestOptions: { prompt: '', options: { ...options } },\n        },\n      )\n      scrollToBottom()\n    }\n    finally {\n      recording.value = false\n    }\n    // },function(msg:string){\n  }, () => {\n    // console.log(\"rec error:\"+msg);\n    rec.close()\n    rec = null\n  })\n}\n\nfunction removeDataPrefix(str: string) {\n  if (str && str.startsWith('data: '))\n    return str.slice(6)\n\n  return str\n}\n\nasync function onConversation() {\n  const message = prompt.value\n\n  if (loading.value)\n    return\n\n  if (!message || message.trim() === '')\n    return\n\n  controller = new AbortController()\n\n  if (isAudioPrompt.value) {\n    updateChat(\n      +uuid,\n      dataSources.value.length - 1,\n      {\n        dateTime: new Date().toLocaleString(),\n        text: message ?? '',\n        inversion: true,\n        error: false,\n        loading: false,\n        conversationOptions: null,\n        requestOptions: { prompt: message, options: null },\n      },\n    )\n  }\n  else {\n    addChat(\n      +uuid,\n      {\n        dateTime: new Date().toLocaleString(),\n        text: message,\n        inversion: true,\n        error: false,\n        conversationOptions: null,\n        requestOptions: { prompt: message, options: null },\n      },\n      t('chat.newChat'),\n    )\n  }\n  scrollToBottom()\n\n  isAudioPrompt.value = false\n\n  loading.value = true\n  prompt.value = ''\n\n  let options: Chat.ConversationRequest = {}\n  const lastContext = conversationList.value[conversationList.value.length - 1]?.conversationOptions\n\n  if (lastContext)\n    options = { ...lastContext }\n\n  addChat(\n    +uuid,\n    {\n      dateTime: new Date().toLocaleString(),\n      text: '',\n      loading: true,\n      inversion: false,\n      error: false,\n      conversationOptions: null,\n      requestOptions: { prompt: message, options: { ...options } },\n    },\n    t('chat.newChat'),\n  )\n  scrollToBottom()\n\n  try {\n    await fetchChatAPIProcess<Chat.ConversationResponse>({\n      prompt: message,\n      memory: userInfo.value.chatgpt_memory,\n      top_p: userInfo.value.chatgpt_top_p,\n      options,\n      signal: controller.signal,\n      onDownloadProgress: ({ event }) => {\n        const xhr = event.target\n        let { responseText } = xhr\n        responseText = removeDataPrefix(responseText)\n\n        const isError = isServerError(responseText)\n        if (isError) {\n          updateChat(\n            +uuid,\n            dataSources.value.length - 1,\n            {\n              dateTime: new Date().toLocaleString(),\n              text: t(`server.${getServerErrorType(responseText)}`),\n              inversion: false,\n              error: true,\n              loading: false,\n              requestOptions: { prompt: message, ...options },\n            },\n          )\n          scrollToBottom()\n          return\n        }\n\n        // SSE response format \"data: xxx\"\n        const lastIndex = responseText.lastIndexOf('data: ')\n        let chunk = responseText\n        if (lastIndex !== -1)\n          chunk = responseText.substring(lastIndex)\n\n        chunk = removeDataPrefix(chunk)\n        try {\n          const data = JSON.parse(chunk)\n          updateChat(\n            +uuid,\n            dataSources.value.length - 1,\n            {\n              dateTime: new Date().toLocaleString(),\n              text: data.text ?? '',\n              inversion: false,\n              error: false,\n              loading: false,\n              conversationOptions: { conversationId: data.conversationId, parentMessageId: data.id },\n              requestOptions: { prompt: message, options: { ...options } },\n            },\n          )\n          scrollToBottom()\n        }\n        catch (error) {\n          //\n        }\n      },\n    })\n  }\n  catch (error: any) {\n    const errorMessage = error?.message ?? t('common.wrong')\n\n    if (error.message === 'canceled') {\n      updateChatSome(\n        +uuid,\n        dataSources.value.length - 1,\n        {\n          loading: false,\n        },\n      )\n      scrollToBottom()\n      return\n    }\n\n    const currentChat = getChatByUuidAndIndex(+uuid, dataSources.value.length - 1)\n\n    if (currentChat?.text && currentChat.text !== '') {\n      updateChatSome(\n        +uuid,\n        dataSources.value.length - 1,\n        {\n          text: `${currentChat.text}\\n[${errorMessage}]`,\n          error: false,\n          loading: false,\n        },\n      )\n      return\n    }\n\n    updateChat(\n      +uuid,\n      dataSources.value.length - 1,\n      {\n        dateTime: new Date().toLocaleString(),\n        text: errorMessage,\n        inversion: false,\n        error: true,\n        loading: false,\n        conversationOptions: null,\n        requestOptions: { prompt: message, options: { ...options } },\n      },\n    )\n    scrollToBottom()\n  }\n  finally {\n    loading.value = false\n    sendingRecord.value = false\n  }\n}\n\nasync function onRegenerate(index: number) {\n  if (loading.value)\n    return\n\n  controller = new AbortController()\n\n  const { requestOptions } = dataSources.value[index]\n\n  const message = requestOptions?.prompt ?? ''\n\n  let options: Chat.ConversationRequest = {}\n\n  if (requestOptions.options)\n    options = { ...requestOptions.options }\n\n  loading.value = true\n\n  updateChat(\n    +uuid,\n    index,\n    {\n      dateTime: new Date().toLocaleString(),\n      text: '',\n      inversion: false,\n      error: false,\n      loading: true,\n      conversationOptions: null,\n      requestOptions: { prompt: message, ...options },\n    },\n  )\n\n  try {\n    await fetchChatAPIProcess<Chat.ConversationResponse>({\n      prompt: message,\n      memory: userInfo.value.chatgpt_memory,\n      top_p: userInfo.value.chatgpt_top_p,\n      options,\n      signal: controller.signal,\n      onDownloadProgress: ({ event }) => {\n        const xhr = event.target\n        let { responseText } = xhr\n        responseText = removeDataPrefix(responseText)\n\n        const isError = isServerError(responseText)\n        if (isError) {\n          updateChat(\n            +uuid,\n            dataSources.value.length - 1,\n            {\n              dateTime: new Date().toLocaleString(),\n              text: t(`server.${getServerErrorType(responseText)}`),\n              inversion: false,\n              error: true,\n              loading: false,\n              requestOptions: { prompt: message, ...options },\n            },\n          )\n          scrollToBottom()\n          return\n        }\n\n        // SSE response format \"data: xxx\"\n        const lastIndex = responseText.lastIndexOf('data: ')\n        let chunk = responseText\n        if (lastIndex !== -1)\n          chunk = responseText.substring(lastIndex)\n\n        chunk = removeDataPrefix(chunk)\n        try {\n          const data = JSON.parse(chunk)\n          updateChat(\n            +uuid,\n            index,\n            {\n              dateTime: new Date().toLocaleString(),\n              text: data.text ?? '',\n              inversion: false,\n              error: false,\n              loading: false,\n              conversationOptions: { conversationId: data.conversationId, parentMessageId: data.id },\n              requestOptions: { prompt: message, ...options },\n            },\n          )\n        }\n        catch (error) {\n          //\n        }\n      },\n    })\n  }\n  catch (error: any) {\n    if (error.message === 'canceled') {\n      updateChatSome(\n        +uuid,\n        index,\n        {\n          loading: false,\n        },\n      )\n      return\n    }\n\n    const errorMessage = error?.message ?? t('common.wrong')\n\n    updateChat(\n      +uuid,\n      index,\n      {\n        dateTime: new Date().toLocaleString(),\n        text: errorMessage,\n        inversion: false,\n        error: true,\n        loading: false,\n        conversationOptions: null,\n        requestOptions: { prompt: message, ...options },\n      },\n    )\n  }\n  finally {\n    loading.value = false\n  }\n}\n\nwatch(\n  focusTextarea,\n  () => {\n    setTimeout(() => {\n      const textarea = document.documentElement.querySelector('.n-input__textarea-el') as HTMLInputElement\n      if (textarea)\n        textarea.focus()\n    }, 0)\n  },\n)\n\nfunction handleDelete(index: number) {\n  if (loading.value)\n    return\n\n  dialog.warning({\n    title: t('chat.deleteMessage'),\n    content: t('chat.deleteMessageConfirm'),\n    positiveText: t('common.yes'),\n    negativeText: t('common.no'),\n    onPositiveClick: () => {\n      chatStore.deleteChatByUuid(+uuid, index)\n    },\n  })\n}\n\n// function handleClear() {\n//   if (loading.value)\n//     return\n//\n//   dialog.warning({\n//     title: t('chat.clearChat'),\n//     content: t('chat.clearChatConfirm'),\n//     positiveText: t('common.yes'),\n//     negativeText: t('common.no'),\n//     onPositiveClick: () => {\n//       chatStore.clearChatByUuid(+uuid, t('chat.newChat'))\n//     },\n//   })\n// }\n\nfunction handleEnter(event: KeyboardEvent) {\n  if (!isMobile.value) {\n    if (event.key === 'Enter' && !event.shiftKey) {\n      event.preventDefault()\n      handleSubmit()\n    }\n  }\n  else {\n    if (event.key === 'Enter' && event.ctrlKey) {\n      event.preventDefault()\n      handleSubmit()\n    }\n  }\n}\n\nfunction handleStop() {\n  if (loading.value) {\n    controller.abort()\n    loading.value = false\n  }\n}\n\nfunction handleRec() {\n  if (recording.value === true)\n    recStop()\n  else\n    recStart()\n}\n\nfunction onInputFocus() {\n  if (isMobile.value)\n    actionVisible.value = false\n}\nfunction onInputBlur() {\n  if (isMobile.value)\n    actionVisible.value = true\n}\n\nconst placeholder = computed(() => {\n  if (isMobile.value)\n    return t('chat.placeholderMobile')\n  return t('chat.placeholder')\n})\n\nconst buttonDisabled = computed(() => {\n  return loading.value || !prompt.value || prompt.value.trim() === ''\n})\n\nconst recButtonDisabled = computed(() => {\n  return recProtectionPeriod.value || sendingRecord.value\n})\n\nconst wrapClass = computed(() => {\n  if (isMobile.value)\n    return ['pt-14']\n  return []\n})\n\nconst footerClass = computed(() => {\n  let classes = ['p-4']\n  if (isMobile.value)\n    classes = ['sticky', 'left-0', 'bottom-0', 'right-0', 'p-2', 'overflow-hidden']\n  return classes\n})\n\nonMounted(() => {\n  scrollToBottom()\n  if (inputRef.value && !isMobile.value)\n    inputRef.value?.focus()\n})\n\nonUnmounted(() => {\n  if (loading.value)\n    controller.abort()\n})\n</script>\n\n<template>\n  <div class=\"flex flex-col w-full h-full\" :class=\"wrapClass\">\n    <main class=\"flex-1 overflow-hidden\">\n      <div\n        id=\"scrollRef\"\n        ref=\"scrollRef\"\n        class=\"h-full overflow-hidden overflow-y-auto\"\n        :class=\"[isMobile ? 'p-2' : 'p-4']\"\n      >\n        <div class=\"w-full max-w-screen-xl m-auto\">\n          <template v-if=\"!dataSources.length\">\n            <div class=\"flex items-center justify-center mt-4 text-center text-neutral-300\">\n              <SvgIcon icon=\"ri:bubble-chart-fill\" class=\"mr-2 text-3xl\" />\n              <span>ChatGpt Web</span>\n            </div>\n          </template>\n          <template v-else>\n            <div>\n              <Message\n                v-for=\"(item, index) of dataSources\"\n                :key=\"index\"\n                :date-time=\"item.dateTime\"\n                :text=\"item.text\"\n                :inversion=\"item.inversion\"\n                :error=\"item.error\"\n                :loading=\"item.loading\"\n                @regenerate=\"onRegenerate(index)\"\n                @delete=\"handleDelete(index)\"\n              />\n              <div class=\"sticky bottom-0 left-0 flex justify-center\">\n                <NButton v-if=\"loading\" type=\"warning\" @click=\"handleStop\">\n                  <template #icon>\n                    <SvgIcon icon=\"ri:stop-circle-line\" />\n                  </template>\n                  {{ $t('chat.stopResponding') }}\n                </NButton>\n              </div>\n            </div>\n          </template>\n        </div>\n      </div>\n    </main>\n    <footer :class=\"footerClass\">\n      <div class=\"w-full max-w-screen-xl m-auto\">\n        <div class=\"flex items-center justify-between space-x-2\">\n          <div v-if=\"actionVisible\" class=\"flex items-center space-x-2\">\n            <!--            <HoverButton @click=\"handleClear\"> -->\n            <!--              <span class=\"text-xl text-[#4f555e] dark:text-white\"> -->\n            <!--                <SvgIcon icon=\"ri:delete-bin-line\" /> -->\n            <!--              </span> -->\n            <!--            </HoverButton> -->\n            <HoverButton\n              v-if=\"!audioMode\"\n              @click=\"audioMode = !audioMode\"\n            >\n              <span class=\"text-xl text-[#4f555e] dark:text-white\">\n                <SvgIcon icon=\"ph:microphone-bold\" />\n              </span>\n            </HoverButton>\n            <HoverButton\n              v-else\n              @click=\"audioMode = !audioMode\"\n            >\n              <span class=\"text-xl text-[#4f555e] dark:text-white\">\n                <SvgIcon icon=\"material-symbols:keyboard\" />\n              </span>\n            </HoverButton>\n          </div>\n          <NInput\n            v-if=\"!audioMode\"\n            ref=\"inputRef\"\n            v-model:value=\"prompt\"\n            autofocus\n            type=\"textarea\"\n            :autosize=\"{ minRows: 1.4, maxRows: 10 }\"\n            :placeholder=\"placeholder\"\n            @keypress=\"handleEnter\"\n            @focus=\"onInputFocus\"\n            @blur=\"onInputBlur\"\n          />\n          <NButton\n            v-if=\"audioMode\"\n            :disabled=\"recButtonDisabled\"\n            style=\"flex-grow: 2;\"\n            type=\"default\"\n            @click=\"handleRec\"\n          >\n            <span v-if=\"recording\">{{ $t('chat.clickToSend') }}</span>\n            <span v-if=\"!recording\">{{ $t('chat.clickToTalk') }}</span>\n          </NButton>\n          <NButton\n            v-if=\"!audioMode\"\n            type=\"primary\"\n            :disabled=\"buttonDisabled\" @click=\"handleSubmit\"\n          >\n            <template #icon>\n              <span class=\"dark:text-black\">\n                <SvgIcon icon=\"ri:send-plane-fill\" />\n              </span>\n            </template>\n          </NButton>\n        </div>\n      </div>\n    </footer>\n  </div>\n</template>\n"
  },
  {
    "path": "src/views/chat/layout/Layout.vue",
    "content": "<script setup lang='ts'>\nimport { computed } from 'vue'\nimport { NLayout, NLayoutContent } from 'naive-ui'\nimport { useRouter } from 'vue-router'\nimport Sider from './sider/index.vue'\nimport Header from './header/index.vue'\nimport { useBasicLayout } from '@/hooks/useBasicLayout'\nimport { useAppStore, useChatStore } from '@/store'\n\nconst router = useRouter()\nconst appStore = useAppStore()\nconst chatStore = useChatStore()\n\nrouter.replace({ name: 'Chat', params: { uuid: chatStore.active } })\n\nconst { isMobile } = useBasicLayout()\n\nconst collapsed = computed(() => appStore.siderCollapsed)\n\nconst getMobileClass = computed(() => {\n  if (isMobile.value)\n    return ['rounded-none', 'shadow-none']\n  return ['border', 'rounded-md', 'shadow-md', 'dark:border-neutral-800']\n})\n\nconst getContainerClass = computed(() => {\n  return [\n    'h-full',\n    { 'pl-[260px]': !isMobile.value && !collapsed.value },\n  ]\n})\n</script>\n\n<template>\n  <div class=\"h-full dark:bg-[#24272e] transition-all\" :class=\"[isMobile ? 'p-0' : 'p-4']\">\n    <div class=\"h-full overflow-hidden\" :class=\"getMobileClass\">\n      <NLayout class=\"z-40 transition\" :class=\"getContainerClass\" has-sider>\n        <Sider />\n        <Header v-if=\"isMobile\" />\n        <NLayoutContent class=\"h-full\">\n          <RouterView v-slot=\"{ Component, route }\">\n            <component :is=\"Component\" :key=\"route.fullPath\" />\n          </RouterView>\n        </NLayoutContent>\n      </NLayout>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "src/views/chat/layout/header/index.vue",
    "content": "<script lang=\"ts\" setup>\nimport { computed, defineAsyncComponent, nextTick, ref } from 'vue'\nimport { SvgIcon } from '@/components/common'\nimport { useAppStore, useChatStore } from '@/store'\nimport { useBasicLayout } from '@/hooks/useBasicLayout'\n\nconst appStore = useAppStore()\nconst chatStore = useChatStore()\n\nconst collapsed = computed(() => appStore.siderCollapsed)\nconst currentChatHistory = computed(() => chatStore.getChatHistoryByCurrentActive)\n\nconst Setting = defineAsyncComponent(() => import('@/components/common/Setting/index.vue'))\nconst showSetting = ref(false)\n\nfunction handleUpdateCollapsed() {\n  appStore.setSiderCollapsed(!collapsed.value)\n}\n\nfunction onScrollToTop() {\n  const scrollRef = document.querySelector('#scrollRef')\n  if (scrollRef)\n    nextTick(() => scrollRef.scrollTop = 0)\n}\n\nfunction onScrollToBottom() {\n  const scrollRef = document.querySelector('#scrollRef')\n  if (scrollRef)\n    nextTick(() => scrollRef.scrollTop = scrollRef.scrollHeight)\n}\n\nconst { isMobile } = useBasicLayout()\n</script>\n\n<template>\n  <header\n    class=\"fixed top-0 left-0 right-0 z-30 border-b dark:border-neutral-800 bg-white/80 dark:bg-black/20 backdrop-blur\"\n  >\n    <div class=\"relative flex items-center justify-between h-14\">\n      <button\n        class=\"flex items-center justify-center w-11 h-11\"\n        @click=\"handleUpdateCollapsed\"\n      >\n        <SvgIcon v-if=\"collapsed\" class=\"text-2xl\" icon=\"ri:align-justify\" />\n        <SvgIcon v-else class=\"text-2xl\" icon=\"ri:align-right\" />\n      </button>\n      <h1\n        class=\"flex-1 px-4 overflow-hidden text-center cursor-pointer select-none text-ellipsis whitespace-nowrap\"\n        @dblclick=\"onScrollToTop\"\n      >\n        {{ currentChatHistory?.title ?? '' }}\n      </h1>\n      <button\n        v-if=\"!isMobile\"\n        class=\"flex items-center justify-center w-11 h-11\"\n        @click=\"onScrollToBottom\"\n      >\n        <SvgIcon class=\"text-2xl\" icon=\"ri:arrow-down-s-line\" />\n      </button>\n      <HoverButton class=\"flex items-center justify-center w-11 h-11\" :tooltip=\"$t('setting.setting')\" @click=\"showSetting = true\">\n        <span class=\"text-xl text-[#4f555e] dark:text-white\">\n          <SvgIcon icon=\"ri:settings-4-line\" />\n        </span>\n      </HoverButton>\n      <Setting v-if=\"isMobile\" v-model:visible=\"showSetting\" />\n    </div>\n  </header>\n</template>\n"
  },
  {
    "path": "src/views/chat/layout/index.ts",
    "content": "import ChatLayout from './Layout.vue'\n\nexport { ChatLayout }\n"
  },
  {
    "path": "src/views/chat/layout/sider/Footer.vue",
    "content": "<script setup lang='ts'>\nimport { defineAsyncComponent, ref } from 'vue'\nimport { HoverButton, SvgIcon, UserAvatar } from '@/components/common'\n\nconst Setting = defineAsyncComponent(() => import('@/components/common/Setting/index.vue'))\n\nconst show = ref(false)\n</script>\n\n<template>\n  <footer class=\"flex items-center justify-between min-w-0 p-4 overflow-hidden border-t dark:border-neutral-800\">\n    <div class=\"flex-1 flex-shrink-0 overflow-hidden\">\n      <UserAvatar />\n    </div>\n\n    <HoverButton :tooltip=\"$t('setting.setting')\" @click=\"show = true\">\n      <span class=\"text-xl text-[#4f555e] dark:text-white\">\n        <SvgIcon icon=\"ri:settings-4-line\" />\n      </span>\n    </HoverButton>\n\n    <Setting v-if=\"show\" v-model:visible=\"show\" />\n  </footer>\n</template>\n"
  },
  {
    "path": "src/views/chat/layout/sider/List.vue",
    "content": "<script setup lang='ts'>\nimport { computed } from 'vue'\nimport { NInput, NPopconfirm, NScrollbar } from 'naive-ui'\nimport { SvgIcon } from '@/components/common'\nimport { useAppStore, useChatStore } from '@/store'\nimport { useBasicLayout } from '@/hooks/useBasicLayout'\n\nconst { isMobile } = useBasicLayout()\n\nconst appStore = useAppStore()\nconst chatStore = useChatStore()\n\nconst dataSources = computed(() => chatStore.history)\n\nasync function handleSelect({ uuid }: Chat.History) {\n  if (isActive(uuid))\n    return\n\n  if (chatStore.active)\n    chatStore.updateHistory(chatStore.active, { isEdit: false })\n  await chatStore.setActive(uuid)\n\n  if (isMobile.value)\n    appStore.setSiderCollapsed(true)\n}\n\nfunction handleEdit({ uuid }: Chat.History, isEdit: boolean, event?: MouseEvent) {\n  event?.stopPropagation()\n  chatStore.updateHistory(uuid, { isEdit })\n}\n\nfunction handleDelete(index: number, event?: MouseEvent | TouchEvent) {\n  event?.stopPropagation()\n  chatStore.deleteHistory(index)\n  if (isMobile.value)\n    appStore.setSiderCollapsed(true)\n}\n\nfunction handleEnter({ uuid }: Chat.History, isEdit: boolean, event: KeyboardEvent) {\n  event?.stopPropagation()\n  if (event.key === 'Enter')\n    chatStore.updateHistory(uuid, { isEdit })\n}\n\nfunction isActive(uuid: number) {\n  return chatStore.active === uuid\n}\n</script>\n\n<template>\n  <NScrollbar class=\"px-4\">\n    <div class=\"flex flex-col gap-2 text-sm\">\n      <template v-if=\"!dataSources.length\">\n        <div class=\"flex flex-col items-center mt-4 text-center text-neutral-300\">\n          <SvgIcon icon=\"ri:inbox-line\" class=\"mb-2 text-3xl\" />\n          <span>{{ $t('common.noData') }}</span>\n        </div>\n      </template>\n      <template v-else>\n        <div v-for=\"(item, index) of dataSources\" :key=\"index\">\n          <a\n            class=\"relative flex items-center gap-3 px-3 py-3 break-all border rounded-md cursor-pointer hover:bg-neutral-100 group dark:border-neutral-800 dark:hover:bg-[#24272e]\"\n            :class=\"isActive(item.uuid) && ['border-[#4b9e5f]', 'bg-neutral-100', 'text-[#4b9e5f]', 'dark:bg-[#24272e]', 'dark:border-[#4b9e5f]', 'pr-14']\"\n            @click=\"handleSelect(item)\"\n          >\n            <span>\n              <SvgIcon icon=\"ri:message-3-line\" />\n            </span>\n            <div class=\"relative flex-1 overflow-hidden break-all text-ellipsis whitespace-nowrap\">\n              <NInput\n                v-if=\"item.isEdit\"\n                v-model:value=\"item.title\"\n                size=\"tiny\"\n                @keypress=\"handleEnter(item, false, $event)\"\n              />\n              <span v-else>{{ item.title }}</span>\n            </div>\n            <div v-if=\"isActive(item.uuid)\" class=\"absolute z-10 flex visible right-1\">\n              <template v-if=\"item.isEdit\">\n                <button class=\"p-1\" @click=\"handleEdit(item, false, $event)\">\n                  <SvgIcon icon=\"ri:save-line\" />\n                </button>\n              </template>\n              <template v-else>\n                <button class=\"p-1\">\n                  <SvgIcon icon=\"ri:edit-line\" @click=\"handleEdit(item, true, $event)\" />\n                </button>\n                <NPopconfirm placement=\"bottom\" @positive-click=\"handleDelete(index, $event)\">\n                  <template #trigger>\n                    <button class=\"p-1\">\n                      <SvgIcon icon=\"ri:delete-bin-line\" />\n                    </button>\n                  </template>\n                  {{ $t('chat.deleteHistoryConfirm') }}\n                </NPopconfirm>\n              </template>\n            </div>\n          </a>\n        </div>\n      </template>\n    </div>\n  </NScrollbar>\n</template>\n"
  },
  {
    "path": "src/views/chat/layout/sider/index.vue",
    "content": "<script setup lang='ts'>\nimport type { CSSProperties } from 'vue'\nimport { computed, watch } from 'vue'\nimport { NButton, NLayoutSider } from 'naive-ui'\nimport List from './List.vue'\nimport Footer from './Footer.vue'\nimport { useAppStore, useChatStore } from '@/store'\nimport { useBasicLayout } from '@/hooks/useBasicLayout'\n\nconst appStore = useAppStore()\nconst chatStore = useChatStore()\n\nconst { isMobile } = useBasicLayout()\n\nconst collapsed = computed(() => appStore.siderCollapsed)\n\nfunction handleAdd(newChatText: string) {\n  chatStore.addHistory({ title: newChatText, uuid: Date.now(), isEdit: false })\n  appStore.setFocusTextarea()\n  if (isMobile.value)\n    appStore.setSiderCollapsed(true)\n}\n\nfunction handleUpdateCollapsed() {\n  appStore.setSiderCollapsed(!collapsed.value)\n}\n\nconst getMobileClass = computed<CSSProperties>(() => {\n  if (isMobile.value) {\n    return {\n      position: 'fixed',\n      zIndex: 50,\n    }\n  }\n  return {}\n})\n\nconst mobileSafeArea = computed(() => {\n  if (isMobile.value) {\n    return {\n      paddingBottom: 'env(safe-area-inset-bottom)',\n    }\n  }\n  return {}\n})\n\nwatch(\n  isMobile,\n  (val) => {\n    appStore.setSiderCollapsed(val)\n  },\n  {\n    immediate: true,\n    flush: 'post',\n  },\n)\n</script>\n\n<template>\n  <NLayoutSider\n    :collapsed=\"collapsed\"\n    :collapsed-width=\"0\"\n    :width=\"260\"\n    :show-trigger=\"isMobile ? false : 'arrow-circle'\"\n    collapse-mode=\"transform\"\n    position=\"absolute\"\n    bordered\n    :style=\"getMobileClass\"\n    @update-collapsed=\"handleUpdateCollapsed\"\n  >\n    <div class=\"flex flex-col h-full\" :style=\"mobileSafeArea\">\n      <main class=\"flex flex-col flex-1 min-h-0\">\n        <div class=\"p-4\">\n          <NButton dashed block @click=\"handleAdd($t('chat.newChat'))\">\n            {{ $t(\"chat.newChat\") }}\n          </NButton>\n        </div>\n        <div class=\"flex-1 min-h-0 pb-4 overflow-hidden\">\n          <List />\n        </div>\n      </main>\n      <Footer />\n    </div>\n  </NLayoutSider>\n  <template v-if=\"isMobile\">\n    <div v-show=\"!collapsed\" class=\"fixed inset-0 z-40 bg-black/40\" @click=\"handleUpdateCollapsed\" />\n  </template>\n</template>\n"
  },
  {
    "path": "src/views/exception/403/index.vue",
    "content": "<script lang=\"ts\" setup>\nimport { NButton } from 'naive-ui'\nimport { useRouter } from 'vue-router'\n\nconst router = useRouter()\n\nfunction goHome() {\n  router.push('/')\n}\n</script>\n\n<template>\n  <div class=\"flex h-full\">\n    <div class=\"px-4 m-auto space-y-4 text-center max-[400px]\">\n      <h1 class=\"text-4xl text-slate-800 dark:text-neutral-200\">\n        No permission\n      </h1>\n      <p class=\"text-base text-slate-500 dark:text-neutral-400\">\n        The page you're trying access has restricted access.\n        Please refer to your system administrator\n      </p>\n      <div class=\"flex items-center justify-center text-center\">\n        <div class=\"w-[300px]\">\n          <div class=\"w-[300px]\">\n            <img src=\"../../../icons/403.svg\" alt=\"404\">\n          </div>\n        </div>\n      </div>\n      <NButton type=\"primary\" @click=\"goHome\">\n        Go to Home\n      </NButton>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "src/views/exception/404/index.vue",
    "content": "<script lang=\"ts\" setup>\nimport { NButton } from 'naive-ui'\nimport { useRouter } from 'vue-router'\n\nconst router = useRouter()\n\nfunction goHome() {\n  router.push('/')\n}\n</script>\n\n<template>\n  <div class=\"flex h-full\">\n    <div class=\"px-4 m-auto space-y-4 text-center max-[400px]\">\n      <h1 class=\"text-4xl text-slate-800 dark:text-neutral-200\">\n        Sorry, page not found!\n      </h1>\n      <p class=\"text-base text-slate-500 dark:text-neutral-400\">\n        Sorry, we couldn’t find the page you’re looking for. Perhaps you’ve mistyped the URL? Be sure to check your spelling.\n      </p>\n      <div class=\"flex items-center justify-center text-center\">\n        <div class=\"w-[300px]\">\n          <img src=\"../../../icons/404.svg\" alt=\"404\">\n        </div>\n      </div>\n      <NButton type=\"primary\" @click=\"goHome\">\n        Go to Home\n      </NButton>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "tailwind.config.js",
    "content": "/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  darkMode: 'class',\n  content: [\n    './index.html',\n    './src/**/*.{vue,js,ts,jsx,tsx}',\n  ],\n  theme: {\n    extend: {\n      animation: {\n        blink: 'blink 1.2s infinite steps(1, start)',\n      },\n      keyframes: {\n        blink: {\n          '0%, 100%': { 'background-color': 'currentColor' },\n          '50%': { 'background-color': 'transparent' },\n        },\n      },\n    },\n  },\n  plugins: [],\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"module\": \"ESNext\",\n    \"target\": \"ESNext\",\n    \"lib\": [\"DOM\", \"ESNext\"],\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"jsx\": \"preserve\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"noUnusedLocals\": false,\n    \"strictNullChecks\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"skipLibCheck\": true,\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    },\n    \"types\": [\"vite/client\", \"node\", \"naive-ui/volar\"]\n  },\n  \"exclude\": [\"node_modules\", \"dist\", \"service\"]\n}\n"
  },
  {
    "path": "vite.config.ts",
    "content": "import path from 'path'\nimport type { PluginOption } from 'vite'\nimport { defineConfig, loadEnv } from 'vite'\nimport vue from '@vitejs/plugin-vue'\nimport { VitePWA } from 'vite-plugin-pwa'\n\nfunction setupPlugins(env: ImportMetaEnv): PluginOption[] {\n  return [\n    vue(),\n    env.VITE_GLOB_APP_PWA === 'true' && VitePWA({\n      injectRegister: 'auto',\n      manifest: {\n        name: 'chatGPT',\n        short_name: 'chatGPT',\n        icons: [\n          { src: 'pwa-192x192.png', sizes: '192x192', type: 'image/png' },\n          { src: 'pwa-512x512.png', sizes: '512x512', type: 'image/png' },\n        ],\n      },\n    }),\n  ]\n}\n\nexport default defineConfig((env) => {\n  const viteEnv = loadEnv(env.mode, process.cwd()) as unknown as ImportMetaEnv\n\n  return {\n    resolve: {\n      alias: {\n        '@': path.resolve(process.cwd(), 'src'),\n      },\n    },\n    plugins: setupPlugins(viteEnv),\n    server: {\n      host: '0.0.0.0',\n      port: 1002,\n      open: false,\n      proxy: {\n        '/api': {\n          target: viteEnv.VITE_APP_API_BASE_URL,\n          changeOrigin: true, // 允许跨域\n          rewrite: path => path.replace('/api/', '/'),\n        },\n      },\n    },\n    build: {\n      reportCompressedSize: false,\n      sourcemap: false,\n      commonjsOptions: {\n        ignoreTryCatch: false,\n      },\n    },\n  }\n})\n"
  }
]